强大的ViewDragHelper和ViewDragHelper的妙用 一

来源:互联网 发布:重庆优化公司哪家好 编辑:程序博客网 时间:2024/06/05 22:50


          文如其名,本篇博文我们将详细介绍强大的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中这样写:


@Overridepublic 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);}
@Overridepublic 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框定拖动范围,让他可以移动
1 0
原创粉丝点击