文如其名,本篇博文我们将详细介绍强大的ViewDragHelper,但是这次我们将他们分开,本篇我们将完全解析 ViewDragHelper,下一篇我们我们将系统的说明ViewDragHelper的妙用
一般情况下,当我们希望我们的UI动起来(变得灵活的)的时候我们一般会首先想到在onInterceptTouchEvent 和OnTouchEvent做出配合处理,这样的话,我们就可以灵活的控制我们的UI,做出拖拽效果等等,当然onInterceptTouchEvent 和OnTouchEvent做出来的效果好不好呢,答案是肯定的,所有的逻辑都按照你的设定去走,那么便不回有什么大的偏差,但是如果你想偷懒的话呢,不要着急神奇的ViewDragHelper就应用而生了。举个最基本的例子Android中的SlidingPaneLayout和DrawerLayout就是用的ViewDragHelper来实现的。所以说ViewDragHelper是便捷且强大的。
那么到底什么是ViewDragHelper,我们来一起看看ViewDragHelper的神奇和强大。
我觉得我们首先需要明确一个概念ViewDragHelper虽然神奇和强大,但是如果你希望你的UI灵动起来,从根本上来讲都需要onInterceptTouchEvent 和OnTouchEvent来处理,那么既然躲不掉,那么为什么我们还要用ViewDragHelper呢,理由很简单Google用ViewDragHelper 封装了对onInterceptTouchEvent 和OnTouchEvent的处理,也就是说Google已经替我们写好了逻辑,我们只需要设定我们的UI动起来的轨迹就好了
我们先来看一下 Google对于ViewDragHelper 的定义
ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number of useful operations and state tracking for allowing a user to drag and reposition views within their parent ViewGroup.
什么意思呢,很明显如果你自定义一个ViewGroup(用来实现灵活的UI)的话,ViewDragHelper 将会是一个非常有用的内部对象,他提供了一系列操作来让你拖拽和重定位 你的 UI 子View。
根据目前我们所知道,我们知道ViewDragHelper 将会是自定义VieGroup的内部对象,并且他封装了对onInterceptTouchEvent 和OnTouchEvent的处理。
那么,我们应该怎么使用它呢。
如果你熟悉Android Gesture,我们这里可以回想一下Gesture的用法,其实ViewDragHelper和Gesture一样,都是对于我们onTouchEvent的封装 比如如果我们希望我们的Gesture来处理我们的逻辑,我们一般会这么写
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
return mGestureDetector.onTouchEvent(event);
}
这样一来,会不会稍微好理解一下
ViewDragHelper的设计原理呢,其实在Android中,我们所有的屏幕交互事件都只有MotionEvent一种(按键的逻辑除外),包括OnClickListener也只是onTouchEvenet 的处理结果接口,如果对于这一块不是很理解的话,可以返回去读一读我的其他博文。这里不赘述。
那么到了这里,我们就知道如果我们希望使用到ViewDragHelper,那么我们首先第一步需要把MotionEvent处理逻辑交给我们的ViewDragHelper。例如我们需要在自定义的ViewGroup中这样写:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mDragHelper.cancel();
return false;
}
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
mDragHelper.processTouchEvent(ev);
return true;
}
好,现在我们走完了第一步,我们把我们的MotionEvent交给我们的
ViewDragHelper,ViewDragHelper帮我们处理了所有的逻辑,那么现在问题来了,ViewDragHelper怎么知道我们希望怎么移动或者定位VIew呢,依照Android的尿性或者对照一下Gesture的实现,我们知道必定会有一个回调接口来处理我们的移动逻辑。
果不其然,我们在源码中发现
public static abstract class Callback {
public void onViewDragStateChanged(int state) {}
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}
public void onViewCaptured(View capturedChild, int activePointerId) {}
public void onViewReleased(View releasedChild, float xvel, float yvel) {}
public void onEdgeTouched(int edgeFlags, int pointerId) {}
public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
public int getOrderedChildIndex(int index) {
return index;
}
public int getViewHorizontalDragRange(View child) {
return 0;
}
public int getViewVerticalDragRange(View child) {
return 0;
}
public abstract boolean tryCaptureView(View child, int pointerId);
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
}
很明显,这一系列回调函数可以很好的完成我们所需有的拖拽逻辑。关于每一个函数的具体含义稍后再解释。
现在,我们继续我们的逻辑,完事具备,唯一差的就是我们的ViewDragHelper对象了,那么我们应该创建我们的
ViewDragHelper对象呢,Android 提供一种工厂模式来产生我们的ViewDragHelper对象,比如我们可以
mDragHelper = ViewDragHelper.create(this, mDragHelperCallback);
或者
mDragHelper = ViewDragHelper.create(this, 1.0f,mDragHelperCallBack);
其实都一样,创建对象所需要的3个参数分别为ViewGroup、sensitivity、Callback。第一个和第三个没什么说的
sensitivity用我的理解是用来设置触摸Move灵敏度的。我们在源码中可以看到
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
那么什么叫mTouchSlop呢Google给出的解释是
Distance in pixels a touch can wander before we think the user is scrolling
我的理解是我们认为ACTION_MOVE的像素距离,貌似默认值为7
这里就很明显我们的 sensitivity越大,mTouchSlop就越小,那么我们的灵敏度就会更高。
经过了这三步之后,很明显我们已经设好了ViewDragHelper,接下来就需要在CallBack回调去规划了。
这里 我首先来介绍最后三个函数也是最常用的三个函数
☞ boolean tryCaptureView(View child, int pointerId); 这个函数从返回值我们都可以看出来,他是用来判断我们的哪一个Child可以用来做拖拽处理,简而言之,我们的ViewGroup有多个ChildView,是否每一个都可以拖动呢,很显然,要想动,先过tryCaptureView这一关。
public boolean tryCaptureView(View child, int pointerId) {
return true; //所有的子元素都可以移动
}
public boolean tryCaptureView(View child, int pointerId) {
return child1 == child || child2 ==child;//制定子元素
}
☞ public int clampViewPositionHorizontal(View child, int left, int dx) ;从函数名我们也可以知道他框定的是我们的Child水平方向上移动的位置,我们细看一下光方给出的注释
/**
* Restrict the motion of the dragged child view along the horizontal axis.
* The default implementation does not allow horizontal motion; the extending
* class must override this method and provide the desired clamping.
*
*
* @param child Child view being dragged
* @param left Attempted motion along the X axis
* @param dx Proposed change in position for left
* @return The new clamped position for left
*/
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}
那么很明显,我们需要重载他来得到我们移动的距离,这里我就不一一翻译了,我们重点关注param left,官方给的说明是尝试在X轴移动的位置,怎么来理解呢,比如当你拖动一个View,你拖动到的位置就是我们的left,这样是不是就很好理解了呢,所以呢我们可以这样调用
@Override
public int clampViewPositionHorizontal(View child, int left, int dx)
{
final int leftBound = getPaddingLeft();
final int rightBound = getWidth() - mChildView.getWidth() - leftBound;
final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft; //限定在ViewGroup内部移动
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx)
{
return left; //随着你的拖动移动,没有限制
}
☞public int clampViewPositionVertical(View child, int top, int dy) 同理与clampViewPositionHorizontal
好了,有了这三个方法,对于简单的拖动处理应该就不是问题了。
关于其他函数的使用 请阅读:
Android ViewDragHelper完全解析 自定义ViewGroup神器
这里,我们继续,在使用的过程中,我们发现我们的ChildView一旦可以消费到我们的MotionEvent(OnTouch/onClick/Clickable等)时,我们的ViewDragHelper 将对该ChildView无效,为什么呢。
这里,我们还是用源码来说话
/**
* Check if this event as provided to the parent view's onInterceptTouchEvent should
* cause the parent to intercept the touch event stream.
*
* @param ev MotionEvent provided to onInterceptTouchEvent
* @return true if the parent view should return true from onInterceptTouchEvent
*/
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
final int actionIndex = MotionEventCompat.getActionIndex(ev);
if (action == MotionEvent.ACTION_DOWN) {
// Reset things for a new event stream, just in case we didn't get
// the whole previous stream.
cancel();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = MotionEventCompat.getPointerId(ev, 0);
saveInitialMotion(x, y, pointerId);
final View toCapture = findTopChildUnder((int) x, (int) y);
// Catch a settling view if possible.
if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
tryCaptureViewForDrag(toCapture, pointerId);
}
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
final float x = MotionEventCompat.getX(ev, actionIndex);
final float y = MotionEventCompat.getY(ev, actionIndex);
saveInitialMotion(x, y, pointerId);
// A ViewDragHelper can only manipulate one view at a time.
if (mDragState == STATE_IDLE) {
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
} else if (mDragState == STATE_SETTLING) {
// Catch a settling view if possible.
final View toCapture = findTopChildUnder((int) x, (int) y);
if (toCapture == mCapturedView) {
tryCaptureViewForDrag(toCapture, pointerId);
}
}
break;
}
case MotionEvent.ACTION_MOVE: {
// First to cross a touch slop over a draggable view wins. Also report edge drags.
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag
break;
}
final View toCapture = findTopChildUnder((int) x, (int) y);
if (toCapture != null && checkTouchSlop(toCapture, dx, dy) &&
tryCaptureViewForDrag(toCapture, pointerId)) {
break; //这里的条件判断很重要,要注意
}
}
saveLastMotion(ev);
break;
}
case MotionEventCompat.ACTION_POINTER_UP: {
final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
clearMotionHistory(pointerId);
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
cancel();
break;
}
}
return mDragState == STATE_DRAGGING; //很明显true,我们才能走到procrssTouchEvent
}
/**
* Process a touch event received by the parent view. This method will dispatch callback events
* as needed before returning. The parent view's onTouchEvent implementation should call this.
*
* @param ev The touch event received by the parent view
*/
public void processTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
final int actionIndex = MotionEventCompat.getActionIndex(ev);
if (action == MotionEvent.ACTION_DOWN) {
// Reset things for a new event stream, just in case we didn't get
// the whole previous stream.
cancel();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = MotionEventCompat.getPointerId(ev, 0);
final View toCapture = findTopChildUnder((int) x, (int) y);
saveInitialMotion(x, y, pointerId);
// Since the parent is already directly processing this touch event,
// there is no reason to delay for a slop before dragging.
// Start immediately if possible.
tryCaptureViewForDrag(toCapture, pointerId);
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
final float x = MotionEventCompat.getX(ev, actionIndex);
final float y = MotionEventCompat.getY(ev, actionIndex);
saveInitialMotion(x, y, pointerId);
// A ViewDragHelper can only manipulate one view at a time.
if (mDragState == STATE_IDLE) {
// If we're idle we can do anything! Treat it like a normal down event.
final View toCapture = findTopChildUnder((int) x, (int) y);
tryCaptureViewForDrag(toCapture, pointerId);
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
} else if (isCapturedViewUnder((int) x, (int) y)) {
// We're still tracking a captured view. If the same view is under this
// point, we'll swap to controlling it with this pointer instead.
// (This will still work if we're "catching" a settling view.)
tryCaptureViewForDrag(mCapturedView, pointerId);
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {
final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, index);
final float y = MotionEventCompat.getY(ev, index);
final int idx = (int) (x - mLastMotionX[mActivePointerId]);
final int idy = (int) (y - mLastMotionY[mActivePointerId]);
dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy); // 这里,请注意我们的拖动逻辑最终在这里完成
saveLastMotion(ev);
} else {
// Check to see if any pointer is now over a draggable view.
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag.
break;
}
final View toCapture = findTopChildUnder((int) x, (int) y);
if (checkTouchSlop(toCapture, dx, dy) &&
tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
}
break;
}
case MotionEventCompat.ACTION_POINTER_UP: {
final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
// Try to find another pointer that's still holding on to the captured view.
int newActivePointer = INVALID_POINTER;
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int id = MotionEventCompat.getPointerId(ev, i);
if (id == mActivePointerId) {
// This one's going away, skip.
continue;
}
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
if (findTopChildUnder((int) x, (int) y) == mCapturedView &&
tryCaptureViewForDrag(mCapturedView, id)) {
newActivePointer = mActivePointerId;
break;
}
}
if (newActivePointer == INVALID_POINTER) {
// We didn't find another pointer still touching the view, release it.
releaseViewForPointerUp();
}
}
clearMotionHistory(pointerId);
break;
}
case MotionEvent.ACTION_UP: {
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();
}
cancel();
break;
}
case MotionEvent.ACTION_CANCEL: {
if (mDragState == STATE_DRAGGING) {
dispatchViewReleased(0, 0);
}
cancel();
break;
}
}
}
如果你读到了这里,并且还有兴趣继续读下去,首先你需要了解MotionEvent的处理机制。如果说你对
onInterceptTouchEvent 和onTouchEvent还不够了解的话,可以阅读
onInterceptTouchEvent 与 onTouchEvent 分析与MotionEvent在ViewGroup与View中的分发
那么,我们继续往下走。
根据前面的分析,现在我们假定我们的ChildView 是可以消费MotionEvent的,那么,依据我MotionEvent处理博文中的说明,我们首先会在shouldInterceptTouchEvent处理ActionDown 很明显这里返回的false,那么我们的ChildView便得到了ActionDown。
得到ActionDown 是毋庸置疑的,这里我们分两种情况来考虑,
一、我们只是点击了我们的ChildView并没有移动
很显然我们在shouldInterceptTouchEvent 得到ActionMove并走过
if (toCapture != null && checkTouchSlop(toCapture, dx, dy) &&
这里有一个函数很重要checkTouchSlop,现在从字面上来看就是判断是否是移动,稍后我会解释这个函数,现在我们先默认没有移动(我们这里是点击事件),那么同理是返回了了false让我们的ChildView得到了ActionMove,然后就顺理成章的ActionUp,完成点击事件了。
二、如果我们移动我们的ChildView,同理我们走到了
if (toCapture != null && checkTouchSlop(toCapture, dx, dy) &&
这里我们来看
private boolean checkTouchSlop(View child, float dx, float dy) {
if (child == null) {
return false;
}
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;
}
首先我们知道
public int getViewHorizontalDragRange(View child) {
return 0;
}
public int getViewVerticalDragRange(View child) {
return 0;
}
那么,很明显我们的ActionMove还是会返回false,我们的ChildView依然不会被移动,所以说这里我们鞋网一个具有Clickable属性的ChildView被移动,我们需要重写getViewVerticalDragRange这两个函数k框定拖动范围,让他可以移动