自定义ViewGroup最常添加的功能就是子View的拖动,若是你的事件分发及处理的基本功很是扎实,那么彻底能够本身实现这个功能。然而幸运的是,系统提供了一个工具类ViewDragHelper
,它提供了这个功能实现的框架,这样就大大提升了开发的效率。php
本文不只仅告诉你这个工具类该怎么使用,并且也会分析它的设计原理。只有掌握原理了,才能在实际中作到以不变应万变。java
本文须要你对事件的分发和处理有基本的认识,若是你还没掌握,能够参考我以前写的三篇文章android
若是你对事件分发和处理的流程不熟悉,你可能从本文中只学到如何使用ViewDragHelper类,可是并不会掌握它的精华。app
既然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()
中的操做。ide
事件处理的代码是在onInterceptTouchEvent()
和onTouchEvent()
方法中实现的,从代码中能够看到,分别用ViewDragHelper.shouldInterceptTouchEvent()
和ViewDragHelper.processTouchEvent()
来处理事件。工具
如今,咱们已经成功地用ViewDragHelper
实现了事件的处理,那么子View的拖动是在哪里控制的呢?这个实际上是在建立ViewDragHelper
对象的时候,用传入的回调参数控制的。从代码中能够看到,咱们只实现了回调中的一个方法tryCaptureView()
,这个方法也是必需要实现的。源码分析
根据事件分发和处理的原理可知,VDHLayout
的子View是否能处理ACTION_DOWN
事件,关乎着VDHLayout
的事件分发和处理的逻辑。ViewDragHelper
的回调固然也是受这个的影响的,所以我将分两部分来说解如何实现回调。布局
首先咱们来看下子View不处理事件的状况。post
根据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可以被拖动,并且只容许在水平方向上被拖动。
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能处理事件最简单的方式是设置它能够点击,例如
<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()
考虑了多手指的状况,为了简化分析,只考虑单手指的状况。
第一步,判断是否达到拖动的条件,有两个条件
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,若是想让它能被拖动,必须复写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定义的功能,利用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拖动效果。