自定义ViewGroup最常添加的功能就是子View的拖动,如果你的事件分发及处理的基本功非常扎实,那么完全可以自己实现这个功能。然而幸运的是,系统提供了一个工具类ViewDragHelper
,它提供了这个功能实现的框架,这样就大大提高了开发的效率。
本文不仅仅告诉你这个工具类该怎么使用,而且也会分析它的设计原理。只有掌握原理了,才能在实际中做到以不变应万变。
本文需要你对事件的分发和处理有基本的认识,如果你还没掌握,可以参考我之前写的三篇文章
如果你对事件分发和处理的流程不熟悉,你可能从本文中只学到如何使用ViewDragHelper类,但是并不会掌握它的精华。
ViewDragHelper实现事件处理
既然ViewDragHelper
是一个工具框架类,那么对事件的处理肯定也是做好了封装。假设有一个自定义ViewGroup类,名字叫做VDHLayout
。我们来看下如何使用ViewDragHelper
类实现事件的处理。
public class VDHLayout extends ViewGroup {
ViewDragHelper mViewDragHelper;
public VDHLayout(Context context) {
this(context, null);
}
public VDHLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// 创建ViewDragHelper对象,回调参数用来控制子View的拖动
mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
return false;
}
});
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
// 简单点,只操作第一个子View
View first = getChildAt(0);
first.layout(getPaddingLeft(), getPaddingTop(), getPaddingLeft() + first.getMeasuredWidth(),
getPaddingTop() + first.getMeasuredHeight());
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 利用ViewDragHelper来判断是否需要截断
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 利用ViewDragHelper来处理子View的拖拽
mViewDragHelper.processTouchEvent(event);
return true;
}
}
VDHLayout
继承自ViewGroup
,为了简单起见,只对它的第一个子View进行布局,这就是在onLayout()
中的操作。
事件处理的代码是在onInterceptTouchEvent()
和onTouchEvent()
方法中实现的,从代码中可以看到,分别用ViewDragHelper.shouldInterceptTouchEvent()
和ViewDragHelper.processTouchEvent()
来处理事件。
实现ViewDragHelper的回调
现在,我们已经成功地用ViewDragHelper
实现了事件的处理,那么子View的拖动是在哪里控制的呢?这个其实是在创建ViewDragHelper
对象的时候,用传入的回调参数控制的。从代码中可以看到,我们只实现了回调中的一个方法tryCaptureView()
,这个方法也是必须要实现的。
根据事件分发和处理的原理可知,VDHLayout
的子View是否能处理ACTION_DOWN
事件,关乎着VDHLayout
的事件分发和处理的逻辑。ViewDragHelper
的回调当然也是受这个的影响的,因此我将分两部分来讲解如何实现回调。
子View不处理事件
首先我们来看下子View不处理事件的情况。
根据View事件分发和处理的原理可知,如果一个View不设置任何监听事件,并且不可点击,也不可长按,那么这个View就不处理任何事件。
理论上讲的有点抽象,举个例子,例如 在XML布局中给VDHLayout
添加一个ImageView
控件
<?xml version="1.0" encoding="utf-8"?>
<com.bxll.vdhdemo.VDHLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@mipmap/ic_launcher_round" />
</com.bxll.vdhdemo.VDHLayout>
这个ImageView
没有任何监听事件,默认不可点击也不可长按的,因此它就是一个不处理事件的子View。
现在以这个布局为例进行分析,当手指点击ImageView
的时候,由于子View,也就是ImageView
,不处理事件,所以ACTION_DOWN
事件一定会先经过VDHLayout.onInterceptTouchEvent()
,再经过VDHLayout.onTouchEvent()
。
根据事件处理的经验,真正的处理逻辑其实都在VDHLayout.onTouchEvent()
中,它的实现如下
public boolean onTouchEvent(MotionEvent event) {
// 利用ViewDragHelper来处理子View的拖拽
mViewDragHelper.processTouchEvent(event);
return true;
}
由于
VDHLayout
要通过触摸事件控制子View拖动,因此在onTouchEvent()
中必须要返回true
。
可以看到,是用ViewDragHelper.processTouchEvent()
来实现VDHLayout.onTouchEvent()
的,现在来看看ViewDragHelper.processTouchEvent()
是如何处理ACTION_DOWN
事件的
public void processTouchEvent(@NonNull MotionEvent ev) {
// ...
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = ev.getPointerId(0);
// 1. 找到事件作用于哪个子View
final View toCapture = findTopChildUnder((int) x, (int) y);
// 保存坐标值
saveInitialMotion(x, y, pointerId);
// 2. 尝试捕获这个用于拖动的子View
tryCaptureViewForDrag(toCapture, pointerId);
// 边缘触摸回调
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
// ...
}
}
首先通过findTopChildUnder()
方法找到手指按下的那个子View
public View findTopChildUnder(int x, int y) {
final int childCount = mParentView.getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
// getOrderedChildIndex()回调决定了获取哪个子View
final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
if (x >= child.getLeft() && x < child.getRight()
&& y >= child.getTop() && y < child.getBottom()) {
return child;
}
}
return null;
}
原理很简单,就是通过x,y坐标值找到子View,然而我们可以发现,回调方法getOrderedChildIndex()
决定了到底是哪个子View被找到。从这里可以看出,手指操作的并不一定都是最上面的子View。
找到了ACTION_DOWN
作用的子View后,就通过tryCaptureViewForDrag()
来尝试捕获这个子View
boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
if (toCapture == mCapturedView && mActivePointerId == pointerId) {
// Already done!
return true;
}
// 通过回调判断这个子View是否能被捕获
if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
mActivePointerId = pointerId;
captureChildView(toCapture, pointerId);
return true;
}
return false;
}
首先通过tryCaptureView()
回调方法判断子View是否能够被捕获,被捕获的子View才能被用来拖动。
如果能够被捕获,那么就调用captureChildView()
通知子View被捕获
public void captureChildView(@NonNull View childView, int activePointerId) {
// mCapturedView代表被用来拖动的目标
mCapturedView = childView;
mActivePointerId = activePointerId;
// 回调通知View被捕获
mCallback.onViewCaptured(childView, activePointerId);
// 设置为拖动状态
setDragState(STATE_DRAGGING);
}
captureChildView()
是通过onViewCaptured()
进行回调,通知子View已经被捕获。
现在,来总结下ViewDragHelper.processTouchEvent()
对ACTION_DOWN
事件的处理中,回调做了哪些事事情(只列举主要的回调)
- 通过
getOrderedChildIndex()
回调,判断ACTION_DOWN
作用于哪个子View。 - 通过
tryCaptureView()
回调,判断子View是否能被捕获。 - 通过
onViewCaptured()
回调,通知哪个子View被捕获。
ACTION_DOWN
处理完了,现在我们来看看ACTION_MOVE
事件如何处理的。
由于子View不处理事件,ACTION_MOVE
事件交由VDHLayout.onTouchEvent()
处理,也就是交给了ViewDragHelper.processTouchEvent()
处理。
public void processTouchEvent(@NonNull MotionEvent ev) {
// ...
switch (action) {
// ...
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {
// 判断手指是否有效
if (!isValidPointerForActionMove(mActivePointerId)) break;
final int index = ev.findPointerIndex(mActivePointerId);
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]);
// 对于目标View执行拖动
dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
saveLastMotion(ev);
} else {
// ...
}
break;
}
// ...
}
}
ViewDragHelper.processTouchEvent()
对ACTION_MOVE
的处理中,首先计算在x,y轴上移动的距离差,然后通过dragTo()
方法拖动刚刚捕获的子View。
我们注意下dragTo()
第一个参数和第二个参数,它指的是目标View(被捕获的子View)理论上要移动到的坐标点。
private void dragTo(int left, int top, int dx, int dy) {
// clampedX, clampedY表示目标View要拖动到的终点坐标
int clampedX = left;
int clampedY = top;
// 获取目标View的起始坐标
final int oldLeft = mCapturedView.getLeft();
final int oldTop = mCapturedView.getTop();
if (dx != 0) {
// 如果拖动的距离大于0,通过回调获取目标View最终要拖动到的x坐标值
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
// 目标View在水平方向移动
ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
}
if (dy != 0) {
// 如果拖动的距离大于0,通过回调获取目标View最终要拖动到的x坐标值
clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
// 目标View在水平方向移动
ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
}
if (dx != 0 || dy != 0) {
// 计算实际移动的距离差
final int clampedDx = clampedX - oldLeft;
final int clampedDy = clampedY - oldTop;
// 回调通知,目标View实际移动到(clampedX, clampedY),以及x,y轴实际移动的距离差为clampedDx, clampedDy
mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
clampedDx, clampedDy);
}
}
x,y方向上,只要任意一个方向上手指拖动的距离大于0, 那么就通过clampViewPositionHorizontal()
/clampViewPositionVertical()
回调方法,计算目标View实际需要拖动到的终点坐标。
通过回调计算出来终点坐标后,就把目标View移动到这个计算出来的坐标点上。
最后,只要x,y方向上拖动距离大于0,那么就通过onViewPositionChanged()
回调方法,通知目标View实际拖动到哪个坐标,以及实际拖动的距离差。
现在我们明白了,ViewDragHelper.processTouchEvent()
处理ACTION_MOVE
,实际上就是处理目标View的拖动,它用到了如下回调
clampViewPositionHorizontal()
和clampViewPositionVertical()
回调,用来计算目标View拖动的实际坐标。onViewPositionChanged()
回调,通知目标View实际被拖动到哪个坐标,以及在x,y轴上拖动的实际距离差。
实现子View不处理事件回调
有了前面的理论基础,现在我们来实现下回调,让不处理事件的子View能够被拖动,而且只允许在水平方向上被拖动。
mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
// 为简单起见,所有的View都可以被拖动
return true;
}
/**
* 控制目标View在x方向的移动。
*/
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
// 不允许垂直方向移动
return 0;
}
/**
* 控制目标View在y方向的移动。
*/
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
// 水平方向移动不能超出父View范围
return Math.min(Math.max(0, left), getWidth() - child.getWidth());
}
});
由于我们不允许垂直方向的拖动,因此clampViewPositionHorizontal()
要返回0,clampViewPositionVertical()
的返回值要控制在VDHLayout
范围内滑动。效果如下
在前面的分析中还有其它的一些回调,可以根据实际项目要求进行复写实现。
子View处理事件
现在来分析子View能够处理事件的情况。让子View能处理事件最简单的方式是设置它可以点击,例如
<com.bxll.vdhdemo.VDHLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:clickable="true"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@mipmap/ic_launcher_round" />
</com.bxll.vdhdemo.VDHLayout>
当利用这个布局再次运行程序的时候,你会发现原来可以拖动的ImageView
不能被拖动了。这是因为事件的处理逻辑改变了,从而ViewDragHelper
的实现逻辑也改变了。
由于子View能处理事件,因此对于ACTION_DOWN
事件,就只会经过VDHLayout.onInterceptTouchEvent()
方法,而并不会经过VDHLayout.onTouchEvent()
方法。从前面的代码实现可知,VDHLayout.onInterceptTouchEvent()
是由ViewDragHelper.shouldInterceptTouchEvent()
实现。然而ViewDragHelper.shouldInterceptTouchEvent()
方法对于ACTION_DOWN
只是简单一些简单处理,并不会截断事件。
因此我们需要分析ACTION_MOVE
是如何被ViewDragHelper.shouldInterceptTouchEvent()
截断的。
public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev) {
// ...
switch (action) {
// ...
case MotionEvent.ACTION_MOVE: {
// ...
final int pointerCount = ev.getPointerCount();
// 只考虑单手指操作
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) continue;
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);
// 1. 判断是否达到拖动的标准
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
// 一个不截断的情况:如果拖动标准,却没有实际的拖动距离,那就不截断事件
if (pastSlop) {
//获取新,旧坐标值
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);
// 通过回调获取x,y方向的拖动范围
final int hDragRange = mCallback.getViewHorizontalDragRange(toCapture);
final int vDragRange = mCallback.getViewVerticalDragRange(toCapture);
// 没有实际的拖动距离就不截断事件
if ((hDragRange == 0 || (hDragRange > 0 && newLeft == oldLeft))
&& (vDragRange == 0 || (vDragRange > 0 && newTop == oldTop))) {
break;
}
}
// 报告边缘动
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag
break;
}
// 2. 如果达到拖动的临界距离,那么就尝试捕获子View
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
break;
}
}
// 如果成功捕获子View,那么状态就会被设置为STATE_DRAGGING,也就代表截断事件
return mDragState == STATE_DRAGGING;
}
ViewDragHelper.shouldInterceptTouchEvent()
考虑了多手指的情况,为了简化分析,只考虑单手指的情况。
第一步,判断是否达到拖动的条件,有两个条件
- 事件必须要作用于某个子View
checkTouchSlop()
返回true
根据事件处理的经验,如果要截断
ACTION_MOVE
事件,必须要有条件地截断。
checkTouchSlop()
方法用来判断是否达到的拖动的临界距离
private boolean checkTouchSlop(View child, float dx, float dy) {
if (child == null) {
return false;
}
// 通过回调方法判断x,y方向是否允许拖动
final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;
// 如果x或y方向允许拖动,根据拖动的距离计算是否达到拖动的临界值
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;
}
// 如果x和y方向都不允许拖动,那就永远不可能达到拖动临界值
return false;
}
首先通过getViewHorizontalDragRange()
和getViewVerticalDragRange()
获取x,y方向拖动范围,只要这个范围大于0,就代表可以在x,y方向上拖动。然后根据哪个方向可以拖动,就相应的计算拖动的距离是否达到了临界距离。
现在回到shouldInterceptTouchEvent()
方法的第二步,当达到了拖动条件后,就调用tryCaptureViewForDrag()
尝试捕获目标View,这个方法在前面已经分析过,它会首先回调tryCaptureView()
确定目标View是否能被拖动,如果能拖动,再回调onViewCaptured()
通知目标View已经捕获,最后设置状态为STATE_DRAGGING
。
当状态设置为了STATE_DRAGGING
后,那么ViewDragHelper.shouldInterceptTouchEvent()
返回值就是true
,也就是说VDHLayout.onInterceptTouchEvent()
截断了ACTION_MOVE
事件。
VDHLayout.onInterceptTouchEvent()
截断了ACTION_MOVE
事件后,后续的ACTION_MOVE
事件就交给了VDHLayout.onTouchEvent()
方法,也就是交给了ViewDragHelper.processTouchEvent()
处理。这个方法之前分析过,就是处理目标View的拖动。
那么现在我们来总结下ViewDragHelper.shouldInterceptTouchEvent()
在处理ACTION_MOVE
截断的时候,用到哪些关键回调
getViewHorizontalDragRange()
和getViewVerticalDragRange()
方法判断x,y方向上是否可以拖动。返回值大于0表示可以拖动。
实现View处理事件的回调
经过刚才的分析,我们知道,对于一个能处理事件的子View,如果想让它能被拖动,必须复写getViewHorizontalDragRange()
或getViewVerticalDragRange()
回调,用于告诉ViewDragHelper
,在相应的方向上允许被拖动。
那么现在,我们就来解决子View(能处理事件)不能拖动的问题,我们仍然只让子View在水平方向上被拖动
mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
// 为简单起见,所有的View都可以被拖动
return true;
}
/**
* 控制目标View在x方向的移动。
*/
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
// 不允许垂直方向移动
return 0;
}
/**
* 控制目标View在y方向的移动。
*/
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
// 水平方向移动不能超出父View范围
return Math.min(Math.max(0, left), getWidth() - child.getWidth());
}
@Override
public int getViewHorizontalDragRange(@NonNull View child) {
// 由于只允许目标View在VDHLayout中水平拖动,因此水平拖动范围就是VDHLayout的宽度减去目标View宽度
return getWidth() - child.getWidth();
}
@Override
public int getViewVerticalDragRange(@NonNull View child) {
// 由于不允许垂直方向拖动,因此拖动范围也就是0
return 0;
}
});
}
由于我们只允许水平方向拖动,因此getViewVerticalDragRange()
返回的垂直方向的拖动范围就是0,getViewHorizontalDragRange()
返回的水平方向的拖动范围就是getWidth() - child.getWidth()
。
边缘触摸
ViewDragHelper
有一个边缘触摸功能,这个边缘触摸的功能比较简单,因此我并不打算从源码进行分析,而只是从API角度进行说明。
要向触发边缘滑动功能,首先要调用ViewDragHelper.setEdgeTrackingEnabled(int edgeFlags)
方法,设置哪个边缘允许跟踪。参数有如下几个可用值
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;
public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;
边缘触摸的回调有如下几个
/**
* Called when one of the subscribed edges in the parent view has been touched
* by the user while no child view is currently captured.
*/
public void onEdgeTouched(int edgeFlags, int pointerId) {}
/**
* Called when the given edge may become locked. This can happen if an edge drag
* was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)}
* was called. This method should return true to lock this edge or false to leave it
* unlocked. The default behavior is to leave edges unlocked.
*/
public boolean onEdgeLock(int edgeFlags) {
return false;
}
/**
* Called when the user has started a deliberate drag away from one
* of the subscribed edges in the parent view while no child view is currently captured.
*/
public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
注释已经很清楚的解释了这几个回调的时机,我献丑来翻译下
onEdgeTouched()
: 当没有子View被捕获,并且允许边缘触摸,当用户触摸边缘时回调。onEdgeLock()
: 用来锁定锁定哪个边缘。这个回调是在onEdgeTouched()
之后,开始拖动之前调用的。onEdgeDragStarted()
: 当没有子View被捕获,并且允许边缘触摸,当用户已经开始拖动的时候回调。
系统控件DrawerLayout
就是利用ViewDragHelper
的边缘滑动功能实现的。由于篇幅原因,我就不用例子来展示边缘触摸的功能如何使用了。
ViewDragHelper实现View滑动
ViewDragHelper
还有一个View定义的功能,利用OverScroller
实现。有如下几个方法
/**
* Settle the captured view at the given (left, top) position.
* The appropriate velocity from prior motion will be taken into account.
* If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*/
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {}
/**
* Animate the view <code>child</code> to the given (left, top) position.
* If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*/
public boolean smoothSlideViewTo(@NonNull View child, int finalLeft, int finalTop) {}
/**
* Settle the captured view based on standard free-moving fling behavior.
* The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame
* to continue the motion until it returns false.
*/
public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {}
从注释可以可以看出,这个三个方法都需要在下一帧刷新的时候调用continueSettling()
,这个就与OverScroller
的用法是一致的。
现在,来利用settleCapturedViewAt()
方法实现一个功能,让拖动的View被释放后,回到原点。
当拖动的View被释放后,会回调onViewReleased()
方法
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
if (mViewDragHelper.settleCapturedViewAt(0, 0)) {
invalidate();
}
}
由于利用的是OverScroller
来实现的,因此必须调用进行重绘。重绘的时候,会调用控件的computeScroll()
方法,在这里调用刚才说讲的continueSettling()
方法
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
invalidate();
}
}
continueSettling()
也是对OverScroller
逻辑的封装,如果返回true
就代表这个定位操作还在进行中,因此还需要继续调用重绘操作。
想了解其中的原理,你一定要熟悉
OverScroller
的原理。
如此一来就可以实现如下效果
结束
很多绚丽的视图拖动操作,往往都是用ViewDragHelper
实现的,这个工具类简直是一个集大成之作,我们需要完全掌握它,这样我们才能游刃有余地在自定义ViewGroup中实现各种牛逼的View拖动效果。