有好几种方式比如:
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(topbottomBound){
return top-dy;
}
return top;
}
最后来做个总结,ViewDragHelper的工作原理有如下几个步骤:
1)初始化ViewDragHelper对象并定义CallBack对象
2)当手指按下时处理ACTION_DOWN事件,根据手指的的位置找到parentView中对应的childView即toCapture
3)如果toCapture不为空,那么就调用CallBack的tryCaptureView方法判断toView是否允许对其进行拖拽,或者说该方法决定了parentView这个ViewGroup中有哪些childView可以进行拖拽
4)如果tryCaptureView方法返回true,说明允许toCapture进行拖拽,并调用callBack的onViewCaptured方法和ViewDragHelper的captureChildView方法进行mCaptureView的初始化等动作
5)找到mCaptureView后,响应ACTION_MOVE事件,调用dragTo方法进行移动,移动的过程中可以重写callback的相关方法对mCaptureView进行一些自己控制
6)在拖拽结束也就是手指离开屏幕的时候可以重写CallBakc的onViewReleased方法做最后的处理
7)水平或者竖直滚动必须重写CallBack的clampViewPositionHorizontal或者clampViewPositionVertical两个方法
鉴于篇幅原因,ViewDragHelper的原理以及CallBack的几个先关方法及说明完毕,并且提供了一个简单的demo,不过这个demo还有好些问题需要改进,同时关于ViewDragHelper还有好多没有说明,这将在下一篇博文中一 一提起,本篇比较零碎,欢迎批评指正 ,demo代码下载