使用步骤
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 | //事件拦截 |
处理回调Callback
1 | //侧滑回调 |
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 的内部移动,如下图:
上图可以看出蓝色 View 可以移动的水平区域为灰色区域,假设移动区域为 m,则 paddingleft <= m <= viewgroup.getWidth() - paddingright - view.getwidth 。可以按照如下代码编写:
1 |
|
通过重写 onViewReleased(),可以实现当手指离开屏幕之后的操作。这个方法内部是通过 scroller 类实现的,这也是之前重写 computeScroll() 方法的原因。
经过上述3个步骤,就完成了一个简单的自定义 ViewGroup,可以自由的拖动子 View。详见 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 |
|
第三个 View,我们在 onEdgeDragStarted 回调方法中,主动通过 captureChildView 对其进行捕获,该方法可以绕过 tryCaptureView,不管 tryCaptureView 返回真假。如果需要使用边界检测,需要添加上类似如下代码(以左边界为例):
1 | mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)。 |
把 TextView 全部加上 clickable = true,或者修改成 Button,意思就是子 View 可以消耗事件。再次运行,你会发现本来可以拖动的View不动了。原因是什么呢?主要是因为,如果子 View不消耗事件,那么整个手势(DOWN-MOVE-UP)都是直接进入 onTouchEvent,在 onTouchEvent 的 DOWN 的时候就确定了 captureView。如果消耗事件,那么就会先走 onInterceptTouchEvent 方法,判断是否可以捕获,而在判断的过程中会去判断另外两个回调的方法:getViewHorizontalDragRange 和 getViewVerticalDragRange,只有这两个方法返回大于0的值才能正常的捕获。所以,如果你用 Button 测试,或者给 TextView 添加了 clickable = true ,都记得重写下面这两个方法:
1 |
|
方法的返回值应当是该 childView 横向或者纵向的移动的范围,当前如果只需要一个方向移动,可以只复写一个。
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详解(侧滑栏)