Scroller 是一个帮助 View 滚动的辅助类,在使用它之前,用户需要通过 startScroll()
来设置滚动的参数,即起始点坐标和 (x,y) 轴上要滚动的距离。Scroller 它封装了滚动时间(默认的滚动时间为 250 毫秒)、要滚动的目标 x 轴和 y 轴,以及在每个时间内 View 应该滚动到的 (x,y) 轴的坐标点,这样用户就可以在有效的滚动周期内通过 Scroller 的 getCurX()
和 getCurY()
来获取当前时刻 View 应该滚动的位置,然后通过调用 View 的 scrollTo()
或者 scollBy()
方法进行滚动。
startScroll() 在 Scroller 类中定义,源码如下:
/**
* Start scrolling by providing a starting point and the distance to travel.
* The scroll will use the default value of 250 milliseconds for the
* duration.
*
* @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.
*/
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.
*
* @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;
}
getCurX() 和 getCurY() 在 Scroller 类中定义,源码如下:
/**
* Returns the current X offset in the scroll.
*
* @return The new X offset as an absolute distance from the origin.
*/
public final int getCurrX() {
return mCurrX;
}
/**
* Returns the current Y offset in the scroll.
*
* @return The new Y offset as an absolute distance from the origin.
*/
public final int getCurrY() {
return mCurrY;
}
scrollTo() 和 scrollBy() 在 View 类中定义,源码如下:
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
那么如何判断滚动是否结束呢?我们只需要覆写 View 类的 computeScroll()
方法,该方法会在 View 绘制时被调用,在里面调用 Scroller 的 computeScrollOffset()
来判断滚动是否完成,如果返回 true 则表明滚动未完成,否则滚动完成。上述说的 scrollTo()
或者 scrollBy()
的调用就是在 computeScrollOffset()
为 true 的情况下调用,并且最后还要调用目标 View 的 postInvalidate()
或者 invalidate()
以实现 View 的重绘。View 的重绘又会导致 computeScroll()
方法被调用,从而继续整个滚动过程,直至 computeScrollOffset()
返回 false,即滚动结束。
computeScroll() 在 View 类中定义,computeScrollOffset() 在 Scroller 类中定义,源码如下:
/**
* 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() {
}
/**
* 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;
}
注意:Android中实现 View 的更新有两种方法:一种是
invalidate()
,另一种是postInvalidate()
,其中前者是在 UI 线程中使用,而后者在非 UI 线程中使用。
1. 利用 invalidate()
刷新界面
实例化一个 Handler 对象,并重写 handleMessage()
方法调用 invalidate()
实现界面刷新,而在子线程中通过 sendMessage()
发送界面更新消息。
2. 使用 postInvalidate()
刷新界面
使用 postInvalidate()
则比较简单,不需要 Handler,直接在线程中调用 postInvalidate()
即可。
View 类中 postInvalidate()
方法源码如下,可见它也是用到了 Handler:
public void postInvalidate() {
postInvalidateDelayed(0);
}
public void postInvalidateDelayed(long delayMilliseconds) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
整个过程可能不是太好理解,我们先来看一个实例:
public class ScrollLayout extends FrameLayout {
private String TAG = ScrollLayout.class.getSimpleName();
Scroller mScroller;
public ScrollLayout(Context context) {
super(context);
mScroller = new Scroller(context);
}
//该函数会在 View 重绘之时被调用
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
//滚动到此,View 应该滚动到的 x,y 坐标上
this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//请求重绘该 View,从而又会导致 computeScroll 被调用,然后继续滚动,
//直到 computeScrollOffset 返回 false
this.postInvalidate();
}
}
//调用这个方法进行滚动,这里我们只滚动竖直方向
public void scrollTo(int y) {
//参数1和参数2分别为滚动的起始点在水平、竖直方向的滚动偏移量
//参数3和参数4为在水平和竖直方向上滚动的距离
mScroller.startScroll(getScrollX(), getScrollY(), 0, y);
this.invalidate();
}
}
滚动该视图的代码:
ScrollLayout scrollView = new ScrollLayout(getContext());
scrollView.scrollTo(100);
通过上面这段代码会让 scrollView 在 y 轴上向下滚动 100 个像素点。
我们结合代码来分析一下。首先调用 scrollTo(int y)
方法,然后在该方法中通过 mScroller.startScroll()
方法来设置滚动的参数,再调用 invalidate()
方法使得该 View 重绘。重绘时会调用 computeScroll()
方法,在该方法中通过 mScroller.computeScrollOffset()
判断滚动是否完成,如果返回 true,代表没有滚动完成,此时把该 View 滚动到此刻 View 应该滚动到的 x、y 位置,这个位置通过 mScroller 的 getCurX()
和 getCurY()
获得。然后继续调用重绘方法,继续执行滚动过程,直至滚动完成。