[Android] View 工作原理(二)

前言

本文大量參照《Android 開發(fā)藝術探索》及參考資料的內(nèi)容整合,主要幫助自己理清 View 的工作原理衅金。深入學習希望大家更多的關注參考資料耙考。

上一篇文章了解了 MeasureSpec 的概念及獲取耘斩,從名字上看就能了解到這是用來輔助測量過程的對象合陵,本次文章再來完整學習 View 的工作流程枢赔。

View 的工作流程主要指 measure、layout拥知、draw 這三個過程:

  • measure:確定 View 的測量寬/高
  • layout:確定 View 的最終寬/高和四個頂點位置
  • draw:將 View 繪制到屏幕上

measure

如果是一個原始的 View踏拜,那么通過 measure 方法就完成了其測量過程。

如果是一個 ViewGroup低剔,那么除了完成自己的測量過程之外速梗,還會遍歷去調(diào)用所有子元素的 measure()肮塞。

View 的 measure 過程

View 的 measure 過程由其 measure 方法完成,在 measure() 中會調(diào)用 onMeasure() 方法姻锁,這是實際測量的地方枕赵,代碼如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension 會設置 View 的測量寬高,所以只需看 getDefaultSize 方法即可:

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

可以看到邏輯十分簡單位隶,當 measureSpec 的模式為 AT_MOST 或 EXACTLY 時烁设,specSize 即為 View 測量后的大小。

而當為 UNSPECIFIED钓试,View 測量大小為 getSuggestedMinimumWidth() 的值,會根據(jù) View 的 android:minWidth 和 background 屬性獲取一個值副瀑,由于這種情況一般用于系統(tǒng)內(nèi)部弓熏,所以就不深究,源碼十分簡單糠睡,大家可以親自動手一試挽鞠。

另外這里可以留意到,當你的 View 的寬高設置為 wrap_content 時狈孔。該 View 的 specSize 為 parentSize信认,specMode 為 AT_MOST;結果根據(jù)上面代碼 View 的測量寬高就是父容器的寬高均抽,這樣的結果跟 match_parent 就沒有區(qū)別嫁赏!

所以實際上像 TextView 這些控件,都會重寫 onMeasure 方法油挥,根據(jù) View 的 LayoutParams 為 wrap_content 的情況設置一個寬高潦蝇,通常是小于父容器的。

ViewGroup 的 measure 過程

ViewGroup 是一個繼承自 View 的抽象類深寥,它并沒有重寫 View 的 onMeasure 方法攘乒,而是由其抽象實現(xiàn)類例如 LinearLayout 來重寫,因為不同的 ViewGroup 有不同的特性惋鹅。

ViewGroup 提供了一個 measureChildren 方法则酝,代碼如下:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

就是調(diào)用 measureChild 傳入每一個子元素:

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

這里 getChildMeasureSpec 方法就十分熟悉了,根據(jù)子元素的 LayoutParams 和 父容器的 measureSpec 創(chuàng)建子元素的 measureSpec闰集,然后調(diào)用子元素的 measure 方法沽讹,這樣就是上一節(jié)的內(nèi)容了。

如此遞歸完成了 View 的測量過程返十。

layout

layout 的作用是確定元素的位置妥泉,當 ViewGroup 調(diào)用 layout 方法確定自己的位置后,又會調(diào)用 onLayout 來確定所有子元素的位置洞坑。

View 的 layout 過程

實際上 ViewGroup.layout() 中會通過 super.layout 調(diào)用其父類盲链,即 View 的 layout 方法,代碼如下:

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);
        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) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }

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

一些判斷和代碼細節(jié)可以略過,這里主要有兩句本慕,一是調(diào)用 setFrame 方法確定了 View 的四個頂點位置锅尘,接著調(diào)用 onLayout 方法,該方法負責父容器確定子元素的位置。所以 View 和 ViewGroup 的 onLayout 都沒有進行具體的實現(xiàn),因為不同的布局有不同的特性骨杂,我們下面來看一下 LinearLayout 的實現(xiàn)。

LinearLayout 的 onLayout 過程

直接上代碼~

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

這個 mOrientation 很明顯了陕凹,我們只看垂直方向的 layoutVertical 方法拂盯,代碼比較長:

void layoutVertical(int left, int top, int right, int bottom) {
    ...

    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            
            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();
            ...

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

            childTop += lp.topMargin;
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

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

去除了一些細節(jié)內(nèi)容空凸,LinearLayout 的 layoutVertical 會調(diào)用 setChildFrame 來為子元素指定對應的位置紊选;同時變量 childTop 會不斷增加,使得后面的元素位置逐漸靠下巩那,跟我們平時使用 LinearLayout 的效果特性是符合的裆赵。

setChildFrame 方法的代碼如下:

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

調(diào)用了子元素的 layout 方法,這樣又是逐步遞歸調(diào)用陈醒,完成 View 的布局流程钉跷。

draw

Draw 的作用是把 View 繪制到屏幕上來朦促。繪制時的流程圖如下:

具體再來看看 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;
    }
    ...
}

其中逐步執(zhí)行了流程圖中的內(nèi)容箩退,并且在不需要時會跳過 layer 的繪制以提高效率离熏。

在 View 中,onDraw() 和 dispatchDraw() 都為空實現(xiàn)戴涝,因為不同的控件會有不同的繪制方式滋戳。于是我們也意識到钻蔑,自定義繪制過程需要復寫 onDraw 方法來繪制自身的內(nèi)容。

而 ViewGroup 對 dispatchDraw() 則進行了實現(xiàn)胧瓜,在其中通過 drawChild() 調(diào)用了子元素的 draw 方法矢棚,又是遞歸完成了繪制過程。

這里涉及到動畫等內(nèi)容的繪制府喳,比較復雜蒲肋,與理解 View 的工作原理的關系不強,所以可能在以后學習一些簡單的自定義控件時再來學習控件是通過何種方式進行繪制的钝满。

以上便是 View 的工作流程兜粘,再次聲明,大量參考學習了他人的文章內(nèi)容弯蚜。

希望能對大家有所幫助孔轴,感謝這些優(yōu)秀文章的作者們。

參考資料

公共技術點之 View 繪制流程

Android應用層View繪制流程與源碼分析

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末碎捺,一起剝皮案震驚了整個濱河市路鹰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌收厨,老刑警劉巖晋柱,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異诵叁,居然都是意外死亡雁竞,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門拧额,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碑诉,“玉大人,你說我怎么就攤上這事侥锦〗裕” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵恭垦,是天一觀的道長泪幌。 經(jīng)常有香客問我,道長署照,這世上最難降的妖魔是什么祸泪? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮建芙,結果婚禮上没隘,老公的妹妹穿的比我還像新娘。我一直安慰自己禁荸,他們只是感情好右蒲,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布阀湿。 她就那樣靜靜地躺著,像睡著了一般瑰妄。 火紅的嫁衣襯著肌膚如雪陷嘴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天间坐,我揣著相機與錄音灾挨,去河邊找鬼。 笑死竹宋,一個胖子當著我的面吹牛劳澄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蜈七,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼秒拔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了飒硅?” 一聲冷哼從身側響起砂缩,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎三娩,沒想到半個月后庵芭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡尽棕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了彬伦。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片滔悉。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖单绑,靈堂內(nèi)的尸體忽然破棺而出回官,到底是詐尸還是另有隱情,我是刑警寧澤搂橙,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布歉提,位于F島的核電站,受9級特大地震影響区转,放射性物質(zhì)發(fā)生泄漏苔巨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一废离、第九天 我趴在偏房一處隱蔽的房頂上張望侄泽。 院中可真熱鬧,春花似錦蜻韭、人聲如沸悼尾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽闺魏。三九已至未状,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間析桥,已是汗流浹背司草。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留烹骨,地道東北人翻伺。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像沮焕,于是被迫代替她去往敵國和親吨岭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

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