强大的ViewDragHelper和ViewDragHelper的妙用 一


          文如其名,本篇博文我们将详细介绍强大的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框定拖动范围,让他可以移动

你可能感兴趣的:(ViewDragHelper,android开发)