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