转载自:http://www.jianshu.com/p/5cb27a2ce03d
在自定义控件——初识自定义控件里面,我们已经对自定义控件进行描述和分类。其分类分别是
这篇博文里面,我们继续进行自制控件。
本文我们要做的是仿qq侧滑删除。
有图有真相,我们先看一下最终的效果。
对于这个ListView的侧滑删除,ListView方面没有什么问题。主要在于item的实现。我们主要需要解决的就是自制这个可以侧滑拉动之后,显示红色的删除部分,提供红色部分的点击回调。系统显然没有提供这样的控件,所以我们需要自制控件。
我们把item做出来,然后再把item用到ListView里面。
暂且把这个item叫做SlideDelete吧。
SlideDelete继承自ViewGroup,在引用SlideDelete的xml的位置include进两个layout,一个是内容,一个是删除
内容部分
xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#97d8da"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/mTvContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="文本"
android:gravity="center"
android:textSize="28dp"
/>
LinearLayout>
删除部分
xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="130dp"
android:layout_height="60dp"
android:background="#ff0000"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除"
android:textSize="22dp"
android:textColor="#ffffff"
/>
LinearLayout>
activity_main
xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.amqr.slidedelete.MainActivity">
<com.amqr.slidedelete.view.SlideDelete
android:layout_width="match_parent"
android:layout_height="60dp">
<include layout="@layout/slide_content"/>
<include layout="@layout/slide_delete"/>
com.amqr.slidedelete.view.SlideDelete>
RelativeLayout>
public class SlideDelete extends ViewGroup{
private View mContent; // 内容部分
private View mDelete; // 删除部分
public SlideDelete(Context context) {
super(context);
}
public SlideDelete(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlideDelete(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = getChildAt(0);
mDelete = getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 这跟mContent的父亲的大小有关,父亲是宽填充父窗体,高度是和孩子一样是60dp
mContent.measure(widthMeasureSpec,heightMeasureSpec); // 测量内容部分的大小
LayoutParams layoutParams = mDelete.getLayoutParams();
int deleteWidth = MeasureSpec.makeMeasureSpec(layoutParams.width,MeasureSpec.EXACTLY);
int deleteHeight = MeasureSpec.makeMeasureSpec(layoutParams.height,MeasureSpec.EXACTLY);
// 这个参数就需要指定为精确大小
mDelete.measure(deleteWidth,deleteHeight); // 测量删除部分的大小
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int mContentWidth = mContent.getMeasuredWidth();
int mContentHeight = mContent.getMeasuredHeight();
mContent.layout(0,0,mContentWidth,mContentHeight); // 摆放内容部分的位置
int mDeleteWidth = mDelete.getMeasuredWidth();
mDelete.layout(mContentWidth,0,
mContentWidth + mDeleteWidth, mContentHeight); // 摆放删除部分的位置
}
}
至此,像下面这样右侧其实 删除部分 已经绘制显示在手机的右侧了,只是屏幕上现在暂时还看不见
在之前的自制控件2 —— 自制控件 仿qq侧滑菜单一问中,我们是利用弹性滑动——Scroller做动画效果的。
在本文中,我们用ViewDragHelper来做动画效果。
ViewDragHelper简介
ViewDragHelper是v4包种提供的用来的做滑动的
ViewDragHelper是在android4.3的时候发布的,所以必须是android4.3以上的v4包才能支持ViewDragHelper
我们这里的Demo要使用ViewDragHelper,所以我们在当面as的项目的gradle里面引入v4包。
compile 'com.android.support:support-v4:23.1.1'
ViewDragHelper怎么用?
简单说,分两步
第一步,
利用ViewDragHelper.create(ViewGroup forParent, Callback cb)创建一个ViewDragHelper的实例。
第二步,弄一个类继承自ViewDragHelper.Callback,作为第一步中create方法的参数,复写一下这么几个方法并且一些逻辑操作
tryCaptureView()
clampViewPositionHorizontal()
clampViewPositionVertical()
onViewPositionChanged()
onViewReleased()
实现动画效果。
第二步中的复写的那几个方法很重要。这几个方法到底分别的有什么用呢?可以大概这么理解:
我们知道TouchEvent大概可以分为三个状态,Down(按下)、Move(移动)和Up(抬起)。
那么在这三个不同的状态里面,与之关联的就是上面的几个方法:
Touch的down事件:
回调tryCaptureView()
Touch的move事件
回调
clampViewPositionHorizontal()
clampViewPositionVertical()
onViewPositionChanged()
来个代码看一下
继承自ViewDragHelper.Callback的类复写的几个方法。
class MyDrawHelper extends ViewDragHelper.Callback {
/**
* Touch的down事件会回调这个方法 tryCaptureView
*
* @Child:指定要动的孩子 (哪个孩子需要动起来)
* @pointerId: 点的标记
* @return : ViewDragHelper是否继续分析处理 child的相关touch事件
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
System.out.println("调用tryCaptureView");
System.out.println("contentView : " + (mContent == child));
return mContent == child || mDelete == child;
}
// Touch的move事件会回调这面这几个方法
// clampViewPositionHorizontal
// clampViewPositionVertical
// onViewPositionChanged
/**
*
* 捕获了水平方向移动的位移数据
* @param child 移动的孩子View
* @param left 父容器的左上角到孩子View的距离
* @param dx 增量值,其实就是移动的孩子View的左上角距离控件(父亲)的距离,包含正负
* @return 如何动
*
* 调用完此方法,在android2.3以上就会动起来了,2.3以及以下是海动不了的
* 2.3不兼容怎么办?没事,我们复写onViewPositionChanged就是为了解决这个问题的
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
Log.d("Slide","增量值: "+left);
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return super.clampViewPositionVertical(child, top, dy);
}
/**
* 当View的位置改变时的回调
* @param changedView 哪个View的位置改变了
* @param left changedView的left
* @param top changedView的top
* @param dx x方向的上的增量值
* @param dy y方向上的增量值
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
invalidate();
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
/**
* 相当于Touch的up的事件会回调onViewReleased这个方法
*
* @param releasedChild
* @param xvel x方向的速率
* @param yvel y方向的速率
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
}
}
通过上面的详细的备注,我们这些方法的用途都有了相当的了解了。需要注意的是,我们的clampViewPositionHorizontal和clampViewPositionHorizontal所产生的动画效果在2.3以上才会有效果,如果要达到兼容,我们就需要借助onViewPositionChanged方法。
好啦,现在可以开始做动画了。
/**
* 当View的位置改变时的回调 这个方法的价值是结合clampViewPositionHorizontal或者clampViewPositionVertical
* @param changedView 哪个View的位置改变了
* @param left changedView的left
* @param top changedView的top
* @param dx x方向的上的增量值
* @param dy y方向上的增量值
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
//super.onViewPositionChanged(changedView, left, top, dx, dy);
invalidate();
if(changedView == mContent){ // 如果移动的是mContent
//我们移动mContent的实惠要相应的联动改变mDelete的位置
// 怎么改变mDelete的位置,当然是mDelete的layput方法啦
int tempDeleteLeft = mContentWidth+left;
int tempDeleteRight = mContentWidth+left + mDeleteWidth;
mDelete.layout(tempDeleteLeft,0,tempDeleteRight,mDeleteHeight);
}else{ // touch的是mDelete
int tempContentLeft = left - mContentWidth;
int tempContentRight = left;
mContent.layout(tempContentLeft,0,tempContentRight,mContentHeight);
}
}
孩子是一起跟着动起来了,出现越界的问题
这个越界的问题为什么会产生,是因为现在在clampViewPositionHorizontal方法里面我们简单粗暴地返回了left。这样肯定是不行的。所以我们需要在这个方法上做一些处理
/**
*
* 捕获了水平方向移动的位移数据
* @param child 移动的孩子View
* @param left 父容器的左上角到孩子View的距离
* @param dx 增量值,其实就是移动的孩子View的左上角距离控件(父亲)的距离,包含正负
* @return 如何动
*
* 调用完此方法,在android2.3以上就会动起来了,2.3以及以下是海动不了的
* 2.3不兼容怎么办?没事,我们复写onViewPositionChanged就是为了解决这个问题的
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
Log.d("Slide","增量值: "+left);
if(child == mContent){ // 解决内容部分左右拖动的越界问题
if(left>0){
return 0;
}else if(-left>mDeleteWidth){
return -mDeleteWidth;
}
}
if(child == mDelete){ // 解决删除部分左右拖动的越界问题
if(leftreturn mContentWidth - mDeleteWidth;
}else if(left > mContentWidth){
return mContentWidth;
}
}
return left;
}
/**
* 相当于Touch的up的事件会回调onViewReleased这个方法
*
* @param releasedChild
* @param xvel x方向的速率
* @param yvel y方向的速率
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//super.onViewReleased(releasedChild, xvel, yvel);
// 方法的参数里面没有left,那么我们就采用 getLeft()这个方法
int mConLeft = mContent.getLeft();
// 这里没必要分来两个孩子判断
if(-mConLeft>mDeleteWidth/2){
mContent.layout(-mDeleteWidth,0,mContentWidth-mDeleteWidth,mContentHeight);
mDelete.layout(mContentWidth-mDeleteWidth,0,mContentWidth,mDeleteHeight);
}else{
mContent.layout(0,0,mContentWidth,mContentHeight);
mDelete.layout(mContentWidth,0,mContentWidth+mDeleteWidth,mDeleteHeight);
}
super.onViewReleased(releasedChild, xvel, yvel);
}
现在效果是实现了,但是松开手指的一瞬间位置归正得有点突兀,我们需要做一些过渡动画,才显得自然。
ViewDragHelper里面给我们提供了一个方法,smoothSlideViewTo(View child, int finalLeft, int finalTo), smooth是平滑的意思,这个方法就是帮助我们做平滑滑动的。
smoothSlideViewTo(View child, int finalLeft, int finalTop)
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
mCapturedView = child;
mActivePointerId = INVALID_POINTER;
boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
// If we're in an IDLE state to begin with and aren't moving anywhere, we
// end up having a non-null capturedView with an IDLE dragState
mCapturedView = null;
}
return continueSliding;
}
三个参数
child,那个孩子动
finalLeft + finalTop: 孩子运动到最后的左上角的点
通过孩子最后左上角的点就可以确定最后的应该到达的位置
上代码
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//super.onViewReleased(releasedChild, xvel, yvel);
// 方法的参数里面没有left,那么我们就采用 getLeft()这个方法
int mConLeft = mContent.getLeft();
// 这里没必要分来两个孩子判断
if(-mConLeft>mDeleteWidth/2){
//mContent.layout(-mDeleteWidth,0,mContentWidth-mDeleteWidth,mContentHeight);
//mDelete.layout(mContentWidth-mDeleteWidth,0,mContentWidth,mDeleteHeight);
//采用ViewDragHelper的 smoothSlideViewTo 方法让移动变得顺滑自然,不会太生硬
//smoothSlideViewTo只是模拟了数据,但是不会真正的动起来,动起来需要调用 invalidate
// 而 invalidate 通过调用draw()等方法之后最后还是还是会调用 computeScroll 这个方法
// 所以,使用 smoothSlideViewTo 做过渡动画需要结合 invalidate方法 和 computeScroll方法
// smoothSlideViewTo的动画执行时间没有暴露的参数可以设置,但是这个时间是google给我们经过大量计算给出合理时间
viewDragHelper.smoothSlideViewTo(mContent,-mDeleteWidth,0);
viewDragHelper.smoothSlideViewTo(mDelete,mContentWidth-mDeleteWidth,0);
}else{
//mContent.layout(0,0,mContentWidth,mContentHeight);
//mDelete.layout(mContentWidth, 0, mContentWidth + mDeleteWidth, mDeleteHeight);
viewDragHelper.smoothSlideViewTo(mContent, 0, 0);
viewDragHelper.smoothSlideViewTo(mDelete,mContentWidth, 0);
}
invalidate();
super.onViewReleased(releasedChild, xvel, yvel);
}
}
@Override
public void computeScroll() {
//super.computeScroll();
// 把捕获的View适当的时间移动,其实也可以理解为 smoothSlideViewTo 的模拟过程还没完成
if(viewDragHelper.continueSettling(true)){
invalidate();
}
// 其实这个动画过渡的过程大概在怎么走呢?
// 1、smoothSlideViewTo方法进行模拟数据,模拟后就就调用invalidate();
// 2、invalidate()最终调用computeScroll,computeScroll做一次细微动画,
// computeScroll判断模拟数据是否彻底完成,还没完成会再次调用invalidate
// 3、递归调用,知道数据noni完成。
}
}
到此为止,我们单个的侧滑删除的话的效果就实现了
新建一个Activity,假设名为MyActivity,并且把这个Activity设置为启动页。
MyActivity
public class MyActivity extends Activity{
private ListView mLv;
private ArrayList mData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
mLv = (ListView) findViewById(R.id.mLv);
mData=new ArrayList<>();
for(int i=0;i<200;i++){
mData.add("文本"+i);
}
mLv.setAdapter(new MyAdapter());
}
class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
return mData.size();
}
@Override
public Object getItem(int position) {
return mData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(convertView == null){
viewHolder = new ViewHolder();
convertView = View.inflate(MyActivity.this,R.layout.item,null);
viewHolder.mSlideDelete = (SlideDelete) convertView.findViewById(R.id.mSlideDelete);
viewHolder.mLlContent = (LinearLayout) convertView.findViewById(R.id.mLlContent);
viewHolder.mLlDelete = (LinearLayout) convertView.findViewById(R.id.mLlDelete);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.mSlideDelete.setOnSlideDeleteListener(new SlideDelete.OnSlideDeleteListener() {
@Override
public void onOpen(SlideDelete slideDelete) {
}
@Override
public void onClose(SlideDelete slideDelete) {
}
});
return convertView;
}
}
class ViewHolder{
SlideDelete mSlideDelete;
LinearLayout mLlContent;
LinearLayout mLlDelete;
}
}
关联的activity_my
"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.amqr.slidedelete.MainActivity">
"@+id/mLv"
android:layout_width="match_parent"
android:layout_height="match_parent">
adapter里面的item
xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.amqr.slidedelete.view.SlideDelete
android:id="@+id/mSlideDelete"
android:layout_width="match_parent"
android:layout_height="60dp">
<include layout="@layout/slide_content"/>
<include layout="@layout/slide_delete"/>
com.amqr.slidedelete.view.SlideDelete>
LinearLayout>
当前效果
简单的效果是实现了,但是这样不好
实际使用中,我们通常是只能一个item处于打开状态,
即A打开,B必须处于关闭,不能A和B都处于打开的状态
怎么办呢。
只让一个item处于打开状态
整理思路:
1、我们给SlideDelete添加接口和回调,接口里面有onOpen(SlideDelete slideDelete)和onClose(SlideDelete slideDelete)两个方法
// SlideDlete的接口
public interface OnSlideDeleteListener {
void onOpen(SlideDelete slideDelete);
void onClose(SlideDelete slideDelete);
}
2、暴露一个setOnSlideDeleteListener方法给外部调用,把SlideDelete的onViewReleased里面的打开和关闭抽取暴露出来,通过参数boolean决定是否显示delete部分。
然后在onViewReleased里面,真正调用接口的onOpen和onClose方法
private OnSlideDeleteListener onSlideDeleteListener;
public void setOnSlideDeleteListener(OnSlideDeleteListener onSlideDeleteListener){
this.onSlideDeleteListener = onSlideDeleteListener;
}
/**
* 相当于Touch的up的事件会回调onViewReleased这个方法
*
* @param releasedChild
* @param xvel x方向的速率
* @param yvel y方向的速率
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//super.onViewReleased(releasedChild, xvel, yvel);
// 方法的参数里面没有left,那么我们就采用 getLeft()这个方法
int mConLeft = mContent.getLeft();
// 这里没必要分来两个孩子判断
if(-mConLeft>mDeleteWidth/2){ // mDelete展示起来
isShowDelete(true);
if(onSlideDeleteListener != null){
onSlideDeleteListener.onOpen(SlideDelete.this); // 调用接口打开的方法
}
}else{ // mDetele隐藏起来
isShowDelete(false);
if(onSlideDeleteListener != null){
onSlideDeleteListener.onClose(SlideDelete.this); // 调用接口的关闭的方法
}
}
super.onViewReleased(releasedChild, xvel, yvel);
}
3、在MyActivity的Adapter里面调用SlideDelete暴露出来的实现接口的方法。
弄一个集合记录起来已经打开的item,每次getView的执行都先关闭已经打开的item
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(convertView == null){
viewHolder = new ViewHolder();
convertView = View.inflate(MyActivity.this,R.layout.item,null);
viewHolder.mSlideDelete = (SlideDelete) convertView.findViewById(R.id.mSlideDelete);
viewHolder.mTvContent = (TextView) convertView.findViewById(R.id.mTvContent);
viewHolder.mLlDelete = (LinearLayout) convertView.findViewById(R.id.mLlDelete);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.mTvContent.setText(mData.get(position));
viewHolder.mSlideDelete.setOnSlideDeleteListener(new SlideDelete.OnSlideDeleteListener() {
@Override
public void onOpen(SlideDelete slideDelete) {
closeOtherItem();
slideDeleteArrayList.add(slideDelete);
Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size());
}
@Override
public void onClose(SlideDelete slideDelete) {
slideDeleteArrayList.remove(slideDelete);
Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size());
}
});
return convertView;
}
关闭所有已经打开的item的方法
private void closeOtherItem(){
// 采用Iterator的原因是for是线程不安全的,迭代器是线程安全的
ListIterator slideDeleteListIterator = slideDeleteArrayList.listIterator();
while(slideDeleteListIterator.hasNext()){
SlideDelete slideDelete = slideDeleteListIterator.next();
slideDelete.isShowDelete(false);
}
slideDeleteArrayList.clear();
}
至此完成。
附上当前完整的SlideDelete的代码
public class SlideDelete extends ViewGroup{
private View mContent; // 内容部分
private View mDelete; // 删除部分
private ViewDragHelper viewDragHelper;
private int mContentWidth;
private int mContentHeight;
private int mDeleteWidth;
private int mDeleteHeight;
public SlideDelete(Context context) {
super(context);
}
public SlideDelete(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlideDelete(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private OnSlideDeleteListener onSlideDeleteListener;
public void setOnSlideDeleteListener(OnSlideDeleteListener onSlideDeleteListener){
this.onSlideDeleteListener = onSlideDeleteListener;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = getChildAt(0);
mDelete = getChildAt(1);
//public static ViewDragHelper create(ViewGroup forParent, Callback cb)
viewDragHelper = ViewDragHelper.create(this,new MyDrawHelper());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 这跟mContent的父亲的大小有关,父亲是宽填充父窗体,高度是和孩子一样是60dp
mContent.measure(widthMeasureSpec,heightMeasureSpec); // 测量内容部分的大小
LayoutParams layoutParams = mDelete.getLayoutParams();
int deleteWidth = MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY);
int deleteHeight = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
// 这个参数就需要指定为精确大小
mDelete.measure(deleteWidth, deleteHeight); // 测量删除部分的大小
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mContentWidth = mContent.getMeasuredWidth();
mContentHeight = mContent.getMeasuredHeight();
mContent.layout(0, 0, mContentWidth, mContentHeight); // 摆放内容部分的位置
mDeleteWidth = mDelete.getMeasuredWidth();
mDeleteHeight = mDelete.getMeasuredHeight();
mDelete.layout(mContentWidth, 0,
mContentWidth + mDeleteWidth, mContentHeight); // 摆放删除部分的位置
}
class MyDrawHelper extends ViewDragHelper.Callback {
/**
* Touch的down事件会回调这个方法 tryCaptureView
*
* @Child:指定要动的孩子 (哪个孩子需要动起来)
* @pointerId: 点的标记
* @return : ViewDragHelper是否继续分析处理 child的相关touch事件
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
System.out.println("调用tryCaptureView");
System.out.println("contentView : " + (mContent == child));
return mContent == child || mDelete == child;
}
// Touch的move事件会回调这面这几个方法
// clampViewPositionHorizontal
// clampViewPositionVertical
// onViewPositionChanged
/**
*
* 捕获了水平方向移动的位移数据
* @param child 移动的孩子View
* @param left 父容器的左上角到孩子View的距离
* @param dx 增量值,其实就是移动的孩子View的左上角距离控件(父亲)的距离,包含正负
* @return 如何动
*
* 调用完此方法,在android2.3以上就会动起来了,2.3以及以下是海动不了的
* 2.3不兼容怎么办?没事,我们复写onViewPositionChanged就是为了解决这个问题的
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//Log.d("Slide", "增量值: " + left);
if(child == mContent){ // 解决内容部分左右拖动的越界问题
if(left>0){
return 0;
}else if(-left>mDeleteWidth){
return -mDeleteWidth;
}
}
if(child == mDelete){ // 解决删除部分左右拖动的越界问题
if(leftreturn mContentWidth - mDeleteWidth;
}else if(left > mContentWidth){
return mContentWidth;
}
}
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return super.clampViewPositionVertical(child, top, dy);
}
/**
* 当View的位置改变时的回调 这个方法的价值是结合clampViewPositionHorizontal或者clampViewPositionVertical
* @param changedView 哪个View的位置改变了
* @param left changedView的left
* @param top changedView的top
* @param dx x方向的上的增量值
* @param dy y方向上的增量值
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
//super.onViewPositionChanged(changedView, left, top, dx, dy);
invalidate();
if(changedView == mContent){ // 如果移动的是mContent
//我们移动mContent的实惠要相应的联动改变mDelete的位置
// 怎么改变mDelete的位置,当然是mDelete的layput方法啦
int tempDeleteLeft = mContentWidth+left;
int tempDeleteRight = mContentWidth+left + mDeleteWidth;
mDelete.layout(tempDeleteLeft,0,tempDeleteRight,mDeleteHeight);
}else{ // touch的是mDelete
int tempContentLeft = left - mContentWidth;
int tempContentRight = left;
mContent.layout(tempContentLeft,0,tempContentRight,mContentHeight);
}
}
/**
* 相当于Touch的up的事件会回调onViewReleased这个方法
*
* @param releasedChild
* @param xvel x方向的速率
* @param yvel y方向的速率
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//super.onViewReleased(releasedChild, xvel, yvel);
// 方法的参数里面没有left,那么我们就采用 getLeft()这个方法
int mConLeft = mContent.getLeft();
// 这里没必要分来两个孩子判断
if(-mConLeft>mDeleteWidth/2){ // mDelete展示起来
isShowDelete(true);
if(onSlideDeleteListener != null){
onSlideDeleteListener.onOpen(SlideDelete.this); // 调用接口打开的方法
}
}else{ // mDetele隐藏起来
isShowDelete(false);
if(onSlideDeleteListener != null){
onSlideDeleteListener.onClose(SlideDelete.this); // 调用接口的关闭的方法
}
}
super.onViewReleased(releasedChild, xvel, yvel);
}
}
/**
* 是否展示delete部分
* @param isShowDelete
*/
public void isShowDelete(boolean isShowDelete){
if(isShowDelete){
//mContent.layout(-mDeleteWidth,0,mContentWidth-mDeleteWidth,mContentHeight);
//mDelete.layout(mContentWidth-mDeleteWidth,0,mContentWidth,mDeleteHeight);
//采用ViewDragHelper的 smoothSlideViewTo 方法让移动变得顺滑自然,不会太生硬
//smoothSlideViewTo只是模拟了数据,但是不会真正的动起来,动起来需要调用 invalidate
// 而 invalidate 通过调用draw()等方法之后最后还是还是会调用 computeScroll 这个方法
// 所以,使用 smoothSlideViewTo 做过渡动画需要结合 invalidate方法 和 computeScroll方法
// smoothSlideViewTo的动画执行时间没有暴露的参数可以设置,但是这个时间是google给我们经过大量计算给出合理时间
viewDragHelper.smoothSlideViewTo(mContent,-mDeleteWidth,0);
viewDragHelper.smoothSlideViewTo(mDelete,mContentWidth-mDeleteWidth,0);
}else{
//mContent.layout(0,0,mContentWidth,mContentHeight);
//mDelete.layout(mContentWidth, 0, mContentWidth + mDeleteWidth, mDeleteHeight);
viewDragHelper.smoothSlideViewTo(mContent, 0, 0);
viewDragHelper.smoothSlideViewTo(mDelete, mContentWidth, 0);
}
invalidate();
}
@Override
public void computeScroll() {
//super.computeScroll();
// 把捕获的View适当的时间移动,其实也可以理解为 smoothSlideViewTo 的模拟过程还没完成
if(viewDragHelper.continueSettling(true)){
invalidate();
}
// 其实这个动画过渡的过程大概在怎么走呢?
// 1、smoothSlideViewTo方法进行模拟数据,模拟后就就调用invalidate();
// 2、invalidate()最终调用computeScroll,computeScroll做一次细微动画,
// computeScroll判断模拟数据是否彻底完成,还没完成会再次调用invalidate
// 3、递归调用,知道数据noni完成。
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//return super.onTouchEvent(event);
/**Process a touch event received by the parent view. This method will dispatch callback events
as needed before returning. The parent view's onTouchEvent implementation should call this. */
viewDragHelper.processTouchEvent(event); // 使用ViewDragHelper必须复写onTouchEvent并调用这个方法
return true; //消费这个touch
}
// SlideDlete的接口
public interface OnSlideDeleteListener {
void onOpen(SlideDelete slideDelete);
void onClose(SlideDelete slideDelete);
}
}
.
.
附上当前完整的MyActivity代码
public class MyActivity extends Activity{
private ListView mLv;
private ArrayList mData;
// 继续有多少个条目的delete被展示出来的集合
private List slideDeleteArrayList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
mLv = (ListView) findViewById(R.id.mLv);
mData=new ArrayList<>();
for(int i=0;i<200;i++){
mData.add("文本"+i);
}
mLv.setAdapter(new MyAdapter());
}
class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
if(mData!=null){
return mData.size();
}
return 0;
}
@Override
public Object getItem(int position) {
if(mData!=null){
return mData.get(position);
}
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(convertView == null){
viewHolder = new ViewHolder();
convertView = View.inflate(MyActivity.this,R.layout.item,null);
viewHolder.mSlideDelete = (SlideDelete) convertView.findViewById(R.id.mSlideDelete);
viewHolder.mTvContent = (TextView) convertView.findViewById(R.id.mTvContent);
viewHolder.mLlDelete = (LinearLayout) convertView.findViewById(R.id.mLlDelete);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.mTvContent.setText(mData.get(position));
viewHolder.mSlideDelete.setOnSlideDeleteListener(new SlideDelete.OnSlideDeleteListener() {
@Override
public void onOpen(SlideDelete slideDelete) {
closeOtherItem();
slideDeleteArrayList.add(slideDelete);
Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size());
}
@Override
public void onClose(SlideDelete slideDelete) {
slideDeleteArrayList.remove(slideDelete);
Log.d("Slide", "slideDeleteArrayList当前数量:" + slideDeleteArrayList.size());
}
});
return convertView;
}
}
class ViewHolder{
SlideDelete mSlideDelete;
TextView mTvContent;
LinearLayout mLlDelete;
}
private void closeOtherItem(){
// 采用Iterator的原因是for是线程不安全的,迭代器是线程安全的
ListIterator slideDeleteListIterator = slideDeleteArrayList.listIterator();
while(slideDeleteListIterator.hasNext()){
SlideDelete slideDelete = slideDeleteListIterator.next();
slideDelete.isShowDelete(false);
}
slideDeleteArrayList.clear();
}
}
4、滑动屏幕就关闭打开的条目
完善一下,如果当前有item的删除部分是展开的,当这个情况下我们去滑动竖直方向滑动屏幕,那么删除部分就会被隐藏回去。
其实就是做一下ListView的滑动监听而已
mLv.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if(scrollState == SCROLL_STATE_FLING || scrollState == SCROLL_STATE_TOUCH_SCROLL){
closeOtherItem();
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
5、删除按钮按下删除item
viewHolder.mLlDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mData.remove(position);
notifyDataSetChanged();
}
});
最终效果