自定义ViewGroup不可或缺的ViewDragHelper工具类

ViewDragHelper概述

ViewDragHelper实质上是对父ViewGroup中的子view的滑动操作、重新定位视图以及状态跟踪等做了一系列的封装,即只需输入父ViewGroup的TouchEvent,则会通过Callback返回子View的相关操作。省去了程序员需要对ViewGroup中不同子View的各种TouchEvent进行非常复杂的逻辑处理。所以ViewDragHelper是用于编写自定义ViewGroup的实用程序类。例如DrawerLayout中就运用了ViewDragHelper来处理拖动。
官方文档:

https://developer.android.google.cn/reference/android/support/v4/widget/ViewDragHelper

https://developer.android.google.cn/reference/android/support/v4/widget/ViewDragHelper.Callback

ViewDragHelper 常用方法

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

创建新ViewDragHelper的工厂方法
forParent:输入需要被控制的视图容器,必须为ViewGroup
sensitivity:灵敏度倍增器,用于帮助程序检测拖动开始时的灵敏度。数值越大越敏感。默认为1.0f。
cb: 触摸过程中会回调相关方法

boolean shouldInterceptTouchEvent(MotionEvent ev)

检查这个事件是否应该使父视图拦截该触摸事件。相当于父视图中的触摸事件拦截代理。该方法在父视图的onInterceptTouchEvent方法中调用。

ev:提供给onInterceptTouchEvent的MotionEvent

void processTouchEvent(MotionEvent ev)

将父视图的触摸事件传递给ViewDragHelper ,在父视图的onTouchEvent中调用,这里要注意一个问题,处理相应的TouchEvent的时候要将结果返回为true,消费本次事件!否则将无法使用ViewDragHelper处理相应的拖拽事件!

ev:提供给onTouchEvent的MotionEvent

void captureChildView(@NonNull View childView, int activePointerId)

捕获特定子View以在父容器中被拖动。注意:捕获特定子childView不会使mCallback.tryCaptureView()方法被调用,但其他回调不受影响。

boolean settleCapturedViewAt(int finalLeft, int finalTop)

以松手前的滑动速度为初速动,让捕获到的View自动滚动到指定位置。只能在Callback的onViewReleased()中调用。

void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)

以松手前的滑动速度为初速动,让捕获到的View在指定范围内fling。只能在Callback的onViewReleased()中调用。

boolean smoothSlideViewTo(View child, int finalLeft, int finalTop)

指定某个View自动滚动到指定的位置,初速度为0,可在任何地方调用。

ViewDragHelper.Callback 常用方法

public void onViewDragStateChanged(int state) 拖动状态改变时调用state有如下三种值STATE_IDLE:当前视图未被拖动或动画STATE_DRAGGING:当前正在拖动视图STATE_SETTLING:目前正在通过投掷或抛掷当前视图
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 当捕获视图的位置因拖动或沉降而改变时调用changedView:被改变的Viewleft:视图左边缘的新X坐标top:视图上边缘的新Y坐标dx:从上次调用开始改变X位置dy:从上次调用开始改变Y位置
public void onViewCaptured(View capturedChild, int activePointerId) 当捕捉到用于拖动或控制的子视图时调用capturedChild:捕获到的子ViewactivePointerId: 跟踪子捕获的指针id,可以用来区分是手动拖动,还是程序控制。
public void onViewReleased(View releasedChild, float xvel, float yvel) 当子视图不再被主动拖动时调用。releasedChild:被释放的子视图xvel:X指针离开屏幕时的速度,单位是像素/秒。yvel:Y指针离开屏幕时的速度,单位是像素/秒。
public void onEdgeTouched(int edgeFlags, int pointerId) 当用户在当前没有捕获子视图的情况下触摸了父视图中的边缘时调用edgeFlags:描述当前触摸的边缘的边缘标志的组合EDGE_LEFTEDGE_TOPEDGE_RIGHTEDGE_BOTTOMpointerId:触摸所述边缘的指针id
public boolean onEdgeLock(int edgeFlags) 返回true来锁定当前的边缘,返回false来保持它未锁定。默认的行为是不锁定边缘。 edgeFlags:描述被锁定边缘的边缘标志的组合return true锁定边缘,false不锁定边缘
public void onEdgeDragStarted(int edgeFlags, int pointerId) 在父视图中的边缘拖动,且当前没有捕获任何子视图时调用。edgeFlags:描述拖动的边缘的边缘标志的组合EDGE_LEFTEDGE_TOPEDGE_RIGHTEDGE_BOTTOMpointerId: 接触所述边缘的指针ID
public int getOrderedChildIndex(int index) 调用以确定子视图的z顺序,(用于改变同一位置有两个View重叠,如果想让下层的子View被选中,则重写此方法,返回下层子View的index)index:当前捕获的子View的Z轴索引
public int getViewHorizontalDragRange(View child) 返回可拖动子视图水平运动范围的大小(以像素为单位)。对于不能水平移动的视图,该方法应该返回0。child:选中的子Viewreturn:以像素为单位的水平运动范围
public int getViewVerticalDragRange(View child) 返回可拖动子视图垂直运动范围的大小(以像素为单位)。对于不能垂直移动的视图,该方法应该返回0。child:选中的子Viewreturn:以像素为单位的垂直运动范围
public abstract boolean tryCaptureView(View child, int pointerId) 检查是否是需要被捕获的子View,如果允许捕获,返回true;否则返回false.返回ture表示该子View可以被拖动。pointerId:试图捕获的指针id
public int clampViewPositionHorizontal(View child, int left, int dx) 处理被拖动的子view在水平方向上应该移动到的位置child:被滑动的子View实例left:期望移动后的子View左边缘位置dx:移动的距离return:子View在最终位置时的left值
public int clampViewPositionVertical(View child, int top, int dy) 处理被拖动的子view在垂直方向上应该移动到的位置child:被滑动的子View实例top:期望移动后的子View上边缘位置dy:移动的距离return:子View在最终位置时的top值


ViewDragHelper 使用步骤:

  1. 在自定义ViewGroup的构造方法中调用ViewDragHelper的静态工厂方法create()创建ViewDragHelper实例mViewDragHelper
  2. 创建ViewDragHelper.Callback实例,实现其中tryCaptureView、onViewCaptured、onViewReleased、clampViewPositionHorizontal、clampViewPositionVertical、onViewDragStateChanged等方法。
  3. 在自定义ViewGroup中的onInterceptTouchEvent(MotionEvent ev)方法中调用mViewDragHelper的shouldInterceptTouchEvent(ev)方法。
  4. 在自定义ViewGroup中的onTouchEvent(MotionEvent event)方法中调用mViewDragHelper的processTouchEvent(ev)方法。当ACTION_DOWN事件发生时,如果当前点击要拖动的子View没有消费事件,此时应该在OnTouchEvent返回true,否则将不会收到后续点击事件,造成子View无法拖动。为什么无法收到后续点击事件?
  5. 上面四个步骤已经可以实现子View拖动的效果,如果还需要实现松手后自动滑动到指定位置,或者类似于抛东西,使得子View在滑动松手后被抛出去,并逐渐减速直到停止的效果,那么还需要实现自定义ViewGroup中的computeScroll()方法:
    @Override
    public void computeScroll() {
        //判断滑动是否完成
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

computeScroll()方法实现后,可以在ViewDragHelper.Callback的onViewReleased()方法里调用settleCapturedViewAt()、flingCapturedView(),或在任意地方调用smoothSlideViewTo()方法。

  1. 如果要实现边缘拖动的效果,需要调用ViewDragHelper的setEdgeTrackingEnabled()方法,注册想要监听的边缘。然后实现ViewDragHelper.Callback里的onEdgeDragStarted()方法,在此手动调用captureChildView()传递要拖动的子View。

ViewDragHelper 注意事项:

ViewDragHelper一般用于自定义ViewGroup中对子View拖拽功能的实现,但ViewDragHelper并不会保存拖拽子View的状态及位置信息,需要我们手动保存,我们知道ViewGroup从新建到显示会经历measure、layout、draw这三大流程。其中在layout阶段会在onLayout方法中遍历所有子元素并调用子元素的layout方法。 通常我们在自定义ViewGroup的onLayout方法中对子View的位置及大小进行初始化配置,如下:

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                 child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
            }
        }
    }

这样做有什么问题呢?举个例子大家就能理解了,假如我们手动将自定义ViewGroup中的子View拖动到任意位置,而这时因为一些原因(例如:自定义ViewGroup中动态添加子View或者自定义ViewGroup所在的Activity中动态添加了View)导致自定义ViewGroup被重绘了,那么肯定会调用自定义ViewGroup中onLayout()方法。这时所有的子View将会被重新layout(),显而易见所有的子View被复位了。这显然是不能接受的,那么怎么解决这问题呢?
既然是由于重绘导致所有子View被复位,那么就保存下每个子View的位置信息,在ViewGroup调用onLayout中将各子View恢复不就解决了
首先创建DragLayoutParams类用于保存View位置信息:

class DragLayoutParams {
    public int mLeft;
    public int mTop ;
    public int mRight ;
    public int mBottom ;
}

创建一个Map 用来保存自定义ViewGroup中子View的位置:

private Map<View, DragLayoutParams> mViewParamsMap = new HashMap<>();
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
  
            //保存子View的位置
            DragLayoutParams params = new DragLayoutParams();
            params.mLeft = releasedChild.getLeft();
            params.mTop = releasedChild.getTop();
            params.mRight = releasedChild.getRight();
            params.mBottom = releasedChild.getBottom();

            mViewParamsMap.put(releasedChild,params);
        }

然后在重写onLayout方法按照存储的位置来layout子View

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                DragLayoutParams params = mViewParamsMap.get(child);
                if (params != null) {
                    child.layout(params.mLeft,params.mTop,params.mRight,params.mBottom);
                }else {
                    child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
                }
            }
        }
    }

还要考虑一种情况,如果我们没有用手拖动子View,而是通过mViewDragHelper.smoothSlideViewTo()方法将子View移动指定位置,那么如何保存当前子View的位置呢?其实很好办,可以在调用mViewDragHelper.smoothSlideViewTo()方法之前保存子View最终移动的到的位置,也可以在computeScroll()方法中保存,下面代码演示如何在computeScroll()方法中保存子View的位置信息:

    @Override
    public void computeScroll() {
        //判断滑动是否完成
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
            //保存子View的位置
            DragLayoutParams params = new DragLayoutParams();
            params.mLeft = mView.getLeft();
            params.mTop = mtView.getTop();
            params.mRight = mView.getRight();
            params.mBottom = mView.getBottom();

            mViewParamsMap.put(mView,params);
        }
    }

ViewDragHelper Demo:
在这里插入图片描述

myViewGroup.java

public class myViewGroup extends LinearLayout {

    private static final String TAG = "myViewGroup";
    private ViewDragHelper mViewDragHelper;
    private Point mAutoBackOriginPos = new Point();

    private View mDragView;
    private View mAutoBackView;
    private View mEdgeTrackerView;

    public myViewGroup(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public myViewGroup(Context context) {
        super(context);
        initView();
    }

    public myViewGroup(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public myViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView();
    }


    private void initView(){
        mViewDragHelper = ViewDragHelper.create(this, 1.0f, mCallback);
        //设置左边缘监听
        mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
    }



    private ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {

        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
            mViewDragHelper.captureChildView(mEdgeTrackerView, pointerId);
        }

        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            Log.i(TAG, "tryCaptureView");
            return mDragView == child || mAutoBackView == child;
        }


        //触摸到View回调
        @Override
        public void onViewCaptured(View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
            //Log.i(TAG, "onViewCaptured: 触摸到View");
        }

        //处理水平滑动
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            Log.i(TAG, "clampViewPositionHorizontal:"+left +" dx="+dx);
            return left;
        }

        //处理垂直滑动
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            Log.i(TAG, "clampViewPositionHorizontal:"+top +" dy="+dy);
            return top;
        }

        @Override
        public int getViewHorizontalDragRange(@NonNull View child) {
            return getMeasuredWidth()-child.getMeasuredWidth();
        }

        @Override
        public int getViewVerticalDragRange(@NonNull View child) {
            return getMeasuredHeight()-child.getMeasuredHeight();
        }

        // 当拖拽状态改变,比如idle,dragging
        @Override
        public void onViewDragStateChanged(int state) {
            super.onViewDragStateChanged(state);
            switch (state) {
                case ViewDragHelper.STATE_IDLE://当前视图未被拖动或动画。当手指离开后视图动画完成后调用
                    Log.i(TAG, "STATE_IDLE");
                    break;
                case ViewDragHelper.STATE_DRAGGING: //当前正在拖动视图
                    Log.i(TAG, "STATE_DRAGGING");
                    break;
                case ViewDragHelper.STATE_SETTLING: //目前正在通过投掷或抛掷当前视图
                    Log.i(TAG, "STATE_SETTLING");
                    break;
            }
        }

        // 当位置改变的时候调用,常用于滑动时更改scale等
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            //Log.i(TAG, "onViewPositionChanged: 位置改变:left="+left+"top="+top+"dx="+dx+"dy="+dy);
        }

        //拖动结束后调用
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            Log.i(TAG, "onViewReleased: 拖动结束:");

            //mAutoBackView手指释放时可以自动回去
            if (releasedChild == mAutoBackView)
            {
                mViewDragHelper.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);
                invalidate();
            }
        }
    };



    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.i(TAG, "onLayout");
        //记录mAutoBackView初始位置
        mAutoBackOriginPos.x = mAutoBackView.getLeft();
        mAutoBackOriginPos.y = mAutoBackView.getTop();

    }

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

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

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

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        //获取VewGroup中的子View
        mDragView = getChildAt(0);
        mAutoBackView = getChildAt(1);
        mEdgeTrackerView = getChildAt(2);
    }
}

<com.makesky.studydemo02.myViewGroup.myViewGroup
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
    <TextView
        android:clickable="true"
        android:layout_margin="10dp"
        android:gravity="center"
        android:layout_gravity="center"
        android:background="#44ff0000"
        android:text="我可以被随意拖拽"
        android:layout_width="100dp"
        android:layout_height="100dp"/>

    <TextView
        android:clickable="true"
        android:layout_margin="10dp"
        android:layout_gravity="center"
        android:gravity="center"
        android:background="#8800ff00"
        android:text="我被拖动后会回到原点"
        android:layout_width="100dp"
        android:layout_height="100dp"/>

    <TextView
        android:clickable="true"
        android:layout_margin="10dp"
        android:layout_gravity="center"
        android:gravity="center"
        android:background="#880000ff"
        android:text="从左边缘拖动来操作我"
        android:layout_width="100dp"
        android:layout_height="100dp"/>

    </com.makesky.studydemo02.myViewGroup.myViewGroup>

参考:
最后的Demo参考自鸿洋大神
https://blog.csdn.net/lmj623565791/article/details/46858663

发布了87 篇原创文章 · 获赞 28 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/MakerCloud/article/details/87870497