简介
使用scollTo/scollBy方法进行滑动时,这个过程是瞬间完成的,所以用户体验不大好。如果使用 Scroller 来实现有过渡效果的滑动,这个过程不是瞬间完成的,而是在一定的时间间隔内完成的。 Scroller本身是不能实现View的滑动的,它需要与View的computeScroll()方法配合才能实现弹性滑动的效果。
使用
在自定义View的构造函数中初始化Scroller。
public CustomView(Context context, 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 x, int y) {
int scrollX = getScrollX();
int delta = x - scrollX;
mScroller.startScroll(scrollX, 0, delta, 0, 2000);
invalidate();
}
通过以下代码触发平移动作。
mCustomView.smoothScrollTo(-400,0);
效果:
原理
我们通过调用smoothScrollTo来开始活动,在smoothScrollTo中在调用了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; //滑动开始的X位置
mStartY = startY; //滑动开始的Y位置
mFinalX = startX + dx; //滑动结束的X位置
mFinalY = startY + dy; //滑动结束的Y位置
mDeltaX = dx; //X偏移量
mDeltaY = dy; //Y偏移量
mDurationReciprocal = 1.0f / (float) mDuration; //滑动持续时间的倒数
}
可见startScroll只是给一些变量赋值,并没有进行实际的操作。在我们调用invalidate方法后,View会进行重绘,重绘时会调用computeScroll方法。在computeScroll中,我们调用了computeScrollOffset方法,该方法源码如下。
public boolean computeScrollOffset() {
if (mFinished) { //如果滑动已经结束,返回false。
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); //滑动动画已经进行的时长
if (timePassed < mDuration) { //滑动动画已经进行的时长是否超过规定的时长(mDuration)
switch (mMode) { //判断滑动的模式
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
/*
计算出当前应该处于的X位置
计算出当前应该处于的Y位置
*/
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;
}
computeScroll方法:
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) { //判断滑动是否结束
((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); //移动View
invalidate(); //重绘View
}
}
总结
Scroller并不能直接实现View的滑动,它需要配合View的computeScroll()方法。在computeScroll()中不断让View进 行重绘,每次重绘都会计算滑动持续的时间,根据这个持续时间就能算出这次View滑动的位置,我们根据 每次滑动的位置调用scrollTo()方法进行滑动,这样不断地重复上述过程就形成了弹性滑动。