ViewDragHelper是framework中不为人知却非常有用的一个工具。SlidingPaneLayout和DrawerLayout都是通过他来实现拖动的过程的,他里面有对TouchEvent时间进行判断。相比之前的gesturedetector手势操作类,他的功能更加强大。用于实现某个viewgroup中的某个控件的拖动过程。
使用方法:
1.初始化ViewDragHelper
public class MyDragLayout extends LinearLayout {
private ViewDragHelper mDragHelper;
private View mDragView;
public MyDragLayout(Context context) {
super(context);
mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
}
public MyDragLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
}
public MyDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
}
public MyDragLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
}
mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());该方法中传入的1.0,是为了在ViewDragHelper中判断helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));mTouchSlop就是可以拖动的最小距离,只有这个距离大于这个值才可以拖动某个控件。
2.重写onInterceptTouchEvent方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mDragHelper.cancel();
return false;
}
return mDragHelper.shouldInterceptTouchEvent(ev);
}
如果是ACTION_CANCEL或者ACTION_UP事件就让事件传递到viewgroup内部去处理,否则就通过在ViewDragHelper中通过mDragHelper.shouldInterceptTouchEvent(ev)去判断事件是否需要处理,如果事件确定拦截了,就会调用当前viewGroup的onTouchEvent事件,如下:
3.重写onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent ev) {
mDragHelper.processTouchEvent(ev);
return true;
}
通过ViewDragHelper里面的processTouchEvent来分析拖动的各种事件,然后该方法中会回调ViewDragHelper.Callback中的各个方法,如下:
3.ViewDragHelper.Callback
class DragHelperCallback extends ViewDragHelper.Callback {
/**
* 用来判断拖动的是哪个view
*/
@Override
public boolean tryCaptureView(View arg0, int arg1) {
return mDragView == arg0;
}
/**
* 在shouldInterceptTouchEvent中的ACTION_DOWN和ACTION_POINTER_DOWN判断中会调用该方法,说明正在触摸边缘
*/
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
super.onEdgeTouched(edgeFlags, pointerId);
Toast.makeText(getContext(), "edgeTouched", Toast.LENGTH_SHORT).show();
}
/**
* shouldInterceptTouchEvent中的ACTION_MOVE中调用
*/
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
super.onEdgeDragStarted(edgeFlags, pointerId);
Toast.makeText(getContext(), "onEdgeDragStarted", Toast.LENGTH_SHORT).show();
}
/**
* 返回将要移动到新的位置,processTouchEvent中的ACTION_MOVE中调用,返回的就是子view新的位置,
* 通过子view和之前view的位置的距离差 通过 mCapturedView.offsetLeftAndRight(clampedX - oldLeft);来实现子view的拖动
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// left为当前拖动的view距离父布局左边的距离,dx为移动的距离,之前的left加上现在移动的dx,等于现在的left
//一般来说left就是等于newleft的值,就是当前手指拖动后的位置,但是为了防止view被脱出屏幕之外,这个时候left的值可能会是负数,所以取
//left和leftBound的最大值来防止view被拉出左边界时任然停留在初始位置,同时应该防止被拉出右边边界,所以leftBound和rightBound都要计算
//排除padding的值
Log.d("DragLayout", "clampViewPositionHorizontal " + left + "," + dx);
final int leftBound = getPaddingLeft();
final int rightBound = getWidth() - mDragView.getWidth() - getPaddingRight();
final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}
/**
* 返回将要移动到新的位置,processTouchEvent中的ACTION_MOVE中调用,返回的就是子view新的位置,
* 通过子view和之前view的位置的距离差 通过 mCapturedView.offsetLeftAndRight(clampedX - oldLeft);来实现子view的拖动
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
Log.d("DragLayout", "clampViewPositionVertical " + top + "," + dy);
//在最上面的top值
final int topBound = getPaddingTop();
//在最底部时候的top值
final int bottomBound = getHeight() - mDragView.getHeight()-getPaddingBottom();
//移动的时候只能在这两个值之间移动,避免移出这两个值设置的边界
final int newTop = Math.min(Math.max(top, topBound), bottomBound);
return newTop;
}
/**
* 会在拖动的过程中调用,在clampViewPositionHorizontal之后调用,如果view有移动就会调用,
* 推动的过程中调用,在view被释放后利用continueSettling来继续移动的过程中也会移动,是通过offsetLeftAndRight来移动的,
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
// 和clampViewPositionHorizontal中的值一样,这里可以做一些动画效果
Log.d("DragLayout", "onViewPositionChanged " + left + "," + dx);
}
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
Log.d("DragLayout", "onViewCaptured ");
}
/**
* 刚开始拖动,状态为1,释放后先调用onViewReleased,由于在onViewReleased中会调用settleCapturedViewAt或者smoothSlideViewTo来使view沉淀下来,就会调用forceSettleCapturedViewAt
* 然后在forceSettleCapturedViewAt中会将状态置位沉淀的状态2,并调用onViewDragStateChanged会回掉,接着就是不停用continueSettling判断并持续调用onViewPositionChanged,当停止移动的
* 时候将状态置为0,并用onViewDragStateChanged回调
*/
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
Log.d("DragLayout", "onViewDragStateChanged " + state);
}
/**
* 拖出边界,或者主动释放的时候调用,该方法中需要调用settleCapturedViewAt或者smoothSlideViewTo,
* 在这两个方法中会启动 mScroller.startScroll计算,然后需要调用invalidate(),这样就会调用到computeScroll,
* 然后在continueSettling实现 view的位移,并持续调用onViewPositionChanged
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
Log.d("DragLayout", "onViewReleased ");
// mDragHelper.settleCapturedViewAt(20, 20);
//如果受释放的时候不需要view回到原来的位置,可以不设置以下两行代码
mDragHelper.smoothSlideViewTo(mDragView, 20, 20);
invalidate();
}
/**
* 似乎只要不为0就可以拖动
*/
@Override
public int getViewHorizontalDragRange(View child) {
return 10;
}
/**
* 似乎只要不为0就可以拖动
*/
@Override
public int getViewVerticalDragRange(View child) {
return 10;
}
}
4.需要实现computeScroll方法
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
Log.d("DragLayout", "computeScroll ");
ViewCompat.postInvalidateOnAnimation(this);
}
}