学习过view内容滑动相关知识,想实践下view内容的滑动以及惯性滑动如何实现。
完整实现:https://github.com/18839779221/test/blob/main/app/src/main/java/com/example/scroll/ScrollLayout.kt
前提:View只有当其内容高度大于控件高度的情况下,才能够竖直滑动。
使用过ScrollView的朋友应该会清楚,ScrollView只能有一个子控件,于是
本次自定义的控件希望直接定义一个可滑动的LinearLayout(一般LinearLayout的子控件可能超出屏幕范围),并不希望限制只有一个子控件,这样就需要手动计算内容的高度。通过重写onMeasure方法可以计算。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (orientation == VERTICAL) {
var totalLength = paddingTop + paddingBottom
for (child in children) {
totalLength += child.marginTop + child.measuredHeight + child.marginBottom
}
totalHeight = totalLength
}
}
跟手滑动操作中由于TOUCH_MOVE事件的频繁发生和滑动行为的不确定性,很难使用动画来实现,这里采用简单的scrollBy()方法来实现View滑动。
我们应该保证View的滑动不会超出其上下边界,根据上图分析,我们需要保证scrollY的滑动范围为[0,totalHeight - height],可以通过View.canScrollVertically(direction: Int)方法来判断View是否可以朝指定方向继续滑动。
public boolean canScrollVertically(int direction) {
final int offset = computeVerticalScrollOffset();
final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range - 1;
}
}
protected int computeVerticalScrollOffset() {
return mScrollY;
}
protected int computeVerticalScrollRange() {
return getHeight();
}
protected int computeVerticalScrollExtent() {
return getHeight();
}
根据View.canScrollVertically(direction: Int)的源码,我们可以看到
可以看到默认range==0,也就是一定会返回false,想要使用该方法就必须做一些调整,下边来分析下什么方法需要重写
因此需要重写computeVerticalScrollRange()方法
override fun computeVerticalScrollRange(): Int {
return totalHeight
}
处理滑动的完整代码
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event == null) return false
val action = event.action
val currX = event.x
var currY = event.y
velocityTracker.addMovement(event)
when (action) {
MotionEvent.ACTION_DOWN -> {
touchDown(currX, currY)
}
MotionEvent.ACTION_MOVE -> {
touchMove(currX, currY)
}
MotionEvent.ACTION_UP -> {
touchUp()
}
}
lastX = currX
lastY = currY
return true
}
private fun touchMove(currX: Float, currY: Float) {
handleScroll(currX, currY)
}
private fun handleScroll(currX: Float, currY: Float) {
val deltaX = currX - lastX
val deltaY = currY - lastY
if (abs(deltaX) < touchSlop && abs(deltaY) < touchSlop) return
if (canScrollVertically(1) || canScrollVertically(-1)) {
//防止滑出边界
val realDeltaY = limitRange(-deltaY.toInt(), totalHeight - height - scrollY, -scrollY)
scrollBy(0, realDeltaY)
}
}
override fun computeVerticalScrollRange(): Int {
return totalHeight
}
惯性滑动发生在用户滑动View的手指抬起后,一般结合VelocityTracker来获取手指抬起时的速度,根据此速度,来决定滑动的距离。
我们直接采用Scroller控件来方便的实现惯性滑动。
private fun touchUp() {
velocityTracker.computeCurrentVelocity(1000, vc.scaledMaximumFlingVelocity.toFloat())
val yVelocity = velocityTracker.yVelocity
if(abs(yVelocity) >= vc.scaledMinimumFlingVelocity){
scroller.fling(scrollX, scrollY, 0, -yVelocity.toInt(), 0, scrollX, 0, totalHeight - height)
invalidate()
}
}
注意,VelocityTracker要能够计算出TOUCH_UP时的速度,必须将此次整个touch事件序列的event都传递给VelocityTracker。也就上边onTouchEvent()方法中使用到的velocityTracker.addMovement(event)。
此外Scroller只能计算出在相应时间点View应该滑动到的位置,实际的View滑动还需要依靠View.computeScroll()方法,根据其方法注释我们也可以看出其就是为了配合Scroller进行View滑动所预设的方法,因此我们需要重写此方法,以处理惯性滑动期间View的真实滑动。
/**
* Called by a parent to request that a child update its values for mScrollX
* and mScrollY if necessary. This will typically be done if the child is
* animating a scroll using a {@link android.widget.Scroller Scroller}
* object.
*/
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.currX, scroller.currY)
postInvalidate()
}
}
可以将上述Scroller视线中的scroller变量替换为OverScroller的实现可无缝切换到OverScroller
private val scroller = OverScroller(context)