Android 实现简易版ScrollView

前言

学习过view内容滑动相关知识,想实践下view内容的滑动以及惯性滑动如何实现。

实现

完整实现:https://github.com/18839779221/test/blob/main/app/src/main/java/com/example/scroll/ScrollLayout.kt

ScrollView控件的高度和内容的高度

前提:View只有当其内容高度大于控件高度的情况下,才能够竖直滑动。

使用过ScrollView的朋友应该会清楚,ScrollView只能有一个子控件,于是

  • ScrollView内容的高度为ScrollView.child[0].height
  • ScrollView控件的高度为其height属性

Android 实现简易版ScrollView_第1张图片

 

本次自定义的控件希望直接定义一个可滑动的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
    }
}

ScrollView的事件TOUCH_MOVE滑动事件(跟手滑动)

跟手滑动操作中由于TOUCH_MOVE事件的频繁发生和滑动行为的不确定性,很难使用动画来实现,这里采用简单的scrollBy()方法来实现View滑动。

Android 实现简易版ScrollView_第2张图片

 

我们应该保证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)的源码,我们可以看到

  • offset = mScrollY
  • range = getHeight() - getHeight() = 0

可以看到默认range==0,也就是一定会返回false,想要使用该方法就必须做一些调整,下边来分析下什么方法需要重写

  • computeVerticalScrollOffset() = scrollY,没有问题,符合预期,View的偏移量就是scrollY
  • computeVerticalScrollRange() = getHeight(),有问题,该方法表示View可滑动范围,即View的内容高度
  • computeVerticalScrollExtent() = getHeight(),没有问题,代表View的实际可见区域

因此需要重写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
}

ScrollView的惯性滑动(Scroller实现)

惯性滑动发生在用户滑动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()
    }
}

ScrollView的惯性滑动(OverScroller实现)

可以将上述Scroller视线中的scroller变量替换为OverScroller的实现可无缝切换到OverScroller

private val scroller = OverScroller(context)

你可能感兴趣的:(android学习,android,java)