ViewDragHelper的一些总结
View设置了clickable=true不能触发移动
只要设置了clickable=true或者本身就是可被点击的,如button,那么view久无法被移动。原因很简单,就是事件被子view给消费掉了,viewGroup无法被会滴。
源码分析
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
...
case MotionEvent.ACTION_MOVE: {
...
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
...
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
...
}
...
}
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;
}
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;
}
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);
}
shouldInterceptTouchEvent返回true的条件是mDragState == STATE_DRAGGING,然而mDragState是在tryCaptureViewForDrag方法中被设置为STATE_DRAGGING的。
如果horizontalDragRange == 0 && verticalDragRange == 0这个条件一直为true的话,tryCaptureViewForDrag方法就得不到调用了。
而horizontalDragRange和verticalDragRange分别是Callback的getViewHorizontalDragRange和getViewVerticalDragRange方法返回的值,这两个方法默认情况下都返回0。
- getViewHorizontalDragRange:返回子View水平方向可以被拖拽的范围
- getViewVerticalDragRange:返回子View垂直方向可以被拖拽的范围
一般的写法,在Callback中加入:
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight() - child.getMeasuredHeight();
}
@Override
public int getViewHorizontalDragRange(View child) {
return getMeasuredWidth() - child.getMeasuredWidth();
}
思考:为啥非clickable的view不需要加??
这涉及到了android的事件分发机制,由于子view不消费事件,那么事件都会给到上层viewGroup。而上层group只会触发一次shouldInterceptTouchEvent,即down事件,后面就不会了。只触发onTouch了。
它走的逻辑就不一样,它走的是processTouchEvent里的逻辑。
ViewDragHelper.Callback
boolean tryCaptureView(View child, int pointerId)
需要进行事件捕获的view,返回true即表示需要捕获。
void onViewCaptured(View capturedChild, int activePointerId)
一旦tryCaptureView成功,这个方法即被触发。
void onViewDragStateChanged(int state)
拖动状态改变时会调用此方法,状态state有STATE_IDLE、STATE_DRAGGING、STATE_SETTLING三种取值。
- tryCaptureViewForDrag()成功捕获到子View时
- shouldInterceptTouchEvent()的ACTION_DOWN部分捕获到
- shouldInterceptTouchEvent()的ACTION_MOVE部分捕获到
- processTouchEvent()的ACTION_MOVE部分捕获到
- 调用settleCapturedViewAt()、smoothSlideViewTo()、flingCapturedView()时
- 拖动View松手时(processTouchEvent()的ACTION_UP、ACTION_CANCEL)
- 自动滚动停止时(continueSettling()里检测到滚动结束时)
- 外部调用abort()时
int getViewHorizontalDragRange(View child)
int getViewVerticalDragRange(View child)
返回给定的child在相应的方向上可以被拖动的最远距离,默认返回0。?????????
void onViewReleased(View releasedChild, float xvel, float yvel)
拖动View松手时(processTouchEvent()的ACTION_UP)或被父View拦截事件时(processTouchEvent()的ACTION_CANCEL)会调用此方法。
可以在这里做手指离开后的处理逻辑。
void onViewPositionChanged(View changedView, int left, int top, int dx, int dy)
正在被拖动的View或者自动滚动的View的位置改变时会调用此方法。
int clampViewPositionHorizontal(View child, int left, int dx)
int clampViewPositionVertical(View child, int top, int dy)
child在某方向上被拖动时会调用对应方法,返回值是child移动过后的坐标位置,clampViewPositionHorizontal()返回child移动过后的left值,clampViewPositionVertical()返回child移动过后的top值。
int getOrderedChildIndex(int index)
在寻找当前触摸点下的子View时会调用此方法,寻找到的View会提供给tryCaptureViewForDrag()来尝试捕获。如果需要改变子View的遍历查询顺序可改写此方法,例如让下层的View优先于上层的View被选中。
void onEdgeTouched(int edgeFlags, int pointerId)
ACTION_DOWN或ACTION_POINTER_DOWN事件发生时如果触摸到监听的边缘会调用此方法。edgeFlags的取值为EDGE_LEFT、EDGE_TOP、EDGE_RIGHT、EDGE_BOTTOM的组合。
boolean onEdgeLock(int edgeFlags)
返回true表示锁定edgeFlags对应的边缘,锁定后的那些边缘就不会在onEdgeDragStarted()被通知了,默认返回false不锁定给定的边缘,edgeFlags的取值为EDGE_LEFT、EDGE_TOP、EDGE_RIGHT、EDGE_BOTTOM其中之一。
void onEdgeDragStarted(int edgeFlags, int pointerId)
ACTION_MOVE事件发生时,检测到开始在某些边缘有拖动的手势,也没有锁定边缘,会调用此方法。edgeFlags取值为EDGE_LEFT、EDGE_TOP、EDGE_RIGHT、EDGE_BOTTOM的组合。可在此手动调用captureChildView()触发从边缘拖动子View的效果。
三个开启自动滚动的方法
settleCapturedViewAt(int finalLeft, int finalTop)
以松手前的滑动速度为初速动,让捕获到的View自动滚动到指定位置。只能在Callback的onViewReleased()中调用。【如实现,自动回到触发的位置的功能】
flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)
以松手前的滑动速度为初速动,让捕获到的View在指定范围内fling。只能在Callback的onViewReleased()中调用。
smoothSlideViewTo(View child, int finalLeft, int finalTop)
指定某个View自动滚动到指定的位置,初速度为0,可在任何地方调用。
实例
第一个View,就是演示简单的移动 第二个View,演示除了移动后,松手自动返回到原本的位置。(注意你拖动的越快,返回的越快) 第三个View,边界移动时对View进行捕获。
package com.yuqirong.swipelistview.activity;
import android.content.Context;
import android.graphics.Point;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
/**
* Created by panda on 2017/9/13.
*/
public class VDHLinearLayout extends LinearLayout {
private ViewDragHelper mDragger;
private View mDragView;
private View mAutoBackView;
private View mEdgeTrackerView;
private Point mAutoBackOriginPos = new Point();
public VDHLinearLayout(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 int getViewHorizontalDragRange(View child) {
return getMeasuredWidth() - child.getMeasuredWidth();
}
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight() - child.getMeasuredHeight();
}
//手指释放的时候回调
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//mAutoBackView手指释放时可以自动回去
if (releasedChild == mAutoBackView) {
mDragger.flingCapturedView(0, 0, 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);
}
}