李达康书记在给大风厂员工讲话时,后面的墙上写道:“努力能把事情做好,而用心却能把事情做对。”Scroller这个有意思的东西,我们现在就去来简单的了解了解它。

Scroller的由来
当谈到View的滑动的时候,我们知道通常有三种方式:
1.利用View的scrollTo()/scrollBy()方法;
2.使用动画;
3.改变View的LayoutParams;
在实现View的滑动的时候为什么会与Scroller有关系了?好吧,原因来了,因为在View的滑动中有一个名词是叫弹性滑动,而Scroller就是一个弹性滑动对象。言外之意就是用来实现弹性滑动的一个帮助类。
Scroller的组成
打开Scroller类,我们可以发现其就是一个单一类。关于类的介绍说其是一个封装的滑动类,我们可以用Scroller的对象或通过OverScroller收集需要的数据来实现一个弹性滑动。举个例子说吧,要响应一个滑动手势,Scroller会随着时间的推移去跟踪这个滑动的偏移量,但是它们并不会自动的把这些偏移的位置应用在你的View上,需要我们自己去得到这些值并且以一定的速率应用在新的坐标上,这样的话就会使滑动动作看起来更平滑些。
Scroller这个类比较简单,代码也就600行不到。通过对整个类的观察我们可以知道有以下主要成员变量与方法。
Scroller的主要成员变量与方法.png
通过对这个类的大致观看一下,除了一些get()方法,我们可以把注意力多放在set()与startScroll()方法上。
研究Scroller探究其工作机制
接下来我们就通过代码仔细研究Scroller到底是怎么进行工作的?上面我们就说到可以先把注意力放在set()方法与startScroll()方法上。
我们先研究下computeScrollOffset()这个方法,以下是Scroller的源代码:
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
我们对这个方法进行分析:mFinished这个成员变量用来记录scroller是否已经结束滑动。当滑动结束后,返回false,当还在滑动的时候返回true。而通过1.2行代码我们就可以清楚mFinished的重要性,只要改变mFinished就可以直接结束滑动。所以Scroller类中有一个方法可对mFinished进行控制,代码如下:
/**
* Force the finished field to a particular value.
*
* @param finished The new finished value.
*/
public final void forceFinished(boolean finished) {
mFinished = finished;
}
紧接着我们继续研究代码,有一个局部变量timePassed用来记录当前时间与开始时间mStartTime的时间差,好,问题来了,mStartTime这个字段什么时候被赋值就意味着滑动从什么时候开始,那么mStartTime什么时候赋值了?我们可以发现:mStartTime在startScroll()与fling()方法中被赋值,那我们先对这两个方法研究,以下是源代码及发现:
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
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;
}
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
// Continue a scroll or fling in progress
if (mFlywheel && !mFinished) {
float oldVel = getCurrVelocity();
float dx = (float) (mFinalX - mStartX);
float dy = (float) (mFinalY - mStartY);
float hyp = (float) Math.hypot(dx, dy);
float ndx = dx / hyp;
float ndy = dy / hyp;
float oldVelocityX = ndx * oldVel;
float oldVelocityY = ndy * oldVel;
if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
Math.signum(velocityY) == Math.signum(oldVelocityY)) {
velocityX += oldVelocityX;
velocityY += oldVelocityY;
}
}
mMode = FLING_MODE;
mFinished = false;
float velocity = (float) Math.hypot(velocityX, velocityY);
mVelocity = velocity;
mDuration = getSplineFlingDuration(velocity);
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
double totalDistance = getSplineFlingDistance(velocity);
mDistance = (int) (totalDistance * Math.signum(velocity));
mMinX = minX;
mMaxX = maxX;
mMinY = minY;
mMaxY = maxY;
mFinalX = startX + (int) Math.round(totalDistance * coeffX);
// Pin to mMinX <= mFinalX <= mMaxX
mFinalX = Math.min(mFinalX, mMaxX);
mFinalX = Math.max(mFinalX, mMinX);
mFinalY = startY + (int) Math.round(totalDistance * coeffY);
// Pin to mMinY <= mFinalY <= mMaxY
mFinalY = Math.min(mFinalY, mMaxY);
mFinalY = Math.max(mFinalY, mMinY);
}
我们可以发现Scroller提供两种滑动模式,并且在调用这两个方法的时候并不会对View进行滑动,而只是对一些成员变量进行赋值,例如:其中的一个参数就是开始的时间,并且我们可以明白Scroller它并没有对View的对象进行操作,这有点我们不生产水,我们只是大自然的搬运工啊的味道。哈哈。继续,Scroller又是一个滑动对象。那么我们可以猜想我不动你,你要滑动,那么只有你自己动了。要不然没办法了。这个思路没毛病的。那么View它怎么自己通过Scroller的一系列赋值自己动了,我知道你已经想到了,那就是让它自己进行重绘了。所以这就更加验证了Scroller它真的只是一个来辅助实现弹性滑动的帮助类了,它不是主力。但是它却往外提供计算好了的一定时间内的改变的当前位置坐标。这基本上是Scroller的一个工作流程了。
使用Scroller来实现View的弹性滑动
在使用Scroller来实现弹性滑动时,我们需要了解一个方法,不过与其说是一个方法,还不如说是它的一个老搭档,何况官方也是这么说的,是哪个方法了?那就是computeScroll()这个方法,computeScroll()这个方发是在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.
*/
public void computeScroll() {
}
typically be done 这下你明白这两货之间有多配合了吧,下面的一个Demo则是它们普遍的一个配合方法。
public class MoveView extends AppCompatTextView {
private static final String TAG = "MoveView";
private Scroller mScroller;
public MoveView(Context context) {
super(context);
}
public MoveView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MoveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroller = new Scroller(context);
}
public void zoomIn(){
mScroller.forceFinished(true);
mScroller.startScroll(0,0,100,100,1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller != null) {
if (mScroller.computeScrollOffset()) {
int mScrollX = mScroller.getCurrX();
int mScrollY = mScroller.getCurrY();
scrollTo(mScrollX,mScrollY);
postInvalidate(); // So we draw again
}
}
}
}
外部调用zoomIn()方法就可以实现将此View的内容在1秒内水平距离和竖直距离分别移动100个像素。其中调用的invalidate()函数会使整个View进行重绘,当然也会调用onDraw()方法。这是无疑问的,但是跟我computeScroll()有什么联系了?既然亲测能够实现弹性滑动,那么他们之间肯定是有联系的,什么联系我们继续看源码!这个onDraw()跟computeScroll()方法一样在View父类中都是一个空实现。且这个onDraw(Canvas canvas)方法只在View类的void draw(Canvas canvas)中调用且执行这个绘制有几个必须执行的绘图步骤。这里我简要说一下:
- 绘制背景
- 保存画布图层以备褪色(有必要的情况下)
- 绘制视图的内容
- 绘制衰减边并恢复图层(有必要的情况下)
- 画装饰(例如:滚动条)
同时,我们通过View的源码发现,在View的绘制过程中,其中也对computeScroll()方法进行了调用。所以并不是说invalidate()函数会使onDraw()方法调用从而使computeScroll()方法进行调用,他们之间是没有包含联系的,有的只是在绘制的过程的某一个阶段同时被调用了。所以这一点理解我觉得很重要。
总结
其实这个通过Scroller这种方式来实现弹性滑动的思路很高级呀。值得我们去学习。Be in a Googley Mood,总的来说,Scroller就是燃料补给器,invalidate()函数就是发动机整个系统。scrollTo()就是轮子。View就是汽车。汽车要想动起来,就要燃料给其提供燃料值,然后发动机再运行整个系统让轮子动起来从而汽车行驶起来了。可能比喻的不够恰当,可是我就是这么理解的,如果有更好的想法,记得告诉我哦。
网友评论