自制控件3——仿qq侧滑删除

自定义控件——初识自定义控件里面,我们已经对自定义控件进行描述和分类。其分类分别是

  • 自制控件
  • 组合控件
  • 拓展控件

这篇博文里面,我们继续进行自制控件。

本文我们要做的是仿qq侧滑删除。

有图有真相,我们先看一下最终的效果。

最终效果.gif

对于这个ListView的侧滑删除,ListView方面没有什么问题。主要在于item的实现。我们主要需要解决的就是自制这个可以侧滑拉动之后,显示红色的删除部分,提供红色部分的点击回调。系统显然没有提供这样的控件,所以我们需要自制控件。

我们把item做出来,然后再把item用到ListView里面。
暂且把这个item叫做SlideDelete吧。

一、把SlideDelete的简单样式先做出来。

SlideDelete继承自ViewGroup,在引用SlideDelete的xml的位置include进两个layout,一个是内容,一个是删除

一、1、准备两个布局

内容部分


<?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>

内容部分.png

删除部分

<?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>

删除部分.png

一.2、在activity_main里面把放进SlideDelete这个控件,然后include进上面两个布局

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>

一.3、在自定义控件SlideDelete里面把利用onFinishInflate、onMeasure和onLayout让控件显示出来

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); // 摆放删除部分的位置
    }
}

至此,像下面这样右侧其实 删除部分 已经绘制显示在手机的右侧了,只是屏幕上现在暂时还看不见

模样出长成.png

二、开始做滑动效果

在之前的自制控件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()

  • Touch的up事件
    回调:onViewReleased()

来个代码看一下
继承自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方法。

好啦,现在可以开始做动画了。

二.1、当一个孩子动起来另外一个孩子也可以跟随着动起来


        /**
         * 当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);
            }
        }
跟着一起动.gif

孩子是一起跟着动起来了,出现越界的问题

二.2、解决越界问题

这个越界的问题为什么会产生,是因为现在在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(left<mContentWidth - mDeleteWidth){
            return mContentWidth - mDeleteWidth;
        }else if(left > mContentWidth){
            return mContentWidth;
        }
    }
    return left;
}

二、3、释放时位置的归正


/**
 * 相当于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);
}
解决越界.gif

现在效果是实现了,但是松开手指的一瞬间位置归正得有点突兀,我们需要做一些过渡动画,才显得自然。

二、4、位置归正的过渡动画

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完成。
}  
}
item的自定义完成

到此为止,我们单个的侧滑删除的话的效果就实现了

三、在ListView里面嵌入我们的自定义控件

新建一个Activity,假设名为MyActivity,并且把这个Activity设置为启动页。

MyActivity

public class MyActivity extends Activity{
    private ListView mLv;
    private ArrayList<String> 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

<?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">
    <ListView
        android:id="@+id/mLv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></ListView>
</RelativeLayout>

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>

当前效果

当前效果.gif

简单的效果是实现了,但是这样不好

实际使用中,我们通常是只能一个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<SlideDelete> 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(left<mContentWidth - mDeleteWidth){
                    return 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<String> mData;
    // 继续有多少个条目的delete被展示出来的集合
    private List<SlideDelete> 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<SlideDelete> slideDeleteListIterator = slideDeleteArrayList.listIterator();
        while(slideDeleteListIterator.hasNext()){
            SlideDelete slideDelete = slideDeleteListIterator.next();
            slideDelete.isShowDelete(false);
        }
        slideDeleteArrayList.clear();
    }
}
c8103562-42b9-4137-a1ae-b910297c66a9.gif

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) {
    }
});
效果图.gif

5、删除按钮按下删除item

viewHolder.mLlDelete.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mData.remove(position);
        notifyDataSetChanged();
    }
});

最终效果

最终效果.gif

下载链接

本篇完。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 内容抽屉菜单ListViewWebViewSwitchButton按钮点赞按钮进度条TabLayout图标下拉刷新...
    皇小弟阅读 46,422评论 22 663
  • 其实并没有什么要讲的。但是一个多月没有更新了。 就讲讲最近一个月的故事吧。 人总不能像四月五月那样,维持着亢奋的荷...
    段公子阅读 536评论 0 51
  • 看过一部很不错的台剧,《荼蘼》。听起来很像英语里的“two me”,两个不一样的我,而它的英文译名实际是,Life...
    夏与至V阅读 2,157评论 1 4
  • 要查看一个类实现的流程,我们需要先查看它类说明和成员变量。 AsyncTask说明 大概意思就是说AsyncTas...
    Sanisy阅读 417评论 0 1