View 的 layout 布局和 draw 繪制過程

上篇文章View 的測量分析了 View 的工作原理中最復雜的測量過程夺荒,接著測量過程的是布局和繪制的過程岩臣,這里兩個過程相對比較簡單,所以放到一篇文章中來寫

View 的測量過程中,確定了 View 的測量寬高的信息闷板,布局過程則是為了確定 View 在其父 View 中的位置以及 ViewGroup 確定其所有子 View 元素的位置;布局結束后會執(zhí)行繪制過程院塞,繪制過程將 View 需要顯示的內容繪制到屏幕上

一遮晚、layout (布局)過程

依舊從 ViewRootImpl 的 performLayout 方法開始,其中調用 DecorView 的 layout 方法,layout 方法是在 View 類中定義和實現(xiàn)的拦止,其中 getMeasuredWidth县遣、getMeasuredHeight 方法得到的是 DecorView 在測量過程中確定的測量寬高

// ViewRootImpl
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

// View
public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        ...
    }
    ...
}
// View
protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;
    ...
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
    ...
    }

layout 方法中調用 setFrame 方法,可以看到 setFrame 方法中保存了 View 四個頂點在父 View 中的位置汹族,四個頂點確定萧求,View在父容器中的位置也就確定,View 的實際寬高也就確定顶瞒,接著就會調用 onLayout 方法

View 的 onLayout 方法是個空實現(xiàn)夸政,說明 View 在布局過程中的任務就是確定自己在父 View 中的位置,確定了在父 View 中的位置后也就確定了子 View 的最終寬高榴徐,如果不重寫 View 的 layout 方法守问,其最終寬高與測量寬高相等匀归,如果重寫了 layout 方法,并且子 View 的位置不以測量寬高來確定酪碘,此時 View 的最終寬高將不等于測量寬高朋譬。測量寬高賦值于測量過程,最終寬高賦值于布局過程兴垦,兩者賦值時機不同

ViewGroup 中 onLayout 是個抽象方法徙赢,其子類必須重寫該方法以確定其所有子 View 的位置,下面重點分析 ViewGroup 的 onLayout 方法

1探越、ViewGroup 的 onLayout 方法

由于 ViewGroup 的 onLayout 方法是個抽象方法狡赐,所以我們選一個特定的 ViewGroup 實現(xiàn)類來分析,這里分析 LinearLayout 的 onLayout() 方法

// LinearLayout
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}

// LinearLayout
void layoutVertical(int left, int top, int right, int bottom) {
    final int paddingLeft = mPaddingLeft;

    int childTop;
    int childLeft;
        
    // Where right end of child should go
    final int width = right - left;
    int childRight = width - mPaddingRight;
    
    // Space available for child
    int childSpace = width - paddingLeft - mPaddingRight;
    
    final int count = getVirtualChildCount(); // 子 View 數(shù)量

    final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
    final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

    switch (majorGravity) {
       case Gravity.BOTTOM:
           // mTotalLength contains the padding already
           childTop = mPaddingTop + bottom - top - mTotalLength;
           break;

           // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
           break;

       case Gravity.TOP:
       default:
           childTop = mPaddingTop;
           break;
    }
    
    // 遍歷所有子 View 
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
        
            // 獲取子 View 由測量過程確定的的測量寬高
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            
            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();
            
            int gravity = lp.gravity;
            if (gravity < 0) {
                gravity = minorGravity;
            }
            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            
            // 確定子 View 的 left 位置
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                            + lp.leftMargin - lp.rightMargin;
                    break;

                case Gravity.RIGHT:
                    childLeft = childRight - childWidth - lp.rightMargin;
                    break;

                case Gravity.LEFT:
                default:
                    childLeft = paddingLeft + lp.leftMargin;
                    break;
            }

            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }

            childTop += lp.topMargin; // 確定子 View 的 top 位置
            
            // 
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            
            // 確定下一子 View 的 top 位置
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

// LinearLayout
private void setChildFrame(View child, int left, int top, int width, int height) {        
    child.layout(left, top, left + width, top + height);
}

LinearLayout 的 onLayout 方法中根據(jù)設置的 View 線性排列的方向確定如果實現(xiàn)布局钦幔,以豎直排列的 情況為例枕屉,會調用 layoutVertical 方法

layoutVertical 中會遍歷所有的子 View,由子 View 的測量寬高和 ViewGroup 自身業(yè)務邏輯確定子 View 的四個頂點在 ViewGroup 中的位置鲤氢,并通過 setChildFrame 方法來調用子 View 的 layout 方法搀擂。從而將布局過程從 ViewGroup 傳遞到 View 中,View 中的 layout 方法上面已經分析了卷玉,作用為確定自己四個頂點在父 View 中的位置哨颂。這樣一層一層傳遞下去就完成了 View 視圖樹的 layout 過程。

1. ViewGroup 的布局過程的作用為先確定自己在父容器的位置相种,再確定子 View 在該 ViewGroup 中的位置威恼,子 View 的 layout 結果不會影響 ViewGroup 的layout

2. view 的布局過程的作用為確定自己四個頂點在父 View 中的位置

2、View 的 getMeasuredWidth() 和 getWidth() 的區(qū)別

  1. 子元素的layout() 方法中會根據(jù)父容器中傳遞的頂點位置為 mLeft , mTop , mRight , mBottom 等屬性賦值寝并,View 的 getWidth() 方法得到的值為 mRight - mLeft

  2. View 的 getMeasuredWidth() 方法得到的值是 View 的 mMeasuredWidth 參數(shù)的值箫措,該參數(shù)的賦值是在 onMeasure() 方法中

  3. 這兩個方法得到的值不是同一個參數(shù)的值,兩個參數(shù)的賦值時間是不同的衬潦,如果View 重寫 layout 方法,修改四個頂點的位置斤蔓,這樣兩個方法得到的值就是不同的

  4. 所以不能說這兩值一定相等。getWidth() 方法得到的是 View 的最終寬高镀岛,getMeasuredWidth() 方法得到的是 View 的測量寬高

  • getHeight() 和 getMeasuredHeight() 方法同理弦牡。

二、View 的 draw() 繪制過程

測量和布局過程完成之后哎媚,ViewRootImpl 會接著調用 performDraw 方法喇伯,該方法最終會調用 DecorView 的 draw() 方法

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

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

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

        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);
    }
    
    // View
    protected void onDraw(Canvas canvas) {}  
    
    // ViewGroup
    protected void dispatchDraw(Canvas canvas) {
        
        for (int i = 0; i < childrenCount; i++) {
            ...
            drawChild(canvas, transientChild, drawingTime);
            ... 
        }
    }
    
    // ViewGroup
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

由 View 的 draw 方法可分析得出以下繪制流程

1. View 繪制的流程

  1. 繪制背景 background.draw(canvas)
  2. 保存 canvas 圖層
  3. 繪制自己 調用 onDraw(canvas) 方法
  4. 繪制 children (dispatchDraw)
  5. 繪制漸變效果和恢復 canvas 圖層
  6. 繪制裝飾 (onDrawScrollBars)

onDraw() 為空實現(xiàn)喊儡,需要子類根據(jù)需要顯示的內容重寫此方法

dispatchDraw() 方法也是空實現(xiàn)拨与,ViewGroup 中重寫了此方法,dispatchDraw 方法中遍歷所有子 View艾猜,并調用其 draw() 方法买喧,將繪制過程一層層傳遞捻悯,完成了 View 樹的繪制過程。

2. setWillNotDraw() 方法

/**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

setWillNotDraw() 方法的作用可以從其注釋中看出淤毛,如果一個 View 不需要繪制任何內容今缚,那么設置這個標記位 true 后,系統(tǒng)會進行相應的優(yōu)化低淡。

默認情況下 View 沒有開啟這個標記姓言,而 ViewGroup 則開啟了這個標記。

在開發(fā)過程中蔗蹋,如果我們的 View 繼承自 ViewGroup 且沒有進行繪制時就可以開啟這個標記以便于系統(tǒng)對其進行優(yōu)化何荚,如果該 ViewGroup 需要通過 onDraw 來繪制內容,則需要通過調用 setWillNotDraw() 方法來關閉此標記

好啦猪杭,到這里 View 的工作過程中測量餐塘、布局、繪制三大過程的分析就結束啦皂吮,接下來將是 View 的事件分發(fā)機制和自定義 View 的文章戒傻,敬請期待

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蜂筹,隨后出現(xiàn)的幾起案子需纳,更是在濱河造成了極大的恐慌,老刑警劉巖狂票,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件候齿,死亡現(xiàn)場離奇詭異,居然都是意外死亡闺属,警方通過查閱死者的電腦和手機慌盯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來掂器,“玉大人亚皂,你說我怎么就攤上這事」停” “怎么了灭必?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長乃摹。 經常有香客問我禁漓,道長,這世上最難降的妖魔是什么孵睬? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任播歼,我火速辦了婚禮,結果婚禮上掰读,老公的妹妹穿的比我還像新娘秘狞。我一直安慰自己叭莫,他們只是感情好,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布烁试。 她就那樣靜靜地躺著雇初,像睡著了一般。 火紅的嫁衣襯著肌膚如雪减响。 梳的紋絲不亂的頭發(fā)上靖诗,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機與錄音支示,去河邊找鬼呻畸。 笑死,一個胖子當著我的面吹牛悼院,可吹牛的內容都是我干的伤为。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼据途,長吁一口氣:“原來是場噩夢啊……” “哼绞愚!你這毒婦竟也來了?” 一聲冷哼從身側響起颖医,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤位衩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后熔萧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體糖驴,經...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年佛致,在試婚紗的時候發(fā)現(xiàn)自己被綠了贮缕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡俺榆,死狀恐怖感昼,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情罐脊,我是刑警寧澤定嗓,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站萍桌,受9級特大地震影響宵溅,放射性物質發(fā)生泄漏。R本人自食惡果不足惜上炎,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一恃逻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦辛块、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至胞谈,卻和暖如春尘盼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背烦绳。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工卿捎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人径密。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓午阵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親享扔。 傳聞我的和親對象是個殘疾皇子底桂,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內容