ViewDragHelper
是个工具类,用来辅助ViewGroup
内控件的操作和拖拽。
1. ViewDragHelper创建
ViewDragHelper
通过静态方法create()
创建,
public static ViewDragHelper create(ViewGroup forParent, Callback cb)
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb)
Callback
是一个回调,用来处理事件,
主要方法
// 是否需要捕获这个child
abstract boolean tryCaptureView(View child, int pointerId)
// 修改水平位置
public int clampViewPositionHorizontal(View child, int left, int dx)
// 修改垂直位置
public int clampViewPositionVertical(View child, int top, int dy)
// 选择拦截对象
public void onViewCaptured(@NonNull View capturedChild, int activePointerId);
// 手势释放
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel)
// 边缘被触摸
public void onEdgeTouched(int edgeFlags, int pointerId)
// 边缘开始拉拽
public void onEdgeDragStarted(int edgeFlags, int pointerId)
2. 简单的拖拽实现
自定义ViewDragHelperView
实现FrameLayout
,在onInterceptTouchEvent()
和onTouchEvent()
方法中,添加ViewDragHelperView
进行拦截处理。
public class ViewDragHelperView extends FrameLayout {
private ViewDragHelper mViewDragHelper;
public ViewDragHelperView(@NonNull Context context) {
this(context, null);
}
public ViewDragHelperView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ViewDragHelperView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mViewDragHelper = ViewDragHelper.create(this, new CustomCallback());
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}
private class CustomCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(@NonNull View view, int pointerId) {
return true;
}
@Override
public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
return left;
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
return top;
}
}
}
布局文件,ViewDragHelperView
中有两个子控件
<?xml version="1.0" encoding="utf-8"?>
<com.blog.demo.custom.widget.ViewDragHelperView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="@dimen/margin_dpi_50"
android:layout_height="@dimen/margin_dpi_50"
android:layout_margin="@dimen/margin_dpi_50"
android:src="@drawable/icon_pyq"/>
<ImageView
android:layout_width="@dimen/margin_dpi_50"
android:layout_height="@dimen/margin_dpi_50"
android:layout_margin="100dp"
android:src="@drawable/icon_link"/>
</com.blog.demo.custom.widget.ViewDragHelperView>
显示如下
3. 拖拽后的回弹
在CallBack
里,利用onViewCaptured
记录控件的初始位置,并在onViewReleased()
时移动回初始位置。
private class CustomCallback extends ViewDragHelper.Callback {
private int mLeft;
private int mTop;
... ...
// 获取捕捉的子控件
@Override
public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
mLeft = capturedChild.getLeft();
mTop = capturedChild.getTop();
}
// 拖拽结束
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
mViewDragHelper.settleCapturedViewAt(mLeft, mTop);
invalidate();
}
}
在settleCapturedViewAt()
方法里面,实际是使用Scroll
实现的回弹
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
if (!mReleaseInProgress) {
throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to "
+ "Callback#onViewReleased");
}
return forceSettleCapturedViewAt(finalLeft, finalTop,
(int) mVelocityTracker.getXVelocity(mActivePointerId),
(int) mVelocityTracker.getYVelocity(mActivePointerId));
}
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
final int startLeft = mCapturedView.getLeft();
final int startTop = mCapturedView.getTop();
final int dx = finalLeft - startLeft;
final int dy = finalTop - startTop;
if (dx == 0 && dy == 0) {
// Nothing to do. Send callbacks, be done.
mScroller.abortAnimation();
setDragState(STATE_IDLE);
return false;
}
final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
setDragState(STATE_SETTLING);
return true;
}
所以在View
的computeScroll()
方法里面需要添加ViewDragHelper
的continueSettling()
方法进行计算。
public class ViewDragHelperView extends FrameLayout {
private ViewDragHelper mViewDragHelper;
... ...
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
invalidate();
}
}
}
public boolean continueSettling(boolean deferCallbacks) {
if (mDragState == STATE_SETTLING) {
boolean keepGoing = mScroller.computeScrollOffset();
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
final int dx = x - mCapturedView.getLeft();
final int dy = y - mCapturedView.getTop();
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);
}
if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
// Close enough. The interpolator/scroller might think we're still moving
// but the user sure doesn't.
mScroller.abortAnimation();
keepGoing = false;
}
if (!keepGoing) {
if (deferCallbacks) {
mParentView.post(mSetIdleRunnable);
} else {
setDragState(STATE_IDLE);
}
}
}
return mDragState == STATE_SETTLING;
}
显示如下
4. 代码解析
processTouchEvent(MotionEvent ev)
方法处理手势事件
public void processTouchEvent(@NonNull MotionEvent ev) {
final int action = ev.getActionMasked();
final int actionIndex = ev.getActionIndex();
... ...
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 = ev.getPointerId(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 MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(mActivePointerId)) break;
final int index = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(index);
final float y = ev.getY(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);
}
}
... ...
case MotionEvent.ACTION_UP: {
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();
}
cancel();
break;
}
case MotionEvent.ACTION_CANCEL: {
if (mDragState == STATE_DRAGGING) {
dispatchViewReleased(0, 0);
}
cancel();
break;
}
}
}
ACTION_DOWN事件
findTopChildUnder()
查找拦截子控件
public View findTopChildUnder(int x, int y) {
final int childCount = mParentView.getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
if (x >= child.getLeft() && x < child.getRight()
&& y >= child.getTop() && y < child.getBottom()) {
return child;
}
}
return null;
}
tryCaptureViewForDrag()
拦截事件,并调用Callback
的tryCaptureView()
方法回调,查询是否拦截该事件。
boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
if (toCapture == mCapturedView && mActivePointerId == pointerId) {
// Already done!
return true;
}
if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
mActivePointerId = pointerId;
captureChildView(toCapture, pointerId);
return true;
}
return false;
}
captureChildView()
方法设置拦截对象,调用Callback
的onViewCaptured()
方法回调。
public void captureChildView(@NonNull View childView, int activePointerId) {
if (childView.getParent() != mParentView) {
throw new IllegalArgumentException("captureChildView: parameter must be a descendant "
+ "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
}
mCapturedView = childView;
mActivePointerId = activePointerId;
mCallback.onViewCaptured(childView, activePointerId);
setDragState(STATE_DRAGGING);
}
setDragState()
方法修改状态,调用Callback
的onViewDragStateChanged()
方法回调。
void setDragState(int state) {
mParentView.removeCallbacks(mSetIdleRunnable);
if (mDragState != state) {
mDragState = state;
mCallback.onViewDragStateChanged(state);
if (mDragState == STATE_IDLE) {
mCapturedView = null;
}
}
}
ACTION_MOVE事件
dragTo()
方法移动拦截控件,Callback
的clampViewPositionHorizontal()
和clampViewPositionVertical()
分别返回水平和垂直的移动量,最后调用Callback
的onViewPositionChanged()
方法回调
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);
}
}
saveLastMotion()
方法保留当前手势位置。
private void saveLastMotion(MotionEvent ev) {
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip saving on ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) {
continue;
}
final float x = ev.getX(i);
final float y = ev.getY(i);
mLastMotionX[pointerId] = x;
mLastMotionY[pointerId] = y;
}
}
ACTION_UP和ACTION_CANCEL事件
releaseViewForPointerUp()
方法计算移动时的速率,回调Callback
的onViewReleased()
方法
private void releaseViewForPointerUp() {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final float xvel = clampMag(
mVelocityTracker.getXVelocity(mActivePointerId),
mMinVelocity, mMaxVelocity);
final float yvel = clampMag(
mVelocityTracker.getYVelocity(mActivePointerId),
mMinVelocity, mMaxVelocity);
dispatchViewReleased(xvel, yvel);
}
private void dispatchViewReleased(float xvel, float yvel) {
mReleaseInProgress = true;
mCallback.onViewReleased(mCapturedView, xvel, yvel);
mReleaseInProgress = false;
if (mDragState == STATE_DRAGGING) {
// onViewReleased didn't call a method that would have changed this. Go idle.
setDragState(STATE_IDLE);
}
}
public void cancel() {
mActivePointerId = INVALID_POINTER;
clearMotionHistory();
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
5. 边缘触发
利用ViewDragHelper
可以轻松实现边缘触发
// 设置识别哪些方向的触发
public void setEdgeTrackingEnabled(int edgeFlags)
在ViewDragHelperView
设置边缘触发
public ViewDragHelperView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mViewDragHelper = ViewDragHelper.create(this, new CustomCallback());
mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
}
在Callback
中的onEdgeDragStarted()
方法里,加入CaptureView
private class CustomCallback extends ViewDragHelper.Callback {
... ...
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
Log.d("ViewDragHelperView", "onEdgeDragStarted");
if (edgeFlags == ViewDragHelper.EDGE_LEFT) {
mViewDragHelper.captureChildView(getChildAt(0), pointerId);
}
}
}
显示如下
6. ViewDragHelper中的Button
Button
在ViewDragHelperView
中无法被拖动
我们来看一下shouldInterceptTouchEvent()
方法,当发生ACTION_DOWN
事件时,只有mDragState == STATE_SETTLING
时才会调用tryCaptureViewForDrag()
方法,并修改状态,一般返回值都是false
。而Button
会拦截手势,因此不会调用processTouchEvent()
方法了
public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev) {
final int action = ev.getActionMasked();
final int actionIndex = ev.getActionIndex();
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 = ev.getPointerId(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 MotionEvent.ACTION_MOVE: {
if (mInitialMotionX == null || mInitialMotionY == null) break;
// First to cross a touch slop over a draggable view wins. Also report edge drags.
final int pointerCount = ev.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
final int pointerId = ev.getPointerId(i);
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) continue;
final float x = ev.getX(i);
final float y = ev.getY(i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
final View toCapture = findTopChildUnder((int) x, (int) y);
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
if (pastSlop) {
// check the callback's
// getView[Horizontal|Vertical]DragRange methods to know
// if you can move at all along an axis, then see if it
// would clamp to the same value. If you can't move at
// all in every dimension with a nonzero range, bail.
final int oldLeft = toCapture.getLeft();
final int targetLeft = oldLeft + (int) dx;
final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
targetLeft, (int) dx);
final int oldTop = toCapture.getTop();
final int targetTop = oldTop + (int) dy;
final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
(int) dy);
final int hDragRange = mCallback.getViewHorizontalDragRange(toCapture);
final int vDragRange = mCallback.getViewVerticalDragRange(toCapture);
if ((hDragRange == 0 || (hDragRange > 0 && newLeft == oldLeft))
&& (vDragRange == 0 || (vDragRange > 0 && newTop == oldTop))) {
break;
}
}
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag
break;
}
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
break;
}
... ...
}
return mDragState == STATE_DRAGGING;
}
当发生ACTION_MOVE
事件时,会调用checkTouchSlop()
来查看是否拦截手势,默认是不拦截。
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;
}
所以如果需要拦截的话,修改Callback
的getViewHorizontalDragRange()
和getViewVerticalDragRange()
方法
@Override
public int getViewHorizontalDragRange(@NonNull View child) {
if (child == mClickableView) {
return 1;
}
return super.getViewHorizontalDragRange(child);
}
@Override
public int getViewVerticalDragRange(@NonNull View child) {
if (child == mClickableView) {
return 1;
}
return super.getViewVerticalDragRange(child);
}