有好几种方式比如:
1)调用View的layout方法,设置View的布局位置
2)修改View的layoutParam参数
3)ParentView调用scrollTo/scrollBy方法改动childView的位置
当然还有其他方法,这个方法就是本篇博客的主角,这两个方法就是View类中的offsetLeftAndRight和offsetTopAndBottom,通过这两个方法可以用来修改一个View的的位置;比如要让一个View从初始位置水平竖直方向个移动100,简单如下代码就可以:
view.offsetLeftAndRight(100); view.offsetTopAndBottom(100);那么这个滚动View的方法跟scrollTo/scrollBy方法的区别就是scroll/scrollBy方法不会改变一个View的getLeft,getRight,getBottom,getTop的值,而offsetLeftAndRight和offsetTopAndBottom却可以改变上面的四个方法的返回值
这么个简单的调用就实现了View位置的改变,为什么说这两个方法是本篇博客的主角呢,且本篇博客的标题是ViewDragHelper,那么这两个方法与今天要讲的有什么关联?下面就开始正式开始吧!
既然是实现拖动,肯定是手指有按下并移动的动作(MOVE事件),这就是需要从它的事件处理先理一理是怎么实现滚动的:查看ViewDragHelper关于事件处理的方法processTouchEvent,该方法在处理MOVE事件的时候有如下的代码:
case MotionEvent.ACTION_MOVE: { if (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]); //实现拖动 dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy); saveLastMotion(ev); }
/** *(left,right)构成了此时View滚动过后左上角的位置 *@param left 当前View即将滚动到的x轴的位置,即新的x的位置 *@param right 当前View即滚动到的y轴的位置,即新的y的位置 *@dx 水平滚动的距离 *@dy 竖直滚动的距离 **/ private void dragTo(int left, int top, int dx, int dy) { int clampedX = left; int clampedY = top; //获取滚动之前view的位置坐标(oldLeft,oldTop) final int oldLeft = mCapturedView.getLeft(); final int oldTop = mCapturedView.getTop(); //如果水平滚动的距离不为空 if (dx != 0) { //在处理手指滚动距离的时候,可以对这个距离做最后的计算处理 clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); mCapturedView.offsetLeftAndRight(clampedX - oldLeft); } //如果竖直滚动的距离不为空 if (dy != 0) { clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); mCapturedView.offsetTopAndBottom(clampedY - oldTop); } if (dx != 0 || dy != 0) { final int clampedDx = clampedX - oldLeft; final int clampedDy = clampedY - oldTop; //滚动结束后的操作 mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy); } }
那么分析到这个地方,ViewDragHelper拖拽View的基本原理就很清晰了:
1)捕获拖动目标View,在ViewDragHelper里也就是mCaptureView
2)计算滚动的距离,然后处理MOVE时候,调用dragTo方法实现mCaptrueView的拖动
3)在实现拖动之前或者拖动之后的时候可以自定义CallBack的onViewPositionChanged方法来对拖动进行处理。
由于ViewDragHelper的构造器是私有的,所以外部不能直接用new 来初始化,不过它提供了两个create方法来完成获取ViewDragHelper对象,这两个方法最终会调用ViewDragHelper的私有构造器来完成了初始化:
private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { if (forParent == null) { throw new IllegalArgumentException("Parent view may not be null"); } if (cb == null) { throw new IllegalArgumentException("Callback may not be null"); } mParentView = forParent; mCallback = cb; //此处有省略代码 ..... mScroller = ScrollerCompat.create(context, sInterpolator); }
构造器初始化了一个ViewGroup即forParent和一个Callback即cb;同时还初始化了一个mScroller这个ScrollerCompat,这个类有好几种实现,mScroller这个对象主要负责View的拖拽滚动等效果,简单的原理可参考《 View的滚动原理简单解析(二)》 ,其实ScrollerCompatcreate方法返回的一个对象ScrollerCompatImplBase就是Scroller这个在起到作用,具体的内部细节就不多说.
下面就说说怎么初始化mCaptureView这个拖动的View的,ViewDragHelper提供了两个方法对他进行初始化:
public void captureChildView(View childView, int activePointerId) { //mCaptureView的parentView必须是mParentView if (childView.getParent() != mParentView) { throw new IllegalArgumentException("captureChildView: parameter must be a descendant " + "of the ViewDragHelper's tracked parent view (" + mParentView + ")"); } //初始化mCaptureView mCapturedView = childView; mActivePointerId = activePointerId; //调用callback的onViewCaptured,对要移动的view可以在这个方法在移动之前做一些处理 mCallback.onViewCaptured(childView, activePointerId); //表明该childView处于拖动drag状态 setDragState(STATE_DRAGGING); } //设置被拖动的view的状态 void setDragState(int state) { mParentView.removeCallbacks(mSetIdleRunnable); //更新状态 if (mDragState != state) { mDragState = state; //拖动状态发生了改变的时候调用 mCallback.onViewDragStateChanged(state); if (mDragState == STATE_IDLE) { mCapturedView = null; } } }
1)要拖动的captureView的parentView,必须是创建ViewDragHelper对象时初始化的mParentView
2)在捕获到mCaptureView的时候,可以调用Callback的onViewCapture方法里面做一些自定义的处理工作
3)此时设置mCaptureView的状态为STATE_DRAGGING状态
4)在拖拽状态发生改变的时候android monkey可以设置callback的onViewDragState根据不同的状态做一些处理工作
先不说这个captureChidView方法具体的怎么调用,要想获取mCaptureView正常逻辑来说应该是在手指按下屏幕的时候进行捕获,所以来分析ViewDragHelper这个类里面关于ACTION_DOWN事件的处理:在processTouchEvent这个方法里面有如下代码:
case MotionEvent.ACTION_DOWN: {//处理down事件 final float x = ev.getX(); final float y = ev.getY(); final int pointerId = MotionEventCompat.getPointerId(ev, 0); //查找手指位置对应的childView final View toCapture = findTopChildUnder((int) x, (int) y); saveInitialMotion(x, y, pointerId); //尝试捕获要拖动的View或者检查是否有childView可以拖动 tryCaptureViewForDrag(toCapture, pointerId);
关于手指按下事件有两个要点
1)根据手指所在的位置调用findTopChildUnde(x,y)来获取parentView中的子View,它的实现很简单,就是循环Viewgoup或者parentView中的子View,如果手指在子View的布局范围内就返回(注意该法方法循环的的起始下标):
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; }
boolean tryCaptureViewForDrag(View toCapture, int pointerId) { //如果在之前调用tryCaptureViewForDrag方法的时候已经获取了mCapturedView //就直接返回true对mCaptureView进行拖拽 if (toCapture == mCapturedView && mActivePointerId == pointerId) { // Already done! return true; } //第一次调用tryCaptrueViewForDrag时候 if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) { mActivePointerId = pointerId; captureChildView(toCapture, pointerId); return true; } return false; }上面的方法第一个if条件很简单,就不多作解释,第二个条件是决定parentView中是否有可拖拽的childView的关键所在:toCapture!=null即手指所在的位置有childView存在,并且callback的tryCaptureView返回true,关键就在于tryCaptureView,它的意义所在就是判断toCapture这个View是否允许对它进行拖拽,如果允许就返回true,同时调用了captureChildView对mCaptureView进行了初始化(详见上面关于captureChildView方法的说明):如果返回false则不允许toCapture拖拽
所以我们在设置重写Callback的tryCaptureView的时候可以这么写:
@Override public boolean tryCaptureView(View child, int pointerId) { return true; } 或者 @Override public boolean tryCaptureView(View child, int pointerId) { return child == getChildAt(0); }
从上面的分析可以知道我们可以通过定义一个继承了Callback类来重写里面的onViewCapture,onViewDragState,onViewPositionChanged来分别对mCaptureView拖拽的过程中做一些我们自己内的一些操作。上面都是说的callback的方法都是拖拽过程中让我们控制,那么在拖拽结束的时候能否继续让我们这么monkey做一些其他的处理呢?正如上面我们分析dragTo的方式,我们去看看processTouchEvent这个方法里面对ACTION_UP是怎么处理的:
case MotionEvent.ACTION_UP: { if (mDragState == STATE_DRAGGING) {//如果处于拖动状态 releaseViewForPointerUp(); } cancel(); break; }如上面的代码,在处理up事件的时候调用了方法,这个方法里面有调用 dispatchViewReleased方法:
private void dispatchViewReleased(float xvel, float yvel) { mReleaseInProgress = true; mCallback.onViewReleased(mCapturedView, xvel, yvel);//调用callback的onViewRelease方法 mReleaseInProgress = false; if (mDragState == STATE_DRAGGING) { //改变状态 setDragState(STATE_IDLE); } } 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; }
发现在dispatchViewReleased方法里面调用了callback的onViewReleased方法,这样的话我们就可以在定义自己的CallBack对象的时候重写这个方法在drag结束的时候做一些其他的操作!同时在dispatchViewReleased方法里面还设置了mCaptureView的拖拽状态为IDLE状态
所以简单自定义一个简单的拖拽效果的View就很简单了:扩展LinearLayouttuo拖拽它的第一个子View,简单的页面效果如下(博文最后悔附上demo代码)
:
上图就是一个自定义带有ViewDragHelper的LinearLayout,该LinearLayout里面就是ImageView.ViewDragHelper的初始化以及callBack的代码如下:
@Override protected void onFinishInflate() { super.onFinishInflate(); //获取要拖拽的对象 mDragHelper.captureChildView(getChildAt(0), 0); } private class Callback extends ViewDragHelper.Callback { @Override public void onViewCaptured(View capturedChild, int activePointerId) { Log.e(tag,"start drag"); } @Override public boolean tryCaptureView(View child, int pointerId) { return child == getChildAt(0); } /** * 当view的位置发生变化的时候调用,具体是在调用dragTo方法的时候调用 * if (dx != 0 || dy != 0) { * final int clampedDx = clampedX - oldLeft; * final int clampedDy = clampedY - oldTop; * mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, * clampedDx, clampedDy); * } * 该方法在clampViewPositionHorizontal之后在调用 * * @param left view改变后的新的X轴位置 * @param top view位置改变后的新的y的位置 * @param dx 水平方向滚动的距离 * @param dy 水平方向滚动的距离 */ @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { super.onViewPositionChanged(changedView, left, top, dx, dy); Log.e(tag,"(left,right)===("+left + ","+ top+")"); } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { Log.e(tag," drag over"); } }
这很明显可以发现onViewPositionChanged方法随着手指的移动会持续调用,但是里面的left和top值始终不变,事实上在拖动的过程中图片并没有发生移动。这是为什么呢?其实这个问题要从dragTo方法上入手,在进行水平或者竖直移动的时候关键是下面两个方法:
if (dx != 0) { //在处理手指滚动距离的时候,可以对这个距离做最后的计算处理 clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); mCapturedView.offsetLeftAndRight(clampedX - oldLeft); } //如果竖直滚动的距离不为空 if (dy != 0) { clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); mCapturedView.offsetTopAndBottom(clampedY - oldTop); }传入offsetLeftAndRight/offsetTopAndBottom方法里传入的offset的值需要经过clampViewPositionHorizontal/offsetTopAndBottom方法计算过后的结果,然后拿计算过后的结果减去mCamptureView的当前位置的坐标left/top.且因为此时(left,top)==(0,0),并且clampViewPositionHorizontal/offsetTopAndBottom默认的返回值也是0,所以传入offsetLeftAndRight/offsetTopAndBottom的offset的偏移量也是0,所以拖动起来位置不变。
所以这个方法解决起来也很简单,在ImageView的xml配置里面设置一下marginLeft或者marginRight值就可以:
android:layout_marginLeft="100dp" android:layout_marginTop="100dp"但是这个还有个问题你会发现,因为没有重写clampViewPositionHorizontal/clampViewPositionVertical的返回值 ,clampedX-oldLeft/clampedY-oldTop)的计算记过实际上位-oldLeft/-oldTop.也就是说在这种情况下上面的小狗图片拖动一下位置会立马回到(0,0)坐标点,然后继续拖拽小狗图片位置又不会变化了(之所以说是立马,是因为实际并没有过度效果,因为直接相当于调用了offseetLeftAndRight)。
所以看来才扩展CallBack的时候我们必须重写clampViewPositionHorizontal/clampViewPositionVertical这两个方法,正如ViewDragHelper对这两个方法的注释上说的那样:
the extending class must override this method and provide the desired clamping.
所以为了真正让上图中的小狗随着手指的拖拽运动起来,我们在重写了clampViewPositionHorizontal/clampViewPositionVertical这两个方法,代码如下:
/** * 该方法在dragTo方法中调用,计算的结果借给View的offsetLeftAndRight或者 * offsetTopAndBottom来用,该方法调用在onViewPositionChanged之前 * 计算的结果交给 view的offsetLeftAndRight方法 * @param child 要拖动的View * @param left 下一时刻view的左上角的X坐标 * @param dx 水平方向即将滚动的距离 */ @Override public int clampViewPositionHorizontal(View child, int left, int dx) { return left; } /** * 该方法在dragTo方法中调用,计算的结果借给View的offsetLeftAndRight或者 * offsetTopAndBottom来用,该方法调用在onViewPositionChanged之前 * 计算的结果交给 view的offsetLeftAndRight方法 * @param child 要拖动的View * @param top 下一时刻view的左上角的Y坐标 * @param dy 竖直向即将滚动的距离 */ @Override public int clampViewPositionVertical(View child, int top, int dy) { return top; }
为了方便说明问题,以下只对clampViewPositionHorizontal进行说明,如果按照上面所说的直接返回left话会发现小狗图片会超过parentView也就是会变的在屏幕中看不到,那么怎么做限制呢?比如当childView的左边缘拖动到parentView的左/右边缘的时候,手指继续向左/右移动的时候childView的位置不在变化,该怎么办呢?这个很简单,其实按照刚才所说如果offsetLeftAndRight(0)的话,View的水平位置是不会发生改变的。那么我们需要做的就很简单了,就是让clampedX - oldLeft = 0即可(见如下两行代码)
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); mCapturedView.offsetLeftAndRight(clampedX - oldLeft);其中clampViewPositionHorizontal的第二个参数有如下关系:
left=oldLeft+dx
所以为了让clampedX - oldLeft = 0就不是问题了,如果达到左边界或者右边界的话clampViewPositionHorizontal方法直接返回left-dx即可。代码如下:
public int clampViewPositionHorizontal(View child, int left, int dx) { final int leftPadding = getPaddingLeft(); //parentView可用的右边界的位置 final int rightBound = getWidth()-getPaddingRight(); //如果达到了左边界或者右边界 if(leftPadding>left||left+ child.getWidth()>rightBound){ return left-dx; } //child没有碰到parentView的边缘直接滚动到left的位置 return left; }
@Override public int clampViewPositionVertical(View child, int top, int dy) { final int topPadding = getPaddingTop(); final int bottomBound = getHeight()-getPaddingBottom(); //当childView遇到了parentView的上边缘或者下边缘 if(top<topPadding||top+child.getHeight()>bottomBound){ return top-dy; } return top; }
最后来做个总结,ViewDragHelper的工作原理有如下几个步骤:
1)初始化ViewDragHelper对象并定义CallBack对象
2)当手指按下时处理ACTION_DOWN事件,根据手指的的位置找到parentView中对应的childView即toCapture
3)如果toCapture不为空,那么就调用CallBack的tryCaptureView方法判断toView是否允许对其进行拖拽
4)如果tryCaptureView方法返回true,说明允许toCapture进行拖拽,并调用callBack的onViewCaptured方法和ViewDragHelper的captureChildView方法进行mCaptureView的初始化等动作
5)找到mCaptureView后,响应ACTION_MOVE事件,调用dragTo方法进行移动,移动的过程中可以重写callback的相关方法对mCaptureView进行一些自己控制
6)水平或者竖直滚动必须重写CallBack的clampViewPositionHorizontal或者clampViewPositionVertical两个方法
鉴于篇幅原因,ViewDragHelper的原理以及CallBack的几个先关方法及说明完毕,并且提供了一个简单的demo,不过这个demo还有好些问题需要改进,同时关于ViewDragHelper还有好多没有说明,这将在下一篇博文中一 一提起,本篇比较零碎,欢迎批评指正 ,demo代码下载