如果我们想滑动view的内容,直接使用scrollTo()方法,会发现滑动的效果十分生硬。那该如何流畅的滑动呢?这就需要使用Scroller。
首先看一下View中的两段源码:
1.computeScroll():
/**
* 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() {
}
2.draw():
if (!drawingWithRenderNode) {
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
从第一段代码可以发现,View中computeScroll()是一个空实现。
从第二段代码和第一段代码的注释中能够看出,computeScroll()在view重绘的过程中会被调用。
下面介绍一下Scroller,首先看一下Scroller的源码:
1.startScroll():
/**
* Start scrolling by providing a starting point, the distance to travel,
* and the duration of the scroll.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
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;
}
2.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;
}
从第一段代码中可以看出,startScroll()中对view没有进行任何操作,没有让view去正真地滑动,只是对一些传入的值进行了记录,可以看成是其内部模拟滑动的过程。
从第二段代码中可以看出,返回的值是和scoller所模拟的滑动是否结束有关。倘若结束了,就返回false,反之返回true。
有了以上对于源码的了解下面就可以实现view流畅的滑动了,如下是我之前写的一个自定义View中的部分代码:
private void init() {
if (mScroller == null) {
mScroller = new Scroller(getContext());
}
}
private void smoothScrollBy(int dx) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
postInvalidate();
}
}
需要滑动时,我们调用smoothScrollBy()。
mScroller开始模拟滑动。
invalidate()让view刷新,从而在draw()中调用computeScroll()。
判断是否模拟滑动结束,如果没有结束就继续滑动,并且让view刷新。
就这样不断通过computeScroll()和invalidate()的互调,实现view一点一点的scrollTo(),显示出来就是一个流畅的滑动了。
直到mScroller的模拟滑动结束。