Scroller的使用及解析

Android開(kāi)發(fā)中View的滑動(dòng)處理源碼,該怎么看焊夸?


Android 帶你從源碼的角度解析Scroller的滾動(dòng)實(shí)現(xiàn)原理,

今天給大家講解的是Scroller類的滾動(dòng)實(shí)現(xiàn)原理,可能很多朋友不太了解該類是用來(lái)干嘛的舞萄,但是研究Launcher的朋友應(yīng)該對(duì)他很熟悉,Scroller類是滾動(dòng)的一個(gè)封裝類低滩,可以實(shí)現(xiàn)View的平滑滾動(dòng)效果缤言,什么是實(shí)現(xiàn)View的平滑滾動(dòng)效果呢,舉個(gè)簡(jiǎn)單的例子审胚,一個(gè)View從在我們指定的時(shí)間內(nèi)從一個(gè)位置滾動(dòng)到另外一個(gè)位置匈勋,我們利用Scroller類可以實(shí)現(xiàn)勻速滾動(dòng),可以先加速后減速膳叨,可以先減速后加速等等效果洽洁,而不是瞬間的移動(dòng)的效果,所以Scroller可以幫我們實(shí)現(xiàn)很多滑動(dòng)的效果菲嘴。

在介紹Scroller類之前饿自,我們先去了解View的scrollBy() 和scrollTo()方法的區(qū)別,在區(qū)分這兩個(gè)方法的之前龄坪,我們要先理解View 里面的兩個(gè)成員變量mScrollX昭雌, mScrollY,X軸方向的偏移量和Y軸方向的偏移量健田,這個(gè)是一個(gè)相對(duì)距離烛卧,相對(duì)的不是屏幕的原點(diǎn),而是View的左邊緣妓局,舉個(gè)通俗易懂的例子唱星,一列火車從吉安到深圳,途中經(jīng)過(guò)贛州跟磨,那么原點(diǎn)就是贛州间聊,偏移量就是 負(fù)的吉安到贛州的距離,大家從getScrollX()方法中的注釋中就能看出答案來(lái)

 /**
 * 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;
 }

現(xiàn)在我們知道了向右滑動(dòng) mScrollX就為負(fù)數(shù)抵拘,向左滑動(dòng)mScrollX為正數(shù)哎榴,接下來(lái)我們先來(lái)看看scrollTo()方法的源碼

注 : 此處應(yīng)該參考: 藝術(shù)探索 里所說(shuō),移動(dòng)的是 蓋板

    /**
     * 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();
            }
        }
    }

從該方法中我們可以看出,先判斷傳進(jìn)來(lái)的(x, y)值是否和View的X, Y偏移量相等,如果不相等尚蝌,就調(diào)用onScrollChanged()方法來(lái)通知界面發(fā)生改變迎变,然后重繪界面,所以這樣子就實(shí)現(xiàn)了移動(dòng)效果啦飘言, 現(xiàn)在我們知道了scrollTo()方法是滾動(dòng)到(x, y)這個(gè)偏移量的點(diǎn)衣形,他是相對(duì)于View的開(kāi)始位置來(lái)滾動(dòng)的。在看看scrollBy()這個(gè)方法的代碼

    /**
     * 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);
    }

原來(lái)他里面調(diào)用了scrollTo()方法姿鸿,那就好辦了谆吴,他就是相對(duì)于View上一個(gè)位置根據(jù)(x, y)來(lái)進(jìn)行滾動(dòng),可能大家腦海中對(duì)這兩個(gè)方法還有點(diǎn)模糊苛预,沒(méi)關(guān)系句狼,還是舉個(gè)通俗的例子幫大家理解下,假如一個(gè)View,調(diào)用兩次scrollTo(-10, 0)热某,第一次向右滾動(dòng)10腻菇,第二次就不滾動(dòng)了,因?yàn)閙ScrollX和x相等了昔馋,當(dāng)我們調(diào)用兩次scrollBy(-10, 0),第一次向右滾動(dòng)10筹吐,第二次再向右滾動(dòng)10,他是相對(duì)View的上一個(gè)位置來(lái)滾動(dòng)的秘遏。

對(duì)于scrollTo()和scrollBy()方法還有一點(diǎn)需要注意骏令,這點(diǎn)也很重要,假如你給一個(gè)LinearLayout調(diào)用scrollTo()方法垄提,并不是LinearLayout滾動(dòng)榔袋,而是LinearLayout里面的內(nèi)容進(jìn)行滾動(dòng),比如你想對(duì)一個(gè)按鈕進(jìn)行滾動(dòng)铡俐,直接用Button調(diào)用scrollTo()一定達(dá)不到你的需求凰兑,大家可以試一試,如果真要對(duì)某個(gè)按鈕進(jìn)行scrollTo()滾動(dòng)的話审丘,我們可以在Button外面包裹一層Layout吏够,然后對(duì)Layout調(diào)用scrollTo()方法。

了解了scrollTo()和scrollBy()方法之后我們就了解下Scroller類了滩报,先看其構(gòu)造方法


    /**
     * 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. "Flywheel" behavior will
     * be in effect for apps targeting Honeycomb or newer.
     */
    public Scroller(Context context, Interpolator interpolator) {
        this(context, interpolator,
                context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
    }


    /**
     * Create a Scroller with the specified interpolator. If the interpolator is
     * null, the default (viscous) interpolator will be used. Specify whether or
     * not to support progressive "flywheel" behavior in flinging.
     */
    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
    }

共計(jì)三個(gè)構(gòu)造方法锅知,第一個(gè)只有一個(gè)Context參數(shù),第二個(gè)構(gòu)造方法中指定了Interpolator脓钾,什么Interpolator呢?中文意思插補(bǔ)器售睹,了解Android動(dòng)畫的朋友都應(yīng)該熟悉

Interpolator,他指定了動(dòng)畫的變化率可训,比如說(shuō)勻速變化昌妹,先加速后減速捶枢,正弦變化等等,不同的Interpolator可以做出不同的效果出來(lái)飞崖,第一個(gè)使用默認(rèn)的Interpolator(viscous)

第三個(gè)構(gòu)造方法,是對(duì)滑動(dòng)的 "慣性輪"特性的支持.

接下來(lái)我們就要在Scroller類里面找滾動(dòng)的方法烂叔,我們從名字上面可以看出startScroll()應(yīng)該是個(gè)滾動(dòng)的方法,我們來(lái)看看其源碼吧

    /**
     * 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;
    }

?
在這個(gè)方法中我們只看到了對(duì)一些滾動(dòng)的基本設(shè)置動(dòng)作固歪,比如設(shè)置滾動(dòng)模式蒜鸡,開(kāi)始時(shí)間,持續(xù)時(shí)間等等牢裳,并沒(méi)有任何對(duì)View的滾動(dòng)操作逢防,也許你正納悶,不是滾動(dòng)的方法干嘛還叫做startScroll(),稍安勿躁贰健,既然叫開(kāi)始滾動(dòng),那就是對(duì)滾動(dòng)的滾動(dòng)之前的基本設(shè)置咯恬汁。

接下來(lái)是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;
    }

?
《注:》這里想補(bǔ)充一句個(gè)人理解:isFinish()方法返回的就是該方法里面的mFinished伶椿。當(dāng)時(shí)間截止或者路程走完都會(huì)使mFinished =true,但是computeScrollOffset()的返回值是依賴于mFinished的
?
我們?cè)趕tartScroll()方法的時(shí)候獲取了當(dāng)前的動(dòng)畫毫秒賦值給了mStartTime氓侧,在computeScrollOffset()中再一次調(diào)用AnimationUtils.currentAnimationTimeMillis()來(lái)獲取動(dòng)畫毫秒減去mStartTime就是動(dòng)畫已過(guò)時(shí)間了脊另,然后進(jìn)去if判斷,如果動(dòng)畫已過(guò)時(shí)間時(shí)間小于我們?cè)O(shè)置的滾動(dòng)持續(xù)時(shí)間mDuration约巷,進(jìn)去switch的SCROLL_MODE偎痛,然后根據(jù)Interpolator來(lái)計(jì)算出在該時(shí)間段里面移動(dòng)的距離,賦值給mCurrX独郎, mCurrY踩麦, 所以該方法的作用是,計(jì)算在0到mDuration時(shí)間段內(nèi)滾動(dòng)的偏移量氓癌,并且判斷滾動(dòng)是否結(jié)束谓谦,true代表還沒(méi)結(jié)束,false則表示滾動(dòng)介紹了贪婉,Scroller類的其他的方法我就不提了反粥,大都是一些get(), set()方法。
?
看了這么多疲迂,到底要怎么才能觸發(fā)滾動(dòng)才顿,你心里肯定有很多疑惑,在說(shuō)滾動(dòng)之前我要先提另外一個(gè)方法View 的 computeScroll()尤蒿,該方法是滑動(dòng)的控制方法郑气,在繪制View時(shí),會(huì)在draw()過(guò)程調(diào)用該方法腰池。我們先看看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() {
    }

沒(méi)錯(cuò)竣贪,他是一個(gè)空的方法军洼,需要子類去重寫該方法來(lái)實(shí)現(xiàn)邏輯,到底該方法在哪里被觸發(fā)呢演怎。我們繼續(xù)看看View的繪制方法draw()

    /**
     * This method is called by ViewGroup.drawChild() to have each child view draw itself.
     *
     * This is where the View specializes rendering behavior based on layer type,
     * and hardware acceleration.
     */
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
        /* If an attached view draws to a HW canvas, it may use its RenderNode + DisplayList.
         *
         * If a view is dettached, its DisplayList shouldn't exist. If the canvas isn't
         * HW accelerated, it can't handle drawing RenderNodes.
         */
        boolean drawingWithRenderNode = mAttachInfo != null
                && mAttachInfo.mHardwareAccelerated
                && hardwareAcceleratedCanvas;

        boolean more = false;
        final boolean childHasIdentityMatrix = hasIdentityMatrix();
        final int parentFlags = parent.mGroupFlags;

        if ((parentFlags & ViewGroup.FLAG_CLEAR_TRANSFORMATION) != 0) {
            parent.getChildTransformation().clear();
            parent.mGroupFlags &= ~ViewGroup.FLAG_CLEAR_TRANSFORMATION;
        }

        Transformation transformToApply = null;
        boolean concatMatrix = false;
        final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;
        final Animation a = getAnimation();
        if (a != null) {
            more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            transformToApply = parent.getChildTransformation();
        } else {
            if ((mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_TRANSFORM) != 0) {
                // No longer animating: clear out old animation matrix
                mRenderNode.setAnimationMatrix(null);
                mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            if (!drawingWithRenderNode
                    && (parentFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
                final Transformation t = parent.getChildTransformation();
                final boolean hasTransform = parent.getChildStaticTransformation(this, t);
                if (hasTransform) {
                    final int transformType = t.getTransformationType();
                    transformToApply = transformType != Transformation.TYPE_IDENTITY ? t : null;
                    concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
                }
            }
        }

        concatMatrix |= !childHasIdentityMatrix;

        // Sets the flag as early as possible to allow draw() implementations
        // to call invalidate() successfully when doing animations
        mPrivateFlags |= PFLAG_DRAWN;

        if (!concatMatrix &&
                (parentFlags & (ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS |
                        ViewGroup.FLAG_CLIP_CHILDREN)) == ViewGroup.FLAG_CLIP_CHILDREN &&
                canvas.quickReject(mLeft, mTop, mRight, mBottom, Canvas.EdgeType.BW) &&
                (mPrivateFlags & PFLAG_DRAW_ANIMATION) == 0) {
            mPrivateFlags2 |= PFLAG2_VIEW_QUICK_REJECTED;
            return more;
        }
        mPrivateFlags2 &= ~PFLAG2_VIEW_QUICK_REJECTED;

        if (hardwareAcceleratedCanvas) {
            // Clear INVALIDATED flag to allow invalidation to occur during rendering, but
            // retain the flag's value temporarily in the mRecreateDisplayList flag
            mRecreateDisplayList = (mPrivateFlags & PFLAG_INVALIDATED) != 0;
            mPrivateFlags &= ~PFLAG_INVALIDATED;
        }

        RenderNode renderNode = null;
        Bitmap cache = null;
        int layerType = getLayerType(); // TODO: signify cache state with just 'cache' local
        if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) {
             if (layerType != LAYER_TYPE_NONE) {
                 // If not drawing with RenderNode, treat HW layers as SW
                 layerType = LAYER_TYPE_SOFTWARE;
                 buildDrawingCache(true);
            }
            cache = getDrawingCache(true);
        }

        if (drawingWithRenderNode) {
            // Delay getting the display list until animation-driven alpha values are
            // set up and possibly passed on to the view
            renderNode = updateDisplayListIfDirty();
            if (!renderNode.isValid()) {
                // Uncommon, but possible. If a view is removed from the hierarchy during the call
                // to getDisplayList(), the display list will be marked invalid and we should not
                // try to use it again.
                renderNode = null;
                drawingWithRenderNode = false;
            }
        }

        int sx = 0;
        int sy = 0;
        if (!drawingWithRenderNode) {
//******************************************
            computeScroll();
//******************************************
            sx = mScrollX;
            sy = mScrollY;
        }

        final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
        final boolean offsetForScroll = cache == null && !drawingWithRenderNode;

        int restoreTo = -1;
        if (!drawingWithRenderNode || transformToApply != null) {
            restoreTo = canvas.save();
        }
        if (offsetForScroll) {
            canvas.translate(mLeft - sx, mTop - sy);
        } else {
            if (!drawingWithRenderNode) {
                canvas.translate(mLeft, mTop);
            }
            if (scalingRequired) {
                if (drawingWithRenderNode) {
                    // TODO: Might not need this if we put everything inside the DL
                    restoreTo = canvas.save();
                }
                // mAttachInfo cannot be null, otherwise scalingRequired == false
                final float scale = 1.0f / mAttachInfo.mApplicationScale;
                canvas.scale(scale, scale);
            }
        }

        float alpha = drawingWithRenderNode ? 1 : (getAlpha() * getTransitionAlpha());
        if (transformToApply != null
                || alpha < 1
                || !hasIdentityMatrix()
                || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
            if (transformToApply != null || !childHasIdentityMatrix) {
                int transX = 0;
                int transY = 0;

                if (offsetForScroll) {
                    transX = -sx;
                    transY = -sy;
                }

                if (transformToApply != null) {
                    if (concatMatrix) {
                        if (drawingWithRenderNode) {
                            renderNode.setAnimationMatrix(transformToApply.getMatrix());
                        } else {
                            // Undo the scroll translation, apply the transformation matrix,
                            // then redo the scroll translate to get the correct result.
                            canvas.translate(-transX, -transY);
                            canvas.concat(transformToApply.getMatrix());
                            canvas.translate(transX, transY);
                        }
                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                    }

                    float transformAlpha = transformToApply.getAlpha();
                    if (transformAlpha < 1) {
                        alpha *= transformAlpha;
                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                    }
                }

                if (!childHasIdentityMatrix && !drawingWithRenderNode) {
                    canvas.translate(-transX, -transY);
                    canvas.concat(getMatrix());
                    canvas.translate(transX, transY);
                }
            }

            // Deal with alpha if it is or used to be <1
            if (alpha < 1 || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
                if (alpha < 1) {
                    mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_ALPHA;
                } else {
                    mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_ALPHA;
                }
                parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                if (!drawingWithDrawingCache) {
                    final int multipliedAlpha = (int) (255 * alpha);
                    if (!onSetAlpha(multipliedAlpha)) {
                        if (drawingWithRenderNode) {
                            renderNode.setAlpha(alpha * getAlpha() * getTransitionAlpha());
                        } else if (layerType == LAYER_TYPE_NONE) {
                            canvas.saveLayerAlpha(sx, sy, sx + getWidth(), sy + getHeight(),
                                    multipliedAlpha);
                        }
                    } else {
                        // Alpha is handled by the child directly, clobber the layer's alpha
                        mPrivateFlags |= PFLAG_ALPHA_SET;
                    }
                }
            }
        } else if ((mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
            onSetAlpha(255);
            mPrivateFlags &= ~PFLAG_ALPHA_SET;
        }

        if (!drawingWithRenderNode) {
            // apply clips directly, since RenderNode won't do it for this draw
            if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) {
                if (offsetForScroll) {
                    canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight());
                } else {
                    if (!scalingRequired || cache == null) {
                        canvas.clipRect(0, 0, getWidth(), getHeight());
                    } else {
                        canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
                    }
                }
            }

            if (mClipBounds != null) {
                // clip bounds ignore scroll
                canvas.clipRect(mClipBounds);
            }
        }

        if (!drawingWithDrawingCache) {
            if (drawingWithRenderNode) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            } else {
                // Fast path for layouts with no backgrounds
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
//******************************************
                    dispatchDraw(canvas);
//******************************************
                } else {
                    draw(canvas);
                }
            }
        } else if (cache != null) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
                // no layer paint, use temporary paint to draw bitmap
                Paint cachePaint = parent.mCachePaint;
                if (cachePaint == null) {
                    cachePaint = new Paint();
                    cachePaint.setDither(false);
                    parent.mCachePaint = cachePaint;
                }
                cachePaint.setAlpha((int) (alpha * 255));
                canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
            } else {
                // use layer paint to draw the bitmap, merging the two alphas, but also restore
                int layerPaintAlpha = mLayerPaint.getAlpha();
                if (alpha < 1) {
                    mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
                }
                canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
                if (alpha < 1) {
                    mLayerPaint.setAlpha(layerPaintAlpha);
                }
            }
        }

        if (restoreTo >= 0) {
            canvas.restoreToCount(restoreTo);
        }

        if (a != null && !more) {
            if (!hardwareAcceleratedCanvas && !a.getFillAfter()) {
                onSetAlpha(255);
            }
            parent.finishAnimatingView(this, a);
        }

        if (more && hardwareAcceleratedCanvas) {
            if (a.hasAlpha() && (mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
                // alpha animations should cause the child to recreate its display list
                invalidate(true);
            }
        }

        mRecreateDisplayList = false;

        return more;
    }

這上面星號(hào)標(biāo)注的部分,寫出了繪制一個(gè)View的幾個(gè)步驟匕争,我們看看第四步繪制孩子的時(shí)候會(huì)觸發(fā)dispatchDraw()這個(gè)方法,來(lái)看看源碼是什么內(nèi)容


    @Override
    protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;

        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean buildCache = !isHardwareAccelerated();
            for (int i = 0; i < childrenCount; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                    final LayoutParams params = child.getLayoutParams();
                    attachLayoutAnimationParameters(child, params, i, childrenCount);
                    bindLayoutAnimation(child);
                }
            }

            final LayoutAnimationController controller = mLayoutAnimationController;
            if (controller.willOverlap()) {
                mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
            }

            controller.start();

            mGroupFlags &= ~FLAG_RUN_ANIMATION;
            mGroupFlags &= ~FLAG_ANIMATION_DONE;

            if (mAnimationListener != null) {
                mAnimationListener.onAnimationStart(controller.getAnimation());
            }
        }

        int clipSaveCount = 0;
        final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
        if (clipToPadding) {
            clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
            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 (usingRenderNodeProperties) canvas.insertReorderBarrier();
        final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
        int transientIndex = transientCount != 0 ? 0 : -1;
        // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
        // draw reordering internally
        final ArrayList<View> preorderedList = usingRenderNodeProperties
                ? null : buildOrderedChildList();
        final boolean customOrder = preorderedList == null
                && isChildrenDrawingOrderEnabled();
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }

            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        while (transientIndex >= 0) {
            // there may be additional transient views after the normal views
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                break;
            }
        }
        if (preorderedList != null) preorderedList.clear();

        // 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 (usingRenderNodeProperties) canvas.insertInorderBarrier();

        if (debugDraw()) {
            onDebugDraw(canvas);
        }

        if (clipToPadding) {
            canvas.restoreToCount(clipSaveCount);
        }

        // 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() {
               @Override
               public void run() {
                   notifyAnimationListener();
               }
            };
            post(end);
        }
    }

?
這個(gè)方法代碼有點(diǎn)多爷耀,但是我們還是挑重點(diǎn)看吧甘桑,從星號(hào)標(biāo)注的行可以看出 在dispatchDraw()里面會(huì)對(duì)ViewGroup里面的子View調(diào)用drawChild()來(lái)進(jìn)行繪制,接下來(lái)我們來(lái)看看drawChild()方法的代碼

    /**
     * Draw one child of this View Group. This method is responsible for getting
     * the canvas in the right state. This includes clipping, translating so
     * that the child's scrolled origin is at 0, 0, and applying any animation
     * transformations.
     *
     * @param canvas The canvas on which to draw the child
     * @param child Who to draw
     * @param drawingTime The time at which draw is occurring
     * @return True if an invalidate() was issued
     */
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

最終又回到了View的draw() 方法,我們可以看到如下代碼:

        int sx = 0;
        int sy = 0;
        if (!drawingWithRenderNode) {
//******************************************
            computeScroll();
//******************************************
            sx = mScrollX;
            sy = mScrollY;
        }

看到computeScroll()你大概明白什么了吧歹叮,轉(zhuǎn)了老半天終于找到了computeScroll()方法被觸發(fā)跑杭,就是ViewGroup在分發(fā)繪制自己的孩子的時(shí)候,會(huì)對(duì)其子View調(diào)用computeScroll()方法

因此我們?cè)谑褂肧croller可以這么寫:


    Scroller scroller = new Scroller(getContext());

    //緩慢滑動(dòng)到指定位置
    private void smoothScrollXTo(int targetX) {
        int scrollX = getScrollX();
        int deltaX
                = targetX - scrollX;
        //1000ms內(nèi) 緩慢 滑動(dòng)到targetX處
        scroller.startScroll(scrollX, 0, deltaX, 0, 1000);
        invalidate();
    }

    @Override
    public void computeScroll() {
        //動(dòng)畫未完成
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            //觸發(fā)draw()
            postInvalidate();
        }
    }

整理下思路咆耿,來(lái)看看View滾動(dòng)的實(shí)現(xiàn)原理德谅,我們先調(diào)用Scroller的startScroll()方法來(lái)進(jìn)行一些滾動(dòng)的初始化設(shè)置,然后迫使View進(jìn)行繪制萨螺,我們調(diào)用View的invalidate()或postInvalidate()就可以重新繪制View窄做,繪制View的時(shí)候會(huì)觸發(fā)computeScroll()方法,我們重寫computeScroll()慰技,在computeScroll()里面先調(diào)用Scroller的computeScrollOffset()方法來(lái)判斷滾動(dòng)有沒(méi)有結(jié)束椭盏,如果滾動(dòng)沒(méi)有結(jié)束我們就調(diào)用scrollTo()方法來(lái)進(jìn)行滾動(dòng),該scrollTo()方法雖然會(huì)重新繪制View,但是我們還是要手動(dòng)調(diào)用下invalidate()或者postInvalidate()來(lái)觸發(fā)界面重繪吻商,重新繪制View又觸發(fā)computeScroll()掏颊,所以就進(jìn)入一個(gè)循環(huán)階段,這樣子就實(shí)現(xiàn)了在某個(gè)時(shí)間段里面滾動(dòng)某段距離的一個(gè)平滑的滾動(dòng)效果也許有人會(huì)問(wèn)艾帐,干嘛還要調(diào)用來(lái)調(diào)用去最后在調(diào)用scrollTo()方法乌叶,還不如直接調(diào)用scrollTo()方法來(lái)實(shí)現(xiàn)滾動(dòng),其實(shí)直接調(diào)用是可以柒爸,只不過(guò)scrollTo()是瞬間滾動(dòng)的枉昏,給人的用戶體驗(yàn)不太好,所以Android提供了Scroller類實(shí)現(xiàn)平滑滾動(dòng)的效果揍鸟。為了方面大家理解兄裂,我畫了一個(gè)簡(jiǎn)單的調(diào)用示意圖

scroller_原理.jpg

好了,講到這里就已經(jīng)講完了Scroller類的滾動(dòng)實(shí)現(xiàn)原理啦阳藻,我們來(lái)總結(jié)一下:

(1)Scroller類能夠幫助我們實(shí)現(xiàn)高級(jí)的滑動(dòng)功能晰奖,如手指抬起后的慣性滑動(dòng)功能。使用流程為腥泥,首先通過(guò)Scroller類的startScroll()+invalidate()觸發(fā)View的computeScroll()匾南,在computeScroll()中讓Scroller類去計(jì)算最新的坐標(biāo)信息,拿到最新的坐標(biāo)偏移信息后還是要調(diào)用View的scrollTo來(lái)實(shí)現(xiàn)滑動(dòng)蛔外∏悖可以看到溯乒,使用Scroller的整個(gè)流程比較簡(jiǎn)單,關(guān)鍵的是控制滑動(dòng)的一些邏輯計(jì)算豹爹,比如上面例子中的計(jì)算什么時(shí)候該往哪一頁(yè)滑動(dòng)...(2)Android后面推出了OverScroller類裆悄,OverScroller在整體功能上和Scroller類似,使用也相同臂聋。OverScroller類可以完全代替Scroller光稼,相比Scroller,OverScroller主要是增加了對(duì)滑動(dòng)到邊界的一些控制孩等,如增加一些回彈效果等艾君,功能更加強(qiáng)大。如果對(duì)android技術(shù)比較感興趣肄方,可以關(guān)注一下微信公眾號(hào):終端研發(fā)部冰垄,和我一塊交流和學(xué)習(xí)。終端研發(fā)部是一個(gè)以技術(shù)為主的學(xué)習(xí)交流技術(shù)號(hào)权她,談的是技術(shù)虹茶,是產(chǎn)品,更是我們的人生伴奥。做東半球最會(huì)思考写烤,最有味道的互聯(lián)網(wǎng)開(kāi)發(fā)者

作者:終端研發(fā)部
鏈接:https://www.zhihu.com/question/278916471/answer/403341655
來(lái)源:知乎
著作權(quán)歸作者所有翼闽。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)拾徙,非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末感局,一起剝皮案震驚了整個(gè)濱河市尼啡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌询微,老刑警劉巖崖瞭,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異撑毛,居然都是意外死亡书聚,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門藻雌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)雌续,“玉大人,你說(shuō)我怎么就攤上這事胯杭⊙倍牛” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵做个,是天一觀的道長(zhǎng)鸽心。 經(jīng)常有香客問(wèn)我滚局,道長(zhǎng),這世上最難降的妖魔是什么顽频? 我笑而不...
    開(kāi)封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任藤肢,我火速辦了婚禮,結(jié)果婚禮上冲九,老公的妹妹穿的比我還像新娘谤草。我一直安慰自己,他們只是感情好莺奸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布丑孩。 她就那樣靜靜地躺著,像睡著了一般灭贷。 火紅的嫁衣襯著肌膚如雪温学。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天甚疟,我揣著相機(jī)與錄音仗岖,去河邊找鬼。 笑死览妖,一個(gè)胖子當(dāng)著我的面吹牛轧拄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播讽膏,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼檩电,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了府树?” 一聲冷哼從身側(cè)響起俐末,我...
    開(kāi)封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奄侠,沒(méi)想到半個(gè)月后卓箫,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡垄潮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年烹卒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弯洗。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡旅急,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出坠非,到底是詐尸還是另有隱情盟迟,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站辖众,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏啤它。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望台妆。 院中可真熱鬧办成,春花似錦某弦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)冤馏。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間副女,已是汗流浹背塞绿。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人睛竣。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓验夯,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容