1. Scroller原理分析
经典使用
Scroller scroller = new Scroller(getContext());
// 缓慢滚动到指定位置
private void smoothScrollTo(int destX, int destY) {
int startX = (int) getX();
int deltaX = startX - destX;
int startY = (int) getY();
int deltaY = startY - destY;
// 2000ms内滑向目标位置,效果就是慢慢滑动
// dx,dy的计算是:起始坐标-目标坐标,否则会反向跑
scroller.startScroll(startX, startY, deltaX, deltaY, 2000);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
首先看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;
}
这个方法只是保存了一些值,没有对View有操作。真正让View弹性滑动的是invalidate方法。invalidate方法会导致View重绘,在viwe的draw方法中会调用computeScroll方法,这个方法在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() {
}
在我们重写的方法中,我们首先调用scroller的computeScrollOffset方法去判断是否继续滚动到新位置,返回true则我们执行scrollTo方法,使View的内容滚动,并继续调用postInvalide方法。直到整个滑动过程结束。
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;
}
2. 通过动画模拟Scroller
final int startX = 0;
final int deltaX = 200;
ValueAnimator animator = ObjectAnimator.ofInt(0, 1).setDuration(2000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
// 内容向左滚动是正的
view.scrollTo(startX + (int) (deltaX * fraction), 0);
}
});
animator.start();
我们动画本质上没有作用于任何对象,它只是在2秒内完成动画过程,利用这个特性,我们在动画每一帧到来时获取动画的完成比例,再根据比例计算当前View要滚动的距离,再通过Vieww本身的scrollTo方法完成滚动。这个过程和Scroller的过程很相似。
3. 通过延时策略完成弹性滚动
思路和上面的很相似。以handler为例,如果没有完成滚动距离,就不断发消息让view滚动,完成滚动距离则停止发送。代码如下:
private final int MESSAGE_SCROLL_TO = 1;
private int frame_count = 30;
private int count = 0;
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case MESSAGE_SCROLL_TO:
count++;
if (count<=frame_count){
float fraction = count/(float)frame_count;
int scrollX = (int) (fraction*200);
view.scrollTo(scrollX,0);
handler.sendEmptyMessage(MESSAGE_SCROLL_TO);
}
break;
}
}
};