Android ViewDragHelper类

ViewDragHelper是个工具类,用来辅助ViewGroup内控件的操作和拖拽。

1. ViewDragHelper创建

ViewDragHelper通过静态方法create()创建,

public static ViewDragHelper create(ViewGroup forParent, Callback cb)
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb)

Callback是一个回调,用来处理事件,
主要方法

// 是否需要捕获这个child
abstract boolean tryCaptureView(View child, int pointerId)

// 修改水平位置
public int clampViewPositionHorizontal(View child, int left, int dx)

// 修改垂直位置
public int clampViewPositionVertical(View child, int top, int dy)

// 选择拦截对象
public void onViewCaptured(@NonNull View capturedChild, int activePointerId)// 手势释放
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel)

// 边缘被触摸
public void onEdgeTouched(int edgeFlags, int pointerId)

// 边缘开始拉拽
public void onEdgeDragStarted(int edgeFlags, int pointerId)

2. 简单的拖拽实现

自定义ViewDragHelperView实现FrameLayout,在onInterceptTouchEvent()onTouchEvent()方法中,添加ViewDragHelperView进行拦截处理。

public class ViewDragHelperView extends FrameLayout {
    private ViewDragHelper mViewDragHelper;

    public ViewDragHelperView(@NonNull Context context) {
        this(context, null);
    }

    public ViewDragHelperView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ViewDragHelperView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mViewDragHelper = ViewDragHelper.create(this, new CustomCallback());
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

    private class CustomCallback extends ViewDragHelper.Callback {

        @Override
        public boolean tryCaptureView(@NonNull View view, int pointerId) {
            return true;
        }

        @Override
        public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
            return left;
        }

        @Override
        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
            return top;
        }
    }

}

布局文件,ViewDragHelperView中有两个子控件

<?xml version="1.0" encoding="utf-8"?>
<com.blog.demo.custom.widget.ViewDragHelperView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:layout_width="@dimen/margin_dpi_50"
        android:layout_height="@dimen/margin_dpi_50"
        android:layout_margin="@dimen/margin_dpi_50"
        android:src="@drawable/icon_pyq"/>

    <ImageView
        android:layout_width="@dimen/margin_dpi_50"
        android:layout_height="@dimen/margin_dpi_50"
        android:layout_margin="100dp"
        android:src="@drawable/icon_link"/>

</com.blog.demo.custom.widget.ViewDragHelperView>

显示如下
在这里插入图片描述

3. 拖拽后的回弹

CallBack里,利用onViewCaptured记录控件的初始位置,并在onViewReleased()时移动回初始位置。

private class CustomCallback extends ViewDragHelper.Callback {
    private int mLeft;
    private int mTop;

    ...  ...

    // 获取捕捉的子控件
    @Override
    public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
        mLeft = capturedChild.getLeft();
        mTop = capturedChild.getTop();
    }

    // 拖拽结束
    @Override
    public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
        mViewDragHelper.settleCapturedViewAt(mLeft, mTop);
        invalidate();
    }
}

settleCapturedViewAt()方法里面,实际是使用Scroll实现的回弹

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) mVelocityTracker.getXVelocity(mActivePointerId),
            (int) mVelocityTracker.getYVelocity(mActivePointerId));
}

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

所以在ViewcomputeScroll()方法里面需要添加ViewDragHelpercontinueSettling()方法进行计算。

public class ViewDragHelperView extends FrameLayout {
    private ViewDragHelper mViewDragHelper;

    ... ...

    @Override
    public void computeScroll() {
        if (mViewDragHelper.continueSettling(true)) {
            invalidate();
        }
    }
}

public boolean continueSettling(boolean deferCallbacks) {
    if (mDragState == STATE_SETTLING) {
        boolean keepGoing = mScroller.computeScrollOffset();
        final int x = mScroller.getCurrX();
        final int y = mScroller.getCurrY();
        final int dx = x - mCapturedView.getLeft();
        final int dy = y - mCapturedView.getTop();

        if (dx != 0) {
            ViewCompat.offsetLeftAndRight(mCapturedView, dx);
        }
        if (dy != 0) {
            ViewCompat.offsetTopAndBottom(mCapturedView, dy);
        }

        if (dx != 0 || dy != 0) {
            mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
        }

        if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
            // Close enough. The interpolator/scroller might think we're still moving
            // but the user sure doesn't.
            mScroller.abortAnimation();
            keepGoing = false;
        }

        if (!keepGoing) {
            if (deferCallbacks) {
                mParentView.post(mSetIdleRunnable);
            } else {
                setDragState(STATE_IDLE);
            }
        }
    }

    return mDragState == STATE_SETTLING;
}

显示如下
在这里插入图片描述

4. 代码解析

processTouchEvent(MotionEvent ev)方法处理手势事件

public void processTouchEvent(@NonNull MotionEvent ev) {
    final int action = ev.getActionMasked();
    final int actionIndex = ev.getActionIndex();

    ... ...

    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 = ev.getPointerId(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 MotionEvent.ACTION_MOVE: {
            if (mDragState == STATE_DRAGGING) {
                // If pointer is invalid then skip the ACTION_MOVE.
                if (!isValidPointerForActionMove(mActivePointerId)) break;

                final int index = ev.findPointerIndex(mActivePointerId);
                final float x = ev.getX(index);
                final float y = ev.getY(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);
            }
        }

        ... ...
        
        case MotionEvent.ACTION_UP: {
            if (mDragState == STATE_DRAGGING) {
                releaseViewForPointerUp();
            }
            cancel();
            break;
        }

        case MotionEvent.ACTION_CANCEL: {
            if (mDragState == STATE_DRAGGING) {
                dispatchViewReleased(0, 0);
            }
            cancel();
            break;
        }
    }
}

ACTION_DOWN事件

findTopChildUnder()查找拦截子控件

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

tryCaptureViewForDrag()拦截事件,并调用CallbacktryCaptureView()方法回调,查询是否拦截该事件。

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

captureChildView()方法设置拦截对象,调用CallbackonViewCaptured()方法回调。

public void captureChildView(@NonNull 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);
}

setDragState()方法修改状态,调用CallbackonViewDragStateChanged()方法回调。

void setDragState(int state) {
    mParentView.removeCallbacks(mSetIdleRunnable);
    if (mDragState != state) {
        mDragState = state;
        mCallback.onViewDragStateChanged(state);
        if (mDragState == STATE_IDLE) {
            mCapturedView = null;
        }
    }
}

ACTION_MOVE事件

dragTo()方法移动拦截控件,CallbackclampViewPositionHorizontal()clampViewPositionVertical()分别返回水平和垂直的移动量,最后调用CallbackonViewPositionChanged()方法回调

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);
        ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
    }
    if (dy != 0) {
        clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
        ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
    }

    if (dx != 0 || dy != 0) {
        final int clampedDx = clampedX - oldLeft;
        final int clampedDy = clampedY - oldTop;
        mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
                clampedDx, clampedDy);
    }
}

saveLastMotion()方法保留当前手势位置。

private void saveLastMotion(MotionEvent ev) {
    final int pointerCount = ev.getPointerCount();
    for (int i = 0; i < pointerCount; i++) {
        final int pointerId = ev.getPointerId(i);
        // If pointer is invalid then skip saving on ACTION_MOVE.
        if (!isValidPointerForActionMove(pointerId)) {
            continue;
        }
        final float x = ev.getX(i);
        final float y = ev.getY(i);
        mLastMotionX[pointerId] = x;
        mLastMotionY[pointerId] = y;
    }
}

ACTION_UP和ACTION_CANCEL事件

releaseViewForPointerUp()方法计算移动时的速率,回调CallbackonViewReleased()方法

private void releaseViewForPointerUp() {
    mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
    final float xvel = clampMag(
            mVelocityTracker.getXVelocity(mActivePointerId),
            mMinVelocity, mMaxVelocity);
    final float yvel = clampMag(
            mVelocityTracker.getYVelocity(mActivePointerId),
            mMinVelocity, mMaxVelocity);
    dispatchViewReleased(xvel, yvel);
}

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

public void cancel() {
    mActivePointerId = INVALID_POINTER;
    clearMotionHistory();

    if (mVelocityTracker != null) {
        mVelocityTracker.recycle();
        mVelocityTracker = null;
    }
}

5. 边缘触发

利用ViewDragHelper可以轻松实现边缘触发

// 设置识别哪些方向的触发
public void setEdgeTrackingEnabled(int edgeFlags)

ViewDragHelperView设置边缘触发

public ViewDragHelperView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    mViewDragHelper = ViewDragHelper.create(this, new CustomCallback());

    mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
}

Callback中的onEdgeDragStarted()方法里,加入CaptureView

private class CustomCallback extends ViewDragHelper.Callback {

    ... ...

    @Override
    public void onEdgeDragStarted(int edgeFlags, int pointerId) {
        Log.d("ViewDragHelperView", "onEdgeDragStarted");
        if (edgeFlags == ViewDragHelper.EDGE_LEFT) {
            mViewDragHelper.captureChildView(getChildAt(0), pointerId);
        }
    }
}

显示如下
在这里插入图片描述

6. ViewDragHelper中的Button

ButtonViewDragHelperView中无法被拖动
在这里插入图片描述
我们来看一下shouldInterceptTouchEvent()方法,当发生ACTION_DOWN事件时,只有mDragState == STATE_SETTLING时才会调用tryCaptureViewForDrag()方法,并修改状态,一般返回值都是false。而Button会拦截手势,因此不会调用processTouchEvent()方法了

public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev) {
    final int action = ev.getActionMasked();
    final int actionIndex = ev.getActionIndex();

    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 = ev.getPointerId(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 MotionEvent.ACTION_MOVE: {
            if (mInitialMotionX == null || mInitialMotionY == null) break;

            // First to cross a touch slop over a draggable view wins. Also report edge drags.
            final int pointerCount = ev.getPointerCount();
            for (int i = 0; i < pointerCount; i++) {
                final int pointerId = ev.getPointerId(i);

                // If pointer is invalid then skip the ACTION_MOVE.
                if (!isValidPointerForActionMove(pointerId)) continue;

                final float x = ev.getX(i);
                final float y = ev.getY(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 hDragRange = mCallback.getViewHorizontalDragRange(toCapture);
                    final int vDragRange = mCallback.getViewVerticalDragRange(toCapture);
                    if ((hDragRange == 0 || (hDragRange > 0 && newLeft == oldLeft))
                            && (vDragRange == 0 || (vDragRange > 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;
        }

        ... ...
    }

    return mDragState == STATE_DRAGGING;
}

当发生ACTION_MOVE事件时,会调用checkTouchSlop()来查看是否拦截手势,默认是不拦截。

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

所以如果需要拦截的话,修改CallbackgetViewHorizontalDragRange()getViewVerticalDragRange()方法

@Override
public int getViewHorizontalDragRange(@NonNull View child) {
    if (child == mClickableView) {
        return 1;
    }
    return super.getViewHorizontalDragRange(child);
}

@Override
public int getViewVerticalDragRange(@NonNull View child) {
    if (child == mClickableView) {
        return 1;
    }
    return super.getViewVerticalDragRange(child);
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ViewDragHelperAndroid系统提供的一个辅助,可以帮助我们实现拖拽、滑动等交互效果。下面是ViewDragHelper的使用步骤: 1. 定义一个ViewDragHelper对象 ```java ViewDragHelper mDragHelper = ViewDragHelper.create(parent, callback); ``` 其中parent是父容器,callback是ViewDragHelper.Callback对象,负责处理拖拽事件。 2. 重写ViewGroup的onInterceptTouchEvent和onTouchEvent方法 ```java @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { mDragHelper.processTouchEvent(ev); return true; } ``` 这两个方法是必须的,用来处理触摸事件。 3. 在ViewDragHelper.Callback中处理拖拽事件 ```java private class MyCallback extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(View child, int pointerId) { return true; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { int leftBound = getPaddingLeft(); int rightBound = getWidth() - child.getWidth() - getPaddingRight(); return Math.min(Math.max(left, leftBound), rightBound); } @Override public int clampViewPositionVertical(View child, int top, int dy) { int topBound = getPaddingTop(); int bottomBound = getHeight() - child.getHeight() - getPaddingBottom(); return Math.min(Math.max(top, topBound), bottomBound); } } ``` 上面是一个示例的Callback代码,其中tryCaptureView方法返回true表示可以拖拽该view,clampViewPositionHorizontal和clampViewPositionVertical方法分别控制view在水平和垂直方向上的移动边界。 以上就是ViewDragHelper的基本使用方法了,可以根据实际需求进行扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值