ViewDragHelper: 实现ViewGroup的子View拖动

自定义ViewGroup最常添加的功能就是子View的拖动,若是你的事件分发及处理的基本功很是扎实,那么彻底能够本身实现这个功能。然而幸运的是,系统提供了一个工具类ViewDragHelper,它提供了这个功能实现的框架,这样就大大提升了开发的效率。php

本文不只仅告诉你这个工具类该怎么使用,并且也会分析它的设计原理。只有掌握原理了,才能在实际中作到以不变应万变。java

本文须要你对事件的分发和处理有基本的认识,若是你还没掌握,能够参考我以前写的三篇文章android

  1. 事件分发之View事件处理
  2. ViewGroup事件分发和处理源码分析
  3. 手把手教你如何写事件处理的代码

若是你对事件分发和处理的流程不熟悉,你可能从本文中只学到如何使用ViewDragHelper类,可是并不会掌握它的精华。app

ViewDragHelper实现事件处理

既然ViewDragHelper是一个工具框架类,那么对事件的处理确定也是作好了封装。假设有一个自定义ViewGroup类,名字叫作VDHLayout。咱们来看下如何使用ViewDragHelper类实现事件的处理。框架

public class VDHLayout extends ViewGroup {
    ViewDragHelper mViewDragHelper;
    public VDHLayout(Context context) {
        this(context, null);
    }

    public VDHLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 建立ViewDragHelper对象,回调参数用来控制子View的拖动
        mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(@NonNull View child, int pointerId) {
                return false;
            }
        });
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            // 简单点,只操做第一个子View
            View first = getChildAt(0);
            first.layout(getPaddingLeft(), getPaddingTop(), getPaddingLeft() + first.getMeasuredWidth(),
                    getPaddingTop() + first.getMeasuredHeight());
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 利用ViewDragHelper来判断是否须要截断
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 利用ViewDragHelper来处理子View的拖拽
        mViewDragHelper.processTouchEvent(event);
        return true;
    }
}
复制代码

VDHLayout继承自ViewGroup,为了简单起见,只对它的第一个子View进行布局,这就是在onLayout()中的操做。ide

事件处理的代码是在onInterceptTouchEvent()onTouchEvent()方法中实现的,从代码中能够看到,分别用ViewDragHelper.shouldInterceptTouchEvent()ViewDragHelper.processTouchEvent()来处理事件。工具

实现ViewDragHelper的回调

如今,咱们已经成功地用ViewDragHelper实现了事件的处理,那么子View的拖动是在哪里控制的呢?这个实际上是在建立ViewDragHelper对象的时候,用传入的回调参数控制的。从代码中能够看到,咱们只实现了回调中的一个方法tryCaptureView(),这个方法也是必需要实现的。源码分析

根据事件分发和处理的原理可知,VDHLayout的子View是否能处理ACTION_DOWN事件,关乎着VDHLayout的事件分发和处理的逻辑。ViewDragHelper的回调固然也是受这个的影响的,所以我将分两部分来说解如何实现回调。布局

子View不处理事件

首先咱们来看下子View不处理事件的状况。post

根据View事件分发和处理的原理可知,若是一个View不设置任何监听事件,而且不可点击,也不可长按,那么这个View就不处理任何事件。

理论上讲的有点抽象,举个例子,例如 在XML布局中给VDHLayout添加一个ImageView控件

<?xml version="1.0" encoding="utf-8"?>
<com.bxll.vdhdemo.VDHLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">

    <ImageView android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/ic_launcher_round" />

</com.bxll.vdhdemo.VDHLayout>
复制代码

这个ImageView没有任何监听事件,默认不可点击也不可长按的,所以它就是一个不处理事件的子View。

如今以这个布局为例进行分析,当手指点击ImageView的时候,因为子View,也就是ImageView,不处理事件,因此ACTION_DOWN事件必定会先通过VDHLayout.onInterceptTouchEvent(),再通过VDHLayout.onTouchEvent()

根据事件处理的经验,真正的处理逻辑其实都在VDHLayout.onTouchEvent()中,它的实现以下

public boolean onTouchEvent(MotionEvent event) {
        // 利用ViewDragHelper来处理子View的拖拽
        mViewDragHelper.processTouchEvent(event);
        return true;
    }
复制代码

因为VDHLayout要经过触摸事件控制子View拖动,所以在onTouchEvent()中必需要返回true

能够看到,是用ViewDragHelper.processTouchEvent()来实现VDHLayout.onTouchEvent()的,如今来看看ViewDragHelper.processTouchEvent()是如何处理ACTION_DOWN事件的

public void processTouchEvent(@NonNull MotionEvent ev) {
        // ...

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                final float x = ev.getX();
                final float y = ev.getY();
                final int pointerId = ev.getPointerId(0);
                // 1. 找到事件做用于哪一个子View
                final View toCapture = findTopChildUnder((int) x, (int) y);
                // 保存坐标值
                saveInitialMotion(x, y, pointerId);
                // 2. 尝试捕获这个用于拖动的子View
                tryCaptureViewForDrag(toCapture, pointerId);
                // 边缘触摸回调
                final int edgesTouched = mInitialEdgesTouched[pointerId];
                if ((edgesTouched & mTrackingEdges) != 0) {
                    mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
                }
                break;
            }

            // ...
        }
    }
复制代码

首先经过findTopChildUnder()方法找到手指按下的那个子View

public View findTopChildUnder(int x, int y) {
        final int childCount = mParentView.getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            // getOrderedChildIndex()回调决定了获取哪一个子View
            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;
    }
复制代码

原理很简单,就是经过x,y坐标值找到子View,然而咱们能够发现,回调方法getOrderedChildIndex()决定了究竟是哪一个子View被找到。从这里能够看出,手指操做的并不必定都是最上面的子View。

找到了ACTION_DOWN做用的子View后,就经过tryCaptureViewForDrag()来尝试捕获这个子View

boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
        if (toCapture == mCapturedView && mActivePointerId == pointerId) {
            // Already done!
            return true;
        }
        // 经过回调判断这个子View是否能被捕获
        if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
            mActivePointerId = pointerId;
            captureChildView(toCapture, pointerId);
            return true;
        }
        return false;
    }
复制代码

首先经过tryCaptureView()回调方法判断子View是否可以被捕获,被捕获的子View才能被用来拖动。

若是可以被捕获,那么就调用captureChildView()通知子View被捕获

public void captureChildView(@NonNull View childView, int activePointerId) {
        // mCapturedView表明被用来拖动的目标
        mCapturedView = childView;
        mActivePointerId = activePointerId;
        // 回调通知View被捕获 
        mCallback.onViewCaptured(childView, activePointerId);
        // 设置为拖动状态
        setDragState(STATE_DRAGGING);
    }
复制代码

captureChildView()是经过onViewCaptured()进行回调,通知子View已经被捕获。

如今,来总结下ViewDragHelper.processTouchEvent()ACTION_DOWN事件的处理中,回调作了哪些事事情(只列举主要的回调)

  1. 经过getOrderedChildIndex()回调,判断ACTION_DOWN做用于哪一个子View。
  2. 经过tryCaptureView()回调,判断子View是否能被捕获。
  3. 经过onViewCaptured()回调,通知哪一个子View被捕获。

ACTION_DOWN处理完了,如今咱们来看看ACTION_MOVE事件如何处理的。

因为子View不处理事件,ACTION_MOVE事件交由VDHLayout.onTouchEvent()处理,也就是交给了ViewDragHelper.processTouchEvent()处理。

public void processTouchEvent(@NonNull MotionEvent ev) {
        // ...

        switch (action) {
            // ...

            case MotionEvent.ACTION_MOVE: {
                if (mDragState == STATE_DRAGGING) {
                    // 判断手指是否有效
                    if (!isValidPointerForActionMove(mActivePointerId)) break;

                    final int index = ev.findPointerIndex(mActivePointerId);
                    final float x = ev.getX(index);
                    final float y = ev.getY(index);
                    // 获取x,y轴上拖动的距离差
                    final int idx = (int) (x - mLastMotionX[mActivePointerId]);
                    final int idy = (int) (y - mLastMotionY[mActivePointerId]);
                    // 对于目标View执行拖动
                    dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);

                    saveLastMotion(ev);
                } else {
                    // ...
                }
                break;
            }

            // ...
        }
    }
复制代码

ViewDragHelper.processTouchEvent()ACTION_MOVE的处理中,首先计算在x,y轴上移动的距离差,而后经过dragTo()方法拖动刚刚捕获的子View。

咱们注意下dragTo()第一个参数和第二个参数,它指的是目标View(被捕获的子View)理论上要移动到的坐标点。

private void dragTo(int left, int top, int dx, int dy) {
        // clampedX, clampedY表示目标View要拖动到的终点坐标
        int clampedX = left;
        int clampedY = top;
        // 获取目标View的起始坐标
        final int oldLeft = mCapturedView.getLeft();
        final int oldTop = mCapturedView.getTop();
        if (dx != 0) {
            // 若是拖动的距离大于0,经过回调获取目标View最终要拖动到的x坐标值
            clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
            // 目标View在水平方向移动
            ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
        }
        if (dy != 0) {
            // 若是拖动的距离大于0,经过回调获取目标View最终要拖动到的x坐标值
            clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
            // 目标View在水平方向移动
            ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
        }
        
        if (dx != 0 || dy != 0) {
            // 计算实际移动的距离差
            final int clampedDx = clampedX - oldLeft;
            final int clampedDy = clampedY - oldTop;
            // 回调通知,目标View实际移动到(clampedX, clampedY),以及x,y轴实际移动的距离差为clampedDx, clampedDy
            mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
                    clampedDx, clampedDy);
        }
    }
复制代码

x,y方向上,只要任意一个方向上手指拖动的距离大于0, 那么就经过clampViewPositionHorizontal()/clampViewPositionVertical()回调方法,计算目标View实际须要拖动到的终点坐标。

经过回调计算出来终点坐标后,就把目标View移动到这个计算出来的坐标点上。

最后,只要x,y方向上拖动距离大于0,那么就经过onViewPositionChanged()回调方法,通知目标View实际拖动到哪一个坐标,以及实际拖动的距离差。

如今咱们明白了,ViewDragHelper.processTouchEvent()处理ACTION_MOVE,实际上就是处理目标View的拖动,它用到了以下回调

  1. clampViewPositionHorizontal()clampViewPositionVertical()回调,用来计算目标View拖动的实际坐标。
  2. onViewPositionChanged()回调,通知目标View实际被拖动到哪一个坐标,以及在x,y轴上拖动的实际距离差。

实现子View不处理事件回调

有了前面的理论基础,如今咱们来实现下回调,让不处理事件的子View可以被拖动,并且只容许在水平方向上被拖动。

mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(@NonNull View child, int pointerId) {
                // 为简单起见,全部的View均可以被拖动
                return true;
            }

            /** * 控制目标View在x方向的移动。 */
            @Override
            public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
                // 不容许垂直方向移动
                return 0;
            }

            /** * 控制目标View在y方向的移动。 */
            @Override
            public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
                // 水平方向移动不能超出父View范围
                return Math.min(Math.max(0, left), getWidth() - child.getWidth());
            }
        });
复制代码

因为咱们不容许垂直方向的拖动,所以clampViewPositionHorizontal()要返回0,clampViewPositionVertical()的返回值要控制在VDHLayout范围内滑动。效果以下

VDH_H

在前面的分析中还有其它的一些回调,能够根据实际项目要求进行复写实现。

子View处理事件

如今来分析子View可以处理事件的状况。让子View能处理事件最简单的方式是设置它能够点击,例如

<com.bxll.vdhdemo.VDHLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">

    <ImageView android:clickable="true" android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/ic_launcher_round" />

</com.bxll.vdhdemo.VDHLayout>
复制代码

当利用这个布局再次运行程序的时候,你会发现原来能够拖动的ImageView不能被拖动了。这是由于事件的处理逻辑改变了,从而ViewDragHelper的实现逻辑也改变了。

因为子View能处理事件,所以对于ACTION_DOWN事件,就只会通过VDHLayout.onInterceptTouchEvent()方法,而并不会通过VDHLayout.onTouchEvent()方法。从前面的代码实现可知,VDHLayout.onInterceptTouchEvent()是由ViewDragHelper.shouldInterceptTouchEvent()实现。然而ViewDragHelper.shouldInterceptTouchEvent()方法对于ACTION_DOWN只是简单一些简单处理,并不会截断事件。

所以咱们须要分析ACTION_MOVE是如何被ViewDragHelper.shouldInterceptTouchEvent()截断的。

public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev) {
        // ...

        switch (action) {
            // ...

            case MotionEvent.ACTION_MOVE: {
                // ...
                
                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);
                    // 1. 判断是否达到拖动的标准
                    final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
                    // 一个不截断的状况:若是拖动标准,却没有实际的拖动距离,那就不截断事件
                    if (pastSlop) {
                        //获取新,旧坐标值
                        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);
                        // 经过回调获取x,y方向的拖动范围
                        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;
                    }
                    
                    // 2. 若是达到拖动的临界距离,那么就尝试捕获子View
                    if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
                        break;
                    }
                }
                saveLastMotion(ev);
                break;
            }

        
        }
        // 若是成功捕获子View,那么状态就会被设置为STATE_DRAGGING,也就表明截断事件
        return mDragState == STATE_DRAGGING;
    }
复制代码

ViewDragHelper.shouldInterceptTouchEvent()考虑了多手指的状况,为了简化分析,只考虑单手指的状况。

第一步,判断是否达到拖动的条件,有两个条件

  1. 事件必需要做用于某个子View
  2. checkTouchSlop()返回true

根据事件处理的经验,若是要截断ACTION_MOVE事件,必需要有条件地截断。

checkTouchSlop()方法用来判断是否达到的拖动的临界距离

private boolean checkTouchSlop(View child, float dx, float dy) {
        if (child == null) {
            return false;
        }
        // 经过回调方法判断x,y方向是否容许拖动
        final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
        final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;

        // 若是x或y方向容许拖动,根据拖动的距离计算是否达到拖动的临界值
        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;
        }
        // 若是x和y方向都不容许拖动,那就永远不可能达到拖动临界值
        return false;
    }
复制代码

首先经过getViewHorizontalDragRange()getViewVerticalDragRange()获取x,y方向拖动范围,只要这个范围大于0,就表明能够在x,y方向上拖动。而后根据哪一个方向能够拖动,就相应的计算拖动的距离是否达到了临界距离。

如今回到shouldInterceptTouchEvent()方法的第二步,当达到了拖动条件后,就调用tryCaptureViewForDrag()尝试捕获目标View,这个方法在前面已经分析过,它会首先回调tryCaptureView()肯定目标View是否能被拖动,若是能拖动,再回调onViewCaptured()通知目标View已经捕获,最后设置状态为STATE_DRAGGING

当状态设置为了STATE_DRAGGING后,那么ViewDragHelper.shouldInterceptTouchEvent()返回值就是true,也就是说VDHLayout.onInterceptTouchEvent()截断了ACTION_MOVE事件。

VDHLayout.onInterceptTouchEvent()截断了ACTION_MOVE事件后,后续的ACTION_MOVE事件就交给了VDHLayout.onTouchEvent()方法,也就是交给了ViewDragHelper.processTouchEvent()处理。这个方法以前分析过,就是处理目标View的拖动。

那么如今咱们来总结下ViewDragHelper.shouldInterceptTouchEvent()在处理ACTION_MOVE截断的时候,用到哪些关键回调

  1. getViewHorizontalDragRange()getViewVerticalDragRange()方法判断x,y方向上是否能够拖动。返回值大于0表示能够拖动。

实现View处理事件的回调

通过刚才的分析,咱们知道,对于一个能处理事件的子View,若是想让它能被拖动,必须复写getViewHorizontalDragRange()getViewVerticalDragRange()回调,用于告诉ViewDragHelper,在相应的方向上容许被拖动。

那么如今,咱们就来解决子View(能处理事件)不能拖动的问题,咱们仍然只让子View在水平方向上被拖动

mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(@NonNull View child, int pointerId) {
                // 为简单起见,全部的View均可以被拖动
                return true;
            }

            /** * 控制目标View在x方向的移动。 */
            @Override
            public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
                // 不容许垂直方向移动
                return 0;
            }

            /** * 控制目标View在y方向的移动。 */
            @Override
            public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
                // 水平方向移动不能超出父View范围
                return Math.min(Math.max(0, left), getWidth() - child.getWidth());
            }

            @Override
            public int getViewHorizontalDragRange(@NonNull View child) {
                // 因为只容许目标View在VDHLayout中水平拖动,所以水平拖动范围就是VDHLayout的宽度减去目标View宽度
                return getWidth() - child.getWidth();
            }

            @Override
            public int getViewVerticalDragRange(@NonNull View child) {
                // 因为不容许垂直方向拖动,所以拖动范围也就是0
                return 0;
            }
        });
    }
复制代码

因为咱们只容许水平方向拖动,所以getViewVerticalDragRange()返回的垂直方向的拖动范围就是0,getViewHorizontalDragRange()返回的水平方向的拖动范围就是getWidth() - child.getWidth()

边缘触摸

ViewDragHelper有一个边缘触摸功能,这个边缘触摸的功能比较简单,所以我并不打算从源码进行分析,而只是从API角度进行说明。

要向触发边缘滑动功能,首先要调用ViewDragHelper.setEdgeTrackingEnabled(int edgeFlags)方法,设置哪一个边缘容许跟踪。参数有以下几个可用值

public static final int EDGE_LEFT = 1 << 0;

    public static final int EDGE_RIGHT = 1 << 1;

    public static final int EDGE_TOP = 1 << 2;

    public static final int EDGE_BOTTOM = 1 << 3;

    public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;
复制代码

边缘触摸的回调有以下几个

/** * Called when one of the subscribed edges in the parent view has been touched * by the user while no child view is currently captured. */
        public void onEdgeTouched(int edgeFlags, int pointerId) {}
        
        /** * Called when the given edge may become locked. This can happen if an edge drag * was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)} * was called. This method should return true to lock this edge or false to leave it * unlocked. The default behavior is to leave edges unlocked. */
        public boolean onEdgeLock(int edgeFlags) {
            return false;
        }    
        
        /** * Called when the user has started a deliberate drag away from one * of the subscribed edges in the parent view while no child view is currently captured. */
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {}        
复制代码

注释已经很清楚的解释了这几个回调的时机,我献丑来翻译下

  1. onEdgeTouched(): 当没有子View被捕获,而且容许边缘触摸,当用户触摸边缘时回调。
  2. onEdgeLock(): 用来锁定锁定哪一个边缘。这个回调是在onEdgeTouched()以后,开始拖动以前调用的。
  3. onEdgeDragStarted(): 当没有子View被捕获,而且容许边缘触摸,当用户已经开始拖动的时候回调。

系统控件DrawerLayout就是利用ViewDragHelper的边缘滑动功能实现的。因为篇幅缘由,我就不用例子来展现边缘触摸的功能如何使用了。

ViewDragHelper实现View滑动

ViewDragHelper还有一个View定义的功能,利用OverScroller实现。有以下几个方法

/** * Settle the captured view at the given (left, top) position. * The appropriate velocity from prior motion will be taken into account. * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} * on each subsequent frame to continue the motion until it returns false. If this method * returns false there is no further work to do to complete the movement. */    
    public boolean settleCapturedViewAt(int finalLeft, int finalTop) {}
    
    /** * Animate the view <code>child</code> to the given (left, top) position. * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} * on each subsequent frame to continue the motion until it returns false. If this method * returns false there is no further work to do to complete the movement. */
    public boolean smoothSlideViewTo(@NonNull View child, int finalLeft, int finalTop) {}
    
    /** * Settle the captured view based on standard free-moving fling behavior. * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame * to continue the motion until it returns false. */
    public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {}
复制代码

从注释能够能够看出,这个三个方法都须要在下一帧刷新的时候调用continueSettling(),这个就与OverScroller的用法是一致的。

如今,来利用settleCapturedViewAt()方法实现一个功能,让拖动的View被释放后,回到原点。

当拖动的View被释放后,会回调onViewReleased()方法

public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
    if (mViewDragHelper.settleCapturedViewAt(0, 0)) {
        invalidate();
    }
}
复制代码

因为利用的是OverScroller来实现的,所以必须调用进行重绘。重绘的时候,会调用控件的computeScroll()方法,在这里调用刚才说讲的continueSettling()方法

public void computeScroll() {
    if (mViewDragHelper.continueSettling(true)) {
        invalidate();
    }
}
复制代码

continueSettling()也是对OverScroller逻辑的封装,若是返回true就表明这个定位操做还在进行中,所以还须要继续调用重绘操做。

想了解其中的原理,你必定要熟悉OverScroller的原理。

如此一来就能够实现以下效果

Settling

结束

不少绚丽的视图拖动操做,每每都是用ViewDragHelper实现的,这个工具类简直是一个集大成之做,咱们须要彻底掌握它,这样咱们才能游刃有余地在自定义ViewGroup中实现各类牛逼的View拖动效果。

相关文章
相关标签/搜索