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); } }