前言
近几工作比较忙,挺长时间没更新掘金了,今天抽点时间更新一篇吧(好习惯不能丢😂 )。之前在介绍View滑动的相关姿势的时候,文末简单聊了下Scroller,也没有细扣源码细节,今天就一起过一下Scroller的源码细节吧。
常规套路及代码
我们经常看到网上说的Scroller是这么使用:
class CustomView constructor(context: Context, attributeSet: AttributeSet?) :
View(context, attributeSet) {
...
//创建Scroller
private val scroller = Scroller(context)
...
//定义个丝滑滚动的方法
fun smoothScroll(startX: Int, startY: Int, deltaX: Int, deltaY: Int, duration: Int = 2000) {
//1
scroller.startScroll(startX, startY, deltaX, deltaY, duration)
//重新绘制 会导致computeScroll()方法回调
postInvalidateOnAnimation()
}
...
//view中方法
override fun computeScroll() {
//2
if (scroller.computeScrollOffset()) {
//拿到scroller的当前x
val currX = scroller.currX
//更新left right
left = currX
right = left + measuredWidth
//拿到scroller的当前y
val currY = scroller.currY
//更新top bottom
top = currY
bottom = top + measuredHeight
//重新绘制 会导致computeScroll()方法回调
postInvalidateOnAnimation()
}
}
}
从上面的自定义View代码中我们定义了一个比较常见的方法smoothScroll,这个方法里面的参数含义具体如下:
- startX 水平方向x的初始值
- startY 竖直方向y的初始值
- deltaX 水平方向x滑动距离
- deltaY 竖直方向y滑动距离
- duration 滑动整体执行下来的时间
如果我们像下面这样在fragment里面调用这块代码出现的现象是啥样的:
...
//定义相关参数
private val startX = 20.dp
private val startY = 20.dp
private val deltaX = 200.dp
private val deltaY = 200.dp
...
//给buttom设置点击事件
binding.btnStartSmoothScroll.setOnClickListener {
//调用我们自定义view的smoothScroll方法
binding.svTest.smoothScroll(startX, startY, deltaX, deltaY)
}
...
现象如下图所示:
乍一看,感觉像是做了个动画,从视觉上确实跟动画一样。那么接下来我们从自定义view的smoothScroll方法开始研究起来。
源码解释
自定义view的smoothScroll方法实际调用了下面两行代码:
fun smoothScroll(startX: Int, startY: Int, deltaX: Int, deltaY: Int, duration: Int = 2000) {
//1 调用scroller的startScroll方法
scroller.startScroll(startX, startY, deltaX, deltaY, duration)
//2 重新绘制
postInvalidateOnAnimation()
}
标注1处scroller的startScroll方法做了些啥:
/**
* 方法描述:
* 通过提供一个 起始滑动点、水平和竖直滑动距离、滑动时间来开始一段滑动
* 起始滑动点由 startX startY决定
* 滑动距离由dx dy决定
* 滑动时间由duration决定
*
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
//当前模式滚动模式
mMode = SCROLL_MODE;
//是否结束置为false
mFinished = false;
//复制滑动时间
mDuration = duration;
//记录滑动开始时间
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
//最终水平方向位置
mFinalX = startX + dx;
//最终竖直方向位置
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
上面的方法我们可以看到:Scroller的startScroll方法只是对各个成员变量进行了赋值,并没有开始滑动。那问题来了,我们是如何让view滑动起来的呢?接着我们看一下标注2处的postInvalidateOnAnimation(),这个方法会导致view重绘,间接也导致了View的computeScroll方法调用:
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
...
}
}
在view的computeScroll方法中我们调用了Scroller的computeScrollOffset()方法,这个方法名字翻译过来意思是计算滚动偏移量,接着我们瞅瞅方法里面到底做了什么:
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
* 调用这个方法当你想知道当前新位置的时候,如果返回true,那代表当前动画还没结束
*/
public boolean computeScrollOffset() {
//如果结束 返回false
if (mFinished) {
return false;
}
//从调用startScroll到现在过去的时间
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
//如果过去的时间小于动画总时间 说明滑动动画还没结束
if (timePassed < mDuration) {
switch (mMode) {
//滚动模式
case SCROLL_MODE:
//通过interpolator并根据当前过去的时间计算当前的动画进度
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
//根据初始值 和动画进度获取当前水平方向坐标 竖直方向坐标同理
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
...
}
}
else {
//如果过去的时间大于等于动画总时间 说明滑动动画结束
//直接将mCurrX、mCurrY置为最终坐标值,同时mFinished置为true
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
上面的代码注释比较详细,大体逻辑我稍微总结一下:调用这个方法的时候会判断当前时间是否动画结束了,如果没结束,则计算出当前mCurrX、mCurrY的值,也就是当前滑动的坐标点,同时放回true代表动画还没结束。所以我们可以知道调用这个computeScrollOffset方法是用来更新当前应该滑动到的坐标点及判断滑动过程是否结束.
这个时候我们再看文章开头中的这段代码:
override fun computeScroll() {
//如果动画没结束 进入代码块执行逻辑
if (scroller.computeScrollOffset()) {
//拿到scroller当前水平坐标值
val currX = scroller.currX
//更新left right
left = currX
right = left + measuredWidth
//拿到scroller当前竖直坐标值
val currY = scroller.currY
//更新top bottom
top = currY
bottom = top + measuredHeight
//重绘
postInvalidateOnAnimation()
}
}
是不是可以看出来上述代码的逻辑就是一直在轮询获取当前scroller计算出来的坐标值,然后更新view的矩形区域,直到scroller告诉我们动画结束,当然文章开头gif动画效果也就理解了。
总结
我们从上面一个小的demo入手再加上给大家的一些源码解释,可以对Scroller有个整体认知:
- startScroll 用于赋值初始值(滑动起始点,滑动距离,滑动时间)
- computeScrollOffset 计算当前滑动到的位置 并返回滑动动画是否结束
- scroller 只是一个计算的工具,给我们提供了很多滑动相关的方法,我们需要拿到scroller计算出来的值,去动态更新view的位置就行
在做滑动相关的东西的时候,要灵活运用Scroller/OverScroller,再结合着我们之前铺垫的【View系列】让View滑动起来的几种方式, 就可以做个比较炫酷的滑动view了。
上述源码见Github
最后推荐一下我维护的一个开源库《超好用的Android高亮引导库》。Github地址如下:github.com/hyy920109/H…
拓展
Scroller另外一些比较常用的方法如下:
/**
* 强制停止动画,停留到了当前帧计算出来的坐标点
*/
public final void forceFinished(boolean finished) {
mFinished = finished;
}
/**
* 中断动画 直接就滑动到最终位置了
*/
public void abortAnimation() {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}