-
Notifications
You must be signed in to change notification settings - Fork 779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
2019-12-05:谈一谈ViewDragHelper的工作原理? #204
Comments
ViewDragHelper类,是用来处理View边界拖动相关的类,比如我们这里要用的例子—侧滑拖动关闭页面(类似微信),该功能很明显是要处理在View上的触摸事件,记录触摸点、计算距离、滚动动画、状态回调等,如果我们自己手动实现自然会很麻烦还可能出错,而这个类会帮助我们大大简化工作量。 1.初始化 private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
...
mParentView = forParent;//BaseView
mCallback = cb;//callback
final ViewConfiguration vc = ViewConfiguration.get(context);
final float density = context.getResources().getDisplayMetrics().density;
mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);//边界拖动距离范围
mTouchSlop = vc.getScaledTouchSlop();//拖动距离阈值
mScroller = new OverScroller(context, sInterpolator);//滚动器
}
2.拦截事件处理 override fun onInterceptTouchEvent(ev: MotionEvent?) =
dragHelper?.shouldInterceptTouchEvent(ev) ?: super.onInterceptTouchEvent(ev) 该方法用于处理mParentView是否拦截此次事件 public boolean shouldInterceptTouchEvent(MotionEvent ev) {
...
switch (action) {
...
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);
...
//判断pointer的拖动边界
reportNewEdgeDrags(dx, dy, pointerId);
...
}
saveLastMotion(ev);
break;
}
...
}
return mDragState == STATE_DRAGGING;
} 拦截事件的前提是mDragState为STATE_DRAGGING,也就是正在拖动状态下才会拦截,那么什么时候会变为拖动状态呢?当ACTION_MOVE时,调用reportNewEdgeDrags方法: private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
int dragsStarted = 0;
//判断是否在Left边缘进行滑动
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
dragsStarted |= EDGE_LEFT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
dragsStarted |= EDGE_TOP;
}
...
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);
//是否支持edge的拖动以及是否满足拖动距离的阈值
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;
} 可以看到,当ACTION_MOVE时,会尝试找到pointer对应的拖动边界,这个边界可以由我们来制定,比如侧滑关闭页面是从左侧开始的,所以我们可以调用setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)来设置只支持左侧滑动。而一旦有滚动发生,就会回调callback的onEdgeDragStarted方法,交由我们做如下操作: override fun onEdgeDragStarted(edgeFlags: Int, pointerId: Int) {
super.onEdgeDragStarted(edgeFlags, pointerId)
dragHelper?.captureChildView(getChildAt(0), pointerId)
} 我们调用了ViewDragHelper的captureChildView方法: public void captureChildView(View childView, int activePointerId) {
mCapturedView = childView;//记录拖动view
mActivePointerId = activePointerId;
mCallback.onViewCaptured(childView, activePointerId);
setDragState(STATE_DRAGGING);//设置状态为开始拖动
} 此时,我们就记录了拖动的View,并将状态置为拖动,那么在下次ACTION_MOVE的时候,该mParentView就会拦截事件,交由自己的onTouchEvent方法处理拖动了! 3.拖动事件处理 override fun onTouchEvent(event: MotionEvent?): Boolean {
dragHelper?.processTouchEvent(event)//交由ViewDragHelper处理
return true
} 该方法用于处理mParentView拦截事件后的拖动处理: public void processTouchEvent(MotionEvent ev) {
...
switch (action) {
...
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);//记录当前触摸点
}...
break;
}
...
case MotionEvent.ACTION_UP: {
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();//释放拖动view
}
cancel();
break;
}...
}
} (1)拖动 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);//通过callback获取真正的移动值
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);//callback回调移动后的位置
}
} 通过callback的clampViewPositionHorizontal方法决定实际移动的水平距离,通常都是返回left值,即拖动了多少就移动多少 通过callback的onViewPositionChanged方法,可以对View拖动后的新位置做一些处理,如: override fun onViewPositionChanged(changedView: View?, left: Int, top: Int, dx: Int, dy: Int) {
super.onViewPositionChanged(changedView, left, top, dx, dy)
//当新的left位置到达width时,即滑动除了界面,关闭页面
if (left >= width && context is Activity && !context.isFinishing) {
context.finish()
}
} (2)释放 private void releaseViewForPointerUp() {
...
dispatchViewReleased(xvel, yvel);
}
private void dispatchViewReleased(float xvel, float yvel) {
mReleaseInProgress = true;
mCallback.onViewReleased(mCapturedView, xvel, yvel);//callback回调释放
mReleaseInProgress = false;
if (mDragState == STATE_DRAGGING) {
// onViewReleased didn't call a method that would have changed this. Go idle.
setDragState(STATE_IDLE);//重置状态
}
} 通常在callback的onViewReleased方法中,我们可以判断当前释放点的位置,从而决定是要回弹页面还是滑出屏幕: override fun onViewReleased(releasedChild: View?, xvel: Float, yvel: Float) {
super.onViewReleased(releasedChild, xvel, yvel)
//滑动速度到达一定值时直接关闭
if (xvel >= 300) {//滑动页面到屏幕外,关闭页面
dragHelper?.settleCapturedViewAt(width, 0)
} else {//回弹页面
dragHelper?.settleCapturedViewAt(0, 0)
}
//刷新,开始关闭或重置动画
invalidate()
} 如滑动速度大于300时,我们调用settleCapturedViewAt方法将页面滚动出屏幕,否则调用该方法进行回弹 (3)滚动 public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
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;
...
final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
//使用Scroller对象开始滚动
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
//重置状态为滚动
setDragState(STATE_SETTLING);
return true;
} 其内部使用的是Scroller对象:是View的滚动机制,其回调是View的computeScroll()方法,在其内部通过Scroller对象的computeScrollOffset方法判断是否滚动完毕,如仍需滚动,需要调用invalidate方法进行刷新 ViewDragHelper据此提供了一个类似的方法continueSettling,需要在computeScroll中调用,判断是否需要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) {
if (deferCallbacks) {
mParentView.post(mSetIdleRunnable);
} else {
setDragState(STATE_IDLE);
}
}
}
return mDragState == STATE_SETTLING;
} 在我们的View中: override fun computeScroll() {
super.computeScroll()
if (dragHelper?.continueSettling(true) == true) {
invalidate()
}
} |
一、状态变更之shouldInterceptTouchEvent前提是拖动的子View设置了点击事件且重写了getViewHorizontalDragRange和getViewVerticalDragRange方法,两方法返回值均大于0,mDragState才能变更为STATE_DRAGGING
checkTouchSlop(View child, float dx, float dy)
二、状态变更之processTouchEvent在此方法中修改状态的前提是拖动目标无点击事件,当down事件传递到目标View时,它不做处理导致它的父View的onTouchEvent得到运行,由于我们在onTouchEvent执行了ViewDragHelper的processTouchEvent,因此,状态切换操作来到了这里
三、主要工作流程
四、参考链接 |
No description provided.
The text was updated successfully, but these errors were encountered: