1. Scroller的实现
- scrollTo/scrollBy方法进行滑动时,这个过程是瞬间完成的。我们可以使用Scroller实现有过渡效果的滑动,不是瞬间完成的,而是在一定的时间间隔内完成的。
- Scroller本身是不能实现View的滑动的,它需要与View的computeScroll方法配合才能实现弹性滑动的效果。
- 在这里我们实现CustomView平滑地向右移动,首先我们需要初始化Scroller:
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
- 接下来重写computeScroll方法,此方法会在View绘制的时候在draw方法中被调用。我们调用父类的scrollTo方法并通过Scroller来不断获取当前的滚动值,每次滑动一小段距离我们就调用invalidate方法不断进行重绘,重绘会调用computeScroll方法,我们通过不断地移动一个小的距离并连贯起来就实现了平滑移动的效果。
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
- 我们在CustomView中写一个smoothScrollTo方法,调用Scroller的startScroll方法,2000ms内沿X轴平移delta像素,代码如下:
public void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.startScroll(scrollX,0,delta,0,2000);
invalidate();
}
- 调用代码,设定CustomView沿着X轴向右平移400像素。为了看到效果,我们延迟3秒。向右移动就是负数,向左移动就是正数。
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
mCustomView.smoothScrollTo(-300, 0);
}
}, 3000);
2. Scroller的解析
- 先看Scroller的构造方法:
public Scroller(Context context) {
this(context, null);
}
public Scroller(Context context, Interpolator interpolator) {
this(context, interpolator,
context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}
public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
mFinished = true;
if (interpolator == null) {
mInterpolator = new ViscousFluidInterpolator();
} else {
mInterpolator = interpolator;
}
mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
mFlywheel = flywheel;
mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
}
说明:Scroller有三个构造方法:通常我们都用第一个,第二个需要传入一个插值器Interpolator,如果不传则采用默认的插值器ViscousFluidInterpolator。
- 接下来我们来看Scroller的startScroll方法:
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;
}
说明:
- startScroll并没有调用类似开启滑动的方法,而是保存了存进来的各种参数:startX和startY表示滑动开始的起点,dx和dy表示滑动的距离,duration则表示滑动的时间。证明startScroll只是做滑动之前的准备的,并不能使View滑动。
- 关键是我们在startScroll方法后调用了invalidate方法,这个方法会导致View的重绘,而View的重绘会调用View的draw方法,draw方法会调用View的computeScroll方法。
- 接下来我们来看我们重写的computeScroll方法:
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
说明:在computeScroll方法中我们获取了当前的ScrollX,ScrollY,然后调用scrollTo方法进行View的滑动,接着调用invalidate方法来让View进行重绘,重绘又会调用computeScroll方法实现View的滑动。这样不断移动一个小距离并连贯起来就实现了平滑移动的效果。
- mScroller是如何获取当前位置的ScrollX,ScrollY的呢?那就是在调用scrollTo之前,会调用Scroller的computeScrollOffset方法。我们看看这个方法:
/**
* 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;
}
说明:
- 首先会计算动画持续的时间timePassed,如果timePassed小于我们设置的时间mDuration,则执行switch语句,因为在startScroll方法中的mMode为SCROLL_MODE,所以会执行分支语句 SCROLL_MODE,然后根据插值器Intepolator来计算出该时间段内移动的距离,赋值给mCurrX和mCurrY,这样我们就通过Scroller获取了当前的ScrollX和ScrollY.
- computeScrollOffset返回值如果是true则表示滑动未结束,为false则表示滑动结束,所以如果没有结束的话,我们就会执行scrollTo方法和invalidate方法来进行View的滑动。
3. Scroll的原理
综上所述:Scroller并不能直接实现View的重绘,它需要配合View的computeScroll方法,在computeScroll中不断进行重绘,每次重绘都会计算滑动持续的时间,根据这个持续的时间就能算出这次View滑动的位置,我们根据每次滑动的位置调用scrollTo方法进行滑动,这样不断的重复就形成了弹性滑动。