自定義View(九)-View的工作原理- View的layout()和draw()

前言

上一節(jié)我們將View的測(cè)量流程理的差不多了,這篇我們來(lái)看下View的剩下的2大流程layout(布局)和draw(繪制)。相對(duì)測(cè)量來(lái)說(shuō),布局與繪制就簡(jiǎn)單了許多,所以我們將這的兩大流程放在一起講解。


performLayout()布局
由上上篇我們知道,布局是從ViewRootImpl#performLayout()發(fā)起的,那我們進(jìn)入這個(gè)方法看一下:

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        
        ......
        //標(biāo)記開(kāi)始當(dāng)前布局
        mInLayout = true;//2062
        //將全局變量mView(DecorView)賦值給host
        final View host = mView;//2064
        
        ......
        
        //DecorView開(kāi)始布局自己
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//2072

        //標(biāo)志布局結(jié)束
        mInLayout = false;
    }        

首先我們知道host是就DecorView(FrameLayout),那我們進(jìn)入ViewGroup看看,發(fā)現(xiàn)ViewGroup也是調(diào)用了它的父類(View)的layout方法,所以這里host.layout()就是調(diào)用了View#layout()。layout()方法的四個(gè)參數(shù)分別是當(dāng)前View的左(left),上(top),右(right),下(bottom)。由上一篇我們知道,host.getMeasuredWidth(),host.getMeasuredHeight()就是屏幕的大小,所以從參數(shù)我們清楚的知道DecorView的四個(gè)頂點(diǎn)是從屏幕左上角到屏幕右下角,即整個(gè)屏幕。 那我們進(jìn)入View#layout():

小提示:這里我們需要區(qū)分下測(cè)量的寬高與最終的寬高:
我們知道測(cè)量寬高和最后的寬高在多數(shù)情況下都是相等的,因?yàn)閺纳厦嫖覀冎?,在layout的時(shí)候是調(diào)用的getMeasuredWidth()與getMeasuredHeight(),即:測(cè)量完成后的寬高作為參數(shù)來(lái)布局的。不過(guò)這是指大多數(shù)的情況下,如果你自定義View重寫了layout()方法那么最后的寬高就不會(huì)不同。

public void layout(int l, int t, int r, int b) {
        //判斷是否需要重新測(cè)量
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
        
        //保存上一次View的四個(gè)點(diǎn)的位置
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        
        //設(shè)置當(dāng)前View的左頂右低四個(gè)位置,并判斷布局是否有變換
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        //在第一次或是位置改變時(shí)changed=true 條件成立視圖View重新布局
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    //布局有變換時(shí)的回調(diào) 將上面保存的最新的四個(gè)位置和上一次的四個(gè)位置傳給回調(diào)監(jiān)聽(tīng)
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

這里會(huì)首先通過(guò)setFrame方法來(lái)設(shè)置當(dāng)前View的四個(gè)頂點(diǎn)的位置,即初始化mLeft,mTop,mBottom,mRight這四個(gè)值,這四個(gè)值一旦確定,那么當(dāng)前View在父容器中的位置也就確定了。也就是說(shuō)當(dāng)setFrame()方法完成后,就基本上完成了當(dāng)前View的布局。那我們來(lái)看下這個(gè)方法:

  protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (DBG) {
            Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;
            
            //得到上次的寬和高
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            //得到這次的寬和高
            int newWidth = right - left;
            int newHeight = bottom - top;
            //將新舊寬高做比較 判斷是否想相等
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            //清除上次布局的位置 重新繪制
            invalidate(sizeChanged);

            //將最新的位置賦值給全局變量
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            mPrivateFlags |= PFLAG_HAS_BOUNDS;
             
            判斷當(dāng)前位置是否有變化
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            ......
            
        return changed;
    }


這個(gè)方法完成后,當(dāng)前View的布局也就基本完成了,并且將最新的4個(gè)位置賦值給mLeft,mTop,mBottom,mRight。這里我們?cè)趤?lái)分析上面的小提示。來(lái)區(qū)分下測(cè)量寬高和最終寬高,其實(shí)比較這兩個(gè)的不同就是比較getWidth()/getHeight與getMeasuredWidth()/getMeasuredHeight()。那我們來(lái)看下getWidth()/getHeight()方法:

public final int getWidth() {
        return mRight - mLeft;
    }
 public final int getHeight() {
        return mBottom - mTop;
    }

上面這4個(gè)值正是我們?cè)趌ayout布局中得到的(具體我們可知是在setFrame()方法中),那我們總結(jié)下兩者的關(guān)系:

  1. 最終寬高的生成需要一般需要測(cè)量寬高作為參數(shù)。
  2. 測(cè)量寬高的生成比最終寬高的生成要早。
  3. 最終寬高是由layout來(lái)決定的,也就是View在父布局中顯示的位置,通常情況下2著相同 (這里用到通常情況,因?yàn)樵谖抑貙憀ayout時(shí)如果改變layout的參數(shù),那么最終在父布局中顯示的位置也會(huì)改變)

通過(guò)setFrame()方法完成了對(duì)自己的布局,那么onLayout()他的作用是什么呢?其實(shí)onLayout方法的用途是父容器確定子元素的位置。我們來(lái)看下View#onLayout():

   /**
     * Called from layout when this view should
     * assign a size and position to each of its children.
     *
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * @param changed This is a new size or position for this view
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     */
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

可以發(fā)現(xiàn)這是一個(gè)空的方法,既然上面提到了他的作用是父容器確定子元素的位置。那我們就是容器所有的父類,View的直接子類ViewGroup去看一下:

  @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

可以發(fā)現(xiàn)他是一個(gè)抽象方法,那么就說(shuō)明所有直接繼承ViewGroup的容器都要實(shí)現(xiàn)這個(gè)方法。其實(shí)也容易理解,想想平時(shí)我們用到的LinearLayout,RelativeLayout都是直接繼承ViewGroup的。很明顯在使用的時(shí)候,在布局子View的時(shí)候位置使不用的。在回到開(kāi)始處,是由host.layout()發(fā)起的布局,并且host就是我們的頂級(jí)View(DecorView),同時(shí)知道DecorView是繼承FrameLayout的那么我進(jìn)入FrameLayout#onLayout(),如下:

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

這里直接調(diào)用了layoutChildren()方法,那么我們繼續(xù)往下走:

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();
        
        //遍歷所有FrameLayout下的ziView
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //判斷當(dāng)前子View的可見(jiàn)度。不過(guò)是GONE那么就不進(jìn)行布局
            if (child.getVisibility() != GONE) {
                //獲取子子View的getLayoutParams
                final LayoutParams lp = (getLayoutParams) child.getLayoutParams();
                獲取子View的寬高
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;
                //根據(jù)子View的LayoutParams來(lái)獲取gravity的設(shè)置
                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
                //下面都是根據(jù)gravity屬性的設(shè)置來(lái)決定如何設(shè)置子View四個(gè)點(diǎn)的值
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }
                //設(shè)置完成子View的四個(gè)點(diǎn)的值傳入子View的layout方法開(kāi)始布局
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

通過(guò)代碼和注釋,我們了解到FrameLayout調(diào)用了這里直接調(diào)用了layoutChildren()方法,在這個(gè)方法中完成每個(gè)子View的布局。這個(gè)方法中通過(guò)對(duì)對(duì)齊方式和Margin的計(jì)算,來(lái)獲得子View四個(gè)點(diǎn)的位置,最后調(diào)用child.layout()方法,如果是View就會(huì)走上面View的布局如果是ViewGrouop那么就和上面FrameLayout的布局邏輯一致(這里說(shuō)的邏輯一致是因?yàn)橹苯永^承ViewGroup的容器都會(huì)根據(jù)自己的特點(diǎn)重寫onLayout()方法。比如LinearLayout和FrameLayout的布局方式是不同。但是最后都是會(huì)調(diào)用child.layout()方法,也就是邏輯都是一樣的)

總結(jié):

上面我們就完成了整個(gè)布局的繪制流程。下面我就對(duì)我們將到的知識(shí)點(diǎn)進(jìn)行一下總結(jié)
通過(guò)整個(gè)layout(布局)我們可以總結(jié)如下:

  1. 直接繼承ViewGroup的容器要重寫onLayout方法,根據(jù)自己的特點(diǎn),完成對(duì)子View的布局。
  2. 直接繼承ViewGroup的容器要自己處理子View的Margin屬性,否則會(huì)到時(shí)失效。
  3. 通過(guò)上面我們知道,在View設(shè)置可見(jiàn)度為GONE是不會(huì)布局。這個(gè)是為什么設(shè)置View.GONE不會(huì)占用布局的原因。
  4. 必須要在布局完成后才能獲取到調(diào)用getHeight()和getWidth()方法獲取到的View的寬高否則為0。

關(guān)于其他容器是如何重寫onLayout()的大家可以自己看下。相信在理解上面的內(nèi)容,媽媽就再也不用擔(dān)心我不敢看源碼啦~~!我們將我們的流程用流程圖來(lái)表示,如下:

View樹(shù)layout繪制流程.png

到此View的繪制也就完成了。下面我們來(lái)看下draw(繪制)。


performDraw()繪制
現(xiàn)在我們來(lái)看看View三大流程最后一個(gè)流程-->draw(繪制)。它的作用就是講View繪制在屏幕上。我們還是來(lái)看下繪制發(fā)起的方法ViewRootImpl#performDraw():


private void performDraw() {

        ......
        
        try {
            draw(fullRedrawNeeded);//2337
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
       
       ......
    }

調(diào)用了draw()方法,我們繼續(xù)走:

    private void draw(boolean fullRedrawNeeded) {
      
      ...... 
       
      if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {//2519
                    return;
                }
      ......    
    }

在draw()方法的最后調(diào)用了drawSoftware(),這個(gè)方法比較中重要。我們進(jìn)入這個(gè)方法來(lái)看下:

/**
     * @return true if drawing was successful, false if an error occurred
     */
  private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

        // Draw with software renderer.
        final Canvas canvas;
        try {
            //從surface對(duì)象中獲得canvas變量
            canvas = mSurface.lockCanvas(dirty);

            // 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 || xoff != 0) {
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }

           ......

            try {
                //調(diào)整畫布的位置
                canvas.translate(-xoff, -yoff);
                if (mTranslator != null) {
                    mTranslator.translateCanvas(canvas);
                }
                canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
                attachInfo.mSetIgnoreDirtyState = false;
                //調(diào)用View類中的成員方法draw開(kāi)始繪制View視圖
                mView.draw(canvas);
            } 

        ......

        return true;
    }

從這個(gè)方法名字我們可以看出繪制成功返回true,失敗返回false,說(shuō)明繪制是在這里進(jìn)行的。在這個(gè)方法中我們獲得了畫布canvas并將這個(gè)參數(shù)傳遞給 mView.draw(canvas);方法,我們?cè)邳c(diǎn)進(jìn)去看看:


 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) {
            drawBackground(canvas);
        }

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

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(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)
         */
         
        /*第二步開(kāi)始 */
        
        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;

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

        int left = mScrollX + paddingLeft;
        int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
        int top = mScrollY + getFadeTop(offsetRequired);
        int bottom = top + getFadeHeight(offsetRequired);

        if (offsetRequired) {
            right += getRightPaddingOffset();
            bottom += getBottomPaddingOffset();
        }

        final ScrollabilityCache scrollabilityCache = mScrollCache;
        final float fadeHeight = scrollabilityCache.fadingEdgeLength;
        int length = (int) fadeHeight;

        // clip the fade length if top and bottom fades overlap 如果頂部和底部淡入淡出,則剪切淡入淡出長(zhǎng)度
        // 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;
        }

        if (verticalEdges) {
            topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
            drawTop = topFadeStrength * fadeHeight > 1.0f;
            bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
            drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
        }

        if (horizontalEdges) {
            leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
            drawLeft = leftFadeStrength * fadeHeight > 1.0f;
            rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
            drawRight = rightFadeStrength * fadeHeight > 1.0f;
        }

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

        /*第二步結(jié)束 */
        
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers  繪制淡入淡出效果
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;

        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            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);
            p.setShader(fade);
            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);
            p.setShader(fade);
            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);
            p.setShader(fade);
            canvas.drawRect(right - length, top, right, bottom, p);
        }
        //恢復(fù)圖層
        canvas.restoreToCount(saveCount);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
    }

這個(gè)方法有點(diǎn)長(zhǎng),但是很好理解。已進(jìn)入方法就提示了繪制的過(guò)程遵循以下6個(gè)步驟:

  1. 繪制當(dāng)前視圖的背景。

  2. 保存當(dāng)前畫布的堆棧狀態(tài),并且在在當(dāng)前畫布上創(chuàng)建額外的圖層,以便接下來(lái)可以用來(lái)繪制當(dāng)前視圖在滑動(dòng)時(shí)的邊框漸變效果。

  3. 繪制當(dāng)前視圖的內(nèi)容。

  4. 繪制當(dāng)前視圖的子視圖的內(nèi)容。

  5. 繪制當(dāng)前視圖在滑動(dòng)時(shí)的邊框漸變效果。

  6. 繪制當(dāng)前視圖的滾動(dòng)條。

在一般情況下2和5我們?cè)谧远xView時(shí)是不會(huì)去修改的。但是為了記錄,還是簡(jiǎn)單講解下。

1.繪制視圖View的背景
通過(guò)上面我們知道繪制背景首先通過(guò)dirtyOpaque表示位來(lái)判斷是否需要繪制背景,如果需要就到用drawBackground(canvas)方法。如下:

  /**
     * Draws the background onto the specified canvas.
     *
     * @param canvas Canvas on which to draw the background
     */
    private void drawBackground(Canvas canvas) {
        //獲取背景drawable
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        //在繪制背景之前先設(shè)置背景的矩形大小
        setBackgroundBounds();

        ......
        
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        //利用 background.draw(canvas);來(lái)繪制背景
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

這里的流程就是先回去背景,然后在繪制背景之前先設(shè)置背景的矩形大小,最后利用background.draw(canvas);方法來(lái)完成繪制背景。

2.保存畫布canvas的邊框參數(shù)
首先通過(guò)下面的獲得當(dāng)前視圖View水平或者垂直方向是否需要繪制邊框漸變效果

boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;

如果不需要繪制邊框的漸變效果,就無(wú)需執(zhí)行上面的2,5了。那么就直接執(zhí)行上面的3,4,6步驟。這里描述的就是我們的ListView滑動(dòng)到最底端時(shí),底部會(huì)有一個(gè)淡藍(lán)色的半圓形的邊框漸變背景效果。既然說(shuō)要記錄下我們就把所有過(guò)程都走一遍。

在標(biāo)記第二步開(kāi)始和結(jié)束的位置之間的這段代碼用來(lái)檢查是否需要保存參數(shù)canvas所描述的一塊畫布的堆棧狀態(tài),并且創(chuàng)建額外的圖層來(lái)繪制當(dāng)前視圖在滑動(dòng)時(shí)的邊框漸變效果。視圖的邊框是繪制在內(nèi)容區(qū)域的邊界位置上的,而視圖的內(nèi)容區(qū)域是需要排除成員變量mPaddingLeft、mPaddingRight、mPaddingTop和mPaddingBottom所描述的視圖內(nèi)邊距的。此外,視圖的邊框有四個(gè),分別位于視圖的左、右、上以及下內(nèi)邊界上。因此,這段代碼首先需要計(jì)算出當(dāng)前視圖的左、右、上以及下內(nèi)邊距的大小,以便得到邊框所要繪制的區(qū)域。

3.繪制視圖View的內(nèi)容onDraw

第三步是調(diào)用onDraw()方法繪制內(nèi)容。發(fā)現(xiàn)是一個(gè)空的方法,也就是說(shuō)所有View繼承View的控件都要重寫這個(gè)方法來(lái)實(shí)現(xiàn)對(duì)自己內(nèi)容的繪制。也很好理解,TextView繪制文本,ImageView繪制圖片,控件他是什么屬性就繪制什么樣的內(nèi)容。所以我們?cè)谧远xView的時(shí)候要重寫onDraw()方法來(lái)完成自己的繪制。因?yàn)槟阆朐趺磳?shí)現(xiàn)什么樣的效果View也不知道就只好給你一個(gè)空方法你自己去實(shí)現(xiàn)。

4.繪制當(dāng)前視圖的子視圖的內(nèi)容dispatchDraw()</font>
從這個(gè)方法的注釋我們就知道,這是繪制子View的。我們知道之后ViewGroup才有可以有子視圖,那么我進(jìn)入ViewGroup#dispatchDraw()方法看下:

    @Override
    protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;
       //判斷當(dāng)前ViewGroup容器是否設(shè)置的布局動(dòng)畫
        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean buildCache = !isHardwareAccelerated();
            //遍歷給每個(gè)子視圖View設(shè)置動(dòng)畫效果
            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);
                }
            }
            //獲得布局動(dòng)畫的控制器
            final LayoutAnimationController controller = mLayoutAnimationController;
            if (controller.willOverlap()) {
                mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
            }
            //開(kāi)始布局動(dòng)畫
            controller.start();

            mGroupFlags &= ~FLAG_RUN_ANIMATION;
            mGroupFlags &= ~FLAG_ANIMATION_DONE;
            //設(shè)置布局動(dòng)畫的監(jiān)聽(tīng)事回調(diào)
            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();
            //對(duì)當(dāng)前視圖的畫布canvas進(jìn)行邊距裁剪,把不需要繪制內(nèi)容的邊距裁剪掉。
            canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                    mScrollX + mRight - mLeft - mPaddingRight,
                    mScrollY + mBottom - mTop - mPaddingBottom);
        }

        ......
        
        //遍歷繪制當(dāng)前視圖的子視圖View
        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) {
                    //將畫布傳入看是繪制子View    
                    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
        //當(dāng)子視圖設(shè)置了消失動(dòng)畫時(shí),遍歷繪制布局容器中需要消失的子視圖
        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);
        }
    }

關(guān)于ViewGroup的dispatchDraw()方法里面的重要方法都留了下來(lái)并都有注釋。大家可以在自己看看,結(jié)合自己的理解。通過(guò)遍歷每個(gè)子View,并調(diào)用drawChild(canvas, child, drawingTime)方法來(lái)完成對(duì)子View的繪制。代碼如下:

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

這里又會(huì)調(diào)用到了View#draw()方法。這樣循環(huán)調(diào)用完成View的繪制。

5.<font color=#006400 size=3>繪制滑動(dòng)時(shí)邊框的漸變效果</font>
這部分我們就是我們上面提到的ListView滑動(dòng)到最底端時(shí),底部會(huì)有一個(gè)淡藍(lán)色的半圓形的邊框漸變背景效果。這部分通過(guò)4個(gè)判斷并在判斷內(nèi)部完成4個(gè)變寬的漸變效果。基本都不會(huì)去修改,所以也不用太深入了解。

6.<font color=#006400 size=3>繪制滾動(dòng)條(onDrawScrollBars)</font>

繪制滾動(dòng)條的邏輯在onDrawScrollBars方法中,所以我們直接在onDrawForeground()方法中進(jìn)入此方法,如下:

 protected final void onDrawScrollBars(Canvas canvas) {
        // scrollbars are drawn only when the animation is running
        //滾動(dòng)條僅在動(dòng)畫運(yùn)行時(shí)繪制
        final ScrollabilityCache cache = mScrollCache;
         //滾動(dòng)條是否有緩存
        if (cache != null) {
            //獲取滾動(dòng)條的設(shè)置狀態(tài)
            int state = cache.state;
            //滾動(dòng)條不顯示時(shí),直接返回,也就是不繪制滾動(dòng)條
            if (state == ScrollabilityCache.OFF) {                 //-----------(1)
                return;
            }

            boolean invalidate = false;
             //滾動(dòng)條是否可見(jiàn)
            if (state == ScrollabilityCache.FADING) {                 //-----------(2)
                // We're fading -- get our fade interpolation
                if (cache.interpolatorValues == null) {
                    cache.interpolatorValues = new float[1];
                }

                float[] values = cache.interpolatorValues;

                // Stops the animation if we're done
                if (cache.scrollBarInterpolator.timeToValues(values) ==
                        Interpolator.Result.FREEZE_END) {
                    cache.state = ScrollabilityCache.OFF;
                } else {
                    cache.scrollBar.mutate().setAlpha(Math.round(values[0]));
                }

                // This will make the scroll bars inval themselves after
                // drawing. We only want this when we're fading so that
                // we prevent excessive redraws
                invalidate = true;
            } else {
                // We're just on -- but we may have been fading before so
                // reset alpha
                 //設(shè)置滾動(dòng)條完全可見(jiàn)
                cache.scrollBar.mutate().setAlpha(255);
            }

            final boolean drawHorizontalScrollBar = isHorizontalScrollBarEnabled();
            final boolean drawVerticalScrollBar = isVerticalScrollBarEnabled()
                    && !isVerticalScrollBarHidden();            //-----------(3)

            // Fork out the scroll bar drawing for round wearable devices.
            if (mRoundScrollbarRenderer != null) {
                if (drawVerticalScrollBar) {
                    final Rect bounds = cache.mScrollBarBounds;
                    getVerticalScrollBarBounds(bounds);
                    mRoundScrollbarRenderer.drawRoundScrollbars(
                            canvas, (float) cache.scrollBar.getAlpha() / 255f, bounds);
                    if (invalidate) {
                        invalidate();
                    }
                }
                // Do not draw horizontal scroll bars for round wearable devices.
            } else if (drawVerticalScrollBar || drawHorizontalScrollBar) {
                final ScrollBarDrawable scrollBar = cache.scrollBar;
                //繪制水平滾動(dòng)條
                if (drawHorizontalScrollBar) {
                    scrollBar.setParameters(computeHorizontalScrollRange(),
                            computeHorizontalScrollOffset(),
                            computeHorizontalScrollExtent(), false);
                    final Rect bounds = cache.mScrollBarBounds;
                    getHorizontalScrollBarBounds(bounds);
                    onDrawHorizontalScrollBar(canvas, scrollBar, bounds.left, bounds.top,
                            bounds.right, bounds.bottom);
                    if (invalidate) {
                        invalidate(bounds);
                    }
                }
                //繪制垂直滾動(dòng)條
                if (drawVerticalScrollBar) {
                    scrollBar.setParameters(computeVerticalScrollRange(),
                            computeVerticalScrollOffset(),
                            computeVerticalScrollExtent(), true);
                    final Rect bounds = cache.mScrollBarBounds;
                    getVerticalScrollBarBounds(bounds);
                    onDrawVerticalScrollBar(canvas, scrollBar, bounds.left, bounds.top,
                            bounds.right, bounds.bottom);
                    if (invalidate) {
                        invalidate(bounds);
                    }
                }
            }
        }
    }

代碼分析:

  • (1)處:判斷是否需要繪制當(dāng)前視圖View的滾動(dòng)條。如果你給當(dāng)前視圖View設(shè)置了android:scrollbars=”none”屬性,時(shí)就不會(huì)繪制滾動(dòng)條,也就是不顯示滾動(dòng)條。
  • (2)處:判斷當(dāng)前視圖View的滾動(dòng)條是否可消失。如果你給當(dāng)前視圖View設(shè)置了android:fadeScrollbars=”true”屬性時(shí),你不滑動(dòng),滾動(dòng)條隱藏,你滑動(dòng)時(shí),滾動(dòng)條顯示,有代碼可以看出,此處是通過(guò)改變滾動(dòng)條的透明度來(lái)實(shí)現(xiàn)滾動(dòng)條隱藏和顯示的。
  • (3)處:當(dāng)前視圖View的滾動(dòng)條設(shè)置成完全可見(jiàn),也就是你設(shè)置了該屬性android:fadeScrollbars=”false”。不管你是否滑動(dòng)View,滾動(dòng)條一直可見(jiàn)。
  • 身下部分就是繪制水平或者垂直滾動(dòng)條的邏輯。

總結(jié) :

到此View的draw繪制流程6步我們已經(jīng)完全理清了。其實(shí)不是所有的我們都需要掌握其實(shí)在我們自定義View的時(shí)候只需要注意1,3,4,6即可而在這4個(gè)步驟中往往我們只是需要重寫第三步或第四步而已。下面按照管理我們上流程圖講我們的流程串聯(lián)起來(lái),加深理解。如下:

View的繪制流程.png

View繪制6步分析.png

我們?cè)趤?lái)總結(jié)幾個(gè)關(guān)于View繪制相關(guān)的知識(shí)點(diǎn):

  • 父類View繪制主要是繪制背景,邊框漸變效果,進(jìn)度條,View具體的內(nèi)容繪制調(diào)用了onDraw方法,通過(guò)該方法把View內(nèi)容的繪制邏輯留給子類去實(shí)現(xiàn)。因此,我們?cè)谧远xView的時(shí)候都一般都需要重寫父類的onDraw方法來(lái)實(shí)現(xiàn)View內(nèi)容繪制。

  • onDraw,dispatchDraw區(qū)別

    • View還是ViewGroup對(duì)它們倆的調(diào)用順序都是onDraw()->dispatchDraw()
    • 在ViewGroup中,當(dāng)它有背景的時(shí)候就會(huì)調(diào)用onDraw()方法,否則就會(huì)跳過(guò)onDraw()直接調(diào)用dispatchDraw();所以如果要在ViewGroup中繪圖時(shí),往往是重寫dispatchDraw()方法
    • 在View中,onDraw()和dispatchDraw()都會(huì)被調(diào)用的,所以我們無(wú)論把繪圖代碼放在onDraw()或者dispatchDraw()中都是可以得到效果的,但是由于dispatchDraw()的含義是繪制子控件,所以原則來(lái)上講,在繪制View控件時(shí),我們是重新onDraw()函數(shù)
    • 最后總結(jié):在繪制View控件時(shí),需要重寫onDraw()函數(shù),在繪制ViewGroup時(shí),需要重寫dispatchDraw()函數(shù)。
  • .不管任何情況,每一個(gè)View視圖都會(huì)繪制 scrollBars滾動(dòng)條,且繪制滾動(dòng)條的邏輯是在父類View中實(shí)現(xiàn),子類無(wú)需自己實(shí)現(xiàn)滾動(dòng)條的繪制。其實(shí)TextView也是有滾動(dòng)條的,可以通過(guò)代碼讓其顯示滾動(dòng)條和內(nèi)容滾動(dòng)效果。你只需在TextView布局設(shè)置android:scrollbars=”vertical”屬性,同時(shí)在代碼中進(jìn)行如下設(shè)置:

textView.setMovementMethod(ScrollingMovementMethod.getInstance()); 
  • ViewGroup繪制的過(guò)程會(huì)對(duì)每個(gè)子視圖View設(shè)置布局容器動(dòng)畫效果,如果你在ViewGroup容器布局里面設(shè)置了如下屬性的話
android:animateLayoutChanges="true"

結(jié)語(yǔ)

至此View的三大流程就結(jié)束了。當(dāng)然里面的只是不管我寫的這些,但是我覺(jué)的這也應(yīng)該是比較全了。不過(guò)自定義View是個(gè)熟能生巧的一個(gè)技術(shù),光理解原理是不夠的,但是不理解原理寫起來(lái)出現(xiàn)問(wèn)題就不好處理。希望大家在對(duì)完后自己去多看下源碼,一遍不行就多看幾遍把他變成自己的東西。對(duì)于菜鳥(niǎo)的寫這些文章也不容易,只是希望能對(duì)入門android的小伙伴有些幫助。如果真的對(duì)您有幫助,就關(guān)注、評(píng)論下。我會(huì)更有動(dòng)力。如果有問(wèn)題留言,我知道的一定會(huì)回復(fù)你。最后希望大家都能成為大神。加油?。?!

感謝

從ViewRootImpl類分析View繪制的流程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容