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

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)