标签:
SlidingPaneLayout和DrawerLayout,现在这俩个类被广泛的运用,其实研究他们的源码你会发现这两个类都运用了ViewDragHelper来处理拖动。
ViewDragHelper并不是第一个用于分析手势处理的类,gesturedetector也是,但是在和拖动相关的手势分析方面gesturedetector只能说是勉为其难。
ViewDragHelper.Callback是连接ViewDragHelper与view之间的桥梁(这个view一般是指拥子view的容器即parentView)
ViewDragHelper可以检测到是否触及到边缘
ViewDragHelper并不是直接作用于要被拖动的View,而是使其控制的视图容器中的子View可以被拖动,如果要指定某个子view的行为,需要在Callback中想办法;
ViewDragHelper的本质其实是分析onInterceptTouchEvent和onTouchEvent的MotionEvent参数,然后根据分析的结果去改变一个容器中被拖动子View的位置( 通过offsetTopAndBottom(int offset)和offsetLeftAndRight(int offset)方法 ),他能在触摸的时候判断当前拖动的是哪个子View;
虽然ViewDragHelper的实例方法 ViewDragHelper.create(ViewGroup forParent, Callback cb) 可以指定一个被ViewDragHelper处理拖动事件的对象 ,但ViewDragHelper类的设计决定了其适用于被包含在一个自定义ViewGroup之中,而不是对任意一个布局上的视图容器使用ViewDragHelper。
public class VDHLayout extends LinearLayout{
private ViewDragHelper mDragger;
public VDHLayout(Context context, AttributeSet attrs){
super(context, attrs);
//第二个参数就是滑动灵敏度的意思
mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback(){
//这个地方实际上函数返回值为true就代表可以滑动 为false 则不能滑动
@Override
public boolean tryCaptureView(View child, int pointerId){
return true;
}
//这个地方实际上left就代表 你将要移动到的位置的坐标。返回值就是最终确定的移动的位置。
// 我们要让view滑动的范围在我们的layout之内
//实际上就是判断如果这个坐标在layout之内 那我们就返回这个坐标值。
//如果这个坐标在layout的边界处 那我们就只能返回边界的坐标给他。不能让他超出这个范围
//除此之外就是如果你的layout设置了padding的话,也可以让子view的活动范围在padding之内的.
@Override
public int clampViewPositionHorizontal(View child, int left, int dx){
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy){
return top;
}
});
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event){
return mDragger.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event){
mDragger.processTouchEvent(event);
return true;
}
}
onInterceptTouchEvent
中通过使用mDragger.shouldInterceptTouchEvent(event)
来决定我们是否应该拦截当前的事件。onTouchEvent
中通过mDragger.processTouchEvent(event)
处理事件。
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
final int leftBound = getPaddingLeft();
final int rightBound = getWidth() - mDragView.getWidth();
final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - mDragView.getHeight();
final int newTop = Math.min(Math.max(top, topBound), bottomBound);
return newTop;
}
public class VDHLayout extends LinearLayout{
private ViewDragHelper mDragger;
private View mDragView;
private View mAutoBackView;
private View mEdgeTrackerView;
private Point mAutoBackOriginPos = new Point();
public VDHLayout(Context context, AttributeSet attrs){
super(context, attrs);
mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback(){
@Override
public boolean tryCaptureView(View child, int pointerId){
//mEdgeTrackerView禁止直接移动
return child == mDragView || child == mAutoBackView;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx){
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy){
return top;
}
//手指释放的时候回调
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel){
//mAutoBackView手指释放时可以自动回去
if (releasedChild == mAutoBackView){
mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);
invalidate();
}
}
//在边界拖动时回调
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId){
mDragger.captureChildView(mEdgeTrackerView, pointerId);
}
});
mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event){
return mDragger.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event){
mDragger.processTouchEvent(event);
return true;
}
@Override
public void computeScroll(){
if(mDragger.continueSettling(true)){
invalidate();
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b){
super.onLayout(changed, l, t, r, b);
mAutoBackOriginPos.x = mAutoBackView.getLeft();
mAutoBackOriginPos.y = mAutoBackView.getTop();
}
@Override
protected void onFinishInflate(){
super.onFinishInflate();
mDragView = getChildAt(0);
mAutoBackView = getChildAt(1);
mEdgeTrackerView = getChildAt(2);
}
}
第一个View基本没做任何修改,就是演示简单的移动 。
第二个View,实现的是除了移动后,松手自动返回到原本的位置。(注意你拖动的越快,返回的越快)。我们在onLayout之后保存了最开启的位置信息,最主要还是重写了Callback中的onViewReleased
,我们在onViewReleased中判断如果是mAutoBackView则调用settleCapturedViewAt
回到初始的位置。大家可以看到紧随其后的代码是invalidate();因为其内部使用的是mScroller.startScroll,所以别忘了需要invalidate()以及结合computeScroll
方法一起。
第三个View,实现的是边界移动时对View进行捕获。我们在onEdgeDragStarted
回调方法中,主动通过captureChildView
对其进行捕获,该方法可以绕过tryCaptureView,所以我们的tryCaptureView虽然并为返回true,但却不影响。注意如果需要使用边界检测需要添加上mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
。
将View全部加上clickable=true,意思就是子View可以消耗事件。再次运行,你会发现本来可以拖动的View不动了。原因是什么呢?主要是因为,如果子View不消耗事件,那么整个手势(DOWN-MOVE*-UP)都是直接进入onTouchEvent,在onTouchEvent的DOWN的时候就确定了captureView。如果消耗事件,那么就会先走onInterceptTouchEvent
方法,判断是否可以捕获,而在判断的过程中会去判断另外两个回调的方法:getViewHorizontalDragRange
和getViewVerticalDragRange
,只有这两个方法返回大于0的值才能正常的捕获。
所以,记得重写下面这两个方法:
@Override
public int getViewHorizontalDragRange(View child){
return getMeasuredWidth()-child.getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(View child){
return getMeasuredHeight()-child.getMeasuredHeight();
}
ViewDragHelper中拦截和处理事件时,需要会回调CallBack中的很多方法来决定一些事,比如:哪些子View可以移动、对个移动的View的边界的控制等等。
当ViewDragHelper状态发生变化时回调(IDLE,DRAGGING,SETTING[自动滚动时])
当captureview的位置发生改变时回调
当captureview被捕获时回调
当capture view被释放的时候
当captureview被捕获时回调
当触摸到边界时回调。
true的时候会锁住当前的边界,false则unLock。
改变同一个坐标(x,y)去寻找captureView位置的方法。(具体在:findTopChildUnder方法中)
houldInterceptTouchEvent:
DOWN:
getOrderedChildIndex(findTopChildUnder)
->onEdgeTouched
MOVE:
getOrderedChildIndex(findTopChildUnder)
->getViewHorizontalDragRange &
getViewVerticalDragRange(checkTouchSlop)(MOVE中可能不止一次)
->clampViewPositionHorizontal&
clampViewPositionVertical
->onEdgeDragStarted
->tryCaptureView
->onViewCaptured
->onViewDragStateChanged
processTouchEvent:
DOWN:
getOrderedChildIndex(findTopChildUnder)
->tryCaptureView
->onViewCaptured
->onViewDragStateChanged
->onEdgeTouched
MOVE:
->STATE==DRAGGING:dragTo
->STATE!=DRAGGING:
onEdgeDragStarted
->getOrderedChildIndex(findTopChildUnder)
->getViewHorizontalDragRange&
getViewVerticalDragRange(checkTouchSlop)
->tryCaptureView
->onViewCaptured
->onViewDragStateChanged
ViewDragHelper重载了两个create()静态方法,先看两个参数的create()方法:
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}
create()的两个参数很好理解,第一个是我们自定义的ViewGroup,第二个是控制子View拖拽需要的回调对象。create()直接调用了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;
final ViewConfiguration vc = ViewConfiguration.get(context);
final float density = context.getResources().getDisplayMetrics().density;
mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);
mTouchSlop = vc.getScaledTouchSlop();
mMaxVelocity = vc.getScaledMaximumFlingVelocity();
mMinVelocity = vc.getScaledMinimumFlingVelocity();
mScroller = ScrollerCompat.create(context, sInterpolator);
}
这个构造函数是私有的,也是仅有的构造函数,所以外部只能通过create()工厂方法来创建ViewDragHelper实例了。 这里要求了我们传递的自定义ViewGroup和回调对象不能为空,否则会直接抛出异常中断程序。在这里也初始化了一些触摸滑动需要的参考值和辅助类。
再看三个参数的create()方法:
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
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;
}
第二个参数sensitivity是用来调节mTouchSlop的值。sensitivity越大,mTouchSlop越小,对滑动的检测就越敏感。 例如sensitivity为1时,前后触摸点距离超过20dp才进行滑动处理,现在sensitivity为2的话,前后触摸点距离超过10dp就进行处理了。
当mParentView(自定义ViewGroup)被触摸时,首先会调用mParentView的onInterceptTouchEvent(MotionEvent ev), 接着就调用shouldInterceptTouchEvent(MotionEvent ev) ,所以先来看看这个方法的ACTION_DOWN部分:
/**
* Check if this event as provided to the parent view‘s onInterceptTouchEvent should
* cause the parent to intercept the touch event stream.
*
* @param ev MotionEvent provided to onInterceptTouchEvent
* @return true if the parent view should return true from onInterceptTouchEvent
*/
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
final int actionIndex = MotionEventCompat.getActionIndex(ev);
if (action == MotionEvent.ACTION_DOWN) {
// Reset things for a new event stream, just in case we didn‘t get
// the whole previous stream.
cancel();
}
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);
saveInitialMotion(x, y, pointerId);
final View toCapture = findTopChildUnder((int) x, (int) y);
// Catch a settling view if possible.
if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
tryCaptureViewForDrag(toCapture, pointerId);
}
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
// 其他case暂且省略
}
return mDragState == STATE_DRAGGING;
}
看9~21行,首先是关于多点触控.mVelocityTracker记录下触摸的各个点信息,稍后可以用来计算本次滑动的速率,每次发生ACTION_DOWN事件都会调用cancel(), 而在cancel()方法里mVelocityTracker又被清空了,所以mVelocityTracker记录下的是本次ACTION_DOWN事件直至ACTION_UP事件发生后 (下次ACTION_DOWN事件发生前)的所有触摸点的信息。
再来看24~42行case MotionEvent.ACTION_DOWN部分,先是调用saveInitialMotion(x, y, pointerId)保存手势的初始信息,即ACTION_DOWN发生时的触摸点坐标(x、y)、 触摸手指编号(pointerId),如果触摸到了mParentView的边缘还会记录触摸的是哪个边缘。接着调用findTopChildUnder((int) x, (int) y); 来获取当前触摸点下最顶层的子View,看findTopChildUnder的源码:
/**
* Find the topmost child under the given point within the parent view‘s coordinate system.
* The child order is determined using {@link Callback#getOrderedChildIndex(int)}.
*
* @param x X position to test in the parent‘s coordinate system
* @param y Y position to test in the parent‘s coordinate system
* @return The topmost child view under (x, y) or null if none found.
*/
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;
}
代码很简单,注释里也说明的很清楚了。如果在同一个位置有两个子View重叠,想要让下层的子View被选中, 那么就要实现Callback里的getOrderedChildIndex(int index)方法来改变查找子View的顺序;例如topView(上层View)的index是4, bottomView(下层View)的index是3,按照正常的遍历查找方式(getOrderedChildIndex()默认直接返回index),会选择到topView, 要想让bottomView被选中就得这么写:
public int getOrderedChildIndex(int index) {
int indexTop = mParentView.indexOfChild(topView);
int indexBottom = mParentView.indexOfChild(bottomView);
if (index == indexTop) {
return indexBottom;
}
return index;
}
32~35行,这里还看到了一个mDragState成员变量,它共有三种取值:
mCapturedView默认为null,所以一开始不会执行这里的代码,mDragState处于STATE_SETTLING状态时才会执行tryCaptureViewForDrag(), 执行的情况到后面再分析.
37~40行调用了Callback.onEdgeTouched向外部通知mParentView的某些边缘被触摸到了,mInitialEdgesTouched是在刚才调用过的saveInitialMotion方法里进行赋值的。
ACTION_DOWN部分处理完了,跳过switch语句块,剩下的代码就只有return mDragState == STATE_DRAGGING;。在ACTION_DOWN部分没有对mDragState进行赋值,其默认值为STATE_IDLE,所以此处返回false。
那么返回false后接下来应该是会调用哪个方法呢,接下来会在mParentView的所有子View中寻找响应这个Touch事件的View(会调用每个子View 的dispatchTouchEvent()方法,dispatchTouchEvent里一般又会调用onTouchEvent()).
1.如果没有子View消费这次事件(子View的dispatchTouchEvent()返回都是false),会调用mParentView的super.dispatchTouchEvent (ev),即View中的dispatchTouchEvent(ev),然后调用mParentView的onTouchEvent()方法, 再调用ViewDragHelper的processTouchEvent(MotionEvent ev)方法。此时(ACTION_DOWN事件发生时)mParentView的onTouchEvent()要返回true, onTouchEvent()才能继续接受到接下来的ACTION_MOVE、ACTION_UP等事件,否则无法完成拖动(除了ACTION_DOWN外的其他事件发生时返回true或false都 不会影响接下来的事件接受),因为拖动的相关代码是写在processTouchEvent()里的ACTION_MOVE部分的。 要注意的是返回true后mParentView的onInterceptTouchEvent()就不会收到后续的ACTION_MOVE、ACTION_UP等事件了。
2.如果有子View消费了本次ACTION_DOWN事件,mParentView的onTouchEvent()就收不到ACTION_DOWN事件了, 也就是ViewDragHelper的processTouchEvent(MotionEvent ev)收不到ACTION_DOWN事件了。 不过只要该View没有调用过requestDisallowInterceptTouchEvent(true),mParentView的onInterceptTouchEvent()的ACTION_MOVE部分还是会执行的, 如果在此时返回了true拦截了ACTION_MOVE事件,processTouchEvent()里的ACTION_MOVE部分也就会正常执行,拖动也就没问题了。 onInterceptTouchEvent()的ACTION_MOVE部分具体做了怎样的处理,稍后再来解析。
接下来对这两种情况逐一解析。
假设没有子View消费这次事件,根据刚才的分析最终就会调用processTouchEvent(MotionEvent ev)的ACTION_DOWN部分:
/**
* Process a touch event received by the parent view. This method will dispatch callback events
* as needed before returning. The parent view‘s onTouchEvent implementation should call this.
*
* @param ev The touch event received by the parent view
*/
public void processTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
final int actionIndex = MotionEventCompat.getActionIndex(ev);
if (action == MotionEvent.ACTION_DOWN) {
// Reset things for a new event stream, just in case we didn‘t get
// the whole previous stream.
cancel();
}
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);
final View toCapture = findTopChildUnder((int) x, (int) y);
saveInitialMotion(x, y, pointerId);
// Since the parent is already directly processing this touch event,
// there is no reason to delay for a slop before dragging.
// Start immediately if possible.
tryCaptureViewForDrag(toCapture, pointerId);
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
// 其他case暂且省略
}
}
这段代码跟shouldInterceptTouchEvent()里ACTION_DOWN那部分基本一致,唯一区别就是这里没有约束条件直接调用了tryCaptureViewForDrag()方法,现在来看看这个方法:
/**
* Attempt to capture the view with the given pointer ID. The callback will be involved.
* This will put us into the "dragging" state. If we‘ve already captured this view with
* this pointer this method will immediately return true without consulting the callback.
*
* @param toCapture View to capture
* @param pointerId Pointer to capture with
* @return true if capture was successful
*/
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;
}
这里调用了Callback的tryCaptureView(View child, int pointerId)方法,把当前触摸到的View和触摸手指编号传递了过去,在tryCaptureView()中决定是否需要拖动当前触摸到的View,如果要拖动当前触摸到的View就在tryCaptureView()中返回true,让ViewDragHelper把当前触摸的View捕获下来, 接着就调用了captureChildView(toCapture, pointerId)方法:
/**
* Capture a specific child view for dragging within the parent. The callback will be notified
* but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to
* capture this view.
*
* @param childView Child view to capture
* @param activePointerId ID of the pointer that is dragging the captured child view
*/
public void captureChildView(View childView, int activePointerId) {
if (childView.getParent() != mParentView) {
throw new IllegalArgumentException("captureChildView: parameter must be a descendant " +
"of the ViewDragHelper‘s tracked parent view (" + mParentView + ")");
}
mCapturedView = childView;
mActivePointerId = activePointerId;
mCallback.onViewCaptured(childView, activePointerId);
setDragState(STATE_DRAGGING);
}
代码很简单,在captureChildView(toCapture, pointerId)中将要拖动的View和触摸的手指编号记录下来,并调用Callback的onViewCaptured(childView, activePointerId)通知外部有子View被捕获到了, 再调用setDragState()设置当前的状态为STATE_DRAGGING,看setDragState()源码:
void setDragState(int state) {
if (mDragState != state) {
mDragState = state;
mCallback.onViewDragStateChanged(state);
if (mDragState == STATE_IDLE) {
mCapturedView = null;
}
}
}
状态改变后会调用Callback的onViewDragStateChanged()通知状态的变化。
假设ACTION_DOWN发生后在mParentView的onTouchEvent()返回了true,接下来就会执行ACTION_MOVE部分:
public void processTouchEvent(MotionEvent ev) {
switch (action) {
// 省略其他case...
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);
} else {
// 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];
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag.
break;
}
final View toCapture = findTopChildUnder((int) x, (int) y);
if (checkTouchSlop(toCapture, dx, dy) &&
tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
}
break;
}
// 省略其他case...
}
}
要注意的是,如果一直没松手,这部分代码会一直调用。这里先判断mDragState是否为STATE_DRAGGING,而唯一调用setDragState(STATE_DRAGGING)的地方就是tryCaptureViewForDrag()了, 刚才在ACTION_DOWN里调用过tryCaptureViewForDrag(),现在又要分两种情况。
如果刚才在ACTION_DOWN里捕获到要拖动的View,那么就执行if部分的代码,这个稍后解析,先考虑没有捕获到的情况。没有捕获到的话,mDragState依然是STATE_IDLE,然后会执行else部分的代码。这里主要就是检查有没有哪个手指触摸到了要拖动的View上,触摸上了就尝试捕获它,然后让mDragState变为STATE_DRAGGING,之后就会执行if部分的代码了。这里还有两个方法涉及到了Callback里的方法,需要来解析一下, 分别是reportNewEdgeDrags()和checkTouchSlop(),先看reportNewEdgeDrags():
private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
int dragsStarted = 0;
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
dragsStarted |= EDGE_LEFT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
dragsStarted |= EDGE_TOP;
}
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) {
dragsStarted |= EDGE_RIGHT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
dragsStarted |= EDGE_BOTTOM;
}
if (dragsStarted != 0) {
mEdgeDragsInProgress[pointerId] |= dragsStarted;
mCallback.onEdgeDragStarted(dragsStarted, pointerId);
}
}
这里对四个边缘都做了一次检查,检查是否在某些边缘产生拖动了,如果有拖动,就将有拖动的边缘记录在mEdgeDragsInProgress中,再调用Callback的onEdgeDragStarted(int edgeFlags, int pointerId)通知某个边缘开始产生拖动了。虽然reportNewEdgeDrags()会被调用很多次(因为processTouchEvent()的ACTION_MOVE部分会执行很多次), 但mCallback.onEdgeDragStarted(dragsStarted, pointerId)只会调用一次,具体的要看checkNewEdgeDrag()这个方法:
private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
final float absDelta = Math.abs(delta);
final float absODelta = Math.abs(odelta);
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;
}
再来看checkTouchSlop()方法:
/**
* Check if we‘ve crossed a reasonable touch slop for the given child view.
* If the child cannot be dragged along the horizontal or vertical axis, motion
* along that axis will not count toward the slop check.
*
* @param child Child to check
* @param dx Motion since initial position along X axis
* @param dy Motion since initial position along Y axis
* @return true if the touch slop has been crossed
*/
private boolean checkTouchSlop(View child, float dx, float dy) {
if (child == null) {
return false;
}
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;
}
这个方法主要就是检查手指移动的距离有没有超过触发处理移动事件的最短距离(mTouchSlop)了,注意dx和dy指的是当前触摸点到ACTION_DOWN触摸到的点的距离。这里先检查Callback的getViewHorizontalDragRange(child)和getViewVerticalDragRange(child)是否大于0,如果想让某个View在某个方向上滑动,就要在那个方向对应的方法里返回大于0的数。否则在processTouchEvent()的ACTION_MOVE部分就不会调用tryCaptureViewForDrag()来捕获当前触摸到的View了,拖动也就没办法进行了。
回到processTouchEvent()的ACTION_MOVE部分,假设现在我们的手指已经滑动到可以被捕获到的View上了,也都正常的实现了Callback中的相关方法,让tryCaptureViewForDrag()正常的捕获到触摸到的View了,下一次ACTION_MOVE时就执行if部分的代码了,也就是开始不停的调用dragTo()对mCaptureView进行真正拖动了,看dragTo()方法:
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);
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);
}
}
参数dx和dy是前后两次ACTION_MOVE移动的距离,left和top分别为mCapturedView.getLeft() + dx, mCapturedView.getTop() + dy,也就是期望的移动后的坐标,对View的getLeft()等方法不理解的请参阅Android View坐标getLeft, getRight, getTop, getBottom。
这里通过调用offsetLeftAndRight()和offsetTopAndBottom()来完成对mCapturedView移动,这两个是View中定义的方法,看它们的源码就知道内部是通过改变View的mLeft、mRight、mTop、mBottom,即改变View在父容器中的坐标位置,达到移动View的效果,所以如果调用mCapturedView的layout(int l, int t, int r, int b)方法也可以实现移动View的效果。
具体要移动到哪里,由Callback的clampViewPositionHorizontal()和clampViewPositionVertical()来决定的,如果不想在水平方向上移动,在clampViewPositionHorizontal(View child, int left, int dx)里直接返回child.getLeft()就可以了,这样clampedX - oldLeft的值为0,这里调用mCapturedView.offsetLeftAndRight(clampedX - oldLeft)就不会起作用了。垂直方向上同理。
最后会调用Callback的onViewPositionChanged(mCapturedView, clampedX, clampedY,clampedDx, clampedDy)通知捕获到的View位置改变了,并把最终的坐标(clampedX、clampedY)和最终的移动距离(clampedDx、 clampedDy)传递过去。
ACTION_MOVE部分就算告一段落了,接下来应该是用户松手触发ACTION_UP,或者是达到某个条件导致后续的ACTION_MOVE被mParentView的上层View给拦截了而收到ACTION_CANCEL,一起来看这两个部分:
public void processTouchEvent(MotionEvent ev) {
// 省略
switch (action) {
// 省略其他case
case MotionEvent.ACTION_UP: {
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();
}
cancel();
break;
}
case MotionEvent.ACTION_CANCEL: {
if (mDragState == STATE_DRAGGING) {
dispatchViewReleased(0, 0);
}
cancel();
break;
}
}
}
这两个部分都是重置所有的状态记录,并通知View被放开了,再看下releaseViewForPointerUp()和dispatchViewReleased()的源码:
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);
}
releaseViewForPointerUp()里也调用了dispatchViewReleased(),只不过传递了速率给它,这个速率就是由processTouchEvent()的mVelocityTracker追踪算出来的。再看dispatchViewReleased():
/**
* Like all callback events this must happen on the UI thread, but release
* involves some extra semantics. During a release (mReleaseInProgress)
* is the only time it is valid to call {@link #settleCapturedViewAt(int, int)}
* or {@link #flingCapturedView(int, int, int, int)}.
*/
private void dispatchViewReleased(float xvel, float yvel) {
mReleaseInProgress = true;
mCallback.onViewReleased(mCapturedView, xvel, yvel);
mReleaseInProgress = false;
if (mDragState == STATE_DRAGGING) {
// onViewReleased didn‘t call a method that would have changed this. Go idle.
setDragState(STATE_IDLE);
}
}
首先这两个方法是干什么的呢。在现实生活中保龄球的打法是,先做扔的动作让球的速度达到最大,然后突然松手,由于惯性,保龄球就以最后松手前的速度为初速度抛出去了,直至自然停止,或者撞到边界停止,这种效果叫fling。 flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)就是对捕获到的View做出这种fling的效果,用户在屏幕上滑动松手之前也会有一个滑动的速率。fling也引出来的一个问题,就是不知道View最终会滚动到哪个位置,最后位置是在启动fling时根据最后滑动的速度来计算的(flingCapturedView的四个参数int minLeft, int minTop, int maxLeft, int maxTop可以限定最终位置的范围),假如想要让View滚动到指定位置应该怎么办,答案就是使用settleCapturedViewAt(int finalLeft, int finalTop)。
为什么唯一可以调用settleCapturedViewAt()和flingCapturedView()的地方是Callback的onViewReleased()呢?看看它们的源码
/**
* Settle the captured view at the given (left, top) position.
* The appropriate velocity from prior motion will be taken into account.
* If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*
* @param finalLeft Settled left edge position for the captured view
* @param finalTop Settled top edge position for the captured view
* @return true if animation should continue through {@link #continueSettling(boolean)} calls
*/
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
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));
}
/**
* Settle the captured view based on standard free-moving fling behavior.
* The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame
* to continue the motion until it returns false.
*
* @param minLeft Minimum X position for the view‘s left edge
* @param minTop Minimum Y position for the view‘s top edge
* @param maxLeft Maximum X position for the view‘s left edge
* @param maxTop Maximum Y position for the view‘s top edge
*/
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");
}
mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(),
(int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
(int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
minLeft, maxLeft, minTop, maxTop);
setDragState(STATE_SETTLING);
}
这两个方法里一开始都会判断mReleaseInProgress为false,如果为false就会抛一个IllegalStateException异常, 而mReleaseInProgress唯一为true的时候就是在dispatchViewReleased()里调用onViewReleased()的时候。
ViewDragHelper还有一个移动View的方法是smoothSlideViewTo(View child, int finalLeft, int finalTop),看下它的源码:
/**
* Animate the view <code>child</code> to the given (left, top) position.
* If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*
* <p>This operation does not count as a capture event, though {@link #getCapturedView()}
* will still report the sliding view while the slide is in progress.</p>
*
* @param child Child view to capture and animate
* @param finalLeft Final left position of child
* @param finalTop Final top position of child
* @return true if animation should continue through {@link #continueSettling(boolean)} calls
*/
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;
}
可以看到它不受mReleaseInProgress的限制,所以可以在任何地方调用,效果和settleCapturedViewAt()类似,因为它们最终都调用了forceSettleCapturedViewAt()来启动自动滚动,区别在于settleCapturedViewAt()会以最后松手前的滑动速率为初速度将View滚动到最终位置,而smoothSlideViewTo()滚动的初速度是0。 forceSettleCapturedViewAt()里有地方调用了Callback里的方法,所以再来看看这个方法:
/**
* Settle the captured view at the given (left, top) position.
*
* @param finalLeft Target left position for the captured view
* @param finalTop Target top position for the captured view
* @param xvel Horizontal velocity
* @param yvel Vertical velocity
* @return true if animation should continue through {@link #continueSettling(boolean)} calls
*/
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;
if (dx == 0 && dy == 0) {
// Nothing to do. Send callbacks, be done.
mScroller.abortAnimation();
setDragState(STATE_IDLE);
return false;
}
final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
setDragState(STATE_SETTLING);
return true;
}
可以看到自动滑动是靠Scroll类完成,在这里生成了调用mScroller.startScroll()需要的参数。再来看看计算滚动时间的方法computeSettleDuration():
private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {
xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
final int absDx = Math.abs(dx);
final int absDy = Math.abs(dy);
final int absXVel = Math.abs(xvel);
final int absYVel = Math.abs(yvel);
final int addedVel = absXVel + absYVel;
final int addedDistance = absDx + absDy;
final float xweight = xvel != 0 ? (float) absXVel / addedVel :
(float) absDx / addedDistance;
final float yweight = yvel != 0 ? (float) absYVel / addedVel :
(float) absDy / addedDistance;
int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));
int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));
return (int) (xduration * xweight + yduration * yweight);
}
clampMag()方法确保参数中给定的速率在正常范围之内。最终的滚动时间还要经过computeAxisDuration()算出来,通过它的参数可以看到最终的滚动时间是由dx、 xvel、mCallback.getViewHorizontalDragRange()共同影响的。看computeAxisDuration():
private int computeAxisDuration(int delta, int velocity, int motionRange) {
if (delta == 0) {
return 0;
}
final int width = mParentView.getWidth();
final int halfWidth = width / 2;
final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
final float distance = halfWidth + halfWidth *
distanceInfluenceForSnapDuration(distanceRatio);
int duration;
velocity = Math.abs(velocity);
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
final float range = (float) Math.abs(delta) / motionRange;
duration = (int) ((range + 1) * BASE_SETTLE_DURATION);
}
return Math.min(duration, MAX_SETTLE_DURATION);
}
如果给定的速率velocity不为0,就通过距离除以速率来算出时间;如果velocity为0,就通过要滑动的距离(delta)除以总的移动范围(motionRange,就是Callback里getViewHorizontalDragRange()、getViewVerticalDragRange()返回值)来算出时间。最后还会对计算出的时间做过滤,最终时间反正是不会超过MAX_SETTLE_DURATION的,源码里的取值是600毫秒,所以不用担心在Callback里getViewHorizontalDragRange()、getViewVerticalDragRange()返回错误的数而导致自动滚动时间过长了。
在调用settleCapturedViewAt()、flingCapturedView()和smoothSlideViewTo()时,还需要实现mParentView的computeScroll():
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
至此,整个触摸流程和ViewDragHelper的重要的方法都过了一遍。之前在讨论shouldInterceptTouchEvent()的ACTION_DOWN部分执行完后应该再执行什么的时候,还有一种情况没有展开详解,就是有子View消费了本次ACTION_DOWN事件的情况,现在来看看这种情况。
假设现在shouldInterceptTouchEvent()的ACTION_DOWN部分执行完了,也有子View消费了这次的ACTION_DOWN事件,那么接下来就会调用mParentView的onInterceptTouchEvent()的ACTION_MOVE部分,接着调用ViewDragHelper的shouldInterceptTouchEvent()的ACTION_MOVE部分:
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
// 省略...
switch (action) {
// 省略其他case...
case MotionEvent.ACTION_MOVE: {
// First to cross a touch slop over a draggable view wins. Also report edge drags.
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];
final View toCapture = findTopChildUnder((int) x, (int) y);
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
if (pastSlop) {
// check the callback‘s
// getView[Horizontal|Vertical]DragRange methods to know
// if you can move at all along an axis, then see if it
// would clamp to the same value. If you can‘t move at
// all in every dimension with a nonzero range, bail.
final int oldLeft = toCapture.getLeft();
final int targetLeft = oldLeft + (int) dx;
final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
targetLeft, (int) dx);
final int oldTop = toCapture.getTop();
final int targetTop = oldTop + (int) dy;
final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
(int) dy);
final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
toCapture);
final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
if ((horizontalDragRange == 0 || horizontalDragRange > 0
&& newLeft == oldLeft) && (verticalDragRange == 0
|| verticalDragRange > 0 && newTop == oldTop)) {
break;
}
}
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag
break;
}
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
break;
}
// 省略其他case...
}
return mDragState == STATE_DRAGGING;
}
如果有多个手指触摸到屏幕上了,对每个触摸点都检查一下,看当前触摸的地方是否需要捕获某个View。这里先用findTopChildUnder(int x, int y)寻找触摸点处的子View,再用checkTouchSlop(View child, float dx, float dy)检查当前触摸点到ACTION_DOWN触摸点的距离是否达到了mTouchSlop,达到了才会去捕获View。
接着看19~41行if (pastSlop){…}
部分,这里检查在某个方向上是否可以进行拖动,检查过程涉及到getView[Horizontal|Vertical]DragRange和clampViewPosition[Horizontal|Vertical]四个方法。如果getView[Horizontal|Vertical]DragRange返回都是0,就会认作是不会产生拖动。clampViewPosition[Horizontal|Vertical]返回的是被捕获的View的最终位置,如果和原来的位置相同,说明我们没有期望它移动,也就会认作是不会产生拖动的。不会产生拖动就会在39行直接break,不会执行后续的代码,而后续代码里有调用tryCaptureViewForDrag(),所以不会产生拖动也就不会去捕获View了,拖动也不会进行了。 如果检查到可以在某个方向上进行拖动,就会调用后面的tryCaptureViewForDrag()捕获子View,如果捕获成功,mDragState就会变成STATE_DRAGGING,shouldInterceptTouchEvent()返回true,mParentView的onInterceptTouchEvent()返回true,后续的移动事件就会在mParentView的onTouchEvent()执行了,最后执行的就是mParentView的processTouchEvent()的ACTION_MOVE部分,拖动正常进行。
回头再看之前在shouldInterceptTouchEvent()的ACTION_DOWN部分留下的坑:
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
// 省略其他部分...
switch (action) {
// 省略其他case...
case MotionEvent.ACTION_DOWN: {
// 省略其他部分...
// Catch a settling view if possible.
if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
tryCaptureViewForDrag(toCapture, pointerId);
}
// 省略其他部分...
}
// 省略其他case...
}
return mDragState == STATE_DRAGGING;
}
现在应该明白这部分代码会在什么情况下执行了。当我们松手后捕获的View处于自动滚动的过程中时,用户再次触摸屏幕,就会执行这里的tryCaptureViewForDrag()尝试捕获View,如果捕获成功,mDragState就变为STATE_DRAGGING了,shouldInterceptTouchEvent()就返回true了,然后就是mParentView的onInterceptTouchEvent()返回true,接着执行mParentView的onTouchEvent(),再执行processTouchEvent()的ACTION_DOWN部分。此时(ACTION_DOWN事件发生时)mParentView的onTouchEvent()要返回true,onTouchEvent()才能继续接受到接下来的ACTION_MOVE、ACTION_UP等事件,否则无法完成拖动。
1.tryCaptureViewForDrag()成功捕获到子View时
1.1 shouldInterceptTouchEvent()的ACTION_DOWN部分捕获到
1.2 shouldInterceptTouchEvent()的ACTION_MOVE部分捕获到
1.3 processTouchEvent()的ACTION_MOVE部分捕获到
2.调用settleCapturedViewAt()、smoothSlideViewTo()、flingCapturedView()时
3.拖动View松手时(processTouchEvent()的ACTION_UP、ACTION_CANCEL)
4.自动滚动停止时(continueSettling()里检测到滚动结束时)
5.外部调用abort()时
1.在dragTo()里被调用(正在被拖动时)
2.在continueSettling()里被调用(自动滚动时)
3.外部调用abort()时被调用
1.在shouldInterceptTouchEvent()的ACTION_DOWN里成功捕获
2.在shouldInterceptTouchEvent()的ACTION_MOVE里成功捕获
3.在processTouchEvent()的ACTION_MOVE里成功捕获
4.手动调用captureChildView()
demo:http://pan.baidu.com/s/1dD1Qx01#path=%252FAndroid_cnblogs
ViewDragHelper.zip
参考:http://souly.cn/%E6%8A%80%E6%9C%AF%E5%8D%9A%E6%96%87/2015/09/23/viewDragHelper%E8%A7%A3%E6%9E%90/
标签:
原文地址:http://www.cnblogs.com/android-blogs/p/4961634.html