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

来源:互联网 发布:淘宝日系男装店铺排行 编辑:程序博客网 时间:2024/04/30 15:23

先给大家看效果图吧、

这里写图片描述

需求:

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

使用方法

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;    }
0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 欠钱的人跑路了怎么办 美国非婚生子父亲不认怎么办 孕4个月肚子胀气怎么办 8个月孕妇感冒了怎么办 孕8个月感冒鼻塞怎么办 怀孕八个月感冒了怎么办 孕八个月咳嗽了怎么办 享物说同城自提怎么办 锤基意外怀孕怎么办 08 蛐蛐罐底翻砂了怎么办 剑网3中被盗号后怎么办 本人想离婚对方躲避怎么办 微信不能说语音怎么办 微信语音发不了怎么办 吃鸡语音用不了怎么办 要杀我的人见面怎么办 转晕了想吐怎么办 原地转圈头晕恶心想吐怎么办 孩子吃凉的呕吐头还晕怎么办 孩子转晕了想吐怎么办 转圈晕了想吐怎么办 我爸总是骂我妈怎么办 转圈转的想吐怎么办 大便干燥拉不出来怎么办 吹完头发很干燥怎么办 腿摔伤了结痂疼怎么办 蹭wifi被禁止后怎么办 电脑wifi给拉黑怎么办 电脑想用无线网怎么办 中路被对方打崩了怎么办 英雄联盟队友太坑怎么办 匹配被王者虐了怎么办 lol碰到嘴臭的怎么办 小婴儿脾气大怎么办呢? 玩游戏输入法会跳出出来怎么办 逆水寒fps太低怎么办 我dcj没地速怎么办 电焊看久眼睛疼怎么办 装修忘了窗帘盒怎么办? 纹眉导致眼肿了怎么办 哭泣引起的眼肿怎么办