ViewDragHelper源码浅析与应用实例

ViewDragHelper源码浅析与应用实例

ViewDragHelper简介

ViewDragHelper是Google为Android开发者提供的一个强大的帮助类。使用它几乎可以完成所有和滑动拖拽相关的需求。
示例
下面让我们从源码入手,逐步掌握它的实现逻辑与使用方法。

源码浅析

该部分将首先介绍类中几个关键的域,然后介绍回调接口以及构造器,最后分析一次拖拽操作过程中所涉及的方法。

关键域
/**
* 空闲状态。
*/
public static final int STATE_IDLE = 0;

/**
* 某个View正在被拖拽。
*/
public static final int STATE_DRAGGING = 1;

/**
* 某个View正在被放置。
*/
public static final int STATE_SETTLING = 2;

private int mDragState;

ViewDragHelper使用状态位来判断当前工作状态,有STATE_IDLE(空闲)、STATE_DRAGGING(拖拽)、STATE_SETTLING(放置)三种。这里重点说明一下拖拽与放置的区别:如果View的移动是由于用户的手指在屏幕上滑动造成的,那么当前状态属于拖拽状态;如果View的移动是由于程序内调用某些方法造成的,那么当前状态属于放置状态。

/**
* 左边界
*/
public static final int EDGE_LEFT = 1 << 0;

/**
* 右边界
*/
public static final int EDGE_RIGHT = 1 << 1;

/**
* 上边界
*/
public static final int EDGE_TOP = 1 << 2;

/**
* 下边界
*/
public static final int EDGE_BOTTOM = 1 << 3;

private int mTrackingEdges;

边界的标志位。用于后面的与边界相关的拖拽判定方法中。

private View mCapturedView;

用于记录当前被捕获的View。

private int mTouchSlop;

被判定为滑动之前可以移动的最大距离。这个值越小滑动操作越灵活。

private VelocityTracker mVelocityTracker;

ViewDragHelper使用VelocityTracker计算滑动速度,进而实现对Fling(抛动)的支持。

回调接口

初始化ViewDragHelper对象时需要传入一个ViewDragHelper.Callback对象。正如我们在许多其他场合遇到的Callback一样,它是一个需要编程人员实现的回调接口,里面包含了用于控制该ViewDragHelper行为的回调方法。由于包含的方法较多,这里只对常用到的几个方法进行介绍。

public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}

当某个view的位置由于拖拽或放置而改变时调用。
changedView:位置改变的view;
left,top:新的X、Y的坐标;
dx,dy:X、Y方向上发生的位移。

public abstract boolean tryCaptureView(View child, int pointerId);

即将捕获一个view时调用,返回true表示允许捕获,false表示不允许捕获。
child:即将被捕获的view;
pointerId:即将捕获view的pointer的Id
注意:由于ViewDragHelper不支持同时拖拽多个view的功能,在本文其他地方将不对Pointer相关的部分进行说明。想了解多点触控的知识的话可以去搜索一些相关文章。

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

当view被释放时调用。一般用于实现回弹效果。
releasedChild:被释放的view;
xvel,yvel:释放时X、Y轴上的滑动速度。

public void onEdgeTouched(int edgeFlags, int pointerId) {}

当需要追踪的某条边界被触摸到,并且当前没有子view被捕获时调用。
edgeFlags:用于判断被触摸的是哪条边;

public int clampViewPositionHorizontal(View child, int left, int dx) {return 0;}
public int clampViewPositionVertical(View child, int top, int dy) {return 0;}

在滑动时为限制view的位移量而调用。默认实现为返回0,即不可滑动。
child:正在滑动的view。
left/top:如果不加限制,view的左边缘将到达的位置。
dx/dy:如果不加限制,view将发生的位移量。

public int getViewHorizontalDragRange(View child)  {return 0;}
public int getViewVerticalDragRange(View child) {return 0;}

确定view可以滑动的范围,默认返回0。
view:目标view。

构造器

为确保不同平台版本的兼容性,ViewDragHelper不提供public的构造器,实例需要通过静态方法ViewDragHelper.create(ViewGroup forParent, Callback cb)或是ViewDragHelper.create(ViewGroup forParent, float sensitivity, Callback cb)来获取。首先看一下三参数的create()方法的实现:

public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}

可以很明显地看出,两参数的create()方法是默认实现,三参数的create()方法首先调用两参数的create()方法构造一个实例,然后根据sensitivity参数修改了mTouchSlop域的值,并且sensitivity的值越大,mTouchSlop的值越小。
接下来看一下两参数的create()方法:

public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}

只有一行代码,调用构造器创建了一个实例。接下来看看构造器:

private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
    //确保父view与回调接口不为null
    if (forParent == null) {
        throw new IllegalArgumentException("Parent view may not be null");
    }
    if (cb == null) {
        throw new IllegalArgumentException("Callback may not be null");
    }

    mParentView = forParent;
    mCallback = cb;

    //利用ViewConfiguration获取最小滑动距离、最大抛动速度、最小抛动速度等参数
    final ViewConfiguration vc = ViewConfiguration.get(context);
    //dp与px的转换系数
    final float density = context.getResources().getDisplayMetrics().density;
    //确定边的宽度(边在判定时作为一个矩形考虑)
    mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);

    mTouchSlop = vc.getScaledTouchSlop();
    mMaxVelocity = vc.getScaledMaximumFlingVelocity();
    mMinVelocity = vc.getScaledMinimumFlingVelocity();
    mScroller = ScrollerCompat.create(context, sInterpolator);
}

主要就是通过ViewConfiguration类获取了一些参数,并且创建了Scroller实例。创建Scroller实例时传入的sInterpolator为ViewDragHelper内部实现的一个Interpolator实例,会用在滑动动画的计算中。

一次滑动操作所涉及的方法

想要依靠ViewDragHelper实现滑动,必须首先将它所在的ViewGroup的触摸事件交给它管理。一般我们会这么实现:

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

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

onInterceptTouchEvent()与onTouchEvent()方法是自定义ViewGroup时非常重要的两个方法,用于控制触摸事件的分发过程。如果对View触摸事件分发不是很了解的话,可以去查查相关文章,有很多。
首先说明为什么在onTouchEvent()中返回true。ViewGroup在分发ACTION_MOVE与ACTION_UP事件时,会直接将他们交由成功处理了ACTION_DOWN事件的子view处理。因此,如果这里不返回true,就收不到后续的ACTION_MOVE与ACTION_UP事件,所有的方法也就失效了。
接下来我们重点分析一下ViewDragHelper提供的shouldInterceptTouchEvent()与processTouchEvent()两个方法。

顾名思义,shouldInterceptTouchEvent()用于判断是否需要拦截触摸事件,而processTouchEvent()用于处理触摸事件。在分析滑动逻辑的具体实现之前,首先需要对事件流进行分析。由于滑动逻辑是在processTouchEvent()中实现的(这里先记一个结论),因此必须被分发到ViewGroup的onTouchEvent()中,否则ViewDragHelper将不起作用。这里可以分为两种情况:

  1. ViewGroup的子View没能消费掉本次触摸事件,事件被分发到ViewGroup的onTouchEvent()中进行处理。
  2. ViewGroup通过onInterceptTouchEvent()拦截了本次触摸事件,事件被分发到ViewGroup的onTouchEvent()中进行处理;

首先看第一种情况。如果触摸位置的子View无法消费该事件,事件将传回ViewGroup的onTouchEvent()中,并最终在ViewDragHelper的processTouchEvent()方法中得到处理。让我们看看processTouchEvent()的实现。首先是事件类型为ACTION_DOWN时:

case MotionEvent.ACTION_DOWN: {
        //获取该位置的view
        final float x = ev.getX();
        final float y = ev.getY();
        final int pointerId = ev.getPointerId(0);
        final View toCapture = findTopChildUnder((int) x, (int) y);

        //保存初始状态,这里保存了是否触摸到边的信息
        saveInitialMotion(x, y, pointerId);

        //尝试捕获该view
        tryCaptureViewForDrag(toCapture, pointerId);

        //确认是否有需要追踪的边界被触摸到,有的话调用onEdgeTouched回调方法
        final int edgesTouched = mInitialEdgesTouched[pointerId];
        if ((edgesTouched & mTrackingEdges) != 0) {
            mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
        }
        break;
    }

方法思路很简洁,首先根据触摸点坐标获取子view,然后尝试捕获该子view,最后尝试捕获边。很显然,这里最重要的是tryCaptureViewForDrag()方法,让我们看看它的实现:

boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
    //如果已经捕获了该view,那么就直接返回true。
    if (toCapture == mCapturedView && mActivePointerId == pointerId) {
        // Already done!
        return true;
    }
    //调用callback中的tryCaptureView()方法确认是否能够捕获
    if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
        mActivePointerId = pointerId;
        //如果能够捕获,那么就捕获该view
        captureChildView(toCapture, pointerId);
        return true;
    }
    //不能捕获的话返回false
    return false;
}

这里用到了回调接口中的tryCaptureView()方法,如果没有在方法中返回true,将不会调用captureChildView()方法,捕获将失败。这告诉我们:需要在tryCaptureView()中进行判定,如果目标是我们想要拖动的view,那么就应当返回true。
然后说明一下captureChildView(View childView, int activePointerId)方法。它是ViewDragHelper的一个public方法,用于捕获一个view。它的实现很简单,就是将mCapturedView设置为childView,并将当前状态设置为STATE_DRAGGING。这里就不贴代码了。重点是,这个方法本身并不受到tryCaptureView()的限制,只要调用了就一定能捕获成功(当然是在参数合法的前提下)。正是由于这个特性,我们可以在onEdgeTouched()方法中使用它来捕获一个屏幕外的对象。本文开头的gif中的绿色方块就是这么实现的。
接下来看看事件类型为ACTION_MOVE时:

case MotionEvent.ACTION_MOVE: {
    //如果当前状态为正在拖拽
    if (mDragState == STATE_DRAGGING) {
        final int index = ev.findPointerIndex(mActivePointerId);
        //获取当前的x,y坐标
        final float x = ev.getX(index);
        final float y = ev.getY(index);
        //获取当前x,y坐标与上一次移动时的差值,确定本次移动的距离
        final int idx = (int) (x - mLastMotionX[mActivePointerId]);
        final int idy = (int) (y - mLastMotionY[mActivePointerId]);

        //根据x,y方向的位移量拖拽view
        dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);

        //保存本次位移的信息
        saveLastMotion(ev);
    } else {
        //省略
    }
    break;
}

同样是一目了然。首先根据本次触摸事件的坐标与上次触摸事件的坐标计算位移差值,之后调用dragTo()方法移动view,最后保存信息。下面看看dragTo()的实现:

private void dragTo(int left, int top, int dx, int dy) {
    int clampedX = left;
    int clampedY = top;
    //此时view还未移动,因此获取的是当前的位置坐标
    final int oldLeft = mCapturedView.getLeft();
    final int oldTop = mCapturedView.getTop();
    //如果x方向的位移不为0
    if (dx != 0) {
        //调用callback.clampViewPositionHorizontal()方法获取处理后的left位置
        clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
        //计算位移量并进行移动
        ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
    }
    //如果y方向的位移不为0
    if (dy != 0) {
        //调用callback.clampViewPositionHorizontal()方法获取处理后的top位置
        clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
        //计算位移量并进行移动
        ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
    }

    //如果进行了位移
    if (dx != 0 || dy != 0) {
        final int clampedDx = clampedX - oldLeft;
        final int clampedDy = clampedY - oldTop;
        //调用onViewPositionChanged()回调方法
        mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
                clampedDx, clampedDy);
    }
}

这里的重点在于调用回调方法clampViewPositionHorizontal(View child, int left, int dx)与clampViewPositionVertical(View child, int top, int dy)计算实际位移。如果你在这两个方法中简单地返回left/top的话,那么你手指移到哪,被拖拽的view就会跟到哪。如果你希望view只能在一定返回内滑动,可以更改实现。下面为一个示例:

@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
    return left > 0 ? left : 0;
}

这样就保证了view的左边缘不会滑到屏幕外。如果想要一点拉力感,可以按一定比例缩小dx/dy,并计算出实际的left/top值。之后的事情就很简单了,根据最终位置计算出实际位移量,并调用ViewCompat.offsetLeftAndRight()与ViewCompat.offsetTopAndBottom()移动view,并调用onViewPositionChanged()回调方法。
最后看看ACTION_UP部分:

case MotionEvent.ACTION_CANCEL: {
    if (mDragState == STATE_DRAGGING) {
        dispatchViewReleased(0, 0);
    }
    cancel();
    break;
}

其实就是释放被捕获的view,将状态改回STATE_IDLE,并清空之前缓存的信息。dispatchViewReleased()中调用了回调方法onViewReleased()。
到这里为止,实现滑动逻辑的方法processTouchEvent()就差不多分析完了,应该还是很容易理解的。简单来讲,就是在ACTION_DOWN时捕获view,在ACTION_MOVE时拖拽view,在ACTION_UP时释放view。
读到这里,如果熟悉事件分发机制的话,应该已经会产生疑问了。上面这一套流程走下来,等于说ViewGroup处理了所有触摸事件,这在一般情况下是不可能的。如果触摸到的view能够消费掉触摸事件,那么触摸事件就不会被分发到ViewGroup的onTouchEvent()中,ViewDragHelper也就没办法对其进行处理。因此,ViewGroup需要在向子view分发触摸事件之前进行判断,并在需要时通过shouldInterceptTouchEvent()方法将其拦截。这也就是前面提到的第二种情况。下面让我们看看shouldInterceptTouchEvent()的源码,首先看一下它的返回值:

return mDragState == STATE_DRAGGING;

很简单,如果在方法调用过程中,ViewDragHelper的状态变成了STATE_DRAGGING,那么就返回true,否则返回false。由于这个方法是在ViewGroup的onInterceptTouchEvent()中作为返回值调用的,如果它返回了true,ViewGroup就会拦截下这个触摸事件,事件将交由ViewGroup的onTouchEvent()处理,否则事件将交由子view处理。
接下来看看ACTION_DOWN部分:

case MotionEvent.ACTION_DOWN: {
    //获取按下的位置坐标
    final float x = ev.getX();
    final float y = ev.getY();
    //当事件类型为ACTION_DOWN时,MotionEvent对象只会包含一个pointer的信息,因此直接通过getPointerId(0)获取当前pointer ID。
    final int pointerId = ev.getPointerId(0);
    //保存初始状态信息
    saveInitialMotion(x, y, pointerId);

    //获取该MotionEvent想要捕获的子view
    final View toCapture = findTopChildUnder((int) x, (int) y);

    //***和processTouchEvent()最大的不同点。
    if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
        tryCaptureViewForDrag(toCapture, pointerId);
    }

    //获取了一开始保存的关于被触摸的边界的信息
    final int edgesTouched = mInitialEdgesTouched[pointerId];
    //确认其中是否有需要被追踪的边
    if ((edgesTouched & mTrackingEdges) != 0) {
        //如果有的话则调用onEdgeTouched()回调方法
        mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
    }
    break;
}

可以发现,这部分和processTouchEvent()中的几乎一样,唯一不同的是以下部分:

if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
    tryCaptureViewForDrag(toCapture, pointerId);
}

和processTouchEvent()的区别在于,processTouchEvent()中没有外面那一圈if语句。由于ViewDragHelper平时处于STATE_IDLE,因此这个条件判断语句是无法通过的,里面的tryCaptureViewForDrag()也就得不到执行。之所以要这样写,是因为仅凭一个ACTION_DOWN事件,我们无法判断用户是否将要开始滑动,因此不对其进行拦截,让子view去处理。如果这里把ACTION_DOWN拦截了,那么子view所有的Touch逻辑都将失效。
接下来看看ACTION_MOVE部分:

case MotionEvent.ACTION_MOVE: {
    final int pointerCount = ev.getPointerCount();
    for (int i = 0; i < pointerCount; i++) {
        final int pointerId = ev.getPointerId(i);
        //如果pointer无效则continue
        if (!isValidPointerForActionMove(pointerId)) continue;
        //获取本次Move的信息
        final float x = ev.getX(i);
        final float y = ev.getY(i);
        final float dx = x - mInitialMotionX[pointerId];
        final float dy = y - mInitialMotionY[pointerId];
        final View toCapture = findTopChildUnder((int) x, (int) y);
        //(Step 1)判断该位置是否有子view,以及本次移动是否可以被认作滑动
        final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
        if (pastSlop) {
            //(Step 2)判断目标view有没有移动
            final int oldLeft = toCapture.getLeft();
            final int targetLeft = oldLeft + (int) dx;
            final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
                    targetLeft, (int) dx);
            final int oldTop = toCapture.getTop();
            final int targetTop = oldTop + (int) dy;
            final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
                    (int) dy);
            final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
                    toCapture);
            final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
            if ((horizontalDragRange == 0 || horizontalDragRange > 0&& newLeft == oldLeft) && (verticalDragRange == 0|| verticalDragRange > 0 && newTop == oldTop)) {
                break;
            }
        }
        //(Step 3)尝试捕获子view
        if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
            break;
        }
    }
    saveLastMotion(ev);
    break;
}

这段代码有点长,让我们分三步来看。首先看Step 1:

final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);

前半段很简单,判断在该位置有没有找到一个子view,如果没找到,自然也就没有东西可以拖拽了。重点是后半段,让我们看看这个方法的代码:

private boolean checkTouchSlop(View child, float dx, float dy) {
    //child为null则直接返回false
    if (child == null) {
        return false;
    }

    //确认能否在x,y两个方向进行拖拽
    final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
    final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;

    //判断位移量是否足够被认定为滑动
    if (checkHorizontal && checkVertical) {
        return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
    } else if (checkHorizontal) {
        return Math.abs(dx) > mTouchSlop;
    } else if (checkVertical) {
        return Math.abs(dy) > mTouchSlop;
    }
    return false;
}

这个方法的用途是判断某个触摸事件是否能被认作滑动。方法体内调用了getViewHorizontalDragRange()与getViewVerticalDragRange()两个方法。如果其中一个方法返回0,ViewDragHelper会认为view在这个方向不能够拖动。如果两个方向都不能够拖动的话,方法会直接返回false,否则将判断位移量是否大于mTouchSlop。如果位移量大于mTouchSlop,则返回true,否则返回false。
如果子view存在,并且本次触摸事件可以被认作滑动事件,那么接下来进入Step2。Step2主要是判断一下view在本次滑动事件的作用下是否会移动,代码看似很长,实际都是前面讲解过的内容,这里也就不赘述了。
接下来看Step3:

if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
    break;
}

tryCaptureViewForDrag()又出场了。它会尝试捕获该view,如果成功捕获的话,ViewDragHelper的状态会变为STATE_DRAGGING,shouldInterceptTouchEvent()就会返回true,本次滑动事件就会被拦截,交由processTouchEvent()进行处理。
最后,我们对有shouldInterceptTouchEvent()参与的滑动过程进行一下梳理:ACTION_DOWN发生时不拦截,交由子view处理;ACTION_MOVE发生时,根据滑动位移量以及子view的拖拽权限进行判断,在需要时将事件拦截,交由processTouchEvent()实现滑动;ACTION_UP发生时,如果有子view正在被拖拽,则将其释放并调用onViewReleased()回调方法。
到这里为止,ViewDragHelper的源码浅析部分基本上完成了。下面让我们看看怎么实现开头的示例程序。

应用实例

自定义的ViewGroup部分:

/**
 * Created by swt369 on 2017/8/18.
 */

public class DragGroup extends ConstraintLayout {
    private ViewDragHelper viewDragHelper;
    private TextView red;
    private TextView green;
    private TextView blue;
    private int mRedX;
    private int mRedY;
    private int mGreenWidth;
    private int mGreenY;
    private boolean once;
    public DragGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        viewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
            //所有的子view都能够拖拽
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                return true;
            }

            //不对水平位移进行限制
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                return left;
            }

            //不对垂直位移进行限制
            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                return top;
            }

            //对于红色色块与绿色色块,调用viewDragHelper的smoothSlideViewTo()方法实现回弹。注意调用smoothSlideViewTo()后需要调用postInvalidateOnAnimation()
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                if(releasedChild == red){
                    viewDragHelper.smoothSlideViewTo(releasedChild, mRedX, mRedY);
                    ViewCompat.postInvalidateOnAnimation(DragGroup.this);
                }
                if(releasedChild == green){
                    viewDragHelper.smoothSlideViewTo(releasedChild,-mGreenWidth,mGreenY);
                    ViewCompat.postInvalidateOnAnimation(DragGroup.this);
                }
            }

            //简单实现只要返回一个正值代表可拖拽即可,具体数值很少派上用场。
            @Override
            public int getViewHorizontalDragRange(View child) {
                return child.getWidth();
            }

            //简单实现只要返回一个正值代表可拖拽即可,具体数值很少派上用场。
            @Override
            public int getViewVerticalDragRange(View child) {
                return child.getHeight();
            }

            //拖拽左边界时捕获绿色方块
            @Override
            public void onEdgeTouched(int edgeFlags, int pointerId) {
                if(edgeFlags == ViewDragHelper.EDGE_LEFT){
                    viewDragHelper.captureChildView(green,0);
                }
            }
        });

        //设置左边界是可拖拽的
        viewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
    }

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

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

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        red = (TextView)getChildAt(0);
        green = (TextView)getChildAt(1);
        blue = (TextView)getChildAt(2);
    }

    //获取关于色块的大小、位置的信息,并在开始时隐藏掉绿色方块
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if(changed && !once){
            super.onLayout(changed, left, top, right, bottom);
            mRedX = red.getLeft();
            mRedY = red.getTop();
            mGreenWidth = green.getWidth();
            mGreenY = green.getTop();
            ViewCompat.offsetLeftAndRight(green,-(green.getLeft() + mGreenWidth));
            once = false;
        }
    }

    //由于smoothSlideViewTo()方法是通过Scroller实现的,需要重写computeScroll()方法手动刷帧。下面的写法几乎是通用的
    @Override
    public void computeScroll() {
        if(viewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }
}

接下来只要在Activity的layout文件中加入该ViewGroup,并在里面加入View即可。这里贴上加入的三个色块(注意这里将它们的clickable属性设置成了true,即可以消费触摸事件):

<TextView
    android:background="#ff0000"
    android:clickable="true"
    android:id="@+id/red"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:layout_marginTop="8dp"
    app:layout_constraintTop_toBottomOf="@+id/green"
    android:layout_marginLeft="8dp"
    app:layout_constraintLeft_toLeftOf="parent" />

<TextView
    android:background="#00ff00"
    android:clickable="true"
    android:id="@+id/green"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:layout_marginTop="8dp"
    app:layout_constraintTop_toBottomOf="@+id/blue"
    android:layout_marginLeft="8dp"
    app:layout_constraintLeft_toLeftOf="parent" />

<TextView
    android:background="#0000ff"
    android:clickable="true"
    android:id="@+id/blue"
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:layout_constraintTop_toTopOf="parent"
    android:layout_marginTop="8dp"
    android:layout_marginLeft="8dp"
    app:layout_constraintLeft_toLeftOf="parent" />

总结

ViewDragHelper的功能的确非常强大。但是,想要用好它的话,需要对View事件分发机制有一个较为清晰的认识,还需要了解许多常用的工具类。这里再次证明了基础和知识积累的重要性。路漫漫其修远兮,吾将上下而求索。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值