Android ViewDragHelper源码笔记

  1. ViewDragHelper, Google官方的View基础触摸位移实现类,解放了很多动作苦手,具有很大的参考价值, 官方的DrawerLayout就直接使用了ViewDragHelper来作为自己的触摸处理机制。

  2. ViewDragHelper在设计上的意义在于:将常规的对于触摸以及位移的处理单独抽出来封装为了一个类,实现了V和C的进一步分离, 其实我在之前的开发中也有类似的抽离操作,但是当然做不到官方的这么全面以及规范。

  3. 3个状态:

    • IDLE: 当前没有交互,也没有自发的位移动画.
    • DRAGGING: 当前用户正在进行有效的交互
    • SETTLING: 没有用户交互,但是有自发的位移动画(比如用户离开以后的自动恢复原位)
  4. 内部定义的抽象类Callback基本涵盖了所有类型的位移相关的回调:

    • onViewDragStateChanged(…)
    • onViewPositionChanged(…)
    • onViewCaptured(…)
    • onViewReleased(…)
    • onEdgeTouched(…) 在parentView的Edge被Touch,而View本身没有被captured的情况下会被调用.
    • onEdgeLock(…)
    • onEdgeDragStarted(…) 在用户开始drag parentView的某个edge并且当前没有child view被capture的情况下会被回调.
    • getOrderedChildIndex(…): 这个函数在find child view的过程中被使用,给予外部调用者改变 child view遍历次序的机会,默认的是从大到小.
    • getViewHorizontalDragRange(…) 返回一个可以被Drag的childView在水平层面的可移动量,像素为单位,如果view不能水平移动,那么返回0.
    • getViewVerticalDragRange(…) 同上,只是换成了垂直的.
    • tryCaptureView(…) 在用户试图去drag某个childView时会被调用,返回true代表用户可以drag, 会有一个pointerId参数,这个函数甚至在一个View已经被capture的情况下也还可能被调到,这表明一个新的pointer正在尝试获取对该View的control
    • clampViewPositionHorizontal(…), 将某个View在水平方向的位移量限制在某个范围, 缺省实现返回0, 代表着不允许水平方向的移动.
    • clampViewPositionVertical(…), 同上,换成了垂直方向。
  5. 采用了Factory模式,create(ViewGroup forParent, Callback cb)返回一个可用的ViewDragHelper, 另外一个变体可以接受float sensitivity, 代表着对开始drag这个操作侦测的敏感度, 1.0是正常值, helper的mTouchSlop会根据sensitivity调整: (int) (helper.mTouchSlop * (1 / sensitivity)), 即敏感度越高,touchSlop越小., 接受一个callback对象用于外部调用者感知一系列的drag相关事件.

  6. ViewDragHelper在构造时,会根据传入的context获得相应的ViewConfiguration对象: ViewConfiguration.get(context), 以及当前Display的density: context.getResources().getDisplayMetrics().density, mEdgeSize会根据此从dp换算为像素, mTouchSlop = vc.getScaledTouchSlop()(如果指定了sensitivity,那么还会进一步处理), mMaxVelocity = vc.getScaledMaximumFlingVelocity(), mMinVelocity = vc.getScaledMinimumFlingVelocity()直接使用系统默认的velocity处理器, mScroller = ScrollerCompat.create(context, sInterpolator), scroller使用的Interpolator的getInterpolation(float t)函数是t -= 1.0f; return t * t * t * t * t + 1.0f;

  7. ViewDragHelper作为一个状态机,本身是需要外部输入的(即使用者主动的调用某些函数或者将当前的MotionEvent传递给ViewDragHelper)

    • 对于MotionEvent的输入,和View本身的onInterceptTouchEvent和onTouchEvent相对应的也有两个接受MotionEvent输入的函数:
      1. shouldInterceptTouchEvent(…)
      2. processTouchEvent(…)
  8. shouldInterceptTouchEvent(…):

    1. 首先要获取MotionEvent的action和actionIndex, 这里都使用了MotionEventCompat来保证兼容性
    2. 如果action == ACTION_DOWN, 那么会调用cancel(), 因为一个action_down代表着一轮新的touch事务的开始,因此会调用一次cancel()来将之前的一轮cancel掉
    3. mVelocityTracker = VelocityTracker.obtain(), 根据需要来obtain一个velocityTracker, 并将当前的MotionEvent加入到VelocityTracker中以进行速度分析, mVelocityTracker.addMovement(ev)
    4. 根据action的Type进行判断:
      • ACTION_DOWN:
        1. 获取event的X/Y 以及pointerId(MotionEventCompat.getPointerId(ev, 0), 0: the first pointer that is down), 调用saveInitialMotion将这些初始信息保存下来,mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x, mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y, mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y), 这一步是检测该Pointer是否或在Parent的某个Edge中, 被touch到的Edge的index会保存在mInitialEdgesTouched[pointerId]中, 有上面可见ViewDragHelper细化到多点触摸,每个pointer都会自己的独立缓存
        2. 调用findTopChildUnder((int) x, (int) y)来寻找当前的down点的位置是不是落在了某个childView上,遍历child的顺序是逆序,即最上的(Top), 如果找不到就返回null, 找到的View保存在toCapture.
        3. 如果toCapture == mCapturedView(注意这个代表的应该是上一轮的Touch事务中被capture到的childView)并且mDragState == STATE_SETTLING, 那么说明现在是down到了一个正在settling,即回归原位的View, 会调用tryCaptureViewForDrag(toCapture, pointerId);
        4. 如果down到了一个Edge上并且在mTrackingEdges(这个变量代表的是当前有哪些Edge可以drag)中,那么会调用mCallback的onEdgeTouched(edgesTouched & mTrackingEdges(这里做了Tracking过滤), pointerId).
      • ACTION_POINTER_DOWN:
        • 首先ACTION_POINTER_DOWN代表之前已经down了一个pointer(第一个down的pointer被成为primary pointer,也只有这个才会触发ACTION_DOWN),现在有另外的pointer down了下来,即多点触摸的情况出现了,可以用getActionIndex获得这个pointer的index. 同样获取x/y
        • 因为这个本质上也是down,所以也会调用saveInitialMotion(x, y, pointerId)来保存,只不过就是pointerId和ACTION_DOWN时的不一样.
        • 因为ViewDragHelper**一次只能操作一个View**,因此这里如果mDragState是STATE_DRAGGING的话,那么会直接跳过,但是如果是STATE_IDLE/STATE_SETTLING, 那么代表之前的down都没有capture到一个View(否则的话状态应该是STATE_DRAGING)
        • 如果当前状态是STATE_IDLE: 那么不会进行view capture检测,但是仍会进行EdgeTouch的检测和回调saveInitialMotion(…)就做了检测了
        • 如果当前状态是STATE_SETTLING: 会check是否down到了一个settling的View,如果是,那么会tryCaptureViewForDrag(toCapture, pointerId)
      • ACTION_MOVE:
        1. 因为涉及到多点触摸,因此会先调用getPointerCount(…)来的到本次的MotionEvent包含了多少个Pointer的信息。
        2. 对每个Pointer的信息都进行这样的处理, 提取出pointerId和x/y, 和之前的mInitialMotionX/Y[pointerId](初始Down的X/Y, 在InterceptTouchEvent中,不需要mLastMotionX/Y)进行比较来获得在x/y移动的距离dx/dy.
        3. 调用reportNewEdgeDrags,传入 dx/dy 以及PointerId, 来检测某个Pointer的Move是否触发了某个Edge的Drag的start,会对四个方向的Edge都进行检测,最后如果成功了触发了一个或多个的EdgeDrag,会在mEdgeDragsInProgress中记录信息,并且调用callback的onEdgeDragStarted(….)
        4. 下一步会检测是否移动到了某个ChildView上, 如果当前是STATE_DRAGGING, 那么会直接跳过,(因为ViewDragHelper只能同时操作一个View), 否则,说明当前还没有被Drag的View,还是会调用findTopChildUnder(…)找到被touch的View, 并且如果去是找到了,而且checkTouchSlop(toCapture, dx, dy)通过,即移动了可以被认为能够开始drag距离,那么会调用tryCaptureViewForDrag(…)
        5. saveLastMotion(ev) 将当前的Pointer相关信息保存在mLastMotionX/Y中,以在下一次的Move中使用来的到dx/dy(不过是在processTouchEvent中了)
      • ACTION_POINTER_UP:
        1. 代表在多点触摸的情况下,非主要的pointer action_up了, 这种情况没有什么更多影响,只需要把相应的pointerId的信息全部都清除即可。
      • ACTION_UP/ACTION_CANCEL:
        1. 代表着用户彻底离开了屏幕, 那么需要调用cancel()函数来完结这一次的交互,(但是还可能会有fling或者settling)
    5. 最后的return返回true/false至关重要,因为这代表着这一轮的TouchEvent之后的部分是否都由ViewDragHelper来接收, 如果返回true,就代表这ViewDragHelper全权接收了之后的事件,返回false则代表ViewDragHelper暂时放行此事件到childView,但是还会持续的进行Intercept, 这里return的逻辑是mDragState == STATE_DRAGGING. 即如果这一次的Touch事件确实的capture到了某个ChildView(唯一设置STATE_DRAGGING的地方是tryCaptureViewForDrag(…)的captureChildView(…))
  9. processTouchEvent(MotionEvent ev):

    • 该函数会在parentView/Activity的onTouchEvent中被调用,前提是没有子View接收MotionEvent或者ViewDragHelper 主动的Intercept了TouchEvent(返回了true)
    • 如果此次的ACTION是down, 那么代表着一轮新的Touch事务的开始,调用cancel()来进行重置
    • obtain一个mVelocityTracker, 并将该次的MotionEvent加入到其中.
    • 根据Action进行不同操作:
      1. ACTION_DOWN:
        • 获取x/y/pointerId, 然后调用findTopChildUnder(…)看是否down到了View, 并且调用saveInitialMotion(x, y, pointerId)来保存Touch的初始信息.
        • 直接调用tryCaptureViewForDrag(toCapture, pointerId),既然现在轮到了parentVew自己来处理TouchEvent, 那么现在根本不需要再等待一个slop来确认drag, 直接开始即可
        • 从mInitialEdgesTouched[pointerId]中找出此pointer触发的TouchedEdges,如果这些Edge在mTrackingEdges中,,那么确实是一次有效的EdgeTouch,调用callback的onEdgeTouched(…)
      2. ACTION_POINTER_DOWN
        • 其处理逻辑和shouldInterceptTouchEvent()中的基本一致。
      3. ACTION_MOVE
        • 这一步才开始真正的位移captured View, 首先要判断状态是否是STATE_DRAGGING, 只有是正在drag,才能够继续的移动View, 会根据本次的x/y和mLastMotionX/Y来的到位移量dx/dy, 进而调用dragTo()真正的移动View.
        • 如果当前状态不是STATE_DRAGGING: 那么还是会遍历当前MotionEvent中所有的Pointer的信息进行处理: 得到某个pointer这一次的dx/dy, 调用reportNewEdgeDrags来检测以及通知是否发生了onEdgeDragStarted(…)
        • 还是如果是DRAGGING,那么就不再检测是否移动到了某个child view.
        • 如果当前没有一个View在被drag,那么会调用findTopChildUnder以及checkTouchSlop来check**是否移动到了某个ChildView以及位移量是否可以触发对此ChildView的Capture从而开始drag**
        • 最后调用saveLastMotion(ev)来将当前的MotionEvent保存以在下次的ACTION_MOVE处理时使用.
      4. ACTION_POINTER_UP
        • 有pointer松开了,但是还有pointer在顶着.
        • 如果当前状态是STATE_DRAGGING并且这次松开的pointer正好是mActivePointerId即拖着ChildView走的那个Pointer, 那么会试图找一下当前其他还在ChildView内的Pointer
        • 遍历当前所有的Pointer,略过当前已经松开的pointer, 如果还有一个pointer是点在View上的,并且tryCaptureViewForDrag(…)成功,那么就认为View还在被Drag
        • 如果找了一圈也没有满足的,那么就认为ChildView被释放了,调用releaseViewForPointerUp()来释放之前被Dragging的ChildView.
        • 最后clearMotionHistory(pointerId)将松开的Pointer的有关历史记录清除。
      5. ACTION_UP
        • 彻底松开了,那么如果当前状态是STATE_DRAGGING的话,需要调用releaseViewForPointerUp(), 可能后面还有fling或者settle.
        • 调用cancel来重置收尾.
      6. ACTION_CANCEL
        • 如果当前是STATE_DRAGGING,那么会调用dispatchViewReleased(0, 0)(和ACTION_UP不一样, x/y方向的滑动速度都是0,)
  10. findTopChildUnder(…):

    • 简单的函数,根据输入的x,y, 以一定的顺序遍历childView(默认是ChildIndex从大到小,可以使用callback的getOrderedChildIndex(…)来自定义遍历的顺序,其实就是一个自己的Map映射关系)
    • 在遍历过程中,返回第一个包含了x,y的chidl View.
    • 找不到就返回null.
  11. tryCaptureViewForDrag(View toCapture, int pointerId):

    • 如果toCapture == mCapturedView并且mActivePointerId == pointerId, 即这个View已经被Capture了,并且Capture其的Pointer和当前输入的Pointer也相同那么就不需要再继续了, 返回ture代表成功的Capture了View.
    • 如果toCapture != null 并且 mCallback的tryCaptureView(…)也放行了, 那么就会将mActivePointerId设置为当前输入的pointerId(这意味着其实只能有一个Pointer可以真正的capture住View), 调用captureChildView(….), 返回true代表capture 成功.
    • 其他的返回false代表没有capture到什么View.
  12. captureChildView(View childView, int activePointerId):

    • 首先检测childView是不是mParentView的ChildView,不是的话,抛异常.
    • mCapturedView = childView;
    • mActivePointerId = activePointerId;
    • mCallback.onViewCaptured(childView, activePointerId);
    • setDragState(STATE_DRAGGING);, 这里是唯一一个设置STATE_DRAGGING的地方,很喜欢这样的,设置状态的地方太多不好,尤其是这种应用场景
  13. reportNewEdgeDrags(float dx, float dy, int pointerId):

    • 调用checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT/EDGE_TOP/EDGE_RIGHT/EDGE_BOTTOM) 来check这一次移动的dx/dy**是否触发了四个方向Edge的Drag**
    • EDGE_LEFT/EDGE_TOP/EDGE_RIGHT/EDGE_BOTTOM对应 1 << 0, 1 << 1, 1 << 2, 1 << 3, 便于在一个int中表示4个
    • 如果确实触发了几个EdgeDrag(一个int dragsStarted会保存这些信息), 那么会在相应的PointerId设置mEdgeDragsInProgress[pointerId] |= dragsStarted;
    • 调用callback的onEdgeDragStarted(dragsStarte(注意这里int里可能会包含若干个不同方向的EdgeDrag), pointerId)
  14. checkTouchSlop(View child, float dx, float dy):

    • 检查是否在给定的childView上移动了一个可以被接受的TouchSlop
    • 如果child == null, 直接return false.
    • checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0
    • checkVertical = mCallback.getViewVerticalDragRange(child) > 0;
    • 如果外部调用者指定了ChildView可以水平/垂直移动的range, 那么就代表着需要进行水平/垂直方向的移动的Slop检查
    • 检查的逻辑很简单,dx/y > mTouchSlop,
    • 不过对于水平和垂直都需要检测Slop的情况,其公式是: dx * dx + dy * dy > mTouchSlop * mTouchSlop
    • 从其使用情况来看,只有TouchSlop通过,才会将MOVE认定为一次有效的DRAG.
  15. cancel():

    • mActivePointerId = INVALID_POINTER;
    • clearMotionHistory(); 将所有Pointer的本地历史记录都重置.
    • mVelocityTracker 会进行recycle().
    • cancel()调用时机是一次用户交互的彻底结束(ACTION_UP/CANCEL), 但是后续可能还有fling或者settle
  16. abort():

    • 会调用cancel().
    • 还会将当前正在进行的的动画直接干到动画的end.
    • 如果mDragState == STATE_SETTLING, 那么说明有动画, 调用mScroller.abortAnimation()来将settle动画直接干到end, 并且会的到这次abort()引起的dx/dy的变化(oldX/Y = mScroller.getCurrX/Y(), scroller abort以后, newX/Y = mScroller.getCurrX/Y()), 并将这次的dx/dy通告外部调用者: mCallback.onViewPositionChanged(…)
    • 最后将状态设置为STATE_IDLE.
  17. dragTo(int left, int top, int dx, int dy):

    • 将当前drag的View 移动一段距离. left/top是要移动的View的getleft()/getTop().
    • 如果callback的clampViewPositionHorizontal/Vertical(…)限定了capturedView的移动距离,那么还需要结合这个值来的到View的水平和垂直偏移量.
    • 得到了偏移量以后,调用offsetLeftAndRight(…)/offsetTopAndBottom(…)来移动,还真是没有新意…
    • 如果dx != 0 || dy != 0. 还会触发callback的onViewPositionChanged(…), 不过里面给的left/top/dx/dy都是clamp过的
  18. releaseViewForPointerUp()

    • 对于主动ActionUp(processTouchEvent中)而非ActionCancel,会触发这个函数,来进行用户滑动速度的计算
    • 通过mVelocityTracker以及在mMinVelocity和mMaxVelocity之间进行clamp(clampMag() 来得到一个合理的滑动速度.
    • 然后将x/y的速度下发: dispatchViewReleased(xvel, yvel)
  19. dispatchViewReleased(float xvel, float yvel):

    • mReleaseInProgress = true;
    • mCallback.onViewReleased(mCapturedView, xvel, yvel);
    • mReleaseInProgress = false; 看样子settle/fling并不是由ViewDraghelper自己的内部逻辑触发的,而是交由callback的onViewReleased(…)中由调用者自己来负责
    • 如果mDragState == STATE_DRAGGING, 那么setDragState(STATE_IDLE).
  20. mEdgeSize: 检测一次touch或者drag是否真正触发了 edge drag 的阈值,以像素为单位.

  21. smoothSlideViewTo(View child, int finalLeft, int finalTop):

    • 将指定的View移动(渐变的)到指定的位置.
    • 如果返回true,代表此轮位移准备完毕,那么调用者应该持续在每一帧触发continueSettling(boolean)来使得移动继续,直到continueSettling(boolean)返回false
    • 如果返回false,代表着这次的动画没有启动或者不需要启动(比如位移量都是0)。
    • mCapturedView = child;
    • mActivePointerId = INVALID_POINTER; (因为不是pointer来进行的,因此会将mActivePointerId设为无效值)
    • return forceSettleCapturedViewAt(…)的调用结果
  22. forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel):

    • capturedView移动到指定的位置, 返回true代表着此轮位移准备完毕,后续需要持续的调用continueSettling(boolean)
    • 首先获得capturedView的起始X/Y, 求出位移量dx/dy.
    • 如果位移量均为0, 那么没有什么动画可做的,直接mScroller.abortAnimation(), 将当前的状态设置为STATE_IDLE, 然后返回false.
    • 调用 computeSettleDuration()来根据位移量和速度计算出此次动画的持续时间.
    • 然后调用Scroller的startScroll(startLeft, startTop, dx, dy, duration).
    • 将状态设置为STATE_SETTLING, 返回true代表这动画准备就绪.
  23. computeSettleDuration(View child, int dx, int dy, int xvel, int yvel):

    • 首先要在mMinVelocity和mMaxVelocity之间将xvel/yvel进行clamp操作,确保不会有过大和过小的位移速度.
    • 对于x/y移动速度为0的情况,有特殊的计算。
    • 进一步调用computeAxisDuration(…)来的到x/y的位移持续时间
    • TODO: 计算时间的逻辑比较复杂,回头再专门分析.
  24. flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop):

    • 和smoothSlideTo(…)不同的在于就是动作由平滑的slide变成fling这种行为。
    • fling的计算机制还是直接采用了scroller的fling函数.
    • 将状态设置为SETTLING, 即当前有自发的动画.
  25. continueSettling(boolean deferCallbacks):

    • 真正持续执行位移的函数, 返回true当前的位移动画还没有结束,还需要在后续帧中继续调用, 返回false则代表当前位移动画已经结束了,不必再继续调用
    • 因为本质是借助了scroller来计算随着时间的偏移量的, 在当前状态还是Settling时,会首先调用scroller的computeScrollOffset()来计算目前时间对应的位移量,和当前的captureView的x/y进行对比,获得这一帧应该在X/Y方向位移的距离
    • 如果确实需要位移,那么会调用captureView的offsetLeftAndRight(…)来进行实际的View位置变动, 并且调用callback的onViewPositionChanged(….)
    • 如果mScroller计算出还需要继续进行,但是当前的x/y都已经等于了Scroller的FinalX/Y, 那么就不需要继续进行动画了,直接调用Scroller的abortAnimation()
    • 最后检查是否还需要继续动画, 如果已经不需要, 那么如果注册了deferCallbacks, 会将DeferCallback post到主线程执行, 否则,直接将状态设置为IDLE
    • 最后返回mDragState == STATE_SETTLING, 即如果动画还没完,那么还需要在后续帧继续执行这个函数来继续动画
  26. isPointerDown(int pointerId):

    • 用于判断某个Pointer是否已经点下来了
    • 内部一个int变量mPointersDown来保存那些已经down下来的PointerId
  27. canScroll(View v, boolean checkV, int dx, int dy, int x, int y):

    • 判断某个View是否在x/y可以移动指定的dx/y
    • 注意这里使用了ViewCompat(v4 support包)的canScrollHorizontally/canScrollVertically来保证兼容性
    • 如果是一个ViewGroup, 那么会后向遍历其childView, 如果有一个可以scroll的, 那么即返回true, 否则最后还是判断View自身.
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值