ViewDragHelper 部分原理分析

前言

本周用 ViewDragHelper 实现几个自定义 ViewGroup ,如“QQ 5.0 侧滑菜单”、”仿 SlideMenu 的侧滑菜单”、”ListView 的左滑删除”等。发现,ViewDragHelper 确实是一个不错的工具类,相比自己手写 onTouchEvent 可以省很多代码。但是,在使用过程中也产生了不少的疑问。

关于滑动的实现

在 Android 中,滑动有两种方式:

显示位置的改变

ScrollTo、ScrollBy 只改变 View 的显示位置(内容),而不改变真实的位置。使用 ScrollTo 滑动后,会产生一个滑动值:view.getScrollX(),表示该 View,滑动了多少距离。而在此过程中,view.getLeft() 一直为 0。

ScrollTo 将 View 滑动到绝对位置而ScrollBy 是对 ScrollTo 的简单封装,将 View 滑动到相对位置。

View.class:

public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;
        int oldY = mScrollY;
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        if (!awakenScrollBars()) {
            // scrollTo 不断刷新显示位置
            postInvalidateOnAnimation();
        }
    }
}

可以看出,scrollTo 在滚动过程中,不断记录将当前位置记录在 mScrollX 和 mScrollX,然后通过 postInvalidateOnAnimation 刷新显示位置。在此过程中,会不断调用 onScrollChanged,产生回调。

/* x,y 是移动前坐标 */
public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

scrollBy 将本次滚动的相对值累加到 mScrollX 和 mScrollY

真实位置的改变

真实改变 View 位置的方法有这么几种:

  • offsetLeftAndRight、offsetTopAndButtom
  • view.setLeft(left)、view.setRight(right)(属性动画同理)
  • LayoutParams

这种方式将改变 View 的真实位置,底层调用 invalidate(),让 onDraw 方法被调用,view 将在新的位置重绘。通过 getLeft() 获取左边界和父容器的距离。

而 ViewDragHelper 的移动,就是改变其真实位置:

private void dragTo(int left, int top, int dx, int dy) {
    int clampedX = left;
    int clampedY = top;
    final int oldLeft = mCapturedView.getLeft();
    final int oldTop = mCapturedView.getTop();
    if (dx != 0) {
        clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
        ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
    }
    if (dy != 0) {
        clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
        ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
    }

    if (dx != 0 || dy != 0) {
        final int clampedDx = clampedX - oldLeft;
        final int clampedDy = clampedY - oldTop;
        mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
                clampedDx, clampedDy);
    }
}

关于兼容低版本

经过测试,ViewDragHelper 无法 2.3 的模拟器生效,让我们从代码中找出原因。

View.class(api 8)

public void offsetLeftAndRight(int offset) {
     mLeft += offset;
     mRight += offset;
}
public void offsetTopAndBottom(int offset) {
     mTop += offset;
     mBottom += offset;
}

低版本的 offsetLeftAndRight 不会主动重绘。所以,只需要手动刷新即可。

Callback:

/* ViewDragHelper 的拖拽监听回调 */
private class MyCallback extends ViewDragHelper.Callback{
    //...

    /* 当位置发生改变时调用(此时,已经发生了view位置的改变,松开手之后的改变也可以监听) */
    @Override
    public void onViewPositionChanged(View changedView, int left, int top,int dx, int dy) {

        // ...

        // 兼容低版本:低版本view的offsetLeftAndRight不会主动刷新界面,因此需要手动调用
        invalidate();
    }

    // ...
}

smoothSlideView 原理

ViewDragHelper 不仅封装了事件监听,而且封装了动画和滑动逻辑。只需要 调用 helper.smoothSlideView () 即可实现滑动。从代码看出,其内部是 Scroller ,并在的 continueSettling 不断 offsetLeftAndRight、
offsetTopAndBottom

public boolean continueSettling(boolean deferCallbacks) {

    // ...
    if (dx != 0) {
         ViewCompat.offsetLeftAndRight(mCapturedView, dx);
    }
     if (dy != 0) {
         ViewCompat.offsetTopAndBottom(mCapturedView, dy);
    }
     // 位置改变回调
     if (dx != 0 || dy != 0) {
         mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
    }
    // ...
 }

而 offsetLeftAndRight、offsetTopAndBottom 默认刷新,所以这里不需要手动调用 invalidate(),做兼容处理。

ViewCompatBase.java

static void offsetTopAndBottom(View view, int offset) {
    final int currentTop = view.getTop();
    view.offsetTopAndBottom(offset);

    if (offset != 0) {
        // We need to manually invalidate pre-honeycomb
        final ViewParent parent = view.getParent();
        if (parent instanceof View) {
            final int absOffset = Math.abs(offset);
            ((View) parent).invalidate(
                    view.getLeft(),
                    currentTop - absOffset,
                    view.getRight(),
                    currentTop + view.getHeight() + absOffset);
        } else {
            view.invalidate();
        }
    }
}

关于 TouchSlop

TouchSlop 即触摸敏感度,是指能触发移动的最短手指滑动距离,数值越低越灵敏。ViewDragHelper 提供创建对象的 create 方法中,就有指定敏感度的。可以在代码中看到,这里的 sensitivity 经过处理,变成一个更容易理解的值,即 sensitivity 越大越灵敏

ViewDragHelper.java

public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
      final ViewDragHelper helper = create(forParent, cb);
      helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
      return helper;
  }

你可能感兴趣的:(Android)