项目中有个需求,就是防QQ的消息列表侧滑删除。在网上找了一些资料后,弄明白了原理。无非就是利用ViewDragHelper去拖动控件。

我的想法是,在一个FrameLayout中有两个控件,一个是主控件,另一个就是删除控件。主控件宽高充满布局,删除控件在FrameLayout的右侧,并且被主控件遮挡住。

当滑动主控件的时候,从而让删除控件显示出来(这个方法,并不会让删除控件移动)。

当然,也可以将主控件跟删除控件都放到一个LinearLayout中,然后纵向布局,拖动的时候删除控件也会一并移动。

弄清了原理,接下来就是敲代码。

先上代码。


public class SlideLinearLayout extends FrameLayout {

    private ViewDragHelper mViewDragHelper;

    /**删除控件*/
    private View mDeleteView;
    /**主控件*/
    private View mMainView;

    /**主控件可以移动的最大值跟最小值*/
    private int mMaxWidth, mMinWidth;

    /**主控件当前移动的位移*/
    private int mMoveLength;

    /**主View的初始X坐标跟Y坐标*/
    private int mOriginalX,mOriginalY;

    /**删除控件的中心坐标*/
    private int mPrivX,mPrivY;


    public SlideLinearLayout(Context context) {
        super(context);
        init();
    }

    public SlideLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public SlideLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mViewDragHelper = ViewDragHelper.create(this, new Callback() {
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                if (child == mMainView) {
                    return true;
                } else {
                    return false;
                }
            }

            /**
             * 控件移动的范围
             * 默认是为0,但是这里需要设置。因为当为0的时候,会把触摸事件传递给子控件,自己不再使用,这样就无法触发onToucher,也就无法拖动。
             * 如果大于0,自己依然会触发onToucher
             * @param child
             * @return
             */
            @Override
            public int getViewHorizontalDragRange(View child) {
                return mDeleteView.getWidth();
            }

            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                /**拖动距离有效性验证*/
                left = left < mMinWidth ? mMinWidth : left;
                left = left > mMaxWidth ? mMaxWidth : left;
                mMoveLength = Math.abs(left);

                /**拖动的时候为了触发删除控件的动画,调用了此方法*/
                invalidate();
                return left;
            }


            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {

                /***
                 * 当手拖动View,并且释放的时候,根据拖动的距离判断滑动向哪一方
                 */
                if(releasedChild == mMainView) {
                    if(mMoveLength > (mDeleteView.getWidth()/2) ){
                        mViewDragHelper.settleCapturedViewAt(mOriginalX - mDeleteView.getWidth(), mOriginalY);
                    }else{
                        mViewDragHelper.settleCapturedViewAt(mOriginalX, mOriginalY);
                    }
                    invalidate();
                }
            }
        });
    }



    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        /**
         * 对删除控件进行Y坐标缩放动画
         */
        if(child == mDeleteView){

            /**
             * 根据移动的位移以及删除控件的宽来获取缩放比例。
             * 之所以用mMainView.getLeft(),而不是mMoveLength
             * 是因为当拖动的时候mMoveLength是有效的,但是动画自动滑动的时候,
             * MoveLength就没有发生改变,所以使用mMainView.getLeft()
             *
             */
            float scale =  Math.abs( (float) mMainView.getLeft() /(float) mDeleteView.getWidth() );
            /**有效性验证*/
            scale = scale < 0 ? 0:scale;
            scale = scale > 1 ? 1:scale;

            canvas.save();
            /**开始缩放*/
            canvas.scale(1,scale,mPrivX,mPrivY);
            boolean result = super.drawChild(canvas, child, drawingTime);
            canvas.restore();

            return result;
        }
        return super.drawChild(canvas, child, drawingTime);
    }

    @Override
    public void computeScroll() {
        if(mViewDragHelper.continueSettling(true)){
            invalidate();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mViewDragHelper.processTouchEvent(event);
        return true;
    }


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        /**
         * 第一个是删除控件,第二个是主控件。在XML放置的时候要注意
         */
        mMainView = getChildAt(1);
        mDeleteView = getChildAt(0);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);

        /**
         * 初始化坐标跟移动范围
         */
        mOriginalX = mMainView.getLeft();
        mOriginalY = mMainView.getTop();

        mMinWidth = -mDeleteView.getWidth();
        mMaxWidth = 0;

        /***
         * 删除控件的中心坐标
         */
        mPrivX = mDeleteView.getLeft() + mDeleteView.getWidth()/2;
        mPrivY = mDeleteView.getTop() + mDeleteView.getHeight()/2;
    }



首先是init()方法。

这个方法是初始化了ViewDragHlper。创建该实例需要传个Context跟一个接口。这个接口才是重要的。先来看看这个接口有哪些重要的方法。

@Override
public boolean tryCaptureView(View child, int pointerId) {
    if (child == mMainView) {
        return true;
    } else {
        return false;
    }
}

这个方法是什么意思呢?API的说明是这样的。

/**
 * Capture a specific child view for dragging within the parent. The callback will be notified
 * but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to
 * capture this view.
 *
 * @param childView Child view to capture
 * @param activePointerId ID of the pointer that is dragging the captured child view
 */

简单的来说,就是当你触摸屏幕,移动的时候,就会回调这个方法。它会返回两个参数。第一个参数,就是你触摸的那个控件。第二个就是ID。

返回值又代表什么呢?返回ture,就是代笔允许拖动这个控件。返回false就代表不允许拖动这个控件.。这里我只允许拖动主控件。


接下来是


@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
    /**拖动距离有效性验证*/
    left = left < mMinWidth ? mMinWidth : left;
    left = left > mMaxWidth ? mMaxWidth : left;
    mMoveLength = Math.abs(left);

    /**拖动的时候为了触发删除控件的动画,调用了此方法*/
    invalidate();
    return left;
}
这个方法,是拖动距离的时候X方向回调。第一个参数是拖动的控件。第二个是拖动的总距离。第三个参数是此次拖动跟上次拖动之间的距离。
为了防止越界,这里做了个拖动范围。范围是负删除控件宽度 - 0,
同理还有Y方向的回调()。
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
    return super.clampViewPositionVertical(child, top, dy);
}
这里我只需要X方向的拖动,Y方向就没有管.
初始化完ViewDragHelper后,还需要做两件事。
能够拖动控件的原理是获取到触摸事件MotionEvent ,然后根据触摸事件的移动,来动态改变控件的位置。既然需要触摸事件,那么我们就需要给ViewDragHelper设置触摸事件。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return mViewDragHelper.shouldInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    mViewDragHelper.processTouchEvent(event);
    return true;
}复写了父控件的拦截方法跟触摸方法,分别设置给ViewDragHelper进行处理。
这样,就可以实现了拖动。
接下来的问题,就是发现无法点击删除按钮,也无法出发Item的点击事件。这是为什么呢。
原因就在于,ViewDragHelper拦截了事件,事件并没有传递给子控件,包括主控件跟删除控件。自然也就无法触发点击事件了。
网上查了下,说是要复写getViewHorizontalDragRange()方法,使其返回值不返回0即可。我这里返回的是删除控件的宽度。
至于为什么不返回0就可以,我看了源码,没看懂。技术有限。
到这里一个侧滑控件就初步搞定了。