一.拖拽滑动
其中拖住滑动是通过dragTo方法来实现的
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);
}
}
可以看到最终是通过ViewCompat.offsetLeftAndRight()和ViewCompat.offsetTopAndBottom()来实现左右、上下拖动的。
二.自动滑动
ViewDragHelper中有两种方式可以实现子View自动滑动到某个位置,分别是
public boolean smoothSlideViewTo(@NonNull View child, int finalLeft, int finalTop) {
**********
}
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
**********
}
通过代码可以看到它们都会调用forceSettleCapturedViewAt()方法,代码如下
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;
}
可以看到最终滑动是通过OverScroller来实现的,我们知道Scroller是需配合在View的computeScroll()方法中不断调用invalidate(),从而不断重新绘制视图位置达到视图自动滑动的效果的,所以我们使用ViewDragHelper时需要重写自定义View的computeScroll()方法,如
@Override
public void computeScroll() {
super.computeScroll();
if (mDragHelper != null && mDragHelper.continueSettling(true)) {
invalidate();
}
}
ViewDragHelper中还有一个方法可以实现在拖拽滑动松手后进行惯性滑动,最终停留位置由松手时的位置和速度来决定,它和上面的settleCapturedViewAt()都必须在mReleaseInProgress为true的时候才能生效,也就是必须在Callback的onViewReleased()方法中调用。它也是通过调用OverScroller,所以使用时同样需重写自定义View的computeScroll()方法。
public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {
if (!mReleaseInProgress) {
throw new IllegalStateException("Cannot flingCapturedView outside of a call to "
+ "Callback#onViewReleased");
}
mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(),
(int) mVelocityTracker.getXVelocity(mActivePointerId),
(int) mVelocityTracker.getYVelocity(mActivePointerId),
minLeft, maxLeft, minTop, maxTop);
setDragState(STATE_SETTLING);
}
三.边缘滑动
/**
* Enable edge tracking for the selected edges of the parent view.
* The callback's {@link Callback#onEdgeTouched(int, int)} and
* {@link Callback#onEdgeDragStarted(int, int)} methods will only be invoked
* for edges for which edge tracking has been enabled.
*
* @param edgeFlags Combination of edge flags describing the edges to watch
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/
public void setEdgeTrackingEnabled(int edgeFlags) {
mTrackingEdges = edgeFlags;
}
ViewDragHelper通过setEdgeTrackingEnabled()方法设置要监测哪个方向屏幕边缘的滑动,而是否是边缘滑动是通过对触摸事件中Action_Down时的位置进行判令的
public boolean shouldInterceptTouchEvent(@NonNull MotionEvent 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 int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
final int pointerId = ev.getPointerId(actionIndex);
final float x = ev.getX(actionIndex);
final float y = ev.getY(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) {
**********
}
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];
**********
reportNewEdgeDrags(dx, dy, pointerId);
**********
}
saveLastMotion(ev);
break;
}
**********
}
return mDragState == STATE_DRAGGING;
}
public void processTouchEvent(@NonNull MotionEvent 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);
**********
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
final int pointerId = ev.getPointerId(actionIndex);
final float x = ev.getX(actionIndex);
final float y = ev.getY(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 (isCapturedViewUnder((int) x, (int) y)) {
**********
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {
**********
} else {
// Check to see if any pointer is now over a draggable view.
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];
reportNewEdgeDrags(dx, dy, pointerId);
**********
}
saveLastMotion(ev);
}
break;
}
**********
}
}
其中saveInitialMotion()方法如下
private void saveInitialMotion(float x, float y, int pointerId) {
ensureMotionHistorySizeForId(pointerId);
mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
mPointersDown |= 1 << pointerId;
}
private int getEdgesTouched(int x, int y) {
int result = 0;
if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT;
if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP;
if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT;
if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM;
return result;
}
可以看出ACTION_DOWN时会记录触摸点的x,y坐标位置,手指id,以及当前手指是否是触摸在屏幕某一个边缘,具体是通过当前触摸点x或y是否在边缘一个很小的距离范围来来判定。如果是边缘触摸并且方向和setEdgeTrackingEnabled()方法设置要监测的方向一致,则会回调Callback的onEdgeTouched()方法。
通过上面的ACTION_MOVE中代码看到手指滑动过程中,会调用reportNewEdgeDrags()方法来对边缘滑动做进一步的处理
private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
int dragsStarted = 0;
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
dragsStarted |= EDGE_LEFT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
dragsStarted |= EDGE_TOP;
}
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) {
dragsStarted |= EDGE_RIGHT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
dragsStarted |= EDGE_BOTTOM;
}
if (dragsStarted != 0) {
mEdgeDragsInProgress[pointerId] |= dragsStarted;
mCallback.onEdgeDragStarted(dragsStarted, pointerId);
}
}
private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
final float absDelta = Math.abs(delta);
final float absODelta = Math.abs(odelta);
if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0
|| (mEdgeDragsLocked[pointerId] & edge) == edge
|| (mEdgeDragsInProgress[pointerId] & edge) == edge
|| (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
return false;
}
if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
mEdgeDragsLocked[pointerId] |= edge;
return false;
}
return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
}
可以看到会通过mInitialEdgesTouched中记录的该手指最开始Action_down的时候是否是边缘触摸,要监测的是否是该方向,该边缘方向是否是锁定状态,以及是否是Action_down后首次该边缘方向的ACTION_MOVE触发多个条件来进行判定,如果判定成功则会调用Callback的onEdgeDragStarted()方法。
其中某个边缘方向是否是锁定状态通过Callback的onEdgeLock()方法来获取,我们可以在这个回调中动态改变返回值来动态设置是否可以触发onEdgeDragStarted()方法,而真正要实现边缘触摸的时候可以拖拽出某个view(比如某一个下拉菜单)还需要在onEdgeDragStarted()中做其他处理,如
ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
**********
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
super.onEdgeTouched(edgeFlags, pointerId);
}
@Override
public boolean onEdgeLock(int edgeFlags) {
return super.onEdgeLock(edgeFlags);
}
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
mDragHelper.captureChildView(mBottomMenu, pointerId);
}
**********
};
参考链接:
ViewDragHelper 的基本使用(一)
ViewDragHelper原理与使用
ViewDragHelper源码解析
Android ViewDragHelper源码解析