Scroller是一个用于实现弹性滑动的帮助类,它主要是通过时间的流逝,根据设置到Scroller中的插值器,计算出当前滑动到的距离,然后我们根据scrollTo/scrollBy去让view产生弹性滑动的动画效果,与属性动画类似,使用Scroller要明白一点,那就是:
Scroller只能对View中的内容进行滑动,如一个Button使用Scroller以后,Button的背景不会滑动,而是Button中的title滑动,如一个ViewGroup使用Scroller以后,ViewGroup不会滑动,而是ViewGroup中的子View滑动
scrollTo/scrollBy
分析Scroller之前先来看下scrollTo/scrollBy这两个方法:
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();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
scrollTo是实现基于传递参数的绝对滑动,scrollBy调用了scrollTo,是基于传递参数的相对滑动。
scrollTo主要有两个参数mScrollX和mScrollY,单位为像素,我们需要明白这两个属性的变化规则,这两个属性可以通过getScrollX和getScrollY获得,在滑动过程中mScrollX的值总是等于View的左边框和View中内容左边框在水平方向上的距离,mScrollY的值总是等于View的上边框和View中内容上边框在垂直方向上的距离(再次强调,scrollTo/scrollBy只能改变view中内容或者子view的位置而不能改变view在父布局中的位置),当view的左边缘在view内容左边缘的右边的时候,mScrollX为正直,反之为负值,换句话说,就是从右向左滑动的时候,mScrollX为正值,反之为负值,mScrollY也是同理。以图为例(黑色边框部分为view,灰色部分为view中的内容或子view):
mScrollX = 100
mScrollX = -100
mScrollY = 100
mScrollY = -100
Scroller
使用Scroller步骤如下:
1.重写View或者ViewGroup
public ScrollerLayout(Context context) {
this(context,null);
}
public ScrollerLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public ScrollerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
scrollerInit();
}
2.重写View的computeScroll()方法
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
3.写一个方法,传入滑动距离参数
主要调用下面两个方法:
mScroller.startScroll(scrollX,scrollY,deltaX,deltaY,3000);
invalidate();
方法:
public void smoothScrollTo(int destX,int destY)
{
int scrollX = getScrollX();
int deltaX = destX - scrollX;
int scrollY = getScrollY();
int deltaY = destY - scrollY;
mScroller.startScroll(scrollX,scrollY,deltaX,deltaY,3000);
invalidate();
}
这样获得ScrollerLayout对象以后,调用smoothScrollTo方法,传入距离参数就可以实现ScrollerLayout中子view的弹性滑动。
源码分析
1.构造器
先来看构造函数,要实现弹性滑动,需要有插值器,来看Scroller的构造函数,Scroller有三个构造器,但是最终都会调用下面这个:
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
}
interpolator参数可以外面传入,如果未设置,则默认为ViscousFluidInterpolator。
2.startScroll
我们再来看看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方法并未开始滑动,只是初始化了一些参数,那何时开始滑动呢?
我们在startScroll后有调用invalidate方法,这个方法通知view去重绘,view重绘会调用我们重写的computeScroll方法,computeScroll方法中调用了Scroller的computeScrollOffset方法根据时间和插值器的值计算当前view应该滑动到的位置,然后调用view的scrollTo方法对view进行滑动。
3.computeScrollOffset
这个是核心方法,根据时间和插值器的值计算当前view应该滑动到的位置:
/**
* 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;
}
这个方法的返回值有讲究,若返回true则说明Scroller的滑动没有结束;若返回false说明Scroller的滑动结束了。再来看看内部的代码:先是计算出了已经滑动的时间,若已经滑动的时间小于总滑动的时间,则说明滑动没有结束;不然就说明滑动结束了,设置标记mFinished = true;。而在滑动未结束里面又分为了两个mode,不过这两个mode都干了差不多的事,大致就是根据刚才的时间timePassed和插补器来计算出该时间点滚动的距离mCurrX和mCurrY。也就是上面的mScroller.getCurrX(), mScroller.getCurrY()的值。
然后再调用scrollTo()方法滚动到指定点(即上面的mCurrX, mCurrY)。之后又调用了postInvalidate();,让View重绘并重新调用computeScroll()以此循环下去,一直到View滚动到指定位置为止,至此Scroller滚动结束。
其实Scroller的原理还是比较通俗易懂的。我们再来理清一下思路,以一张图的形式来终结今天的Scroller解析: