Android 拖动滑出滑入的布局 自定义ViewDragHelper详解

本文介绍了如何使用ViewDragHelper实现Android中的拖动滑出滑入布局,详细讲解了SlideViewDragHelper的工作原理,包括拖动机制、滑动完成监听和源码解析。通过创建自定义布局并结合ViewDragHelper,可以实现复杂内容布局的平滑拖动和动画效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先给大家看效果图吧、

这里写图片描述

需求:

将复杂的内容布局 通过向右拖拽或者是快速向右滑动将其移动到最右边
当在向左拖动或者是快速向左滑动会将移除的布局恢复到原位

使用方法

github源码 欢迎star fork https://github.com/shf981862482/SlideLayoutApp.git

compile 'com.slidelayout:slipe_layout_library:0.0.3'
//滑动完成监听
        slide.setOnSlideStatusListener(new SlideLayout.OnSlideStatusListener() {
            @Override
            public void slideOutComplete() {
                Log.d("SHF", "slideOutComplete");
            }

            @Override
            public void slideInComplete() {
                Log.d("SHF", "slideInComplete");
            }
        });

注意:
1、SlideLayout使用相当于RelativeLayout
2、 第一个子布局就是可拖动的布局
3、至少一个子布局

    <sun.com.slipelayoutlibrary.SlideLayout
        android:id="@+id/slide"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_centerInParent="true"
            android:background="@color/grayTran"
            android:gravity="center"
            android:orientation="vertical">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:padding="4dp"
                android:text="@string/text" />

            <TextView
                android:id="@+id/gone_view"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="消失的内容" />

        </RelativeLayout>

        <Button
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="改布局" />
    </sun.com.slipelayoutlibrary.SlideLayout>

SlideLayout布局是结合 自定义的ViewDrawHelp来实现的
还没了解ViewDrawHelp的请看鸿祥的博客 Android ViewDragHelper完全解析 自定义ViewGroup神器

SlideViewDragHelper原理

在使用ViewDragHelper的时候、我们需要将onInterceptTouchEvent 和 onTouchEvent 交给
ViewDragHelper处理 代码如下

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

这里主要说一下 processTouchEvent
我们看一下他的部分源码

            case MotionEvent.ACTION_DOWN: {
                final float x = ev.getX();
                final float y = ev.getY();
                mCallback.onViewDown(ev);
                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;
            }

可以看到 在按下的时候 会通过 findTopChildUnder((int) x, (int) y); 找到需要拖动的子view
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;
    }

他是根据点击的位置来找到点击的子child,
那么当我们将布局拖出去之后,显然根据源码是找不到子view的 这个方法就会返回空,那么我们又要拖拽这个处于外头的子布局怎么办呢,看代码

    public View findTopChildUnder(int x, int y) {
        final int childCount = mParentView.getChildCount();
        if (childCount > 0) {
            return mParentView.getChildAt(mCallback.getOrderedChildIndex(0));
        }
        return null;
    }

上面就是SlideViewDragHelper的findTopChildUnder()方法,他是永远获取第一个子view,我们在布局的时候将需要拖动的布局放到第一个即可

最关键的地方我们实现了,那么如何去监听他是否滑动完成呢、我们看代码
在使用的时候 我们自定义的布局要重写这个方法

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

mDragger.continueSettling(true) 这一个我们先放一放 我们先看ViewDragHelper的实现机制

ViewDragHelper的实现机制

大家对ViewDragHelper的实现机制应该很好奇吧
1、是怎么拖动的
2、拖动到中间是怎么完成剩下的滑动动画的

ViewDragHelper是怎么拖动的

其实很简单,在processTouchEvent()方法中 VIewDragHelper是这样处理ACTION_MOVE的

            case MotionEvent.ACTION_MOVE: {
                mCallback.onViewMove(ev);
                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, ev);
                    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;
            }

看到 dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy, ev);这个方法了吧, 这就是手动拖拽的主要实现方法 我们进去看看

    private void dragTo(int left, int top, int dx, int dy, MotionEvent ev) {
        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);
        }
        mCallback.onViewDragMove(ev);
    }

可以看到主要是通过 offsetLeftAndRight 和 offsetTopAndBottom来实现未知的改变

ViewDragHelper拖动到中间是怎么完成剩下的滑动动画的

ViewDragHelper有个ViewDragHelper.Callback
自定义的布局 去创建ViewDragHelper的时候 需要传递ViewDragHelper.Callback的对象
Callback有一个方法是手指释放的时候回调 方法名如下
public void onViewReleased(View releasedChild, float xvel, float yvel) {}
在这个方法内部 我们做一些判断然后调用
mDragger.settleCapturedViewAt(x, y);即可实现剩下的滑动动画

我们看一下settleCapturedViewAt()

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

继续看 forceSettleCapturedViewAt()

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

我们看到熟悉的Scroller.startScroll()了,
想了解Scroller的可以看一下ViewPager的源码

大家以为这就完成滚动了吗,其实不然,每个VIew都有一个可以重写的方法 前面我们说过叫做computeScroll
我们看一下官方解释

/**
 * Called by a parent to request that a child update its values for mScrollX
 * and mScrollY if necessary. This will typically be done if the child is
 * animating a scroll using a {@link android.widget.Scroller Scroller}
 * object.
 */

简单来说,就是Scroller执行的时候 会调用View.computeScroll()

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

看一下源码 continueSettling()

    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) {
                mCapturedView.offsetLeftAndRight(dx);
            }
            if (dy != 0) {
                mCapturedView.offsetTopAndBottom(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;
    }

注意参数 keepGoing 是指正在滑动
位置改变还是跟dragTo一样用的offsetLeftAndRight

SlideViewDragHelper 滑动完成监听

为Callback添加如下属性和方法

    public static abstract class Callback {
        private boolean isScroll;
        /**
        *滚动监听
        */
        public void onStartScrollListener(boolean isComplete) {
            isScroll = !isComplete;
        }
   }

在前面continueSettling的方法中 我们调用这个方法

    public boolean continueSettling(boolean deferCallbacks) {

        if (mDragState == STATE_SETTLING) {
            boolean keepGoing = mScroller.computeScrollOffset();
            if (mCallback != null) {
                mCallback.onStartScrollListener(!keepGoing);
            }
            ...
         }
     }

好了滚动监听就实现了,再复杂的滚动逻辑就需要在我们自定义的SlideLayout中实现了

SlideLayout源码解析

初始化创建SlideViewDragHelper

    public SlideLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
        mDragger = SlideViewDragHelper.create(this, 1.0f, new SlipeCallback());
        mDragger.setEdgeTrackingEnabled(SlideViewDragHelper.EDGE_LEFT);
    }

添加滚动完成监听接口

    public interface OnSlideStatusListener {
        void slideOutComplete();

        void slideInComplete();
    }

添加全局变量

    public OnSlideStatusListener slideStatusListener;

    private final int OUTSLIPESPEED = 3000;//可以滑动到外部滑动速度临界点
    private final int INSLIPESPEED = -3000;//可以归位的滑动速度临界点

    private SlideViewDragHelper mDragger;

    private View mDragView;//要拖动的子布局

    private Point mAutoBackOriginPos = new Point();//记录子布局的初始位置
    private boolean haveSavePoint = false;//有没有记录下位置
    private boolean isOut = false;//有没有滑动到外头
    private boolean isMove = false;//子布局是不是在 移动

重写如下方法

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return mDragger.shouldInterceptTouchEvent(event);
    }


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

    /**
     * http://my.oschina.net/ososchina/blog/600281
     */
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mDragger.continueSettling(true)) {
            invalidate();
        }
    }

这个方法意思是inflate完成后 将第一个子view作为拖动布局

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mDragView = getChildAt(0);
    }

下面重写onLayout方法是重点,因为offsetLeftAndRight 并没有改LayoutParam的mLeft的值
而view的onLayout()源码如下

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //  The layout has actually already been performed and the positions
        //  cached.  Apply the cached values to the children.
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                RelativeLayout.LayoutParams st =
                        (RelativeLayout.LayoutParams) child.getLayoutParams();
                child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
            }
        }
    }

所以说,当子view有Gone Visible操作时,会执行onLayout 会导致画面闪动
解决办法移动时直接用子view的getLeft 没移动时调用super.onLayout(changed, l, t, r, b);

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        if (count == 0) {
            throw new RuntimeException("you must have one child view!!!");
        }
        if (!isOut && !isMove) {
            super.onLayout(changed, l, t, r, b);
        } else {
            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);
                if (child.getVisibility() != GONE) {
                    child.layout(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
                }
            }
        }
        if (!haveSavePoint) {
            //记录初始坐标
            mAutoBackOriginPos.x = mDragView.getLeft();
            mAutoBackOriginPos.y = mDragView.getTop();
            haveSavePoint = true;
        }
    }

注意:必须有一个子布局,不然会抛出异常

拖动布局的主要逻辑

好了重点来了、我们继承SlideViewDragHelper.Callback

添加属性

        private int screenMiddle;//屏幕中间
        private float downLeft;//点击 child0 的left
        private float beforeChildLeft;//点击 child0 的left
        private float childLeft;//移动时  child0 的left
        private float recentChildLeft;//最近执行移动的childLeft
        private float moveLeft;

        private float dragXSpeed;

重写方法

捕捉拖动布局回调

        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            //mEdgeTrackerView禁止直接移动
            return child == mDragView;
        }

点击回调,用来记录拖动前的 拖动布局的位置

        @Override
        public void onViewDown(MotionEvent event) {
            childLeft = getChildAt(0).getLeft();
            if (!isScroll() && !isMove) {//如果正在滚动 不记录按下left
                beforeChildLeft = getChildAt(0).getLeft();
                downLeft = event.getX();
            }
        }

移动回调,用来记录手指移动的位置

        @Override
        public void onViewMove(MotionEvent event) {
            if (!isScroll()) {//如果正在滚动 不记录移动left
                moveLeft = event.getX();
            }
        }

SlipeViewDragHelper dragTo中的拖动移动回调

        @Override
        public void onViewDragMove(MotionEvent event) {
            childLeft = getChildAt(0).getLeft();
        }

拖动布局改变位置的回调

        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            isMove = true;
        }

用来返回子view的位置

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

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return super.clampViewPositionVertical(child, top, dy);
        }
        //手指释放的时候回调
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            dragXSpeed = xvel;
            screenMiddle = getWidth() / 2;

            //mAutoBackView手指释放时可以自动回去
            if (releasedChild == mDragView) {
                float length = moveLeft - downLeft;//手指移动的距离
                recentChildLeft = childLeft;
                Log.d(TAG, "onViewReleased--left or right-->" + (length >= 0 ? "right" : "left") + "--速度xvel-->" + xvel + "--当前位置childLeft-->" + childLeft + "--length-->" + length);

                if (xvel > OUTSLIPESPEED) {//右滑 速度快 不管是左滑右滑直接到外部
                    slideOut();
                    invalidate();
                    return;
                }

                if (xvel < INSLIPESPEED) {//左滑 速度快  不管左滑右滑直接到内部
                    slideIn();
                    invalidate();
                    return;
                }
                if (length >= 0) {//根据手指移动距离判断右滑

                    if (childLeft > getWidth()) {
                        slideOut();
                        invalidate();
                        return;
                    }

                    if (xvel <= 0 && childLeft < screenMiddle) {
                        slideIn();
                        invalidate();
                        return;
                    }

                    if (xvel <= 0 && childLeft > screenMiddle) {//右滑 但是滑动时快速点击 将滑动速度变成了负数
                        slideOut();
                        invalidate();
                        return;
                    }

                    if (xvel >= 0 && xvel <= OUTSLIPESPEED//右滑 速度慢 没过最大滑动距离
                            && (childLeft <= screenMiddle && childLeft >= 0)) {
                        slideIn();
                        invalidate();
                        return;
                    }

                    if (xvel >= 0 && xvel <= OUTSLIPESPEED
                            && (childLeft > screenMiddle)) {//右滑 速度慢 超过最大滑动距离
                        slideOut();
                        invalidate();
                        return;
                    }


                } else {//左滑

                    if (childLeft < 0) {//滑到左屏幕外头
                        slideIn();
                        invalidate();
                        return;
                    }

                    if (xvel >= 0 && childLeft < screenMiddle) {
                        slideIn();
                        invalidate();
                        return;
                    }

                    if (xvel >= 0 && childLeft > screenMiddle) {//左滑 但是滑动时快速点击 将滑动速度变成了整的
                        slideOut();
                        invalidate();
                        return;
                    }

                    if (xvel >= INSLIPESPEED && xvel <= 0
                            && (childLeft > screenMiddle)) {//左滑 速度慢 没过最大滑动距离
                        slideOut();
                        invalidate();
                        return;
                    }

                    if (xvel >= INSLIPESPEED && xvel <= 0
                            && (childLeft <= screenMiddle && childLeft >= 0)) {//左滑 速度慢 超过最大滑动距离
                        slideIn();
                        invalidate();
                        return;
                    }
                }
            }
        }
/**
         * 滚动完成监听
         *
         * @param isComplete  滚动是否完成
         */
        @Override
        public void onStartScrollListener(boolean isComplete) {
            if (isComplete && slideStatusListener != null) {//滚动完成
                float length = moveLeft - downLeft;//手指移动的距离
                isMove = false;
                if (recentChildLeft < 0) {//抬起后子view的left小于零说明没有滑动
                    return;
                }
                if (recentChildLeft > getWidth()) {
                    return;
                }
                Log.d(TAG, "onstartScrollListener--left or right-->"
                        + (length >= 0 ? "right" : "left") + "--dragXSpeed-->"
                        + dragXSpeed + "--当前位置recentChildLeft-->" + recentChildLeft
                        + "--length-->" + length + "--beforeChildLeft-->" + beforeChildLeft);

                if (beforeChildLeft <= mAutoBackOriginPos.x) {
                    if (dragXSpeed > OUTSLIPESPEED) {//右滑 速度快 直接执行滚动外部方法
                        slideStatusListener.slideOutComplete();
                        return;
                    }
                    if (dragXSpeed < INSLIPESPEED) {
                        return;
                    }
                    //抬起手时 子view的left大于screenMiddle 执行滚动外部方法
                    if (dragXSpeed >= INSLIPESPEED && recentChildLeft > screenMiddle) {
                        slideStatusListener.slideOutComplete();
                        return;
                    }
                }
                if (beforeChildLeft >= getWidth()) {
                    if (dragXSpeed < INSLIPESPEED) {
                        slideStatusListener.slideInComplete();
                        return;
                    }
                    if (dragXSpeed > OUTSLIPESPEED) {
                        return;
                    }
                    if (dragXSpeed <= OUTSLIPESPEED && recentChildLeft < screenMiddle) {
                        slideStatusListener.slideInComplete();
                        return;
                    }

                }

            }
            return;
        }

执行剩下的滚动动画

    private void slideOut() {
        mDragger.settleCapturedViewAt(getWidth(), mAutoBackOriginPos.y);
        isOut = true;
    }

    private void slideIn() {
        mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);
        isOut = false;
    }
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值