Android应用ViewDragHelper详解及部分源码浅析
2016-01-03 18:51
861 查看
【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】
关于啥是ViewDragHelper,这里不再解释,官方下面这个解释已经很牛逼了,如下:
下面我们先从一个例子说起,然后再进行源码浅析。duang、duang、duang!!!
【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】
该效果实例源码点我下载
这个效果图其实和去年那个项目中的需求十分接近了,为了保密期间才修改为此;当时的需求是这样的:
能够上下平滑收缩;
能够上下手势收缩;
能够上下跟随手指手势收缩一定比例;
上下两层里下层是一个ScrollView,上层是一个ListView的业务逻辑交互;
当时拿到这个需求时我的内心是崩溃的(哈哈,当然不会崩溃,网络用语而已);起初构思时我心里萌生了三套方案,是用属性动画来做呢,还是滑动来做啊,还是用ViewDragHelper来做啊,记得当时思考了一会决定用ViewDragHelper来做,因为他实现起来比较简单,其他的比较麻烦。哈哈,下面给出ViewDragHelper的实现代码:
怎么样,简单吧。效果也有了,代码也有了,ViewDragHelper也体验了,接下来就该苦逼的看源码了。
【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】
可以看见,三个参数的create方法实质调运的还是两个参数的create。其中forParent一般是我们自定义的ViewGroup,cb是控制子View相关状态的回调抽象类实现对象,sensitivity是用来调节mTouchSlop的,至于mTouchSlop是啥以及sensitivity的作用下面会有解释。接着可以发现两个参数的create实质是调运了ViewDragHelper的构造函数,那我们就来分析一下这个构造函数,如下源码:
可以看见,构造函数其实没有做啥特别的事情,主要就是一些参数的实例化,最主要的就是实例化了一个Scroller的内部成员,而且还在ViewDragHelper中重写了插值器,如下:
关于动画插值器这里不再说了,之前博文有讲过。我们还是把视线回到上面实例部分,可以看见,在获取ViewDragHelper实例之后我们接着重写了ViewGroup的onInterceptTouchEvent和onTouchEvent方法,在其中触发了ViewDragHelper的shouldInterceptTouchEvent和processTouchEvent方法。所以下面我们就来分析这两个方法,首先我们看下shouldInterceptTouchEvent方法,如下:
可以看见,上面代码我们只列出了shouldInterceptTouchEvent关于ACTION_DOWN部分的代码,在注释里我将他分为了5步来叙述。我们先来看当ACTION_DOWN触发时Step 1的代码,他通过saveInitialMotion(x, y, pointerId)保存了事件的初始信息,如下是saveInitialMotion方法代码:
接着目光再回到shouldInterceptTouchEvent方法的Step 2,可以发现他尝试通过findTopChildUnder()方法来获取当前触摸点下最顶层的子View,我们可以看下这个方法的源码,如下:
这个方法你不觉的奇怪么?怎么遍历时getChildAt传入的不是index,而是mCallback.getOrderedChildIndex(i)啊,我勒个去,mCallback不就是我们创建ViewDragHelper实例时传入的CallBack对象么?我们还是跳过去看下getOrderedChildIndex()方法吧,如下:
猫了个咪的,这玩意默认啥也没干啊,看样子是某些情况下给我们用来重写的,啥情况?原来在上面findTopChildUnder()方法中返回当前被触摸的View时会有一种坑爹的情况出现,那就是如果在mParentView的同一个位置有多个子View是重叠的,此时又想让重叠的View中下面指定的那个被选中(默认for循环是倒序额)时getOrderedChildIndex()方法的默认实现就搞不定了,所以就需要我们自己去实现Callback里的getOrderedChildIndex()方法来改变查找子View的顺序。譬如:
好了,扯远了,我们还是把目光回到shouldInterceptTouchEvent方法的Step 3,可以发现这里有一个判断,因为第一次触摸屏幕mCapturedView默认为null,所以一开始不会执行这个判断里的代码,同时因为mDragState第一次也不处于STATE_SETTLING状态,所以不执行,等执行时再分析。我们继续往下看Step 4,可以发现这里首先拿了saveInitialMotion方法赋值的结果,然后判断设置的边沿方向进行Callback的onEdgeTouched()方法回调。到这里boolean shouldInterceptTouchEvent()方法的第一次触摸按下ACTION_DOWN所干的事就完了,接着Step 5时直接return了mDragState == STATE_DRAGGING;,因为上面说了,在ACTION_DOWN时mDragState还是STATE_IDLE状态,所以这里返回了false。
至此第一次手指触摸mParentView上时shouldInterceptTouchEvent的ACTION_DOWN流程就结束了,接着我们就是依据这个返回值的情况进行分析(具体参见之前博文关于Android触摸事件传递的分析)。这里返回false就表示mParentView没有拦截这次事件,所以接下来会在mParentView中触发每个子View的boolean dispatchTouchEvent()方法,这时依据Android触摸事件处理机制又分为了两大类情况来处理,一类就是子View消费了这个ACTION_DOWN,一类是没有消费的情况,而这些情况下又分很多中情况,譬如子View消费了本次ACTION_DOWN,mParentView的onTouchEvent()就不会收到ACTION_DOWN了(即ViewDragHelper的processTouchEvent()方法也就收不到ACTION_DOWN了);这时候又有很多情况,譬如当前子View如果调运了requestDisallowInterceptTouchEvent(true),则ACTION_MOVE等来时mParentView的onInterceptTouchEvent()方法就不会被回调(即ViewDragHelper的相关方法也就没意义了);当前子View没有调用requestDisallowInterceptTouchEvent(true),则ACTION_MOVE等来时mParentView的onInterceptTouchEvent()方法还会被执行,此时若onInterceptTouchEvent()方法返回true,则mParentView的onTouchEvent()就会被调运(即ViewDragHelper的processTouchEvent()会被执行)。
额额额,触摸事件传递本来就很复杂,这里情况又很多,所以我们还是不分情况来说了,浅析源码我们牵一条主线走就行,其它的用到时再分析即可。
所以我们来看下子View没有消费这次ACTION_DOWN事件(即子View的dispatchTouchEvent()方法返回false)的流程。此时mParentView的dispatchTouchEvent()方法会调用自己的super.dispatchTouchEvent()方法(即View的dispatchTouchEvent()),然后super会调用mParentView的onTouchEvent()方法(即调用ViewDragHelper的processTouchEvent()方法)。这时onTouchEvent()方法需要返回true(只用在ACTION_DOWN时返回true,否则onTouchEvent()方法无法接收接下来的ACTION_MOVE等事件),当onTouchEvent()返回true以后ACTION_MOVE、ACTION_UP等事件再来时就不会再执行mParentView的onInterceptTouchEvent()了。
那就牵着主线走吧,我们可以看下processTouchEvent()方法的ACTION_DOWN部份源码,如下:
可以看见,该方法里最核心的东西估计就是tryCaptureViewForDrag()方法了,下面我们看下这个方法,如下:
可以看见,通过Callback的tryCaptureView()重写设置是否可以挪动该View,若可以挪动(返回true)则又调运了captureChildView()方法,继续看下captureChildView()方法源码:
到此一次mParentView自己消费事件,子View无拦截ACTION_DOWN的事件处理就彻底结束了。接着就是主流程的ACTION_MOVE事件了,这玩意由于mParentView的onTouchEvent消费了事件且没进行拦截ACTION_DOWN,所以一旦触发时就直接走进了processTouchEvent()方法里,下面是ACTION_MOVE代码:
可以看见,当ACTION_MOVE事件多次触发时该段代码会依据我们重写CallBack的代码分为可以托拽当前View和不能托拽两种情况。
我们先来看下不能托拽的情况,这种相对比较简单,可以看到上面代码的Step 3部分,reportNewEdgeDrags()方法是我们的重点,如下:
可以发现,当我们在ACTION_DOWN触发时重写CallBack的tryCaptureView()方法返回false(当前View不能托拽)且是边沿触摸时移动时首先会回调Callback的onEdgeDragStarted()方法通知自定义ViewGroup开始边沿托拽。接着我们把目光投向Step 4部分,
到此ACTION_MOVE不能托拽的情况就分析完毕了,我们再来看下可以托拽的情况,请看上面processTouchEvent()代码的Step 1,2部分,重点在于dragTo()方法,当我们正常捕获到View时ACTION_MOVE就不停的调用dragTo()对mCaptureView进行拖动,源码如下:
到此可以发现ACTION_MOVE时如果可以托拽则会实时挪动View的位置,同时回调很多方法。具体移动到哪和范围由Callback的clampViewPositionHorizontal()和clampViewPositionVertical()来决定。到此一次ACTION_MOVE事件的触发处理也就分析完毕了。下面就该是松手时ACTION_UP或者ACTION_MOVE被mParentView的上级View拦截触发的ACTION_CANCEL事件了,他们与ACTION_MOVE类似,直接触发processTouchEvent()的ACTION_UP或者ACTION_CANCEL,如下:
可以看见,ACTION_UP和ACTION_CANCEL的实质都是重置资源和通知View触摸被释放,一个调运了releaseViewForPointerUp方法,另一个调运了dispatchViewReleased方法而已。那我们先看下releaseViewForPointerUp方法,源码如下:
哎哟,握草,dispatchViewReleased(xvel, yvel)不就是ACTION_CANCEL传参为0调运的方法吗?也就是说ACTION_CANCEL与ACTION_UP逻辑一致,只是传入的速率不同而已。那我们就来看看这个方法,如下:
可以看见dispatchViewReleased()方法主要就是通过CallBack通知手指松开了,同时将状态置位为STATE_IDLE。但是如果你留意该方法的注释和方法里的mReleaseInProgress变量的话,你一定会有疑惑。下面我就从注释和该变量出现的地方进行下简单分析,然后回过头就知道咋回事了,该方法注释提到了mReleaseInProgress变量与settleCapturedViewAt()和flingCapturedView()方法有关,那我们就来看下是啥关系,查看这两个方法的开头可以发现一个共性如下:
我靠,默认mReleaseInProgress是false,在dispatchViewReleased()中CallBack回调onViewReleased()方法前把他置位了true,onViewReleased()后置位了false。这就是为啥注释里说唯一可以调用ViewDragHelper的settleCapturedViewAt()和flingCapturedView()的地方就是在Callback的onViewReleased()里,这下你指定就明白了,因为别的地方会抛出异常哇。
握草,这两方法为啥这么神奇,为啥只能在CallBack回调onViewReleased()中使用啊,我们先来分析一下就知道了,如下先看settleCapturedViewAt()方法源码:
再看下flingCapturedView()方法,如下:
到此确实很奇怪,为啥这两个方法要这么限制设计?我还是没想明白,请教大神指点一下!但是我全局搜索mScroller的相关调运又发现了新的方法,如下:
这货也能达到上面受限的settleCapturedViewAt()方法的效果,额额额,我凌乱了,这么设计是为啥额,不清楚。不过到此一次完整的ViewDragHelper的触摸移动主流程就分析完成了,其他相关状态请自行分析,对我来说有这些就差不多够了,主流程掌握了基本就能明白他提供的相关API含义了,其它的遇到问题再现查即可。
【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】
可以看见,上面基本就是整个ViewDragHelper的相关public等可控制的API解释了,我们基本上可以通过他们的组合搞出各种自定义的控件来玩玩的。下面我们再粗略给出ViewDragHelper使用的流程,如下:
自定义ViewGroup里通过ViewDragHelper静态工厂方法create()创建实例并实现ViewDragHelper.CallBack抽象类。
在自定义ViewGroup的onInterceptTouchEvent()方法里调用并返回ViewDragHelper的shouldInterceptTouchEvent()方法,在onTouchEvent()方法里调用ViewDragHelper()的processTouchEvent()方法,且返回true(因为ACTION_DOWN时如果子View没有消费事件,我们需要在onTouchEvent()中返回true,否则收不到后续的事件,从而不会产生拖动等效果)。
依据自己需求实现ViewDragHelper.CallBack中相关方法即可。
至此已经实现了子View拖动效果,如果需要Fling或者惯性滚动效果则还需要实现自定义ViewGroup的computeScroll()方法进行手动刷帧。
以上就是使用ViewDragHelper的全过程,可以发现其真的很牛逼。
该控件效果就不解释了,代码也不多,源码可以点我下载即可。
至此整个ViewDragHelper进阶就介绍完了,如果你还认为不够的话,那就可以自行看看官方的DrawerLayout控件实现即可,可以把它当作该文的进阶实例,这里我就不再分析了,我们的项目中重写了DrawerLayout,因为一些特殊的交互需求。
【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】
1 背景
很久没有更新博客了,忙里偷闲产出一篇。写这片文章主要是去年项目中的一个需求,当时三下五除二的将其实现了,但是源码的阅读却一直扔在那迟迟没有时间理会,现在拣起来看看吧,否则心里一直不踏实。关于啥是ViewDragHelper,这里不再解释,官方下面这个解释已经很牛逼了,如下:
[code]/** * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number * of useful operations and state tracking for allowing a user to drag and reposition * views within their parent ViewGroup. */
下面我们先从一个例子说起,然后再进行源码浅析。duang、duang、duang!!!
【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】
2 基础实例
在开始源码浅析之前有必要先通过一个实例来认识下他的功效,下面呈上的效果图就是他的功效之一:该效果实例源码点我下载
这个效果图其实和去年那个项目中的需求十分接近了,为了保密期间才修改为此;当时的需求是这样的:
能够上下平滑收缩;
能够上下手势收缩;
能够上下跟随手指手势收缩一定比例;
上下两层里下层是一个ScrollView,上层是一个ListView的业务逻辑交互;
当时拿到这个需求时我的内心是崩溃的(哈哈,当然不会崩溃,网络用语而已);起初构思时我心里萌生了三套方案,是用属性动画来做呢,还是滑动来做啊,还是用ViewDragHelper来做啊,记得当时思考了一会决定用ViewDragHelper来做,因为他实现起来比较简单,其他的比较麻烦。哈哈,下面给出ViewDragHelper的实现代码:
[code]/** * 垂直DrawerLayout * 实现步骤: * 1.使用静态方法来构ViewDragHelper,需要传入一个ViewDragHelper.Callback对象. * 2.重写onInterceptTouchEvent和onTouchEvent回调ViewDragHelper中对应方法. * 3.在ViewDragHelper.Callback中对视图做操作. * 4.使用ViewDragHelper.smoothSlideViewTo()方法平滑滚动. * 5.自定义一些交互逻辑的自由实现. */ public class VerticalDrawerLayout extends ViewGroup { private ViewDragHelper mTopViewDragHelper; private View mContentView; private View mDrawerView; private int mCurTop = 0; private boolean mIsOpen = true; public VerticalDrawerLayout(Context context) { super(context); init(); } public VerticalDrawerLayout(Context context, AttributeSet attrs) { super(context, attrs); init(); } public VerticalDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { //Step1:使用静态方法构造ViewDragHelper,其中需要传入一个ViewDragHelper.Callback回调对象. mTopViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelperCallBack()); mTopViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_TOP); } //Step2:定义一个ViewDragHelper.Callback回调实现类 private class ViewDragHelperCallBack extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(View child, int pointerId) { //返回ture则表示可以捕获该view,手指摸上一瞬间调运 return child == mDrawerView; } @Override public void onEdgeDragStarted(int edgeFlags, int pointerId) { //setEdgeTrackingEnabled设置的边界滑动时触发 //captureChildView是为了让tryCaptureView返回false依旧生效 mTopViewDragHelper.captureChildView(mDrawerView, pointerId); } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { //手指触摸移动时实时回调, left表示要到的x位置 return super.clampViewPositionHorizontal(child, left, dx); } @Override public int clampViewPositionVertical(View child, int top, int dy) { //手指触摸移动时实时回调, top表示要到的y位置 //保证手指挪动时只能向上,向下最大到0 return Math.max(Math.min(top, 0), -mDrawerView.getHeight()); } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { //手指释放时回调 float movePrecent = (releasedChild.getHeight() + releasedChild.getTop()) / (float) releasedChild.getHeight(); int finalTop = (xvel >= 0 && movePrecent > 0.5f) ? 0 : -releasedChild.getHeight(); mTopViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), finalTop); invalidate(); } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { //mDrawerView完全挪出屏幕则防止过度绘制 mDrawerView.setVisibility((changedView.getHeight()+top == 0)? View.GONE : View.VISIBLE); mCurTop = top; requestLayout(); } @Override public int getViewVerticalDragRange(View child) { if (mDrawerView == null) return 0; return (mDrawerView == child) ? mDrawerView.getHeight() : 0; } @Override public void onViewDragStateChanged(int state) { super.onViewDragStateChanged(state); if (state == ViewDragHelper.STATE_IDLE) { mIsOpen = (mDrawerView.getTop() == 0); } } } @Override public void computeScroll() { if (mTopViewDragHelper.continueSettling(true)) { invalidate(); } } public void closeDrawer() { if (mIsOpen) { mTopViewDragHelper.smoothSlideViewTo(mDrawerView, mDrawerView.getLeft(), -mDrawerView.getHeight()); invalidate(); } } public void openDrawer() { if (!mIsOpen) { mTopViewDragHelper.smoothSlideViewTo(mDrawerView, mDrawerView.getLeft(), 0); invalidate(); } } public boolean isDrawerOpened() { return mIsOpen; } //Step3:重写onInterceptTouchEvent回调ViewDragHelper中对应的方法. @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mTopViewDragHelper.shouldInterceptTouchEvent(ev); } //Step3:重写onTouchEvent回调ViewDragHelper中对应的方法. @Override public boolean onTouchEvent(MotionEvent event) { mTopViewDragHelper.processTouchEvent(event); return true; } @Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } @Override protected LayoutParams generateLayoutParams(LayoutParams p) { return new MarginLayoutParams(p); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(measureWidth, measureHeight); mContentView = getChildAt(0); mDrawerView = getChildAt(1); MarginLayoutParams params = (MarginLayoutParams) mContentView.getLayoutParams(); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( measureWidth- (params.leftMargin + params.rightMargin), MeasureSpec.EXACTLY); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( measureHeight - (params.topMargin + params.bottomMargin), MeasureSpec.EXACTLY); mContentView.measure(childWidthMeasureSpec, childHeightMeasureSpec); mDrawerView.measure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (changed) { MarginLayoutParams params = (MarginLayoutParams) mContentView.getLayoutParams(); mContentView.layout(params.leftMargin, params.topMargin, mContentView.getMeasuredWidth() + params.leftMargin, mContentView.getMeasuredHeight() + params.topMargin); params = (MarginLayoutParams) mDrawerView.getLayoutParams(); mDrawerView.layout(params.leftMargin, mCurTop + params.topMargin, mDrawerView.getMeasuredWidth() + params.leftMargin, mCurTop + mDrawerView.getMeasuredHeight() + params.topMargin); } } }
怎么样,简单吧。效果也有了,代码也有了,ViewDragHelper也体验了,接下来就该苦逼的看源码了。
【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】
3 ViewDragHelper局部源码浅析
上面的例子中我们可以知道,使用ViewDragHelper的第一步就是通过他提供的静态工厂方法create获取实例,因为ViewDragHelper的构造方法是私有的。既然这样那我们先看下这些静态工厂方法,如下:[code]public class ViewDragHelper { 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; } public static ViewDragHelper create(ViewGroup forParent, Callback cb) { return new ViewDragHelper(forParent.getContext(), forParent, cb); } }
可以看见,三个参数的create方法实质调运的还是两个参数的create。其中forParent一般是我们自定义的ViewGroup,cb是控制子View相关状态的回调抽象类实现对象,sensitivity是用来调节mTouchSlop的,至于mTouchSlop是啥以及sensitivity的作用下面会有解释。接着可以发现两个参数的create实质是调运了ViewDragHelper的构造函数,那我们就来分析一下这个构造函数,如下源码:
[code]private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { ...... //对参数进行赋值 mParentView = forParent; mCallback = cb; //通过ViewConfiguration等将dp转px得到mEdgeSize final ViewConfiguration vc = ViewConfiguration.get(context); final float density = context.getResources().getDisplayMetrics().density; mEdgeSize = (int) (EDGE_SIZE * density + 0.5f); //通过ViewConfiguration获取TouchSlop,默认为8 mTouchSlop = vc.getScaledTouchSlop(); //获得允许执行一个fling手势动作的最大速度值 mMaxVelocity = vc.getScaledMaximumFlingVelocity(); //获得允许执行一个fling手势动作的最小速度值 mMinVelocity = vc.getScaledMinimumFlingVelocity(); //通过兼容包的ScrollerCompat实例化Scroller,动画插值器为sInterpolator mScroller = ScrollerCompat.create(context, sInterpolator); }
可以看见,构造函数其实没有做啥特别的事情,主要就是一些参数的实例化,最主要的就是实例化了一个Scroller的内部成员,而且还在ViewDragHelper中重写了插值器,如下:
[code]private static final Interpolator sInterpolator = new Interpolator() { public float getInterpolation(float t) { t -= 1.0f; return t * t * t * t * t + 1.0f; } };
关于动画插值器这里不再说了,之前博文有讲过。我们还是把视线回到上面实例部分,可以看见,在获取ViewDragHelper实例之后我们接着重写了ViewGroup的onInterceptTouchEvent和onTouchEvent方法,在其中触发了ViewDragHelper的shouldInterceptTouchEvent和processTouchEvent方法。所以下面我们就来分析这两个方法,首先我们看下shouldInterceptTouchEvent方法,如下:
[code]//这玩意返回值的作用在前面博客中有分析,我们先来看下ACTION_DOWN事件 public boolean shouldInterceptTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); final int actionIndex = MotionEventCompat.getActionIndex(ev); if (action == MotionEvent.ACTION_DOWN) { //每次ACTION_DOWN都会调用cancel(),该方法中mVelocityTracker被清空,故mVelocityTracker记录的是本次ACTION_DOWN到ACTION_UP的触摸信息 cancel(); } //获取VelocityTracker实例,记录下各个触摸点信息用来计算本次滑动速率等 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 = MotionEventCompat.getPointerId(ev, 0); //Step 1 saveInitialMotion(x, y, pointerId); //Step 2 final View toCapture = findTopChildUnder((int) x, (int) y); //Step 3 if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { tryCaptureViewForDrag(toCapture, pointerId); } //Step 4 final int edgesTouched = mInitialEdgesTouched[pointerId]; if ((edgesTouched & mTrackingEdges) != 0) { mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); } break; } //暂时忽略 ...... } //Step 5 return mDragState == STATE_DRAGGING; }
可以看见,上面代码我们只列出了shouldInterceptTouchEvent关于ACTION_DOWN部分的代码,在注释里我将他分为了5步来叙述。我们先来看当ACTION_DOWN触发时Step 1的代码,他通过saveInitialMotion(x, y, pointerId)保存了事件的初始信息,如下是saveInitialMotion方法代码:
[code] private void saveInitialMotion(float x, float y, int pointerId) { ensureMotionHistorySizeForId(pointerId); mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x; mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y; //getEdgesTouched就是通过mEdgeSize去判断触摸边沿方向是否OK mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y); mPointersDown |= 1 << pointerId; }
接着目光再回到shouldInterceptTouchEvent方法的Step 2,可以发现他尝试通过findTopChildUnder()方法来获取当前触摸点下最顶层的子View,我们可以看下这个方法的源码,如下:
[code]public View findTopChildUnder(int x, int y) { //获取mParentView中子View个数 final int childCount = mParentView.getChildCount(); //倒序遍历整个子View,因为最上面的子View最后插入 for (int i = childCount - 1; i >= 0; i--) { //遍历拿到最靠上且获得触摸焦点的那个子View final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i)); //判断当前DOWN的触摸点是否在该子View范围,也就是说是不是摸上了该子View if (x >= child.getLeft() && x < child.getRight() && y >= child.getTop() && y < child.getBottom()) { return child; } } return null; }
这个方法你不觉的奇怪么?怎么遍历时getChildAt传入的不是index,而是mCallback.getOrderedChildIndex(i)啊,我勒个去,mCallback不就是我们创建ViewDragHelper实例时传入的CallBack对象么?我们还是跳过去看下getOrderedChildIndex()方法吧,如下:
[code]public static abstract class Callback { ...... public int getOrderedChildIndex(int index) { return index; } ...... }
猫了个咪的,这玩意默认啥也没干啊,看样子是某些情况下给我们用来重写的,啥情况?原来在上面findTopChildUnder()方法中返回当前被触摸的View时会有一种坑爹的情况出现,那就是如果在mParentView的同一个位置有多个子View是重叠的,此时又想让重叠的View中下面指定的那个被选中(默认for循环是倒序额)时getOrderedChildIndex()方法的默认实现就搞不定了,所以就需要我们自己去实现Callback里的getOrderedChildIndex()方法来改变查找子View的顺序。譬如:
[code]public int getOrderedChildIndex(int index) { //实现重叠View时让下面的View获得选中 int topIndex = mParentView.indexOfChild(your_top_view); int BottomSelectedIndex = mParentView.indexOfChild(blow_your_top_view_selected); return ((index == topIndex) ? indexBottom : index); }
好了,扯远了,我们还是把目光回到shouldInterceptTouchEvent方法的Step 3,可以发现这里有一个判断,因为第一次触摸屏幕mCapturedView默认为null,所以一开始不会执行这个判断里的代码,同时因为mDragState第一次也不处于STATE_SETTLING状态,所以不执行,等执行时再分析。我们继续往下看Step 4,可以发现这里首先拿了saveInitialMotion方法赋值的结果,然后判断设置的边沿方向进行Callback的onEdgeTouched()方法回调。到这里boolean shouldInterceptTouchEvent()方法的第一次触摸按下ACTION_DOWN所干的事就完了,接着Step 5时直接return了mDragState == STATE_DRAGGING;,因为上面说了,在ACTION_DOWN时mDragState还是STATE_IDLE状态,所以这里返回了false。
至此第一次手指触摸mParentView上时shouldInterceptTouchEvent的ACTION_DOWN流程就结束了,接着我们就是依据这个返回值的情况进行分析(具体参见之前博文关于Android触摸事件传递的分析)。这里返回false就表示mParentView没有拦截这次事件,所以接下来会在mParentView中触发每个子View的boolean dispatchTouchEvent()方法,这时依据Android触摸事件处理机制又分为了两大类情况来处理,一类就是子View消费了这个ACTION_DOWN,一类是没有消费的情况,而这些情况下又分很多中情况,譬如子View消费了本次ACTION_DOWN,mParentView的onTouchEvent()就不会收到ACTION_DOWN了(即ViewDragHelper的processTouchEvent()方法也就收不到ACTION_DOWN了);这时候又有很多情况,譬如当前子View如果调运了requestDisallowInterceptTouchEvent(true),则ACTION_MOVE等来时mParentView的onInterceptTouchEvent()方法就不会被回调(即ViewDragHelper的相关方法也就没意义了);当前子View没有调用requestDisallowInterceptTouchEvent(true),则ACTION_MOVE等来时mParentView的onInterceptTouchEvent()方法还会被执行,此时若onInterceptTouchEvent()方法返回true,则mParentView的onTouchEvent()就会被调运(即ViewDragHelper的processTouchEvent()会被执行)。
额额额,触摸事件传递本来就很复杂,这里情况又很多,所以我们还是不分情况来说了,浅析源码我们牵一条主线走就行,其它的用到时再分析即可。
所以我们来看下子View没有消费这次ACTION_DOWN事件(即子View的dispatchTouchEvent()方法返回false)的流程。此时mParentView的dispatchTouchEvent()方法会调用自己的super.dispatchTouchEvent()方法(即View的dispatchTouchEvent()),然后super会调用mParentView的onTouchEvent()方法(即调用ViewDragHelper的processTouchEvent()方法)。这时onTouchEvent()方法需要返回true(只用在ACTION_DOWN时返回true,否则onTouchEvent()方法无法接收接下来的ACTION_MOVE等事件),当onTouchEvent()返回true以后ACTION_MOVE、ACTION_UP等事件再来时就不会再执行mParentView的onInterceptTouchEvent()了。
那就牵着主线走吧,我们可以看下processTouchEvent()方法的ACTION_DOWN部份源码,如下:
[code]public void processTouchEvent(MotionEvent ev) { //和shouldInterceptTouchEvent相似,省略 ...... switch (action) { case MotionEvent.ACTION_DOWN: { //和shouldInterceptTouchEvent相似,省略解释 final float x = ev.getX(); final float y = ev.getY(); final int pointerId = MotionEventCompat.getPointerId(ev, 0); final View toCapture = findTopChildUnder((int) x, (int) y); saveInitialMotion(x, y, pointerId); //Step 1 重点!!!! tryCaptureViewForDrag(toCapture, pointerId); //和shouldInterceptTouchEvent相似,省略解释 final int edgesTouched = mInitialEdgesTouched[pointerId]; if ((edgesTouched & mTrackingEdges) != 0) { mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); } break; } //省略其他ACTION ...... } }
可以看见,该方法里最核心的东西估计就是tryCaptureViewForDrag()方法了,下面我们看下这个方法,如下:
[code]boolean tryCaptureViewForDrag(View toCapture, int pointerId) { //正在拽就不管了 if (toCapture == mCapturedView && mActivePointerId == pointerId) { // Already done! return true; } //调用了Callback的tryCaptureView()方法,传递触摸到的View和触摸点编号 //Callback的tryCaptureView()决定是否能够拖动当前触摸的View if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) { mActivePointerId = pointerId; //重点 captureChildView(toCapture, pointerId); return true; } return false; }
可以看见,通过Callback的tryCaptureView()重写设置是否可以挪动该View,若可以挪动(返回true)则又调运了captureChildView()方法,继续看下captureChildView()方法源码:
[code]public void captureChildView(View childView, int activePointerId) { ...... //暂存被捕获的这个View的相关信息及触摸信息 mCapturedView = childView; mActivePointerId = activePointerId; //通过Callback的onViewCaptured()方法回调当前View被捕获了 mCallback.onViewCaptured(childView, activePointerId); //设置当前被捕获的子View状态为STATE_DRAGGING //里面会通过mCallback.onViewDragStateChanged(state)回调告知状态 setDragState(STATE_DRAGGING); }
到此一次mParentView自己消费事件,子View无拦截ACTION_DOWN的事件处理就彻底结束了。接着就是主流程的ACTION_MOVE事件了,这玩意由于mParentView的onTouchEvent消费了事件且没进行拦截ACTION_DOWN,所以一旦触发时就直接走进了processTouchEvent()方法里,下面是ACTION_MOVE代码:
[code]public void processTouchEvent(MotionEvent ev) { ...... switch (action) { ...... case MotionEvent.ACTION_MOVE: { //分两种情况,依赖上一个ACTION_DOWN事件 if (mDragState == STATE_DRAGGING) { //ACTION_DOWN时CallBack的tryCaptureView()返回true时对mDragState赋值了STATE_DRAGGING,故此流程 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, index); final float y = MotionEventCompat.getY(ev, index); final int idx = (int) (x - mLastMotionX[mActivePointerId]); final int idy = (int) (y - mLastMotionY[mActivePointerId]); //Step 1 重点!!!!!! dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy); //Step 2 重点!!!!!! saveLastMotion(ev); } else { //ACTION_DOWN时CallBack的tryCaptureView()返回false时对mDragState没进行赋值,故此流程 // Check to see if any pointer is now over a draggable view. final int pointerCount = MotionEventCompat.getPointerCount(ev); for (int i = 0; i < pointerCount; i++) { final int pointerId = MotionEventCompat.getPointerId(ev, i); final float x = MotionEventCompat.getX(ev, i); final float y = MotionEventCompat.getY(ev, i); final float dx = x - mInitialMotionX[pointerId]; final float dy = y - mInitialMotionY[pointerId]; //Step 3 重点!!!!!! reportNewEdgeDrags(dx, dy, pointerId); if (mDragState == STATE_DRAGGING) { // Callback might have started an edge drag. break; } final View toCapture = findTopChildUnder((int) x, (int) y); //Step 4 重点!!!!!! if (checkTouchSlop(toCapture, dx, dy) && tryCaptureViewForDrag(toCapture, pointerId)) { break; } } saveLastMotion(ev); } break; } ...... } }
可以看见,当ACTION_MOVE事件多次触发时该段代码会依据我们重写CallBack的代码分为可以托拽当前View和不能托拽两种情况。
我们先来看下不能托拽的情况,这种相对比较简单,可以看到上面代码的Step 3部分,reportNewEdgeDrags()方法是我们的重点,如下:
[code] //在托拽时该方法会被多次调运 private void reportNewEdgeDrags(float dx, float dy, int pointerId) { int dragsStarted = 0; ......//四个方向,省略三个 if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) { dragsStarted |= EDGE_BOTTOM; } if (dragsStarted != 0) { mEdgeDragsInProgress[pointerId] |= dragsStarted; //该方法只会被调运一次,checkNewEdgeDrag方法中有处理 mCallback.onEdgeDragStarted(dragsStarted, pointerId); } }
可以发现,当我们在ACTION_DOWN触发时重写CallBack的tryCaptureView()方法返回false(当前View不能托拽)且是边沿触摸时移动时首先会回调Callback的onEdgeDragStarted()方法通知自定义ViewGroup开始边沿托拽。接着我们把目光投向Step 4部分,
if (checkTouchSlop(toCapture, dx, dy) && tryCaptureViewForDrag(toCapture, pointerId))判断是我们关注的核心,toCapture其实就是当前捕获的View(这个View在边沿模式时一般摸不到,所以其实拿到的不是想要的childView,所以一般我们会在回调onEdgeDragStarted()方法中重写手动调用captureChildView()方法,传入我们摸不到的View,这样就相当于绕过tryCaptureView将状态设置为STATE_DRAGGING了),下面我们先看下checkTouchSlop()方法,如下:
[code]//检查手指移动的距离有没有超过触发处理移动事件的最短距离mTouchSlop private boolean checkTouchSlop(View child, float dx, float dy) { if (child == null) { return false; } //如果想让某个View滑动,就要返回大于0,否则processTouchEvent()的ACTION_MOVE就不会调用tryCaptureViewForDrag()来捕获当前触摸的View 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; }
到此ACTION_MOVE不能托拽的情况就分析完毕了,我们再来看下可以托拽的情况,请看上面processTouchEvent()代码的Step 1,2部分,重点在于dragTo()方法,当我们正常捕获到View时ACTION_MOVE就不停的调用dragTo()对mCaptureView进行拖动,源码如下:
[code]//left、top为mCapturedView.getLeft()+dx、mCapturedView.getTop()+dy,即期望目标坐标 //dx、dy为前后两次ACTION_MOVE移动的距离 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); //这是View中定义的方法,实质是改变View的mLeft、mRight、mTop、mBottom达到移动View的效果,类似layout()方法的效果 //clampedX为新位置,oldLeft为旧位置,若想不动保证插值为0即可!!!! mCapturedView.offsetLeftAndRight(clampedX - oldLeft); } if (dy != 0) { //重写固定纵坐标移动到的位置 clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); //这是View中定义的方法,实质是改变View的mLeft、mRight、mTop、mBottom达到移动View的效果,类似layout()方法的效果 mCapturedView.offsetTopAndBottom(clampedY - oldTop); } if (dx != 0 || dy != 0) { final int clampedDx = clampedX - oldLeft; final int clampedDy = clampedY - oldTop; //当位置有变化时回调Callback的onViewPositionChanged方法实时通知 mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy); } }
到此可以发现ACTION_MOVE时如果可以托拽则会实时挪动View的位置,同时回调很多方法。具体移动到哪和范围由Callback的clampViewPositionHorizontal()和clampViewPositionVertical()来决定。到此一次ACTION_MOVE事件的触发处理也就分析完毕了。下面就该是松手时ACTION_UP或者ACTION_MOVE被mParentView的上级View拦截触发的ACTION_CANCEL事件了,他们与ACTION_MOVE类似,直接触发processTouchEvent()的ACTION_UP或者ACTION_CANCEL,如下:
[code]public void processTouchEvent(MotionEvent ev) { ...... switch (action) { ...... 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_UP和ACTION_CANCEL的实质都是重置资源和通知View触摸被释放,一个调运了releaseViewForPointerUp方法,另一个调运了dispatchViewReleased方法而已。那我们先看下releaseViewForPointerUp方法,源码如下:
[code]private void releaseViewForPointerUp() { //获得相关速率 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); final float xvel = clampMag( VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), mMinVelocity, mMaxVelocity); final float yvel = clampMag( VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), mMinVelocity, mMaxVelocity); //传入速率 dispatchViewReleased(xvel, yvel); }
哎哟,握草,dispatchViewReleased(xvel, yvel)不就是ACTION_CANCEL传参为0调运的方法吗?也就是说ACTION_CANCEL与ACTION_UP逻辑一致,只是传入的速率不同而已。那我们就来看看这个方法,如下:
[code] private void dispatchViewReleased(float xvel, float yvel) { mReleaseInProgress = true; //通知外部View被释放了 mCallback.onViewReleased(mCapturedView, xvel, yvel); mReleaseInProgress = false; //如果之前是STATE_DRAGGING状态,则复位状态为STATE_IDLE if (mDragState == STATE_DRAGGING) { // onViewReleased didn't call a method that would have changed this. Go idle. setDragState(STATE_IDLE); } }
可以看见dispatchViewReleased()方法主要就是通过CallBack通知手指松开了,同时将状态置位为STATE_IDLE。但是如果你留意该方法的注释和方法里的mReleaseInProgress变量的话,你一定会有疑惑。下面我就从注释和该变量出现的地方进行下简单分析,然后回过头就知道咋回事了,该方法注释提到了mReleaseInProgress变量与settleCapturedViewAt()和flingCapturedView()方法有关,那我们就来看下是啥关系,查看这两个方法的开头可以发现一个共性如下:
[code]if (!mReleaseInProgress) { throw new IllegalStateException("Cannot XXXXXXXXXX outside of a call to " + "Callback#onViewReleased"); }
我靠,默认mReleaseInProgress是false,在dispatchViewReleased()中CallBack回调onViewReleased()方法前把他置位了true,onViewReleased()后置位了false。这就是为啥注释里说唯一可以调用ViewDragHelper的settleCapturedViewAt()和flingCapturedView()的地方就是在Callback的onViewReleased()里,这下你指定就明白了,因为别的地方会抛出异常哇。
握草,这两方法为啥这么神奇,为啥只能在CallBack回调onViewReleased()中使用啊,我们先来分析一下就知道了,如下先看settleCapturedViewAt()方法源码:
[code]//限制最终惯性滚动到的终极位置及滚动过去 public boolean settleCapturedViewAt(int finalLeft, int finalTop) { //表明只能在CallBack回调onViewReleased()中使用 if (!mReleaseInProgress) { throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + "Callback#onViewReleased"); } return forceSettleCapturedViewAt(finalLeft, finalTop, (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId)); } private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) { ...... if (dx == 0 && dy == 0) { // Nothing to do. Send callbacks, be done. mScroller.abortAnimation(); setDragState(STATE_IDLE); return false; } //直接用过Scroller滚动到指定位置 final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); mScroller.startScroll(startLeft, startTop, dx, dy, duration); //滚动时设置状态为STATE_SETTLING setDragState(STATE_SETTLING); return true; }
再看下flingCapturedView()方法,如下:
[code]//不限制终点,由松手时加速度决定惯性滚动过去,fling效果 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"); } //直接用过Scroller滚动 mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), minLeft, maxLeft, minTop, maxTop); //滚动时设置状态为STATE_SETTLING setDragState(STATE_SETTLING); }
到此确实很奇怪,为啥这两个方法要这么限制设计?我还是没想明白,请教大神指点一下!但是我全局搜索mScroller的相关调运又发现了新的方法,如下:
[code]public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) { mCapturedView = child; mActivePointerId = INVALID_POINTER; boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0); if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) { // If we're in an IDLE state to begin with and aren't moving anywhere, we // end up having a non-null capturedView with an IDLE dragState mCapturedView = null; } return continueSliding; }
这货也能达到上面受限的settleCapturedViewAt()方法的效果,额额额,我凌乱了,这么设计是为啥额,不清楚。不过到此一次完整的ViewDragHelper的触摸移动主流程就分析完成了,其他相关状态请自行分析,对我来说有这些就差不多够了,主流程掌握了基本就能明白他提供的相关API含义了,其它的遇到问题再现查即可。
【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】
4 源码局部浅析总结
经过上面的浅析与注释总结归纳如下总结方便日后使用;同时我们可以发现ViewDragHelper的本质其实涉及的知识点还是很多的,主要在事件处理上,不得不佩服该工具类考虑的强大。[code]//常用核心API归纳总结 public class ViewDragHelper { //当前View处于空闲状态,静止 public static final int STATE_IDLE = 0; //当前View处于托动状态中 public static final int STATE_DRAGGING = 1; //当前View处于滚动惯性到settling坐标间的状态 public static final int STATE_SETTLING = 2; //可托拽边缘方向常量 public static final int EDGE_LEFT = 1 << 0; public static final int EDGE_RIGHT = 1 << 1; public static final int EDGE_TOP = 1 << 2; public static final int EDGE_BOTTOM = 1 << 3; public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM; ... //公有静态内部抽象回调类,当ViewDragHelper控制的ViewGroup中View变化时会被回调 public static abstract class Callback { //当托拽状态变化时回调,譬如动画结束后回调为STATE_IDLE等 //state有三种状态,均以STATE_XXXX模式 public void onViewDragStateChanged(int state) {} //当前被触摸的View位置变化时回调 //changedView为位置变化的View,left/top变化时新的x左/y顶坐标,dx/dy为从旧到新的偏移量 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {} //tryCaptureViewForDrag()成功捕获到子View时或者手动调用captureChildView()时回调 public void onViewCaptured(View capturedChild, int activePointerId) {} //当子View被松手或者ACTION_CANCEL时时回调,xvel/yvel为离开屏幕时各方向每秒运动的速率,为px public void onViewReleased(View releasedChild, float xvel, float yvel) {} //当触摸ACTION_DOWN或ACTION_POINTER_DOWN边沿时回调 public void onEdgeTouched(int edgeFlags, int pointerId) {} //返回true锁定edgeFlags对应的边缘,锁定后的边缘就不会回调onEdgeDragStarted() public boolean onEdgeLock(int edgeFlags) { return false; } //ACTION_MOVE且没有锁定边缘时触发 //可在此手动调用captureChildView()触发从边缘拖动子View,有点类似略过tryCaptureView返回false响应重定向其他View的效果 public void onEdgeDragStarted(int edgeFlags, int pointerId) {} //寻找当前触摸点View时回调此方法 //如果需要改变子View的倒序遍历查询顺序则可改写此方法,譬如让重叠的下层View先于上层View被捕获 public int getOrderedChildIndex(int index) { return index; } //返回给定子View在相应方向上可以被拖动的最远距离,默认为0,一般是可被挪动View时指定为指定View的大小等 public int getViewHorizontalDragRange(View child) { return 0; } public int getViewVerticalDragRange(View child) { return 0; } //传递当前触摸上的子View,如果需要当前触摸的子View进行拖拽移动就返回true,否则返回false public abstract boolean tryCaptureView(View child, int pointerId); //决定要拖拽的子View在所属方向上应该移动到的位置 //child为拖拽的子View,left为期望值,dx为挪动差值 public int clampViewPositionHorizontal(View child, int left, int dx) { return 0; } public int clampViewPositionVertical(View child, int top, int dy) { return 0; } } ... //构造工厂方法,sensitivity用来调节mTouchSlop的值,默认一般传递1即可 //sensitivity越大,mTouchSlop越小,对滑动的检测就越敏感,譬如手指move多少才算滑动,否则忽略 public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {...} //设置允许父View的某个边缘可以用来响应托拽 //相当于控制了CallBack对象的onEdgeTouched()和onEdgeDragStarted()方法是否被回调 public void setEdgeTrackingEnabled(int edgeFlags) {...} //两个传递MotionEvent的方法 public boolean shouldInterceptTouchEvent(MotionEvent ev) {...} public void processTouchEvent(MotionEvent ev) {...} //主动在父View内捕获指定的子view用于拖曳,会回调tryCaptureView() public void captureChildView(View childView, int activePointerId) {...} //指定某个View自动滚动到指定的位置,初速度为0,可在任何地方调用 //如果这个方法返回true,那么在接下来动画移动的每一帧中都会回调continueSettling(boolean)方法,直到结束 public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {...} //以松手前的滑动速度为初值,让捕获到的子View自动滚动到指定位置,只能在Callback的onViewReleased()中使用 //如果这个方法返回true,那么在接下来动画移动的每一帧中都会回调continueSettling(boolean)方法,直到结束 public boolean settleCapturedViewAt(int finalLeft, int finalTop) {...} //以松手前的滑动速度为初值,让捕获到的子View在指定范围内fling惯性运动,只能在Callback的onViewReleased()中使用 //如果这个方法返回true,那么在接下来动画移动的每一帧中都会回调continueSettling(boolean)方法,直到结束 public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {...} /** * 在整个settle状态中,这个方法会返回true,deferCallbacks决定滑动是否Runnable推迟,一般推迟 * 在调用settleCapturedViewAt()、flingCapturedView()和smoothSlideViewTo()时, * 需要实现mParentView的computeScroll()方法,如下: * @Override * public void computeScroll() { * if (mDragHelper.continueSettling(true)) { * ViewCompat.postInvalidateOnAnimation(this); * } * } */ public boolean continueSettling(boolean deferCallbacks) {...} ... //设置与获取最小速率,一般保持默认 public void setMinVelocity(float minVel) {...} public float getMinVelocity() {...} //获取当前子View所处状态 public int getViewDragState() {...} //返回可触摸反馈区域边缘大小,单位为px public int getEdgeSize() {...} //返回当前捕获的子View,如果没有则为null public View getCapturedView() {...} //获取当前拖曳的View的Pointer ID public int getActivePointerId() {...} //获取最小触发拖曳动作的灵敏度差值,单位为px public int getTouchSlop() {...} //类似ACTION_CANCEL事件的触发调运 public void cancel() {...} //终止手势,结束动画滚动等,恢复初始STATE_IDLE状态 public void abort() {...} ... }
可以看见,上面基本就是整个ViewDragHelper的相关public等可控制的API解释了,我们基本上可以通过他们的组合搞出各种自定义的控件来玩玩的。下面我们再粗略给出ViewDragHelper使用的流程,如下:
自定义ViewGroup里通过ViewDragHelper静态工厂方法create()创建实例并实现ViewDragHelper.CallBack抽象类。
在自定义ViewGroup的onInterceptTouchEvent()方法里调用并返回ViewDragHelper的shouldInterceptTouchEvent()方法,在onTouchEvent()方法里调用ViewDragHelper()的processTouchEvent()方法,且返回true(因为ACTION_DOWN时如果子View没有消费事件,我们需要在onTouchEvent()中返回true,否则收不到后续的事件,从而不会产生拖动等效果)。
依据自己需求实现ViewDragHelper.CallBack中相关方法即可。
至此已经实现了子View拖动效果,如果需要Fling或者惯性滚动效果则还需要实现自定义ViewGroup的computeScroll()方法进行手动刷帧。
以上就是使用ViewDragHelper的全过程,可以发现其真的很牛逼。
5 ViewDragHelper进阶实战与总结
扯了上面那么多,下面我们给出一个使用ViewDragHelper搞出来的东东,大家一看就知道他很牛逼了。如下是效果图(Ubuntu下面,GIF图Low爆了,请谅解):该控件效果就不解释了,代码也不多,源码可以点我下载即可。
至此整个ViewDragHelper进阶就介绍完了,如果你还认为不够的话,那就可以自行看看官方的DrawerLayout控件实现即可,可以把它当作该文的进阶实例,这里我就不再分析了,我们的项目中重写了DrawerLayout,因为一些特殊的交互需求。
【工匠若水 http://blog.csdn.net/yanbober 未经允许严禁转载,请尊重作者劳动成果。私信联系我】
相关文章推荐
- Android笔记(七十三) Android权限问题整理 非常全面
- android.support.v7.widget.Toolbar 详解
- android listview 异步加载图片并防止错位
- Android AIDL使用详解
- Android 振动
- Android 6.0新技术文章总结
- android recovery
- android 图灵机器人聊天程序
- 安卓StateMachine分析举例---WifiStateMachine
- Android开发之MediaPlayer类
- Android应用开发:LoaderManager在Activity/Fragment中的使用分析
- 使用Android ViewPager与PhotoView实现图片滑动查看
- 谈谈android反编译和防止反编译的方法(转)
- 谈谈android反编译和防止反编译的方法(转)
- sc7731 Android 5.1 LCD驱动简明笔记之二
- android自定义圆角按钮
- 百度地图Android SDK的使用
- Android Button click 事件监听
- Android View的事件分发机制
- Android之Adapter用法总结