本周用 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 位置的方法有这么几种:
这种方式将改变 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();
}
// ...
}
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 即触摸敏感度,是指能触发移动的最短手指滑动距离,数值越低越灵敏。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;
}