美文网首页Android开发Android开发经验谈Android技术知识
ViewDragHelper入门和实践,自定义左滑菜单View

ViewDragHelper入门和实践,自定义左滑菜单View

作者: 风少侠 | 来源:发表于2018-11-13 10:46 被阅读7次

    ViewDragHelper是用于编写自定义ViewGroup的帮助类。它提供了许多有用的操作和状态跟踪,允许用户在其父ViewGroup中拖动和重新定位视图。

    ViewDragHelper特点

    • 构造函数私有,通过静态方法create()创建。
    • 必须指定一个父容器ViewGroup。
    • 可以指定允许被拖拽、移动的子View。
    • 可以限定被拖拽的方向、距离、范围。
    • 可以检测子view的位置变化信息。
    • 可以检测是否触及到边缘。
    • 可以检测手势松开的动作。

    ViewDragHelper入门

    一、创建实例

        public static ViewDragHelper create(@NonNull ViewGroup forParent, @NonNull ViewDragHelper.Callback cb) {
            return new ViewDragHelper(forParent.getContext(), forParent, cb);
        }
    
        public static ViewDragHelper create(@NonNull ViewGroup forParent, float sensitivity, @NonNull ViewDragHelper.Callback cb) {
            ViewDragHelper helper = create(forParent, cb);
            helper.mTouchSlop = (int)((float)helper.mTouchSlop * (1.0F / sensitivity));
            return helper;
        }
    

    参数:

    • forParent:要监视的父容器。
    • sensitivity:敏感度,1.0正常,较大的值会更敏感。
    • ViewDragHelper.Callback:最关键的一个参数,回调检测到的状态信息和事件。

    二、Callback回调相关方法

    ViewDragHelper.Callback相当于ViewDragHelper和父ViewGroup的一个通信通道,callback可以决定子view是否可以被拖动,子view的范围和状态等。

    基本方法:

    tryCaptureView

    public abstract boolean tryCaptureView(View child, int pointerId);
    

    参数:

    • child:正在触摸的子view。
    • pointerId:正在触摸的手指id。

    返回值:
    返回true表示可以捕获(拖动)该子view。

    其他功能性方法:

    clampViewPositionHorizontal,clampViewPositionVertical

    /**
    *  限制子view在水平方向上的位置
    */
    public int clampViewPositionHorizontal(View child, int left, int dx) {
          return 0;
    }
    
    /**
    *  限制子view在垂直方向上的位置
    */
    public int clampViewPositionVertical(View child, int top, int dy) {
          return 0;
    }
    

    参数:

    • child:正在拖拽的子view。
    • left / top:在x轴/y轴尝试运动到的位置,相对于初始位置。在初始位置的左/上方为负值,右/下方为正值。
    • dx / dy:在x轴/y轴尝试运动的距离。向左/上方滑为负值,右/下方滑为正值。

    返回值:
    返回值表示最终该子view在x / y轴相对于初始坐标的位置。比如说直接返回left / top表示可以跟随手指随意拖动,默认返回0表示不能拖动。

    getViewHorizontalDragRange,getViewVerticalDragRange

    public int getViewHorizontalDragRange(View child) {
         return 0;
    }
    
    public int getViewVerticalDragRange(View child) {
         return 0;
    }
    

    参数:

    • child:要检查的子view。

    返回值:
    官方文档上写的是以像素为单位返回子view在水平 / 垂直方向的运动范围。但在我的使用过程中,发现这个返回值只要>0,即代表可以在水平 / 垂直方向拖拽移动,且该方法一般在子view有事件监听的情况下使用,因为该方法主要用于ViewDragHelper的shouldInterceptTouchEvent方法,判断是否拦截事件,只有在子view会消费事件的情况下才需要由父ViewGroup判断要不要拦截事件;如果子view不消费事件,那事件最终还是会返回到父ViewGroup,由父ViewGroup消费,也就没有重写onInterceptTouchEvent的必要了。

    getOrderedChildIndex

    public int getOrderedChildIndex(int index) {
        return index;
    }
    

    调整捕获子view时的Z顺序,用于有多个子view层叠时想改变要捕获的子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;
        }
    

    状态回调方法

    onViewCaptured

    public void onViewCaptured(View capturedChild, int activePointerId) {}
    

    在子view被捕获成功进行拖拽或自动回滚时调用。

    onViewDragStateChanged

    public void onViewDragStateChanged(int state) {}
    

    子view的拖拽状态发生改变时回调,state有3个取值:

    • STATE_IDLE:空闲状态。
    • STATE_DRAGGING:拖拽状态。
    • STATE_SETTLING:松手自动回滚状态。

    onViewPositionChanged

    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}
    

    被拖拽或回滚的子view位置发生改变时回调。

    • changedView:位置发生改变的view。
    • left:新位置相对于初始位置的x方向的距离,左负右正。
    • top:新位置相对于初始位置的y方向的距离,上负下正。
    • dx:本次位移的x偏移量,左负右正。
    • dy:本次位移的y偏移量,上负下正。

    onViewReleased

    public void onViewReleased(View releasedChild, float xvel, float yvel) {}
    

    拖拽松手时的回调。

    • releasedChild:被捕获的子view。
    • xvel:松手时x方向的速度,单位px/s。
    • yvel:松手时y方向的速度,单位px/s。

    该方法内可以通过调用settleCapturedViewAt(int finalLeft, int finalTop)或flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)方法使子view进入STATE_SETTLING状态,最终回滚至某个位置,在这个过程中子view的捕获不会完全停止。如果没有调用这些方法,则子view进入STATE_IDLE状态。

    onEdgeTouched,onEdgeLock,onEdgeDragStarted

    public void onEdgeTouched(int edgeFlags, int pointerId) {}
    
    public boolean onEdgeLock(int edgeFlags) {
        return false;
    }
    
    public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
    
    • onEdgeTouched:当用户触摸到边界,且没有捕获子view时调用。
    • onEdgeLock:边缘锁定。
    • onEdgeDragStarted:当用户开始拖拽某个边界,且没有捕获子view时调用。可以在该方法中调用ViewDragHelper的captureChildView方法进行子view捕获。

    edgeFlags表示边界标记,有EDGE_LEFT、EDGE_TOP、EDGE_RIGHT、EDGE_BOTTOM四个值。

    三、触摸事件委托给ViewDragHelper

        /**
        * 拦截事件并交给viewDraghelper判断是否需要拦截。如果子view不消费事件,该方法不重写也可以。
        */
        override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
            return viewDragHelper.shouldInterceptTouchEvent(ev)
        }
    
        /**
        * 将交给viewDraghelper处理,并消费该事件。
        */
        override fun onTouchEvent(event: MotionEvent): Boolean {
            viewDragHelper.processTouchEvent(event)
            return true
        }
    

    四、重写computeScroll

    如果需要释放View有滚动效果,则还需要重写computeScroll,这是因为ViewDragHelper内部使用了Scroller来处理view的滚动。

        override fun computeScroll() {
            super.computeScroll()
            if (viewDragHelper.continueSettling(true)) {
                ViewCompat.postInvalidateOnAnimation(this)
            }
        }
    

    ViewDragHelper实践,自定义MenuCardView

    menuCardView.gif

    这是一个很常见的左滑菜单的自定义View,先思考一下我们达到这样的效果需要做些什么:

    • 一个双层结构的ViewGroup,一层表示上层的展示内容,一层表示菜单。
    • 内容层要可以滑动,且只可以x方向左滑。
    • 内容层的滑动要有最大可滑动距离。
    • 松开手指后内容层要可以自动滚动到最左或最右。
    • 菜单层可以响应点击事件。

    层级结构

    我们让MenuCardView继承FrameLayout,规定该View有且只能有两个子View,第一个子View表示菜单层,第二个子View表示内容层。

    class MenuCardView(
            context: Context,
            attrs: AttributeSet?,
            defStyleAttr: Int
    ) : CardView(context, attrs, defStyleAttr) {
    
        private lateinit var content: View
        private lateinit var menu: View
    
        override fun onFinishInflate() {
            super.onFinishInflate()
            content = getChildAt(1)
            menu = getChildAt(0)
        }
    }
    

    创建ViewDragHelper实例

        private var viewDragHelper: ViewDragHelper
    
        init {
            viewDragHelper = ViewDragHelper.create(this, DragCallback())
        }
    

    委托ViewDragHelper处理触摸事件

        override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
            return viewDragHelper.shouldInterceptTouchEvent(ev)
        }
    
        override fun onTouchEvent(event: MotionEvent): Boolean {
            viewDragHelper.processTouchEvent(event)
            return true
        }
    
    
        override fun computeScroll() {
            super.computeScroll()
            if (viewDragHelper.continueSettling(true)) {
                ViewCompat.postInvalidateOnAnimation(this)
            }
        }
    

    内容层可以滑动

    inner class DragCallback : ViewDragHelper.Callback() {
       override fun tryCaptureView(child: View, pointerId: Int): Boolean {
            return child == content
       }
    }
    

    内容层只能左滑,且限制最大滑动距离

    只能左滑,所以我们只重写clampViewPositionHorizontal方法。

            override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
                this.left = left
                if (left < 0) {
                    return if (Math.abs(left) < menu.width)
                        left
                    else
                        menu.width * -1
                }
                return 0
            }
    

    松开手指自动滚动

    松开手指时计算当前滑动的位置,超过界限则自动滚动到最大边界,否则滚动到初始位置。

            override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
                if (Math.abs(left) > menu.width / 2)
                    viewDragHelper.settleCapturedViewAt(menu.width * -1, 0)
                else
                    viewDragHelper.settleCapturedViewAt(0, 0)
    
                postInvalidate()
            }
    

    菜单层点击事件和内容层滑动兼容的处理

    本来在写demo的时候,进行到上一步已经基本实现效果了,当时还美滋滋的用到了正式环境,结果一运行发现根本滑动不了,因为正式环境下添加了菜单的点击事件,此时viewDragHelper.shouldInterceptTouchEvent(ev)的返回值为false,然后事件被子view消费掉了。简单了解了下shouldInterceptTouchEvent的源码,发现getViewHorizontalDragRange可以影响其返回值,做了以下处理后正常。

            override fun getViewHorizontalDragRange(child: View): Int {
                return menu.width
            }
    

    相关文章

      网友评论

        本文标题:ViewDragHelper入门和实践,自定义左滑菜单View

        本文链接:https://www.haomeiwen.com/subject/pabpzftx.html