通过在自定义的ViewGroup内部使用ViewDragHelper,使得给自定义的ViewGroup在水平方向上并排按序添加多个子View(ViewGroup),可以实现水平左右滚动的效果,类似于ViewPager.

官方解释如下(不做翻译,原汁原味的英语更易理解):

/**
 * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
 * of useful operations and state tracking for allowing a user to drag and reposition
 * views within their parent ViewGroup.
 */

使用

ViewDragHelper内部定义了一个静态内部类Callback,我们需要重写Callback.

val helper : ViewDragHelper =   ViewDragHelper.create(this, object : ViewDragHelper.Callback(){
        //根据需要,重写相关的方法.
})

在你的自定义ViewGroup的onTouchEvent(event)方法内调用ViewDragHelper.processTouchEvent(event).

 override fun onTouchEvent(event: MotionEvent): Boolean {
        helper.processTouchEvent(event)
        return true
    }

在ViewDragHelper.processTouchEvent(event)方法内部调用了Callback的回调方法.这样你只需要重写Callback的回调方法即可.

Callback

先看一下我们需要用到的Callback的方法.

tryCaptureView

当前触摸到的是哪个View,我们定义的这个ViewGroup可以添加多个子View

override fun tryCaptureView(capturedView: View, pointerId: Int): Boolean {
                for (x in 0 until childCount) {
                    val child = getChildAt(x)
                    if (child.visibility == View.GONE) continue
                    if (child == capturedView) return true;
                }
                return false
            }

clampViewPositionHorizontal(@NonNull View child, int left, int dx)

约束水平方向上左右可滚动的边界位置.对于通过tryCaptureView触摸的任意一个view,需要对它的左右两个方向做边界约束.

override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
                for (x in 0 until childCount) {
                    if (getChildAt(x) == child) {
                        //左边界约束,在ScrollerLayout未发生滑动的情况下,当前触摸的子View距离ScrollerLayout的左边界的距离值.
                        var clampLeft = 0
                        //右边界约束,在ScrollerLayout未发生滑动的情况下,当前触摸的子View距离ScrollerLayout的右边界的距离值.
                        var clampRight = 0
                        for (y in 0 until x) {
                            clampLeft += getChildAt(y).width
                        }
                        for (y in x + 1 until childCount) {
                            clampRight += getChildAt(y).width
                        }
                        //当前触摸的子View距离ScrollerLayout的左边界不能超过clampLeft的约束值,子View向右滑动的极限
                        if (left > clampLeft) return clampLeft
                       //当前触摸的子View距离ScrollerLayout的右边界不能超过clampRight的约束值,子View向左滑动的极限 
                        if (left + clampRight < 0) return clampRight
                    }
                }
                return left
            }

clampViewPositionVertical(@NonNull View child, int top, int dy)

竖直方向上的顶部和底部的边界约束.我们这里不做处理,直接返回0.

onViewPositionChanged(@NonNull View changedView, int left, int top, int dx,int dy)

当前触摸的view位置发生改变时的回调.需要对每个子view都重新更改其位置.

override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) {
                super.onViewPositionChanged(changedView, left, top, dx, dy)
                for (x in 0 until childCount) {
                    if (getChildAt(x) == changedView) {
                        changedView.layout(left, 0, left + changedView.width, height)
                        //当前触摸的子View左右两边的View的left值,也就是距离ScrollerLayout的左边界的距离.
                        var totalChildWidth: Int = 0
                        //对于changedView左侧的View,采用由右至左的顺序来改变每个view的位置.方便totalChildWidth做累加操作
                        for (y in x - 1 downTo 0) {
                            val child = getChildAt(y)
                            totalChildWidth += child.width
                            child.layout(left - totalChildWidth, top, left - (totalChildWidth - child.width), height)
                        }
                        //changedView右侧的第一个View距离ScrollerLayout的左边界的默认距离
                        totalChildWidth = changedView.width+left
                        //对于changedView右侧的,采用由左至右的顺序来改变每个view的位置.
                        for (y in x + 1 until childCount) {
                            val child = getChildAt(y)
                            child.layout(totalChildWidth, 0, child.width + totalChildWidth, height)
                            totalChildWidth += child.width
                        }
                        break
                    }
                }
            }

onViewReleased(@NonNull View releasedChild, float xvel, float yvel)

松开手指后的回调.

  • releaseChild.getLeft() > 0;向右滚动.需要判断releaseChild滚动的距离有没有超过其前一个View的宽度的一半.
  • releaseChild.getLeft() < 0;向左滚动.需要判断releaseChild滚动的距离有没有超过自身的宽度的一半.

int getViewHorizontalDragRange(@NonNull View child);

水平滚动的范围.这里等于各个子view宽度之和.

int getViewVerticalDragRange(@NonNull View child);

竖直方向上不做滚动,直接返回0即可.
具体源码看这里ScrollerLayout


idealcn
27 声望4 粉丝