资讯详情

自定义基础之Draw过程

绘制过程主要是把手View如果这个对象画在屏幕上,View是一个ViewGroup,需要递归绘 制该ViewGroup所有子视图都包含在其中。

视图中可绘元素

在介绍视图绘制之前,先了解一个视图中需要绘制的元素,比如一个TextView, 除了具体的文字,还需要绘制文字的背景。那么,视图中包含哪些绘制元素呢? 一般来说,绘制元素有四个:

  1. View背景。

每个视图都有一个背景,例如LinearLayout、TextView,背景可以是颜色值,图片,甚至任务Drawable对象,比如一个shader、一个DrawableState等。可使用应用程序 setBackgroundColor()、setBackgroundDrawable()、setBackgroundResouce()三个函数设置不同的背景。

  1. 视图本身的内容。

比如,对 于 TextView对于一个人来说,内容是具体的文本ImageView而 言语,内容是图片。视图中将使用应用程序onDraw()在函数中绘制具体内容。

  1. 渐变边框。

渐变边框的作用是使视图边框看起来更有层次感, 本质是一个Shader对象

  1. 滚动条。

与 PC上滚动条不同,Android中的滚动条仅仅是显示滚动的状态,而不能被用户直接下拉。

在上述四个元素中,应用程序通常只需要重载View的 0nDraw()函数,绘制视图本身,其他三个 所有元素都是由的View系统自动完成 且 View提供相应的系统API接口,应用程序只需要设置元 使用素的具体颜色或颜色Drawable对象即可。

绘制过程的设计理念

如图所示,图中虚线代表重载关系。 在这里插入图片描述 绘制过程从ViewRoot的 performTraversals()开始函数,先调用,ViewRoot中的draw()函数,在函数中进行一定的前端处理,然后调用mView.draw()。mView对象是窗口的根视图,对 于 Activity而言,就是 PhoneWindow.DecorView 对象。 一般情况下,View对象不得重载draw因此,mView.draw()调用 View类的 draw()函数。函数的内部过程是View在系统绘制过程的核心过程中,上一节依次绘制了四个绘制元素,其中绘制视图本身的具体实现是回调onDraw()函数,应用程序通常重载onDraw()绘制设计的函数View真实界面内容。 绘制界面内容后,如果视图还包含子视图,则贝U调整 用 dispatchDraw()函数,ViewGroup重载函数。因此,实际调用是ViewGroup中的dispatchDraw()函数,应用程序不应重载ViewGroup类中的dispatchDmwO函数,因为函数内部有默认的实现,这代表了 View系统内部流程。 dispatchDraw()内部会在一个for()循环语句,循环语句 环 调 用 drawChild()每个子视图分别绘制, drawChild()内部将再次调用draw()函数完成子视图的内部绘制工作。当然,如果子视图本身也是一个 ViewGroup,上述流程将递归执行。 从上述设计理念来看,它与measure及 layout过程非常相似。 以下是上述主要函数的内部执行过程。

ViewRoot 中 draw ()内部流程

ViewRoot中的draw()函数主要处理根视图中的一些独特属性,处理后也应调用View类 中的draw()具体绘制。 Surface根据底层驱动模式可分为两种,一种是使用图形加速支持Surface,俗称显卡,另一个 种是使用CPU内存模拟Surface。因此,根视图将针对不同的根视图Surface从这里采取不同的方式Surface中获取一个Canvas并将对象Canvas对象发送到整个视图中,对于非根视图,它不区分底层是使用显卡模式还是使用CPU模式。

    /**      * ViewRoot中的draw()函数主要处理一些根视图中的特有属性,并且处理完毕后同样要调用View类      * 中 的draw()具体绘制。      * Surface根据底层驱动模式可分为两种,一种是使用图形加速支持Surface,俗称显卡,另一个      * 种是使用CPU内存模拟Surface。因此,根视图将针对不同的根视图Surface从这里采取不同的方式Surface      * 中获取一个Canvas对象,并 将 该Canvas对象发送到整个视图中,对于非根视图,它不区分底层      * 是使用显卡模式还是使用显卡模式CPU模式。      * @param fullRedrawNeeded      */     private void draw(boolean fullRedrawNeeded) {         /**          * 检 查 Surface是否无效。正常情况下, Surface都是有效的,除了 非WmS异常不能是客人          * 有效分配户端Surface, isValide()返回false。如 果 Surface无效的,终止绘制过程          */         Surface surface = mSurface;         if (surface == null || !surface.isValid()) {             return;         }         /**          * 注册Runnable对象。 ViewRoot静态列表可以添加到静态列表中          * Runnable这一步是对象Runnable对象调用post()发送 到Handler以便在下一个消息循环中          * 处理这些Runnable对象。这个变量的名字是sFirstDrawComplete,有些读者可能会觉得变量名有点奇怪,          * 为什么是Complete还没开始画画?保存在变量中Runnable对象将在下一个消息循环中          * 执行前,必须执行下一个绘制过程。          */         if (!sFirstDrawComplete) {             synchronized (sFirstDrawHandlers) {                 sFirstDrawComplete = true;                 for (int i=0; i<sFirstDrawHandlers.size(); i  ) {                     post(sFirstDrawHandlers.get(i));                 }             }         }         /**          * 调 用 scrollToRectOrFocusO。在几乎所有的情况下,函数内部什么都不会执行,所以它的内部          * 忽略了执行过程。在几乎所有的情况下,函数内部什么都不会执行,所以它的内部          * 忽略执行过程。该函数的原始设计目的是对的mScmllY调整变量的基础是调整到第一个Focus          * 视图中。          */         scrollToRectOrFocus(null, false);         /**          * 若内部包含根视图Scroller对象,调用对象computeScrollOffset()获得新的滚动值,并          * 赋值局部变量yoff,关 于Scroller详细意义将在下面的小节中单独介绍。该对象的computeScrollOffset()          * 该函数返回值的类型是计算是否滚动boolean,比如,当用户在一个ListView上滑动手指          * 后 ,滚动会在短时间内发生,ListView内部有一个Scroller对象,在此期间调用computeScrollOffsetO          * 将 返 回true。          */         if (mAttachInfo.mViewScrollChanged) {             mAttachInfo.mViewScrollChanged = false;             mAttachInfo.mTreeObserver.dispatchOnScrollChanged();         }         /**          * 判 断 该Surface是 否 有SurfaceHolder对象。若有则意味着该Sb*face它是由应用程序创建的,因为          * 应用程序本身应负责所有绘制操作 是 View系统退出绘制。若有则意味着该Sb*face它是由应用程序创建的,因为          * 应用程序本身应负责所有绘制操作 是 View系统退出绘制。如果不是,那就是。 开 始View绘          * 内部流程。          */         int yoff;         final boolean scrolling = mScroller != null && mScroller.computeScrollOffset();        if (scrolling) {
            yoff = mScroller.getCurrY();
        } else {
            yoff = mScrollY;
        }
        if (mCurScrollY != yoff) {
            mCurScrollY = yoff;
            fullRedrawNeeded = true;
        }
        float appScale = mAttachInfo.mApplicationScale;
        boolean scalingRequired = mAttachInfo.mScalingRequired;

        Rect dirty = mDirty;
        if (mSurfaceHolder != null) {
            // The app owns the surface, we won't draw.
            dirty.setEmpty();
            return;
        }
        /**
         * 如 果 Surface是 由 OpenGL实现的,则开始按照G L 的处理方式进行处理。
         */
        if (mUseGL) {
            if (!dirty.isEmpty()) {
                /**
                 * (1 )以全局变量mGlCanvas作 为 canvas的值,并 调 用canvas.save()保 存 该Canvas内部的各种属性
                 * 及状态,因为接下来View树内部在绘制的过程中会修改Canvas对象的相关属性。
                 */
                Canvas canvas = mGlCanvas;
                if (mGL != null && canvas != null) {
                    mGL.glDisable(GL_SCISSOR_TEST);
                    mGL.glClearColor(0, 0, 0, 0);
                    mGL.glClear(GL_COLOR_BUFFER_BIT);
                    mGL.glEnable(GL_SCISSOR_TEST);

                    mAttachInfo.mDrawingTime = SystemClock.uptimeMillis();
                    mAttachInfo.mIgnoreDirtyState = true;
                    mView.mPrivateFlags |= View.DRAWN;
                    /**
                     * (2 )如果根视图内部包含Translator对象,则需要先经过Translator对象对该canvas对象进行一定
                     * 的调整。 Translator的作用主要是根据设备的硬件参数对Canvas的相关绘制属性进行一定的调整,该函
                     * 数的内部一般由驱动设计者支持实现。调整的过程由三个函数调用组成,
                     */
                    int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
                    try {
                        canvas.translate(0, -yoff);
                        if (mTranslator != null) {
                            mTranslator.translateCanvas(canvas);
                        }
                        /**
                         * 调 用canvas的 setScreenDensity()设置屏幕密度。这里需要区分不同语境中density的含义的不同。
                         * 首先,定义屏幕密度(density) 概念的作用是为了让不同分辨率的屏幕显示的视图能够看起来大小
                         * 相同。在传统的编程方式中,如果用像素指定视图的大小,对于分辨率高的显示器而言,像素密度高,
                         * 所以相同像素的实际尺寸会变小,于是引入density的概念。 Android中 160dpi的屏幕的density值定义
                         * 为 1,如果屏幕分辨率增加,比如240dpi,那 么 density的值也就越高,为 240/160=1.5,而分辨率低的
                         * 屏幕,比 如 120dpi,其密度就低,为 120/160=0.75。有了 density后,应用程序可以设置视图的大小单
                         * 位 为 dip,即密度无关像素(density independent pixel),从而在绘制视图时, View系统会根据不同的屏
                         * 幕分辨率将其换算成不同的像素。
                         * 而本步设置的screenDensity,却是指屏幕的分辨率值。当 参 数scalingRequired为 true时,该值为
                         * DisplayMetric.DENSITY_DEVICE,该常量是在系统启动时调用getProp()函数获取的设备参数,比如240、
                         * 160、 120等;如 果 scalingRequired为 false,那 么 screenDensity将被赋值为0, 0 是一个特殊值,并不是
                         * 说屏幕分辨率为0 , 而是指视图绘制没有指定具体的分辨率,从而在绘制时一个dpi将对应一个真实的
                         * 物理像素。
                         */
                        canvas.setScreenDensity(scalingRequired
                                ? DisplayMetrics.DENSITY_DEVICE : 0);
                        /**
                         *(4) 调 用 mView.draw(canvas)。该步骤才真正启动视图树的绘制过程,注意这里是将canvas作为
                         *参数,这也就是为什么应用程序中不能保存这个Canvas的原因,因为它是一个临时变量。 mView.draw()
                         * 实际调用的是View类 的 dmw()函数,关于其内部流程将在后面小节中介绍。
                         */
                        mView.draw(canvas);
                        if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) {
                            mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING);
                        }
                    } finally {
                        /**
                         * ( 5 ) 完成视图树的绘制后,绘制工作就算结束了,因为调用Canvas的 restoreToCount()将 Canvas
                         * 的内部状态恢复到绘制之前,该步骤与前面的canvas.saveO函数是对称调用的。
                         */
                        canvas.restoreToCount(saveCount);
                    }

                    mAttachInfo.mIgnoreDirtyState = false;

                    mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
                    checkEglErrors();
                    /**
                     * 如 果 是 Debug模式,并且模式中要求显示FPS,即CPU的使用率,则调用一个native函数
                     * nativeShowFPSO给屏幕上方绘制一个条状的统计图。
                     */

                    if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) {
                        int now = (int)SystemClock.elapsedRealtime();
                        if (sDrawTime != 0) {
                            nativeShowFPS(canvas, now - sDrawTime);
                        }
                        sDrawTime = now;
                    }
                }
            }
            /**
             * 最后,如果屏幕正在滚动,则需要再次发起一个重绘命令scheduleTravasals(),以便接着绘制,
             * 直到滚动结束,滚动的标志scrolling来 源 于Scroller对 象 的computeScrollOffset()函数返回值。
             */
            if (scrolling) {
                mFullRedrawNeeded = true;
                scheduleTraversals();
            }
            return;
        }
        /**
         * 如 果 Surface不 是 OpenGL实现的,则开始按照非G L 的处理方式进行处理。该步骤内部与上
         * 一步基本上是相同的,唯一的区别在于如何获得Canvas对象。 G L方式中,内部使用mGlCanvas全局
         * 变 量 保 存canvas对象,该变量是在G L 的初始化时进行赋值的;而 非 G L 方式中, Canvas对象需要调
         * 用 surface对 象 的lockCanvas()获取,其他过程完全相同,此处不再赘述。
         * 至此, ViewRoot中 的 draw()函数就执行完毕,一次绘制过程也就结束了,下一次的绘制将在下一
         * 个消息循环中执行。
         */
        if (fullRedrawNeeded) {
            mAttachInfo.mIgnoreDirtyState = true;
            dirty.union(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
        }

        if (DEBUG_ORIENTATION || DEBUG_DRAW) {
            Log.v(TAG, "Draw " + mView + "/"
                    + mWindowAttributes.getTitle()
                    + ": dirty={" + dirty.left + "," + dirty.top
                    + "," + dirty.right + "," + dirty.bottom + "} surface="
                    + surface + " surface.isValid()=" + surface.isValid() + ", appScale:" +
                    appScale + ", width=" + mWidth + ", height=" + mHeight);
        }

        if (!dirty.isEmpty() || mIsAnimating) {
            Canvas canvas;
            try {
                int left = dirty.left;
                int top = dirty.top;
                int right = dirty.right;
                int bottom = dirty.bottom;
                canvas = surface.lockCanvas(dirty);

                if (left != dirty.left || top != dirty.top || right != dirty.right ||
                        bottom != dirty.bottom) {
                    mAttachInfo.mIgnoreDirtyState = true;
                }

                // TODO: Do this in native
                canvas.setDensity(mDensity);
            } catch (Surface.OutOfResourcesException e) {
                Log.e(TAG, "OutOfResourcesException locking surface", e);
                // TODO: we should ask the window manager to do something!
                // for now we just do nothing
                return;
            } catch (IllegalArgumentException e) {
                Log.e(TAG, "IllegalArgumentException locking surface", e);
                // TODO: we should ask the window manager to do something!
                // for now we just do nothing
                return;
            }

            try {
                if (!dirty.isEmpty() || mIsAnimating) {
                    long startTime = 0L;

                    if (DEBUG_ORIENTATION || DEBUG_DRAW) {
                        Log.v(TAG, "Surface " + surface + " drawing to bitmap w="
                                + canvas.getWidth() + ", h=" + canvas.getHeight());
                        //canvas.drawARGB(255, 255, 0, 0);
                    }

                    if (Config.DEBUG && ViewDebug.profileDrawing) {
                        startTime = SystemClock.elapsedRealtime();
                    }

                    // If this bitmap's format includes an alpha channel, we
                    // need to clear it before drawing so that the child will
                    // properly re-composite its drawing on a transparent
                    // background. This automatically respects the clip/dirty region
                    // or
                    // If we are applying an offset, we need to clear the area
                    // where the offset doesn't appear to avoid having garbage
                    // left in the blank areas.
                    if (!canvas.isOpaque() || yoff != 0) {
                        canvas.drawColor(0, PorterDuff.Mode.CLEAR);
                    }

                    dirty.setEmpty();
                    mIsAnimating = false;
                    mAttachInfo.mDrawingTime = SystemClock.uptimeMillis();
                    mView.mPrivateFlags |= View.DRAWN;

                    if (DEBUG_DRAW) {
                        Context cxt = mView.getContext();
                        Log.i(TAG, "Drawing: package:" + cxt.getPackageName() +
                                ", metrics=" + cxt.getResources().getDisplayMetrics() +
                                ", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo());
                    }
                    int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
                    try {
                        canvas.translate(0, -yoff);
                        if (mTranslator != null) {
                            mTranslator.translateCanvas(canvas);
                        }
                        canvas.setScreenDensity(scalingRequired
                                ? DisplayMetrics.DENSITY_DEVICE : 0);
                        mView.draw(canvas);
                    } finally {
                        mAttachInfo.mIgnoreDirtyState = false;
                        canvas.restoreToCount(saveCount);
                    }

                    if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) {
                        mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING);
                    }

                    if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) {
                        int now = (int)SystemClock.elapsedRealtime();
                        if (sDrawTime != 0) {
                            nativeShowFPS(canvas, now - sDrawTime);
                        }
                        sDrawTime = now;
                    }

                    if (Config.DEBUG && ViewDebug.profileDrawing) {
                        EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime);
                    }
                }

            } finally {
                surface.unlockCanvasAndPost(canvas);
            }
        }

        if (LOCAL_LOGV) {
            Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost");
        }

        if (scrolling) {
            mFullRedrawNeeded = true;
            scheduleTraversals();
        }
    }

View 类 中 draw()函数内部流程

     
    public void draw(Canvas canvas) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
        }
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~DIRTY_MASK) | 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
        /**
         * 绘制背景。变 量 dirtyOpaque表示dirty区是否是不透明,只有透明时才需要绘制背景。Android
         * 中的视图几乎都是透明的,因为视图支持阿尔法通道,所以dirtyOpaque总 是为false,所以背景总是需
         * 要绘制。如果View系统不支持阿尔法通道,那么则不需要绘制背景,因为视图本身会占满整个区域,
         * 背景会完全被挡住。
         *
         */
        int saveCount;

        if (!dirtyOpaque) {
            final Drawable background = mBGDrawable;
            if (background != null) {
                final int scrollX = mScrollX;
                final int scrollY = mScrollY;

                if (mBackgroundSizeChanged) {
                    background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
                    mBackgroundSizeChanged = false;
                }
                /**
                 * 绘制背景时,首先根据滚动值对canvas的坐标进行调整,然后再恢复坐标,为什么需要先调用translate()   
                 * 平移Canvas的坐标呢?因为对每一个视图而言,Canvas的坐标原点(0 ,0 )对应的都是该视图的内部区域。
                 * 下图中外面是一个任意ViewGroup的实例,内部包含一个TextView对象,粗实线区域代表该TextView
                 * 在 ViewGroup中的位置, TextView中的文字由于滚动,一部分已经超出了粗实线区域,从而不可见。
                 * 此时,如果调用canvas.getClipBounds()返回的矩形区域是指粗实线所示的区域,该矩形的坐标是相对其
                 * 父视图ViewGroup的左上角,并且如果调用canvas的 getHeight()和 getWidth()方法将返回父视图的高度
                 * 和宽度,此处分别为200dip和 320dip。
                 * 如 果ViewGroup中包含多个子视图,那么每个子视图内部的onDraw()函数中参数canvas的大小都
                 * 是相同的,为父视图的大小。唯一不同的是“剪切区”,这个剪切区正是父视图分配给子视图的显示区
                 * 域 。
                 * canvas之所以被设计成这样正是为了 View树的绘制,对于任何一个View而言,绘制时都可以认
                 * 为原点坐标就是该View本身的原点坐标,从而 对 于View而言,当用户滚动屏幕时,应用程序只需要
                 * 调 用View类 的 scrollBy()函数即可,而不需要在onDraw()函数中做任何额外的处理,View的 onDraw()
                 * 函数内部可以完全忽略滚动值。
                 * 由于背景本身针对的是可视区域的背景,而 不 是 整 个 V iew 内部的背景,因此,本步中先调用
                 * translate()将原点移动到粗实线的左上角,从而使得背景Drawable对象内部绘制的是粗实线的区域。当
                 * 绘制完背景后,还需要重新调用transalte()将原点坐标再移回到TextView本 身 的 (0 ,0 )坐标。
                 */
                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) {
            /**
             * 绘制视图本身,实 际 上 回 调onDraw()函数即可, View 的设计者可以在onDraw()函数中调用
             * canvas的各种绘制函数进行绘制。
             */
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            /**
             * 调 用 dispatchDmw()绘制子视图。如果该视图内部不包含子视图,则不需要重载该函数,而对
             * 所有 的ViewGroup实例而言,都必须重载该函数,否则它也就不是ViewGroup 了
             */
            dispatchDraw(canvas);

            // Step 6, draw decorations (scrollbars)
            /**
             * 回调onDrawScrollbars()绘制滚动条。
             */
            onDrawScrollBars(canvas);

            // we're done...
            return;
        }

        /*
         * Here we do the full fledged routine...
         * (this is an uncommon case where speed matters less,
         * this is why we repeat some of the tests that have been
         * done above)
         */
        /**
         * 下面继续分析需要绘制渐变框(Fading Edge)的情况,应用程序可以调用setVerticalFadingEdge()
         * 和 setHorizontalFadingEdge()告诉系统绘制View对象的垂直方向渐变框及水平方向渐变框。
         */

        boolean drawTop = false;
        boolean drawBottom = false;
        boolean drawLeft = false;
        boolean drawRight = false;

        float topFadeStrength = 0.0f;
        float bottomFadeStrength = 0.0f;
        float leftFadeStrength = 0.0f;
        float rightFadeStrength = 0.0f;

        // Step 2, save the canvas' layers
        int paddingLeft = mPaddingLeft;
        int paddingTop = mPaddingTop;

        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
            paddingTop += getTopPaddingOffset();
        }

        int left = mScrollX + paddingLeft;
        int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
        int top = mScrollY + paddingTop;
        int bottom = top + mBottom - mTop - mPaddingBottom - paddingTop;

        if (offsetRequired) {
            right += getRightPaddingOffset();
            bottom += getBottomPaddingOffset();
        }
        /**
         * 源码中处理渐变框的逻辑中,定义了以下相关变量
         * mScrollCache:该变量的类型是ScrollabilityCache,该类中的作用是保存一个缓存对象,并且该
         * 缓存内部定义了一个Matrix、一 个 Shader、一 个 Paint,这三个对象联合起来可以使用Paint绘
         * 制 一 个Shader,并且可以在绘制时使用Matrix对 该 Shader进行缩放、平移、旋转、扭拉四种操
         * 作,具体见Matrix的介绍。
         */
        final ScrollabilityCache scrollabilityCache = mScrollCache;
        /**
         * length:对于垂直方向上的渐变框, length指的是该Shader的高度,对于水平方向,是 指 Shader
         * 的宽度。
         */
        int length = scrollabilityCache.fadingEdgeLength;
        /**
         * xxxFadeStrength: xxx 代表 left、 top、 right、 bottom, 该变量将作为后面 matrix.setScale()的参数,
         * 表 面 意 思 是“渐变强度”,但从其效果来看应该是“渐变拉伸度”,因 为 Shader对象内部的原始
         * 图像仅仅是一个像素宽,所以才调用matrix.SetSCale()对该像素的Shader进 行 缩 放 (拉伸),以产
         * 生一个矩形。xxxFadeStrength的范围是0〜 1,源码中使用该变量乘以fadeHeight或 者fadeLength。
         * 理解了以上变量的含义后,剩下的过程就变得简单了,具体流程如下。
         * 得到渐变框的length值 ,如 果 length的值大于视图本身的高度,则需要缩小length的值,否则
         * 会出现上下渐变重影或者左右渐变重影,影响视觉效果。
         *
         */
        // clip the fade length if top and bottom fades overlap
        // overlapping fades produce odd-looking artifacts
        if (verticalEdges && (top + length > bottom - length)) {
            length = (bottom - top) / 2;
        }

        // also clip horizontal fades if necessary
        if (horizontalEdges && (left + length > right - length)) {
            length = (right - left) / 2;
        }
        /**
         * 回 调getXXXFadingEdgeStrengthO。该函数一般由View的设计者重载,如前对xxxFadeStrength
         * 变量的解释,如果该函数返回为0,意味着不对Shader拉伸,那么也就不会绘制Shader 了。因此,本
         * 步骤正是通过回调这些函数,从而决定都要绘制上下左右哪些渐变框,并用四个变量drawXXX表示,
         * xxx 代表 Left、 Top、 Right、 Bottom。
         */
        if (verticalEdges) {
            topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
            drawTop = topFadeStrength >= 0.0f;
            bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
            drawBottom = bottomFadeStrength >= 0.0f;
        }

        if (horizontalEdges) {
            leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
            drawLeft = leftFadeStrength >= 0.0f;
            rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
            drawRight = rightFadeStrength >= 0.0f;
        }
        /**
         * 获得Shader渐变的色调。这段代码内部有点差强,源码设计者的本意是想调用canvas.saveLayerO
         * 对后面的绘制进行缓存,然而却仅仅设计成当颜色值为0 时才缓存。在笔者看来,如果要缓存,则无论
         * 什么颜色都可以缓存,因此,那 段 canvas.saveLayer()实际上没有什么意义。在一般情况下,如果颜色不
         * 为 0,则 调 用 setFadeColor()将该颜色设置到mScrollCache内 部 的 画 笔CPaint)中,应用程序可以重载
         * View类 的 getSolideColor()用于设置渐变色。
         */
        saveCount = canvas.getSaveCount();

        int solidColor = getSolidColor();
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }

            if (drawBottom) {
                canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
            }

            if (drawLeft) {
                canvas.saveLayer(left, top, left + length, bottom, null, flags);
            }

            if (drawRight) {
                canvas.saveLayer(right - length, top, right, bottom, null, flags);
            }
        } else {
            scrollabilityCache.setFadeColor(solidColor);
        }

        // Step 3, draw the content
        /**
         * 绘制视图本身
         */
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        /**
         * 调 用 dispatchDmwO绘制子视图。如果该视图内部不包含子视图,则不需要重载该函数,而对
         * 所有 的ViewGroup实例而言,都必须重载该函数,否则它也就不是ViewGroup了
         */
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        /**
         * 此时,才开始真正绘制渐变框,根据第二步中保存的变量drawXXX分别绘制不同的渐变框,
         * 绘制中主要是对Matrix进行变换,然 后 调 用canvas.draw()进行绘制。读者可能觉得奇怪,这 个 Matrix
         * 对 象 是 ScrollAbilityCache对 象 内 部 的 ,为 什 么 设 置 后 影 响 的 却 是 Canvas对 象 呢 ?原因就在于
         * canvas.draw()中最后一个参数p,它是一个Paint对象,该对象正是来源于ScrollAbilityCache中的Paint,
         * 而 该 Paint内部已经和该Matrix关联了。对 Matrix的变换包含以下四点。
//         * • matrix.setScale():该函数的作用正是把只有一个像素宽度的Shader缩放成一个真正的矩形渐
         * 变 框 。
         * • matrix.postTranslate():对坐标进行平移。
         * • matrix.postRotate():对图形进行旋转
         * • fade.setLocalMatrix():该调用正是把该Matrix和 该 Shader关联起来。
         */
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;
        final float fadeHeight = scrollabilityCache.fadingEdgeLength;

        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, top, right, top + length, p);
        }

        if (drawBottom) {
            matrix.setScale(1, fadeHeight * bottomFadeStrength);
            matrix.postRotate(180);
            matrix.postTranslate(left, bottom);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, bottom - length, right, bottom, p);
        }

        if (drawLeft) {
            matrix.setScale(1, fadeHeight * leftFadeStrength);
            matrix.postRotate(-90);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(left, top, left + length, bottom, p);
        }

        if (drawRight) {
            matrix.setScale(1, fadeHeight * rightFadeStrength);
            matrix.postRotate(90);
            matrix.postTranslate(right, top);
            fade.setLocalMatrix(matrix);
            canvas.drawRect(right - length, top, right, bottom, p);
        }

        canvas.restoreToCount(saveCount);
        
        /**最后调用onScmllBar()绘制滚动条,
         */
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
    }

源码中绘制上渐变框和下渐变框调用以上函数(matrix.setScale()、matrix.postTranslate()、matrix.postRotate()、fade.setLocalMatrix())时的执行效果如图1、图2所示,绘制左右 边框与此类似。

ViewGroup类中绘制子视图dispatchDrawO内部流程

  /**
     * dispatchDraw()的作用是绘制父视图中包含的子视图,该函数的本质作用是给不同的子视图分配合
     * 适 的 画 布 (Canvas),至于子视图如何绘制,则又递归到View类 的 draw()函数中。应用程序一般不需要
     * 重 载 dispatchDraw()函数,而只需要在onLayout()中为子视图分配合适的大小, dispatchDraw()将根据前
     * 面分配的大小调整Canvas的内部剪切区,并作为绘制子视图的画布。所有的ViewGroup实例的内部绘
     * 制基本上都是如此,这就是为什么具体的ViewGroup实例不需要重载dispatchDraw()的原因。
     */
    @Override
    protected void dispatchDraw(Canvas canvas) {
        final int count = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;
        /**
         * 判 断 mGroupFlags中是否设置FLAG—RUN—ANIMATION标识,该标识并不是该ViewGroup的
         * “动画标识”,而是 该ViewGroup “布局动画标识”。动画标识指的是一个View自身的动画,而布局动
         * 画只存在于ViewGroup对象中,指的是该ViewGroup在显示内部的子视图时,为内部子视图整体设置
         * 的 动 画 。 典 型 的 例 子 就 是 , 应 用 程 序 可 以 在 X M L 文 件 中 的 LinearLayout标 签 中 设 置
         * android:layoutAnimation属性,从而使 该LinearLayout的子视图在显示时出现逐行显示、随机显示、落
         * 下等不同的动画效果,而这些效果正是在本步骤实现的。关于动画的详细过程见后面小节,本节只分析
         * 没有动画的情况。
         */
        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;

            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);
                        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());
            }
        }
        /**
         * 处理padding属性。该属性是ViewGroup特有的,程序员只能给一个ViewGroup设 置padding,
         * 而不能给一个View设 置padding。如 果ViewGroup包 含padding值 ,则 CLIP_PADDINT—MASK标识将
         * 存在。对 于 View系统而言,当绘制到某个View时, View系统并不区分该View是一个具体的Veiw还
         * 是一个ViewGroup实例,都会在View.draw()函数中调用dispatchDraw(canvas),参 数 Canvas的绘制区原
         * 点坐标是该View内部区域的左上角, Canvas的剪切区仅仅是根据scroll值进行了剪切。由于padding
         * 是 ViewGroup所特有的属性,因此ViewGroup的 dispatchDraw()需要对该属性进行自身的处理。
         * 源码中首先调用canvas.save()保 存 当 前Canvas内部状态,然 后 调 用canvas.clipRect()进行剪切。在
         * 执 行 dispatchDraw()函数前, Canvas的剪切区已经根据scroll值进行了剪切,剪切坐标的原点是View自
         * 身的左上角,所以此处仅仅需要从左边加paddingLeft,从上边加paddingTop,从右边减paddingRight,
         * 从下边减paddingBottom。
         * 执行后,就会根据padding的值缩小剪切区。这里需要注意,缩小的仅仅是剪切区,也就是用户在
         * 屏幕上看到的区域,而 ViewGmup本身的大小没有变化。本步骤执行前后的位置如图13-40和 图 13-41所示。
         */
        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);

        }
        /**
         * 清 除 mPrivateFlags的 DRAW_ANIMATION标 识 ,因为接下来就会绘制视图了;同时清除
         * mGroupFlags的 FLAG—INVALIDATED_REQUJRIED标 识 , 因 为 接 来 绘 制 后 就 意 味 着 已 经 满 足
         * "RECURIED” 这个需求了。
         */
        // We will draw our child's animation, let's reset the flag
        mPrivateFlags &= ~DRAW_ANIMATION;
        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;

        boolean more = false;
        final long drawingTime = getDrawingTime();
        /**
         * 使 用 for()循环,针 对 该ViewGroup的子视图逐个调用drawChild()函数。在一般情况下,绘制
         * 子 视 图 的 顺 序 是 按 照 子 视 图 被 添 加 的 顺 序 逐 个 绘 制 , 但 应 用 程 序 可 以 重 载 ViewGmup的
         * getChildDrawingOrder()函数,提供不同的顺序。关 于 drawChild()的内部过程见后面小节
         */
        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
        /**
         * 绘 制 mDisappearingChildren列表中的子视图。这个变量需要着重解释一下,当 从 ViewGroup
         * 中 removeView()时,指 定 的View对象会从mChildren变量中移除,因此,当进行消息派发时,被删除
         * 的 View就绝不会获得用户消息。当被删除的View对象包含一个移除动画时,则 该 View会被添加到
         * mDisappearingChildren列表中,从而使得在进行dispatchDraw()时,该 View依然会被绘制到屏幕上,直
         * 到动画结束,在动画期间,用户虽然能够看到该视图,但却无法点击该视图,因为它已经从mChildren
         * 列表中被删除,消息处理时会认为没有该View的存在。
         */
        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 (clipToPadding) {
            canvas.restoreToCount(saveCount);
        }

        // mGroupFlags might have been updated by drawChild()
        flags = mGroupFlags;
        /**
         * 6、 重新检查 mGroupFlags 中是否包含 FLAG_INVALIDATED_REQURIED 标识,因为 drawChild()
         * 调用后,可能需要重绘该ViewGroup,如果需要,则调 用 invalidate()发起一个重绘请求。
         */
        if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
            invalidate();
        }
        /**
         * 7 、本步骤与第1 步是对称的,第1步中会先处理“布局动画”,而本步骤则处理布局动画是否完
         * 成,如果完成,发 送 一 个 Handler消息。该 消 息 是 一 个Runnable对象,其 作 用 是 回 调ViewGroup中
         * AnimationListener接 口 的onAnimationEnd()函数,通知应用程序布局动画完成了。
         */
        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);
        }
    }

ViewGroup 类中 drawChild()过程

  /**
     * drawChild()的核心过程是为子视图分配合适的Canvas剪切区,剪切区的大小取决于child的布局大 
     * 小,剪切区的位置取决于child的内部滚动值及child内部的当前动画。
     */
  protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        boolean more = false;

        final int cl = child.mLeft;
        final int ct = child.mTop;
        final int cr = child.mRight;
        final int cb = child.mBottom;

        final int flags = mGroupFlags;

        if ((flags & FLAG_CLEAR_TRANSFORMATION) == FLAG_CLEAR_TRANSFORMATION) {
            if (mChildTransformation != null) {
                mChildTransformation.clear();
            }
            mGroupFlags &= ~FLAG_CLEAR_TRANSFORMATION;
        }
        /**
         * 1 、判断该子View是 否 存 在“变换矩阵”。所 谓 的 “变换矩阵” 是 指 该child在绘制时将通过一个
         * 矩 阵 (Matrix)进行变换,变换的元素包括四点,分别为平移、旋转、缩放、扭曲,该矩阵就称之为变
         * 换矩阵。存在变换矩阵意味着,该图像变换后会改变边框的大小。可以通过两种方式为视图设置变换矩
         * 阵,一种是动画,另一种是静态回调。
         * 所谓动画是指,应用程序可以调用View类 的 setAnimation()为 该View设置一个动画。动画的本质
         * 是 对 View视图在指定的时间内进行某种变换(Transformation),变换包括图像平移、旋转、缩放、扭
         * 曲及图像颜色阿尔法通道变化,然后将变换后的图像绘制到屏幕上,系统会在指定的时间内连续进行绘
         * 制 ,并在不同时间得到不同的变换参数,从 而 使 其 看 起 来 就 像 是 一 个“ 动 画 ”。动画中又使用一个
         * Transformation类来保存这些变换参数, Transaformation类是一个数据类,内部包含变换的相关参数,
         * 变换按照类型分为四种:
         * • TYPEJDENTIFY: identify的 意 思 是“相同的”,即该变换实际上不会引起任何变换。
         * • TYPE_ALPAH:将引起图像颜色阿尔法通道变换。
         * • TYPE—MATRIX:将引起矩阵变换,矩阵变换包括平移、旋转、缩放、扭曲四种。
         * • TYPE_BOTH: both的含义是同时包含ALPHA和 MATRIX。
         * 所 谓 的 静 态 回 调 是 指 , ViewGroup实 例 的 设 计 者 可 以 重 载 ViewGroup类 的
         * getChildStaticTransformation()函数,从而为其包含的子视图指定一个静态的变换对象。
         * 本步骤中包含两个重要局部变量。
         * • Transformation transformToApply:保存了子视图的变换对象,可来源于动画,也可静态指定。
         * 源码中首先判断是否存在动画,如果存在动画,则 调 用Animation对 象 的 getTransformation()获
         * 取变换对象;如果没有动画,则回 调getChildStaticTransformation()获取变换对象。
         * • boolean concatMatrix:该变量代表是否存在变换矩阵。对于动画而言,调 用 Animation对象的
         * willChangeTransformationMatrix()判断是否存在变换矩阵;而 对 于“静态回调”,则直接判断变换
         * 的类型,只有当变换类型是TYPE_MATRIX或 者 TYPE_BOTH时,该变量才为true。
         */
        Transformation transformToApply = null;
        final Animation a = child.getAnimation();
        boolean concatMatrix = false;

        if (a != null) {
            if (mInvalidateRegion == null) {
                mInvalidateRegion = new RectF();
            }
            final RectF region = mInvalidateRegion;

            final boolean initialized = a.isInitialized();
            if (!initialized) {
                a.initialize(cr - cl, cb - ct, getWidth(), getHeight());
                a.initializeInvalidateRegion(0, 0, cr - cl, cb - ct);
                child.onAnimationStart();
            }

            if (mChildTransformation == null) {
                mChildTransformation = new Transformation();
            }
            more = a.getTransformation(drawingTime, mChildTransformation);
            transformToApply = mChildTransformation;

            concatMatrix = a.willChangeTransformationMatrix();

            if (more) {
                if (!a.willChangeBounds()) {
                    if ((flags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) ==
                            FLAG_OPTIMIZE_INVALIDATE) {
                        mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
                    } else if ((flags & FLAG_INVALIDATE_REQUIRED) == 0) {
                        // The child need to draw an animation, potentially offscreen, so
                        // make sure we do not cancel invalidate requests
                        mPrivateFlags |= DRAW_ANIMATION;
                        invalidate(cl, ct, cr, cb);
                    }
                } else {
                    a.getInvalidateRegion(0, 0, cr - cl, cb - ct, region, transformToApply);

                    // The child need to draw an animation, potentially offscreen, so
                    // make sure we do not cancel invalidate requests
                    mPrivateFlags |= DRAW_ANIMATION;

                    final int left = cl + (int) region.left;
                    final int top = ct + (int) region.top;
                    invalidate(left, top, left + (int) region.width(), top + (int) region.height());
                }
            }
        } else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) ==
                FLAG_SUPPORT_STATIC_TRANSFORMATIONS) {
            if (mChildTransformation == null) {
                mChildTransformation = new Transformation();
            }
            final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation);
            if (hasTransform) {
                final int transformType = mChildTransformation.getTransformationType();
                transformToApply = transformType != Transformation.TYPE_IDENTITY ?
                        mChildTransformation : null;
                concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
            }
        }

        // Sets the flag as early as possible to allow draw() implementations
        // to call invalidate() successfully when doing animations
        child.mPrivateFlags |= DRAWN;
        /**
         * 如果以上变换不会改变边框大小,即没有变换矩阵时,调 用Canvas对 象 的quickReject()函数快
         * 速判断该子视图对应的剪切区是否超出了父视图的剪切区,超出意味着该子视图不能显示到屏幕上,所
         * 以就不用绘制了,因为绘制了用户也看不见。 quickReject()调用时参数代表该子视图在父视图中的布局
         * (layout)位置。注意本步成立的条件包含三个,这三个条件最终的意义是指,该 View 内部没有进行
         * 动画,并且不存在“静态回调” 变换,并且剪切区不在父视图的剪切区中。该意义的反义是指,如果当
         * 前正在进行动画或者存在静态回调变化,那么就算当前视图的剪切区不在父视图的剪切区中,都要进行
         * 绘制操作。为什么呢?因为存在矩阵变换后,会引起子视图边框位置改变,而改变后的区域有可能又落
         * 到了父视图的剪切区中,从 而 变 成“可看得见” 的。
         */
        if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) &&
                (child.mPrivateFlags & DRAW_ANIMATION) == 0) {
            return more;
        }
        /**
         * 回 调child.computeScroll(),重新计算子视图的当前滚动值,因为子视图的滚动值在每次绘制之
         * 后都有可能变化。应用程序一般会重载View对 象 的computeScroll()函数,比如对于ListView,当用户
         * 手指在屏幕上滑动时,将导致屏幕做惯性滚轮运动,而在这个运动的过程中, computeScroll()函数会不
         * 断改变该View的滚动值。
         */
        child.computeScroll();

        final int sx = child.mScrollX;
        final int sy = child.mScrollY;
        /**
         * 上一步得到了子视图的滚动值,本步就要根据该滚动值设置子视图Canvas坐标的原点。对当
         * 前 的 Canvas而言,其坐标原点是该ViewGroup布局区域的左上角①点,如 图 13-43所示,而本步正是
         * 要将这个坐标原点移动到指定子视图的自身显示区域的左上角②点。
         *
         * 源码中,针对是否有cache的情况分别处理。在一般情况下,没 有 cache,所 以 translate()参数中水
         * 平方向是先向右移动c l ,然后再向左移动sx,垂直方向类似;而对于有cache的情况,则忽略滚动值,
         * 因为有cache时,视图本身需要处理滚动值,实际上如果有cahce,视图的滚动值都会设置为0,因此水
         * 平方向仅平移cl,垂直方向仅平移ct。
         */
        boolean scalingRequired = false;
        Bitmap cache = null;
        if ((flags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE ||
                (flags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE) {
            cache = child.getDrawingCache(true);
            if (mAttachInfo != null) scalingRequired = mAttachInfo.mScalingRequired;
        }

        final boolean hasNoCache = cache == null;

        final int restoreTo = canvas.save();
        if (hasNoCache) {
            canvas.translate(cl - sx, ct - sy);
        } else {
            canvas.translate(cl, ct);
            if (scalingRequired) {
                // mAttachInfo cannot be null, otherwise scalingRequired == false
                final float scale = 1.0f / mAttachInfo.mApplicationScale;
                canvas.scale(scale, scale);
            }
        }

        float alpha = 1.0f;
        /**
         * 上面第一步得到了变换对象transformToApply,本步就要将该变换对象应用于子视图。首先判
         * 断是否存在变换矩阵concatMatrix,并应用变换矩阵,然后再应用视图颜色阿尔法变换。
         * 变换矩阵针对的是视图本身的整个区域,而不仅是在屏幕上的显示区域,但是变换矩阵中的数据却
         * 是相对子视图可视区域的左上角,因此,在变换前需要先将坐标原点调整到视图本身的左上角,然后再
         * 应用变换,最后再将原点调整回子视图本身的左上角,
         */
        if (transformToApply != null) {
            if (concatMatrix) {
                int transX = 0;
                int transY = 0;
                if (hasNoCache) {
                    transX = -sx;
                    transY = -sy;
                }
                // 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);
                mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
            }

            alpha = transformToApply.getAlpha();
            if (alpha < 1.0f) {
                mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
            }
            /**
             * 针对视图颜色的阿尔法变换主要是调用canvas.saveLayoutAlpha()函数完成,在调用该函数前先回调
             * 子视图的 child.onSetAlpha()。
             */
            if (alpha < 1.0f && hasNoCache) {
                final int multipliedAlpha = (int) (255 * alpha);
                if (!child.onSetAlpha(multipliedAlpha)) {
                    canvas.saveLayerAlpha(sx, sy, sx + cr - cl, sy + cb - ct, multipliedAlpha,
                            Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
                } else {
                    child.mPrivateFlags |= ALPHA_SET;
                }
            }
        } else if ((child.mPrivateFlags & ALPHA_SET) == ALPHA_SET) {
            child.onSetAlpha(255);
        }
        /**
         * 此 时 Canvas内部的变换参数已经确定,子视图的滚动值也确定了,因此也就可以确定子视图
         * 中剪切区的位置了。同样,剪切区也分是否有cache的情况,因为如果有cache的话,系统将认为子视
         * 图没有滚动值,滚动的处理完全由子视图内部控制,所以剪切区将忽略滚动值。
         *  scalingRequired变量是指是否进行了缩放。如果没有缩放,则边框没有变化。边框
         * 的位置就等于该子视图的布局位置,即 child.mLeft、 child.mTop、 child.mRight及 child.mBottom,此时
         * 滚动值一般都为0,所以实际的剪切区就等于子视图布局的大小。如果有缩放,比如缩放大于1,则子
         * 视图的显示边框将大于布局边框,因此,剪切区的大小应该使用cache的大小,即 cache.getWidth()及
         * cache.getHeight()。
         * 该算法的结果实际上就是子视图的布局大小,只是考虑了滚动而已。
         */
        if ((flags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
            if (hasNoCache) {
                /**而在一般情况下视图都没有cache,因此需要根据滚动的位置确定剪切区,
                 *
                 */
                canvas.clipRect(sx, sy, sx + (cr - cl), sy + (cb - ct));
            } else {
                if (!scalingRequired) {

                    canvas.clipRect(0, 0, cr - cl, cb - ct);
                } else {
                    canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
                }
            }
        }
        /**
         * 剪切区设置好后,就可以调用子视图的dmw()函数进行具体的绘制了。同样,源码中分别针对
         * 是 否 有cache的情况做了不同处理。
         * 一般情况下没有cache,那么最简单的过程就是直接调用child.dmw()即可,只是在调用之前先判断
         * 子视图的mPrivateFlags是否包含SKIP_DRAW标识,该标识告知View系统暂时跳过对该子视图的绘制。
         * 如果需要跳过,贝U仅 调 用child.dispatchDmw(),即跳过视图本身的绘制,但要绘制视图可能包含的子视
         * 图。
         * 如 果 有cache,则只需要把视图对应的cache绘制到屏幕上即可,即调用canvas.drawBitmap()函数,
         * 参数中包含缓冲区cache,它实际上是一个Bitmap对象
         */
        if (hasNoCache) {
            // Fast path for layouts with no backgrounds
            if ((child.mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {
                if (ViewDebug.TRACE_HIERARCHY) {
                    ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
                }
                child.mPrivateFlags &= ~DIRTY_MASK;
                child.dispatchDraw(canvas);
            } else {
                child.draw(canvas);
            }
        } else {
            final Paint cachePaint = mCachePaint;
            if (alpha < 1.0f) {
                cachePaint.setAlpha((int) (alpha * 255));
                mGroupFlags |= FLAG_ALPHA_LOWER_THAN_ONE;
            } else if  ((flags & FLAG_ALPHA_LOWER_THAN_ONE) == FLAG_ALPHA_LOWER_THAN_ONE) {
                cachePaint.setAlpha(255);
                mGroupFlags &= ~FLAG_ALPHA_LOWER_THAN_ONE;
            }
            if (Config.DEBUG && ViewDebug.profileDrawing) {
                EventLog.writeEvent(60003, hashCode());
            }
            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
        }
        /**
         * 最后,绘制到此就结束,结束前,需要先 恢 复Canvas绘制前的状态,并且如果该绘制是一次
         * 动画绘制,那么当该动画结束,则调 用 finishAnimatingView()通知应用程序该子视图动画绘制完成了。
         */
        canvas.restoreToCount(restoreTo);

        if (a != null && !more) {
            child.onSetAlpha(255);
            finishAnimatingView(child, a);
        }

        return more;
    }

绘制滚动条

绘制滚动条是通过在View类 的 draw()函数中调用onDrawScrollBar()完成的。每个视图都可以有滚动条,请注意是“每个”,因为滚动条是视图中包含的基本元素,就像视图的背景一样。举个例子,一个 按 钮 Button)也可以有滚动条,读者可能觉得奇怪,按钮怎么会有滚动条呢?但的确是这样,而且让按钮显示滚动条是件非常容易的事情,因为按钮本身也是一个视图,只是从用户的角度来讲,按钮不需要显示这个滚动条而已。 滚动条包含垂直滚动条和水平滚动条,滚动条本身是一个Drawable对象,View类内部使用ScrollBarDrawable类表示滚动条,View可以同时绘制水平滚动条和垂直滚动条。 在 ScrollBarDrawable类中,包含三个基本尺寸,分别是range、 offset、 extent。

  • range:代表该滚动条从头到尾滚动中所跨越的范围有多大。比如想用一个滚动条标识一万行代码,那 么 range可以设为10000。

  • offset:代表滚动条当前的偏移量。比如当前在看第600行代码,那 么 offset就 是 600。

  • extent:代表该滚动条在屏幕上的实际高度,比如200,单位是dip。 有了以上三个尺寸后,ScrollBarDrawable内部就可以计算出滚动条的高度及滚动条的位置。 除了以上尺寸外,ScrollBarDrawable类内部还包含两个Drawable对象,一个标识滚动条的背景,另一个标识滚动条自身。这两个Drawable对象分别是track和 thumble,水平方向和垂直方向分别有两个该Drawable对象。

    protected final void onDrawScrollBars(Canvas canvas) {
        // scrollbars are drawn only when the 

标签: cr表示继电器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台