首页 > 代码库 > Android 带你从源码的角度解析Scroller的滚动实现原理
Android 带你从源码的角度解析Scroller的滚动实现原理
转帖请注明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/17483273),请尊重他人的辛勤劳动成果,谢谢!
今 天给大家讲解的是Scroller类的滚动实现原理,可能很多朋友不太了解该类是用来干嘛的,但是研究Launcher的朋友应该对他很熟 悉,Scroller类是滚动的一个封装类,可以实现View的平滑滚动效果,什么是实现View的平滑滚动效果呢,举个简单的例子,一个View从在我 们指定的时间内从一个位置滚动到另外一个位置,我们利用Scroller类可以实现匀速滚动,可以先加速后减速,可以先减速后加速等等效果,而不是瞬间的 移动的效果,所以Scroller可以帮我们实现很多滑动的效果。
在 介绍Scroller类之前,我们先去了解View的scrollBy() 和scrollTo()方法的区别,在区分这两个方法的之前,我们要先理解View 里面的两个成员变量mScrollX, mScrollY,X轴方向的偏移量和Y轴方向的偏移量,这个是一个相对距离,相对的不是屏幕的原点,而是View的左边缘,举个通俗易懂的例子,一列火 车从吉安到深圳,途中经过赣州,那么原点就是赣州,偏移量就是 负的吉安到赣州的距离,大家从getScrollX()方法中的注释中就能看出答案来
- /**
- * Return the scrolled left position of this view. This is the left edge of
- * the displayed part of your view. You do not need to draw any pixels
- * farther left, since those are outside of the frame of your view on
- * screen.
- *
- * @return The left edge of the displayed part of your view, in pixels.
- */
- public final int getScrollX() {
- return mScrollX;
- }
- /**
- * 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;
- onScrollChanged(mScrollX, mScrollY, oldX, oldY);
- if (!awakenScrollBars()) {
- invalidate();
- }
- }
- }
- /**
- * 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);
- }
原 来他里面调用了scrollTo()方法,那就好办了,他就是相对于View上一个位置根据(x, y)来进行滚动,可能大家脑海中对这两个方法还有点模糊,没关系,还是举个通俗的例子帮大家理解下,假如一个View,调用两次 scrollTo(-10, 0),第一次向右滚动10,第二次就不滚动了,因为mScrollX和x相等了,当我们调用两次scrollBy(-10, 0),第一次向右滚动10,第二次再向右滚动10,他是相对View的上一个位置来滚动的。
对 于scrollTo()和scrollBy()方法还有一点需要注意,这点也很重要,假如你给一个LinearLayout调用scrollTo()方 法,并不是LinearLayout滚动,而是LinearLayout里面的内容进行滚动,比如你想对一个按钮进行滚动,直接用Button调用 scrollTo()一定达不到你的需求,大家可以试一试,如果真要对某个按钮进行scrollTo()滚动的话,我们可以在Button外面包裹一层 Layout,然后对Layout调用scrollTo()方法。
了解了scrollTo()和scrollBy()方法之后我们就了解下Scroller类了,先看其构造方法
- /**
- * Create a Scroller with the default duration and interpolator.
- */
- public Scroller(Context context) {
- this(context, null);
- }
- /**
- * Create a Scroller with the specified interpolator. If the interpolator is
- * null, the default (viscous) interpolator will be used.
- */
- public Scroller(Context context, Interpolator interpolator) {
- mFinished = true;
- mInterpolator = interpolator;
- float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
- mDeceleration = SensorManager.GRAVITY_EARTH // g (m/s^2)
- * 39.37f // inch/meter
- * ppi // pixels per inch
- * ViewConfiguration.getScrollFriction();
- }
Interpolator,他指定了动画的变化率,比如说匀速变化,先加速后减速,正弦变化等等,不同的Interpolator可以做出不同的效果出来,第一个使用默认的Interpolator(viscous)
接下来我们就要在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;
- // This controls the viscous fluid effect (how much of it)
- mViscousFluidScale = 8.0f;
- // must be set to 1.0 (used in viscousFluid())
- mViscousFluidNormalize = 1.0f;
- mViscousFluidNormalize = 1.0f / viscousFluid(1.0f);
- }
- /**
- * Call this when you want to know the new location. If it returns true,
- * the animation is not yet finished. loc will be altered to provide the
- * new location.
- */
- public boolean computeScrollOffset() {
- if (mFinished) {
- return false;
- }
- int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
- if (timePassed < mDuration) {
- switch (mMode) {
- case SCROLL_MODE:
- float x = (float)timePassed * mDurationReciprocal;
- if (mInterpolator == null)
- x = viscousFluid(x);
- else
- x = mInterpolator.getInterpolation(x);
- mCurrX = mStartX + Math.round(x * mDeltaX);
- mCurrY = mStartY + Math.round(x * mDeltaY);
- break;
- case FLING_MODE:
- float timePassedSeconds = timePassed / 1000.0f;
- float distance = (mVelocity * timePassedSeconds)
- - (mDeceleration * timePassedSeconds * timePassedSeconds / 2.0f);
- mCurrX = mStartX + Math.round(distance * mCoeffX);
- // Pin to mMinX <= mCurrX <= mMaxX
- mCurrX = Math.min(mCurrX, mMaxX);
- mCurrX = Math.max(mCurrX, mMinX);
- mCurrY = mStartY + Math.round(distance * mCoeffY);
- // Pin to mMinY <= mCurrY <= mMaxY
- mCurrY = Math.min(mCurrY, mMaxY);
- mCurrY = Math.max(mCurrY, mMinY);
- break;
- }
- }
- else {
- mCurrX = mFinalX;
- mCurrY = mFinalY;
- mFinished = true;
- }
- return true;
- }
毫秒减去mStartTime就是持续时间了,然后进去if判断,如果动画持续时间小于我们设置的滚动持续时间mDuration,进去switch的SCROLL_MODE,然后根据Interpolator来计算出在该时间段里面移动的距离,赋值给mCurrX, mCurrY, 所以该方法的作用是,计算在0到mDuration时间段内滚动的偏移量,并且判断滚动是否结束,true代表还没结束,false则表示滚动介绍了,Scroller类的其他的方法我就不提了,大都是一些get(), set()方法。
看了这么多,到底要怎么才能触发滚动,你心里肯定有很多疑惑,在说滚动之前我要先提另外一个方法computeScroll(),该方法是滑动的控制方法,在绘制View时,会在draw()过程调用该方法。我们先看看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() {
- }
- public void draw(Canvas canvas) {
- final int privateFlags = mPrivateFlags;
- final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
- (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
- mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
- /*
- * Draw traversal performs several drawing steps which must be executed
- * in the appropriate order:
- *
- * 1. Draw the background
- * 2. If necessary, save the canvas‘ layers to prepare for fading
- * 3. Draw view‘s content
- * 4. Draw children
- * 5. If necessary, draw the fading edges and restore layers
- * 6. Draw decorations (scrollbars for instance)
- */
- // Step 1, draw the background, if needed
- int saveCount;
- if (!dirtyOpaque) {
- final Drawable background = mBackground;
- if (background != null) {
- final int scrollX = mScrollX;
- final int scrollY = mScrollY;
- if (mBackgroundSizeChanged) {
- background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
- mBackgroundSizeChanged = false;
- }
- if ((scrollX | scrollY) == 0) {
- background.draw(canvas);
- } else {
- canvas.translate(scrollX, scrollY);
- background.draw(canvas);
- canvas.translate(-scrollX, -scrollY);
- }
- }
- }
- // skip step 2 & 5 if possible (common case)
- final int viewFlags = mViewFlags;
- boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
- boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
- if (!verticalEdges && !horizontalEdges) {
- // Step 3, draw the content
- if (!dirtyOpaque) onDraw(canvas);
- // Step 4, draw the children
- dispatchDraw(canvas);
- // Step 6, draw decorations (scrollbars)
- onDrawScrollBars(canvas);
- // we‘re done...
- return;
- }
- ......
- ......
- ......
我们只截取了draw()的部分代码,这上面11-16行为我们写出了绘制一个View的几个步骤,我们看看第四步绘制孩子的时候会触发dispatchDraw()这个方法,来看看源码是什么内容
- /**
- * Called by draw to draw the child views. This may be overridden
- * by derived classes to gain control just before its children are drawn
- * (but after its own view has been drawn).
- * @param canvas the canvas on which to draw the view
- */
- protected void dispatchDraw(Canvas canvas) {
- }
- @Override
- protected void dispatchDraw(Canvas canvas) {
- final int count = mChildrenCount;
- final View[] children = mChildren;
- int flags = mGroupFlags;
- if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
- final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
- final boolean buildCache = !isHardwareAccelerated();
- for (int i = 0; i < count; i++) {
- final View child = children[i];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
- final LayoutParams params = child.getLayoutParams();
- attachLayoutAnimationParameters(child, params, i, count);
- bindLayoutAnimation(child);
- if (cache) {
- child.setDrawingCacheEnabled(true);
- if (buildCache) {
- child.buildDrawingCache(true);
- }
- }
- }
- }
- final LayoutAnimationController controller = mLayoutAnimationController;
- if (controller.willOverlap()) {
- mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
- }
- controller.start();
- mGroupFlags &= ~FLAG_RUN_ANIMATION;
- mGroupFlags &= ~FLAG_ANIMATION_DONE;
- if (cache) {
- mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;
- }
- if (mAnimationListener != null) {
- mAnimationListener.onAnimationStart(controller.getAnimation());
- }
- }
- int saveCount = 0;
- final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
- if (clipToPadding) {
- saveCount = canvas.save();
- canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
- mScrollX + mRight - mLeft - mPaddingRight,
- mScrollY + mBottom - mTop - mPaddingBottom);
- }
- // We will draw our child‘s animation, let‘s reset the flag
- mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
- mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
- boolean more = false;
- final long drawingTime = getDrawingTime();
- if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
- for (int i = 0; i < count; i++) {
- final View child = children[i];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- } else {
- for (int i = 0; i < count; i++) {
- final View child = children[getChildDrawingOrder(count, i)];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- }
- // Draw any disappearing views that have animations
- if (mDisappearingChildren != null) {
- final ArrayList<View> disappearingChildren = mDisappearingChildren;
- final int disappearingCount = disappearingChildren.size() - 1;
- // Go backwards -- we may delete as animations finish
- for (int i = disappearingCount; i >= 0; i--) {
- final View child = disappearingChildren.get(i);
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- if (debugDraw()) {
- onDebugDraw(canvas);
- }
- if (clipToPadding) {
- canvas.restoreToCount(saveCount);
- }
- // mGroupFlags might have been updated by drawChild()
- flags = mGroupFlags;
- if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
- invalidate(true);
- }
- if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
- mLayoutAnimationController.isDone() && !more) {
- // We want to erase the drawing cache and notify the listener after the
- // next frame is drawn because one extra invalidate() is caused by
- // drawChild() after the animation is over
- mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
- final Runnable end = new Runnable() {
- public void run() {
- notifyAnimationListener();
- }
- };
- post(end);
- }
- }
- protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
- ......
- ......
- if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) &&
- (child.mPrivateFlags & DRAW_ANIMATION) == 0) {
- return more;
- }
- child.computeScroll();
- final int sx = child.mScrollX;
- final int sy = child.mScrollY;
- boolean scalingRequired = false;
- Bitmap cache = null;
- ......
- ......
- }
整理下思路,来看看View滚动的实现原理,我们先调用Scroller的startScroll()方法来进行一些滚动的初始化设置,然后迫使View进行绘制,我们调用View的invalidate()或postInvalidate()就可以重新绘制View,绘制View的时候会触发computeScroll()方法,我们重写computeScroll(),在computeScroll()里面先调用Scroller的computeScrollOffset()方法来判断滚动有没有结束,如果滚动没有结束我们就调用scrollTo()方法来进行滚动,该scrollTo()方法虽然会重新绘制View,但是我们还是要手动调用下invalidate()或者postInvalidate()来触发界面重绘,重新绘制View又触发computeScroll(),所以就进入一个循环阶段,这样子就实现了在某个时间段里面滚动某段距离的一个平滑的滚动效果
也许有人会问,干嘛还要调用来调用去最后在调用scrollTo()方法,还不如直接调用scrollTo()方法来实现滚动,其实直接调用是可以,只不过scrollTo()是瞬间滚动的,给人的用户体验不太好,所以Android提供了Scroller类实现平滑滚动的效果。为了方面大家理解,我画了一个简单的调用示意图
好了,讲到这里就已经讲完了Scroller类的滚动实现原理啦,不知道大家理解了没有,Scroller能实现很多滚动的效果,由于考虑到这篇文章的篇幅有点长,所以这篇文章就不带领大家来使用Scroller类了,我将在下一篇文章将会带来Scroller类的使用,希望大家到时候关注下,有疑问的同学在下面留言,我会为大家解答的!