乐趣区

使用ViewDragHelper自定义左右可滑动内容的ViewGroup

通过在自定义的 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

退出移动版