Android Scroller

564 阅读3分钟

前言

Android Scroller 是一个帮助 View 滚动的辅助类,Scroller 本身不会去滚动 View,它只是一个滚动计算辅助类,用于跟踪控件滑动的轨迹(Scroller 滚动分为两种类型,SCROLL_MODE 类型本质上是一个插值器,FLING_MODE 类型模拟物理世界的摩擦力,计算出减速度作用于滚动)。

Scroller

Android 中使用 Scoroller 类封装滚动。您可以使用滚动器(Scroller 或 OverScroller)来收集生成滚动动画所需的数据,例如,响应猛击手势。滚动器会随着时间的推移为您跟踪滚动偏移,但它们不会自动将这些位置应用到您的视图。您有责任以一定的速度获取并应用新坐标,以使滚动动画看起来平滑。

示例

下面通过示例展示 Scroller 使用方式和它的特性。

class ScrollerTrackFragment : BaseFragment(R.layout.fragment_scroller_track) {
    override val binding: FragmentScrollerTrackBinding by viewBinding()

    private lateinit var scroller: Scroller

    override fun initViews() {
        scroller = Scroller(requireContext())
        initTestButtons()
    }

    private fun initTestButtons() {
        binding.cvBtnContainer.addButton("startScroll", {
            binding.csvCoord.clearPoints()
            scroller.startScroll(0, 0, 300, 300, 1000)
            postNextFrame()
        })

        binding.cvBtnContainer.addButton("forceFinished", {
            scroller.forceFinished(true)
        })

        binding.cvBtnContainer.addButton("fling", {
            binding.csvCoord.clearPoints()
            scroller.fling(0, 0, 1000, 1000, 0, 150, 0, 150)
            postNextFrame()
        })
    }

    private val choreographer = Choreographer.getInstance()
    private fun postNextFrame() {
        debug("scroller:${scroller.toPrint()}")
        choreographer.postFrameCallback {
            if (scroller.computeScrollOffset()) {
                binding.vTarget.translationX = scroller.currX.toFloat()
                binding.vTarget.translationY = scroller.currX.toFloat()
                binding.csvCoord.addPoint(scroller.timePassed() / scroller.duration.toFloat(), scroller.currX / 300f)
                postNextFrame()
            }
        }
    }

}

示例中 startScrollfling 滑动轨迹如下

Scroller 主要成员和方法

public class Scroller  {
	 // 两种模式,SCROLL_MODE(startScroll) 是用 mInterpolator 求值, FLING_MODE(fling) 基于减速度进行求值
	 private int mMode;
	 // 默认 **ViscousFluidInterpolator,**它基于流体物理学,用于创建更复杂的代数插值,SCROLL_MODE 时,用它来求当前位置
	 private final Interpolator mInterpolator;
	 // 调用 computeScrollOffset() 后获取当前时间的 x 位置
	 private int mCurrX;
	 // 调用 computeScrollOffset() 后获取当前时间的 y 位置
    private int mCurrY;
    // stringScroll/fling 开始时间
    private long mStartTime;
    // SCROLL_MODE 为用户设置的值,FLING_MODE 基于减速度计算的值
    private int mDuration;
    // FLING_MODE 基于减速度计算的值
    private int mDistance;
    
    /**
     * 获取最新位置信息。如果返回 true,则动画尚未完成
     */
    public boolean computeScrollOffset() {
		   ...
    }

    /**
     * 通过提供起点、行进距离和滚动持续时间来开始滚动
     * 
     * @param startX 
     * @param startY 
     * @param dx 水平方向行驶距离
     * @param dy 垂直方向行驶距离
     * @param duration 持续时间.
     */
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
	     ...
    }
    
    /**
     * 基于快速滑动手势开始滚动。行进的距离取决于投掷的初始速度
     * 
     * @param startX 滚动的起始点 (X)
     * @param startY 滚动的起始点 (Y)
     * @param velocityX 滚动的初始速度 (X),以每秒像素为单位测量
     * @param velocityY 滚动的初始速度 (Y),以每秒像素为单位测量
     * @param minX 最小 X 值。滚动条不会滚动超过该点
     * @param maxX 最大 X 值。滚动条不会滚动超过该点
     * @param minY 最小 Y 值。滚动条不会滚动超过该点
     * @param maxY 最大 Y 值。滚动条不会滚动超过该点
     */
    public void fling(int startX, int startY, int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY) {
       ...
    }
}

ViscousFluidInterpolator 插值器

ViscousFluidInterpolator 是 Scroller 的内部类,它基于流体物理学,用于创建更复杂的代数插值。在 Android 动画插值器 里面介绍过插值器,对应的 ViscousFluidInterpolator 演示如下

OverScroller

它是Scroller类的一个增强版本,提供了更多的控制选项和更复杂的滚动行为。OverScroller主要用于实现在滚动过程中的惯性滚动效果。

示例

下面通过示例展示OverScroller使用方式和它的特性。

class OverScrollerTrackFragment : BaseFragment(R.layout.fragment_test_over_scroller_track) {
    override val binding: FragmentTestOverScrollerTrackBinding by viewBinding()

    private lateinit var overScroller: OverScroller

    override fun initViews() {
        overScroller = OverScroller(requireContext())
        initTestButtons()
    }

    private fun initTestButtons() {
        binding.cvBtnContainer.addButton("startScroll", {
            overScroller.startScroll(0, 0, 300, 300, 1000)
            startAnim()
        })

        binding.cvBtnContainer.addButton("forceFinished", {
            overScroller.forceFinished(true)
        })

        binding.cvBtnContainer.addButton("fling", {
            overScroller.fling(0, 0, 1000, 1000, 0, 80, 0, 80, 200, 200)
            startAnim()
        })
    }

    /**
     * 开启动画
     */
    private fun startAnim() {
        startTime = AnimationUtils.currentAnimationTimeMillis()
        binding.csvCoord.clearPoints()
        postNextFrame()
    }

    // 动画开启的时间
    private var startTime: Long = 0
    private val choreographer = Choreographer.getInstance()
    private fun postNextFrame() {
        val timePassed: Int = (AnimationUtils.currentAnimationTimeMillis() - startTime).toInt()
        debug("overScroller:${overScroller.toPrint()}")
        choreographer.postFrameCallback {
            if (overScroller.computeScrollOffset()) {
                binding.vTarget.translationX = overScroller.currX.toFloat()
                binding.vTarget.translationY = overScroller.currX.toFloat()
                binding.csvCoord.addPoint(timePassed / 1000f, overScroller.currX / 300f)
                postNextFrame()
            }
        }
    }
}

示例中 fling 滑动轨迹如下

OverScroller 主要成员和方法

OverScroller 封装了滚动,并具有超出滚动操作范围的能力。在大多数情况下,它是 Scroller 的直接替代品。其中一个关键的区别在于对 fling 支持了回弹效果。

public class OverScroller {

  /**
   * 基于快速滑动手势开始滚动。行进的距离取决于投掷的初始速度。
   *
	 * @param startX 滚动的起始点 (X)
   * @param startY 滚动的起始点 (Y)
   * @param velocityX 投掷的初始速度 (X),以每秒像素为单位测量
   * @param velocityY 投掷的初始速度  (Y),以每秒像素为单位测量
   * @param minX 最小 X 值。除非 overX > 0,否则滚动条不会滚动超过此点。如果允许 overfling,它将使用 minX 作为回弹边界
   * @param maxX 最大 X 值。除非 overX > 0,否则滚动条不会滚动超过此点。如果允许 overfling,它将使用 maxX 作为回弹边界。
   * @param minY 最小 Y 值。除非 overY > 0,否则滚动条不会滚动超过此点。如果允许 overfling,它将使用 minY 作为回弹边界。
   * @param maxY 最大 Y 值。除非 overY > 0,否则滚动条不会滚动超过此点。如果允许 overfling,它将使用 maxY 作为回弹边界。
   * @param overX 超出范围。如果 > 0,水平溢出
   * @param overY 超出范围。如果 > 0,垂直溢出
   *
   */
  public void fling(int startX, int startY, int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY, int overX, int overY) {
	  ...
  }

}

回弹的实现在 OverScroller 内部类 SplineOverScroller 中完成的,它把滚动分为三个阶段

  • SPLINE:正常滑动阶段
  • BALLISTIC:越界减速阶段
  • CUBIC:回弹阶段

每个阶段滚动曲线都是不同的,更多细节可以参考 Android 列表滚动优化之 OverScroller 揭秘 这篇博客。

参考文档

Scroller

Android 列表滚动优化之 OverScroller 揭秘