13518219792

建站动态

根据您的个性需求进行定制 先人一步 抢占小程序红利时代

Android源码进阶之ViewDragHelper原理机制解析

本文转载自微信公众号「Android开发编程」,作者Android开发编程 。转载本文请联系Android开发编程公众号。

10余年的南阳网站建设经验,针对设计、前端、开发、售后、文案、推广等六对一服务,响应快,48小时及时工作处理。成都全网营销的优势是能够根据用户设备显示端的尺寸不同,自动调整南阳建站的显示方式,使网站能够适用不同显示终端,在浏览器中调整网站的宽度,无论在任何一种浏览器上浏览网站,都能展现优雅布局与设计,从而大程度地提升浏览体验。成都创新互联从事“南阳网站设计”,“南阳网站推广”以来,每个客户项目都认真落实执行。

前言

ViewDragHelper类,是用来处理View边界拖动相关的类;

主要功能处理在View上的触摸事件,记录触摸点、计算距离、滚动动画、状态回调等,如果我们自己手动实现自然会很麻烦还可能出错,而这个类会帮助我们大大简化工作量;

今天我们就来分析一波;

一、ViewDragHelper的中主要API介绍

1、ViewDragHelper create(ViewGroup forParent, Callback cb)

一个静态的创建方法;

2、shouldInterceptTouchEvent(MotionEvent ev)

处理事件分发的(怎么说这个方法呢?主要是将ViewGroup的事件分发,委托给ViewDragHelper进行处理);

3、processTouchEvent(MotionEvent event)

处理相应TouchEvent的方法,这里要注意一个问题,处理相应的TouchEvent的时候要将结果返回为true,消费本次事件,否则将无法使用ViewDragHelper处理相应的拖拽事件;

4、ViewDragHelper.Callback的API

tryCaptureView(View child, int pointerId) 这是一个抽象类,必须去实现,也只有在这个方法返回true的时候下面的方法才会生效;

onViewDragStateChanged(int state) 当状态改变的时候回调,返回相应的状态(这里有三种状态);

onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 当你拖动的View位置发生改变的时候回调;

onViewCaptured(View capturedChild, int activePointerId)捕获View的时候调用的方法

onViewReleased(View releasedChild, float xvel, float yvel) 当View停止拖拽的时候调用的方法

clampViewPositionVertical(View child, int top, int dy) 竖直拖拽的时候回调的方法

clampViewPositionHorizontal(View child, int left, int dx) 水平拖拽的时候回调的方法

二、实现原理介绍

1、初始化

 
 
 
 
  1. private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { 
  2.         ... 
  3.         mParentView = forParent;//BaseView 
  4.         mCallback = cb;//callback 
  5.         final ViewConfiguration vc = ViewConfiguration.get(context); 
  6.         final float density = context.getResources().getDisplayMetrics().density; 
  7.         mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);//边界拖动距离范围 
  8.         mTouchSlop = vc.getScaledTouchSlop();//拖动距离阈值 
  9.         mScroller = new OverScroller(context, sInterpolator);//滚动器 
  10.     } 

2.拦截事件处理

该类提供了boolean shouldInterceptTouchEvent(MotionEvent)方法:

 
 
 
 
  1. override fun onInterceptTouchEvent(ev: MotionEvent?) = 
  2.             dragHelper?.shouldInterceptTouchEvent(ev) ?: super.onInterceptTouchEvent(ev) 

该方法用于处理mParentView是否拦截此次事件

 
 
 
 
  1. public boolean shouldInterceptTouchEvent(MotionEvent ev) { 
  2.         ... 
  3.         switch (action) { 
  4.             ... 
  5.             case MotionEvent.ACTION_MOVE: { 
  6.                 if (mInitialMotionX == null || mInitialMotionY == null) break; 
  7.                 // First to cross a touch slop over a draggable view wins. Also report edge drags. 
  8.                 final int pointerCount = ev.getPointerCount(); 
  9.                 for (int i = 0; i < pointerCount; i++) { 
  10.                     final int pointerId = ev.getPointerId(i); 
  11.                     // If pointer is invalid then skip the ACTION_MOVE. 
  12.                     if (!isValidPointerForActionMove(pointerId)) continue; 
  13.                     final float x = ev.getX(i); 
  14.                     final float y = ev.getY(i); 
  15.                     final float dx = x - mInitialMotionX[pointerId]; 
  16.                     final float dy = y - mInitialMotionY[pointerId]; 
  17.                     final View toCapture = findTopChildUnder((int) x, (int) y); 
  18.                     final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy); 
  19.                     ... 
  20.                     //判断pointer的拖动边界 
  21.                     reportNewEdgeDrags(dx, dy, pointerId); 
  22.                     ... 
  23.                 } 
  24.                 saveLastMotion(ev); 
  25.                 break; 
  26.             } 
  27.             ... 
  28.         } 
  29.         return mDragState == STATE_DRAGGING; 

拦截事件的前提是mDragState为STATE_DRAGGING,也就是正在拖动状态下才会拦截,那么什么时候会变为拖动状态呢?当ACTION_MOVE时,调用reportNewEdgeDrags方法:

 
 
 
 
  1. private void reportNewEdgeDrags(float dx, float dy, int pointerId) { 
  2.         int dragsStarted = 0; 
  3.   //判断是否在Left边缘进行滑动 
  4.         if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) { 
  5.             dragsStarted |= EDGE_LEFT; 
  6.         } 
  7.         if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) { 
  8.             dragsStarted |= EDGE_TOP; 
  9.         } 
  10.         ... 
  11.         if (dragsStarted != 0) { 
  12.             mEdgeDragsInProgress[pointerId] |= dragsStarted; 
  13.           //回调拖动的边 
  14.             mCallback.onEdgeDragStarted(dragsStarted, pointerId); 
  15.         } 
  16. private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) { 
  17.         final float absDelta = Math.abs(delta); 
  18.         final float absODelta = Math.abs(odelta); 
  19. //是否支持edge的拖动以及是否满足拖动距离的阈值 
  20.         if ((mInitialEdgesTouched[pointerId] & edge) != edge  || (mTrackingEdges & edge) == 0 
  21.                 || (mEdgeDragsLocked[pointerId] & edge) == edge 
  22.                 || (mEdgeDragsInProgress[pointerId] & edge) == edge 
  23.                 || (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) { 
  24.             return false; 
  25.         } 
  26.         if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) { 
  27.             mEdgeDragsLocked[pointerId] |= edge; 
  28.             return false; 
  29.         } 
  30.         return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop; 

可以看到,当ACTION_MOVE时,会尝试找到pointer对应的拖动边界,这个边界可以由我们来制定,比如侧滑关闭页面是从左侧开始的,所以我们可以调用setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)来设置只支持左侧滑动。而一旦有滚动发生,就会回调callback的onEdgeDragStarted方法,交由我们做如下操作:

 
 
 
 
  1. override fun onEdgeDragStarted(edgeFlags: Int, pointerId: Int) { 
  2.                 super.onEdgeDragStarted(edgeFlags, pointerId) 
  3.                 dragHelper?.captureChildView(getChildAt(0), pointerId) 
  4.             } 
  5. 我们调用了ViewDragHelper的captureChildView方法: 
  6. public void captureChildView(View childView, int activePointerId) { 
  7.         mCapturedView = childView;//记录拖动view 
  8.         mActivePointerId = activePointerId; 
  9.         mCallback.onViewCaptured(childView, activePointerId); 
  10.         setDragState(STATE_DRAGGING);//设置状态为开始拖动 

此时,就记录了拖动的View,并将状态置为拖动,那么在下次ACTION_MOVE的时候,该mParentView就会拦截事件,交由自己的onTouchEvent方法处理拖动了;

3.拖动事件处理

该类提供了void processTouchEvent(MotionEvent)方法,通常我们需要这么写:

 
 
 
 
  1. override fun onTouchEvent(event: MotionEvent?): Boolean { 
  2.         dragHelper?.processTouchEvent(event)//交由ViewDragHelper处理 
  3.         return true 

该方法用于处理mParentView拦截事件后的拖动处理:

 
 
 
 
  1. public void processTouchEvent(MotionEvent ev) { 
  2.         ... 
  3.         switch (action) { 
  4.             ... 
  5.             case MotionEvent.ACTION_MOVE: { 
  6.                 if (mDragState == STATE_DRAGGING) { 
  7.                     // If pointer is invalid then skip the ACTION_MOVE. 
  8.                     if (!isValidPointerForActionMove(mActivePointerId)) break; 
  9.                     final int index = ev.findPointerIndex(mActivePointerId); 
  10.                     final float x = ev.getX(index); 
  11.                     final float y = ev.getY(index); 
  12.                     //计算距离上次的拖动距离 
  13.                     final int idx = (int) (x - mLastMotionX[mActivePointerId]); 
  14.                     final int idy = (int) (y - mLastMotionY[mActivePointerId]); 
  15.                     dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);//处理拖动 
  16.                     saveLastMotion(ev);//记录当前触摸点 
  17.                 }... 
  18.                 break; 
  19.             } 
  20.             ... 
  21.             case MotionEvent.ACTION_UP: { 
  22.                 if (mDragState == STATE_DRAGGING) { 
  23.                     releaseViewForPointerUp();//释放拖动view 
  24.                 } 
  25.                 cancel(); 
  26.                 break; 
  27.             }... 
  28.         } 

(1)拖动

ACTION_MOVE时,会计算出pointer距离上次的位移,然后计算出capturedView的目标位置,进行拖动处理;

 
 
 
 
  1. private void dragTo(int left, int top, int dx, int dy) { 
  2.         int clampedX = left; 
  3.         int clampedY = top; 
  4.         final int oldLeft = mCapturedView.getLeft(); 
  5.         final int oldTop = mCapturedView.getTop(); 
  6.         if (dx != 0) { 
  7.             clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);//通过callback获取真正的移动值 
  8.             ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);//进行位移 
  9.         } 
  10.         if (dy != 0) { 
  11.             clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); 
  12.             ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop); 
  13.         } 
  14.         if (dx != 0 || dy != 0) { 
  15.             final int clampedDx = clampedX - oldLeft; 
  16.             final int clampedDy = clampedY - oldTop; 
  17.             mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, 
  18.                     clampedDx, clampedDy);//callback回调移动后的位置 
  19.         } 

通过callback的clampViewPositionHorizontal方法决定实际移动的水平距离,通常都是返回left值,即拖动了多少就移动多少;

通过callback的onViewPositionChanged方法,可以对View拖动后的新位置做一些处理,如;

 
 
 
 
  1. override fun onViewPositionChanged(changedView: View?, left: Int, top: Int, dx: Int, dy: Int) { 
  2.   super.onViewPositionChanged(changedView, left, top, dx, dy) 
  3.     //当新的left位置到达width时,即滑动除了界面,关闭页面 
  4.     if (left >= width && context is Activity && !context.isFinishing) { 
  5.       context.finish() 
  6.     } 

(2)释放

而ACTION_UP动作时,要释放拖动View

 
 
 
 
  1. private void releaseViewForPointerUp() { 
  2.         ... 
  3.         dispatchViewReleased(xvel, yvel); 
  4. private void dispatchViewReleased(float xvel, float yvel) { 
  5.         mReleaseInProgress = true; 
  6.         mCallback.onViewReleased(mCapturedView, xvel, yvel);//callback回调释放 
  7.         mReleaseInProgress = false; 
  8.         if (mDragState == STATE_DRAGGING) { 
  9.             // onViewReleased didn't call a method that would have changed this. Go idle. 
  10.             setDragState(STATE_IDLE);//重置状态 
  11.         } 

通常在callback的onViewReleased方法中,我们可以判断当前释放点的位置,从而决定是要回弹页面还是滑出屏幕

 
 
 
 
  1. override fun onViewReleased(releasedChild: View?, xvel: Float, yvel: Float) { 
  2.   super.onViewReleased(releasedChild, xvel, yvel) 
  3.     //滑动速度到达一定值时直接关闭 
  4.     if (xvel >= 300) {//滑动页面到屏幕外,关闭页面 
  5.       dragHelper?.settleCapturedViewAt(width, 0) 
  6.     } else {//回弹页面 
  7.       dragHelper?.settleCapturedViewAt(0, 0) 
  8.     } 
  9.   //刷新,开始关闭或重置动画 
  10.   invalidate() 

如滑动速度大于300时,我们调用settleCapturedViewAt方法将页面滚动出屏幕,否则调用该方法进行回弹

(3)滚动

 
 
 
 
  1. public boolean settleCapturedViewAt(int finalLeft, int finalTop) { 
  2.   return forceSettleCapturedViewAt(finalLeft, finalTop, 
  3.                                    (int) mVelocityTracker.getXVelocity(mActivePointerId), 
  4.                                    (int) mVelocityTracker.getYVelocity(mActivePointerId)); 
  5. private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) { 
  6.   //当前位置 
  7.   final int startLeft = mCapturedView.getLeft(); 
  8.   final int startTop = mCapturedView.getTop(); 
  9.   //偏移量 
  10.   final int dx = finalLeft - startLeft; 
  11.   final int dy = finalTop - startTop; 
  12.   ... 
  13.   final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); 
  14.   //使用Scroller对象开始滚动 
  15.   mScroller.startScroll(startLeft, startTop, dx, dy, duration); 
  16. //重置状态为滚动 
  17.   setDragState(STATE_SETTLING); 
  18.   return true; 
 
 
 
 
  1. public boolean continueSettling(boolean deferCallbacks) { 
  2.   if (mDragState == STATE_SETTLING) { 
  3.     //是否滚动结束 
  4.     boolean keepGoing = mScroller.computeScrollOffset(); 
  5.     //当前滚动值 
  6.     final int x = mScroller.getCurrX(); 
  7.     final int y = mScroller.getCurrY(); 
  8.     //偏移量 
  9.     final int dx = x - mCapturedView.getLeft(); 
  10.     final int dy = y - mCapturedView.getTop(); 
  11. //便宜操作 
  12.     if (dx != 0) { 
  13.       ViewCompat.offsetLeftAndRight(mCapturedView, dx); 
  14.     } 
  15.     if (dy != 0) { 
  16.       ViewCompat.offsetTopAndBottom(mCapturedView, dy); 
  17.     } 
  18. //回调 
  19.     if (dx != 0 || dy != 0) { 
  20.       mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy); 
  21.     } 
  22.     //滚动结束状态 
  23.     if (!keepGoing) { 
  24.       if (deferCallbacks) { 
  25.         mParentView.post(mSetIdleRunnable); 
  26.       } else { 
  27.         setDragState(STATE_IDLE); 
  28.       } 
  29.     } 
  30.   } 
  31.   return mDragState == STATE_SETTLING; 

在我们的View中

 
 
 
 
  1. override fun computeScroll() { 
  2.   super.computeScroll() 
  3.     if (dragHelper?.continueSettling(true) == true) { 
  4.       invalidate() 
  5.     } 
  6. 以上,就是ViewDragHelper的实现原理和使用方式 
  7. override fun computeScroll() { 
  8.   super.computeScroll() 
  9.     if (dragHelper?.continueSettling(true) == true) { 
  10.       invalidate() 
  11.     } 

以上,就是ViewDragHelper的实现原理和使用方式

总结

ViewDragHelper本质上是对MotionEvent的分析及处理,并提供了一系列的监听回调方法,来帮助我们减轻开发负担,更为方便地处理控件的滑动拖拽逻辑;

是不是觉得很简单,一起加油,各位老铁们;


网站标题:Android源码进阶之ViewDragHelper原理机制解析
网站URL:http://cdbrznjsb.com/article/cccddph.html

其他资讯

让你的专属顾问为你服务