ViewDragHelper 详解

使用步骤

       ViewDragHelper的使用,分为以下几个步骤:

初始化ViewDragHelper

       ViewDragHelper 通常定义在一个 ViewGroup 中,通过其静态方法初始化:

1
mViewDragHelper = ViewDragHelper.create(this,callback);

       它的第一个参数是要监听的 View,第二个参数是一个 Callback 回调,这个回调是整个业务的核心。

       另外,ViewDragHelper 还有一个三个参数的创建方法:

1
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb);

       例如:

1
mViewDragHelper = ViewDragHelper.create(this, 1.0f, callback);

       它的不同是多了一个灵敏度(第二个参数)。第二个参数 sensitivity,主要用于设置 touchSlop(helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity))),可见传入越大,mTouchSlop的值就会越小,越敏感。这个灵敏度的默认值是1.0f,一般设置为1.0f 就行,且参数越大越敏感。

触摸相关方法

1
2
3
4
5
6
7
8
9
10
11
12
13
//事件拦截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}

//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递给ViewDragHelper
mViewDragHelper.processTouchEvent(event);
return true;
}

处理回调Callback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//侧滑回调
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
//何时开始触摸
@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;
}
};

       tryCaptureView() 方法是 IDE 自动帮助重写的,返回 ture 则表示可以捕获该 view,可以根据传入的第一个 view 参数,指定在创建 ViewDragHelper 时,哪一个子 View 可以被移动。

       clampViewPositionHorizontal() 与 clampViewPositionVertical() 的默认值返回0,即不发生滑动。如果只重写其中的一个,那么就只会在该方向上滑动。clampViewPositionVertical(View child, int top, int dy) 中的参数 top,代表在垂直方向上 child 移动的距离,而 dy 则表示比较前一次的增量。clampViewPositionVertical() 同理。通常只需要返回 top 和 left 即可,但当需要更加精确地计算 padding 等属性时,就需要针对它们进行一些处理,返回大小合适的值。

       可以在这两个方法中对 child 移动的边界进行控制,left , top 分别为即将移动到的位置,比如横向的情况下,我希望只在 ViewGroup 的内部移动,如下图:

ViewDragHelper的边界处理

       上图可以看出蓝色 View 可以移动的水平区域为灰色区域,假设移动区域为 m,则 paddingleft <= m <= viewgroup.getWidth() - paddingright - view.getwidth 。可以按照如下代码编写:

1
2
3
4
5
6
7
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
final int leftBound = getPaddingLeft();
final int rightBound = getWidth() - mDragView.getWidth() - leftBound;
final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}

       通过重写 onViewReleased(),可以实现当手指离开屏幕之后的操作。这个方法内部是通过 scroller 类实现的,这也是之前重写 computeScroll() 方法的原因。

       经过上述3个步骤,就完成了一个简单的自定义 ViewGroup,可以自由的拖动子 View。详见 ViewDragHelper 侧滑菜单简单实例

详细功能展示

ViewDragHelper效果图

  • 第一个View,就是演示简单的移动。
  • 第二个View,演示除了移动后,松手自动返回到原本的位置。(注意你拖动的越快,返回的越快)
  • 第三个View,边界移动时对View进行捕获。

       第二个 View,在 onLayout 之后保存了最开始的位置信息,最主要还是重写了 Callback 中的 onViewReleased,我们在 onViewReleased 中判断如果是 mAutoBackView 则调用 settleCapturedViewAt 回到初始的位置。源码中可以看到紧随其后的代码是 invalidate(); 因为其内部使用的是 mScroller.startScroll,所以别忘了需要 invalidate() 以及结合 computeScroll 方法一起。

       在 ViewDragHelper 的滑动中共有三个方法可以调用,smoothSlideViewTo、settleCapturedViewAt、flingCapturedView,动画移动会回调 continueSettling(boolean) 方法,在内部是用的 ScrollerCompat 来实现滑动的。 在 computeScroll 方法中判断 continueSettling(boolean) 的返回值,来动态刷新界面:通常情况下,可以使用如下模版代码:

1
2
3
4
5
6
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
invalidate();//或者 ViewCompat.postInvalidateOnAnimation(this); }
}
}

       第三个 View,我们在 onEdgeDragStarted 回调方法中,主动通过 captureChildView 对其进行捕获,该方法可以绕过 tryCaptureView,不管 tryCaptureView 返回真假。如果需要使用边界检测,需要添加上类似如下代码(以左边界为例):

1
mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)。

       详见 VDHLayoutForTextView

       把 TextView 全部加上 clickable = true,或者修改成 Button,意思就是子 View 可以消耗事件。再次运行,你会发现本来可以拖动的View不动了。原因是什么呢?主要是因为,如果子 View不消耗事件,那么整个手势(DOWN-MOVE-UP)都是直接进入 onTouchEvent,在 onTouchEvent 的 DOWN 的时候就确定了 captureView。如果消耗事件,那么就会先走 onInterceptTouchEvent 方法,判断是否可以捕获,而在判断的过程中会去判断另外两个回调的方法:getViewHorizontalDragRange 和 getViewVerticalDragRange,只有这两个方法返回大于0的值才能正常的捕获。所以,如果你用 Button 测试,或者给 TextView 添加了 clickable = true ,都记得重写下面这两个方法:

1
2
3
4
5
6
7
8
9
@Override
public int getViewHorizontalDragRange(View child) {
return getMeasuredWidth() - child.getMeasuredWidth();
}

@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight() - child.getMeasuredHeight();
}

       方法的返回值应当是该 childView 横向或者纵向的移动的范围,当前如果只需要一个方向移动,可以只复写一个。

       详见 VDHLayoutForButton

ViewDragHelper 的更多方法

所有的Callback方法

  • public void onViewDragStateChanged(int state)

       当 ViewDragHelper 状态发生变化时回调(拖拽状态改变)(STATE_IDLE,STATE_DRAGGING,STATE_SETTLING[自动滚动时])。

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

       被拖拽的 View 位置变化时回调,changedView 为位置变化的 view,left、top 变化后的 x、y 坐标,dx、dy 为新位置与旧位置的偏移量,常用于滑动时更改 scale 等。

  • public void onViewCaptured(View capturedChild, int activePointerId)

       成功捕获到子 View 时或者手动调用 captureChildView() 时回调(触摸到View后回调,可以做一些初始化操作)。

  • public void onViewReleased(View releasedChild, float xvel, float yvel)

       当前拖拽的 view 松手或者 ACTION_CANCEL 时调用,xvel、yvel 为离开屏幕时的速率。

  • public void onEdgeTouched(int edgeFlags, int pointerId)

       当触摸到边界时回调。

  • public boolean onEdgeLock(int edgeFlags)

       true 的时候会锁住当前的边界,false 则 unLock。锁定后的边缘就不会回调 onEdgeDragStarted()。

  • public void onEdgeDragStarted(int edgeFlags, int pointerId)

       ACTION_MOVE 且没有锁定边缘时触发,在此可手动调用 captureChildView() 触发从边缘拖动子 View。

  • public int getOrderedChildIndex(int index)

       寻找当前触摸点 View 时回调此方法,如需改变遍历子 view 顺序可重写此方法;改变同一个坐标(x,y)去寻找 captureView 位置的方法。(具体在:findTopChildUnder方法中)。

  • public int getViewHorizontalDragRange(View child)

       返回拖拽子 View 在水平方向上可以被拖动的最远距离,默认为0。

  • public int getViewVerticalDragRange(View child)

       返回拖拽子 View 在垂直方向上可以被拖动的最远距离,默认为0。

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

       对触摸 view 判断,如果需要当前触摸的子 View 进行拖拽移动就返回 true,否则返回 false。

  • public int clampViewPositionHorizontal(View child, int left, int dx)

       拖拽的子 View 在所属方向上移动的位置,child 为拖拽的子 View,left 为子 view 应该到达的x坐标,dx 为挪动差值。

  • public int clampViewPositionVertical(View child, int top, int dy)

       同上,top 为子 view 应该到达的 y 坐标。

方法的大致的回调顺序

  • shouldInterceptTouchEvent

       DOWN:

       getOrderedChildIndex(findTopChildUnder)
       -> onEdgeTouched

       MOVE:

       getOrderedChildIndex(findTopChildUnder)
       ->getViewHorizontalDragRange & getViewVerticalDragRange(checkTouchSlop)(MOVE中可能不止一次)
       ->clampViewPositionHorizontal & clampViewPositionVertical
       ->onEdgeDragStarted
       ->tryCaptureView
       ->onViewCaptured
       ->onViewDragStateChanged

  • processTouchEvent:

       DOWN:

       getOrderedChildIndex(findTopChildUnder)
       ->tryCaptureView
       ->onViewCaptured
       ->onViewDragStateChanged
       ->onEdgeTouched

       MOVE:

       ->STATE==DRAGGING:dragTo

       ->STATE!=DRAGGING:

       onEdgeDragStarted
       ->getOrderedChildIndex(findTopChildUnder)
       ->getViewHorizontalDragRange & getViewVerticalDragRange(checkTouchSlop)
       ->tryCaptureView
       ->onViewCaptured
       ->onViewDragStateChanged

       从上面也可以解释,我们在之前 TextView(clickable=false) 的情况下,没有编写 getViewHorizontalDragRange 方法时,是可以移动的。因为直接进入 processTouchEvent 的 DOWN,然后就 onViewCaptured、onViewDragStateChanged(进入DRAGGING状态),接下来 MOVE 就直接 dragTo 了。而当子 View 消耗事件的时候,就需要走 shouldInterceptTouchEvent,MOVE 的时候经过一系列的判断(getViewHorizontalDragRange,clampViewPositionVertical等),才能够去 tryCaptureView。

ViewDragHelper的其它方法

  • public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb)

       初始化 ViewDragHelper 中已经介绍,ViewDragHelper 的实例是通过静态工厂方法创建的,sensitivity 越大,对滑动的检测就越敏感,默认传1即可。

  • public void setEdgeTrackingEnabled(int edgeFlags)

       设置允许父 View 的某个边缘可以用来响应托拽事件,edgeFlags 参数是枚举类型,可以从左边,上边,右边,下边拖动。如果想实现左右拖动设置如下:

1
mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT | ViewDragHelper.EDGE_RIGHT);
  • public void setMinVelocity(float minVel)

       setMinVelocity(最小拖动速度)。

  • public boolean shouldInterceptTouchEvent(MotionEvent ev)

       在父 view onInterceptTouchEvent 方法中调用。

  • public void processTouchEvent(MotionEvent ev)

       在父 view onTouchEvent 方法中调用。

  • public void captureChildView(View childView, int activePointerId)

       在父 View 内捕获指定的子 view 用于拖曳,会回调 tryCaptureView()。

  • public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop)

       某个 View 自动滚动到指定的位置,初速度为0,可在任何地方调用,动画移动会回调 continueSettling(boolean) 方法,直到结束。

  • public boolean settleCapturedViewAt(int finalLeft, int finalTop)

       以松手前的滑动速度为初值,让捕获到的子View自动滚动到指定位置,只能在 Callback 的 onViewReleased() 中使用,其余同上。

  • public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)

       以松手前的滑动速度为初值,让捕获到的子 View 在指定范围内 fling 惯性运动,只能在 Callback 的 onViewReleased() 中使用,其余同上。

  • public boolean continueSettling(boolean deferCallbacks)

       在调用 settleCapturedViewAt()、flingCapturedView()和 smoothSlideViewTo( )时,该方法返回 true,一般重写父 view 的 computeScroll 方法,进行该方法判断。

  • public void abort()

       中断动画。

       ViewDragHelper编写DrawerLayout详见 LeftDrawerLayout

参考资料:
《Android 群英传》徐宜生 第5章 Android Scroll 分析
鸿洋_ Android ViewDragHelper完全解析 自定义ViewGroup神器
鸿洋_ ViewDragHelper实战 自己打造Drawerlayout
zhangke3016 自定义控件辅助神器ViewDragHelper
u012551350 ViewDragHelper详解(侧滑栏)

Fork me on GitHub