View的scrollTo或者scrollBy方法可以将View内容滚动到指定位置,但是两者都是瞬时完成的,没有过渡动作。为了提升用户体验,有时我们希望可以实现view平滑的滚动,这时Scroller就派上用场了。
其实Scroller本身并不能滚动View,通常需要跟View的computeScroll配合使用才能发挥作用。例如我们自定义Layout:
public class ScrollerTestLayout extends FrameLayout {
private Scroller mScroller;
public ScrollerTestLayout(Context context) {
this(context, null);
}
public ScrollerTestLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
public void scroll(int delta){
mScroller.startScroll(getScrollX(), 0, delta, 0);
postInvalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
}
当调用srcoll方法时,layout里的内容将在水平方向上整体向左移动delta大小的距离(如果delta<0,将向右移动)。那Scroller到底是怎样工作的呢?首先看下startScroll()方法源码。
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}
/**
* Start scrolling by providing a starting point, the distance to travel,
* and the duration of the scroll.
*/
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;
}
可以看到,Scroller的startScroll方法其实什么也没做,只是记录一些需要的数据。真正的触发应该来源于postInvalidate()或者invalidate()方法,这两个方法会导致view重绘,重绘过程中会调用computeScroll()方法。在该方法中我们手动调用scrollTo滚动view内容到Scroller为我们指定的位置,即getCurrX()和getCurrY()。至于这个具体位置,Scorller的computeScrollOffset()会自动为我们计算,并返回boolean值告知我们滑动是否结束。如果滑动没有结束,我们继续调用postInvalidate()方法重绘,这样就形成一个循环直至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:
// fling
...
} else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
从源码可以看出,Scroller会根据插值器计算时间流逝的占比,最终计算出view内容的位置信息。当然,我们也可以自定义插值器,按照我们需要的规则平滑滚动view内容。