繪制流程

參考:
Android應用層View繪制流程與源碼分析 - 工匠若水
https://blog.csdn.net/yanbober/article/details/46128379

前言

View在Activity的onCreate()方法中通過setContentView()方法添加到Activity的DecorView上。此時ViewRootImpl和DecorView沒有關聯上,不會繪制View。在 Activity的onResume()方法執行后,通過最終執行ViewRootImpl#setView,將DecorView會被添加帶ViewRootImpl中。然后執行requestlayout(),requestLayout()和invalidate()都會觸發ViewRootImpl繪制View => performTraversals()。


Dialog和PopupWindow中View的繪制過程也是一樣的,只是觸發的方式不同。例如Dialog中,是調用dialog.show()時,觸發了WindowManagerImpl的addView()(上圖步驟2),后面的流程就一樣了。

可以通過Activity類的requestWindowFeature()方法來定制Activity關聯PhoneWindow的外觀,這個方法實際上做的是把我們所請求的窗口外觀特性存儲到了PhoneWindow的mFeatures成員中,在窗口繪制生成外觀模板時,根據mFeatures的值繪制特定外觀。

PhoneWindow中有DecorView對象;
DecorView中有PhoneWindow對象;
ViewRootImpl中有DecorView對象;

ViewRootImpl#performTraversals()

View的繪制從ViewRootImp#performTraversals()開始,該方法在另一個線程中被執行。該函數做的執行過程主要是根據之前設置的狀態,判斷是否重新計算視圖大小(measure)、是否重新放置視圖的位置(layout)、以及是否重繪 (draw)。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal(); => performTraversals();
    }
}
private void performTraversals() { 
...... 
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); 
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); 
...... 
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); 
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
...... 
mView.draw(canvas); 
......
}
//mView對于Activty來說就是PhoneWindow.DecorView.

其中,getRootMeasureSpec的兩個參數(mWidth, lp.width)mWith和mHeight 是屏幕的寬度和高度, lp是WindowManager.LayoutParams,它的lp.width和lp.height的默認值是MATCH_PARENT,所以通過getRootMeasureSpec 生成的測量規格MeasureSpec 的mode是MATCH_PARENT ,size是屏幕的高寬,即DecorView的MeasureSpec。

MeasureSpec(View的內部類)測量規格為int型,值由高2位規格模式specMode和低30位具體尺寸specSize組成。其中specMode只有三種值:

MeasureSpec.EXACTLY     //確定模式,父View希望子View的大小是確定的,由specSize決定;
MeasureSpec.AT_MOST     //最多模式,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依據子View的設計值來決定; 

對于DecorView而言,它的MeasureSpec是由窗口尺寸和其自身的LayoutParams共同決定;對于普通的View,它的MeasureSpec由父View的MeasureSpec和其自身的LayoutParams共同決定。

1. measure

父View的measure的過程會先測量子View,等子View測量結果出來后,再來測量自己。

View#onMeasure

onMeasure默認的實現僅僅調用了setMeasuredDimension,setMeasuredDimension函數是一個很關鍵的函數,它對View的成員變量mMeasuredWidth和mMeasuredHeight變量賦值,measure的主要目的就是對View樹中的每個View的mMeasuredWidth和mMeasuredHeight進行賦值,所以一旦這兩個變量被賦值意味著該View的測量工作結束。

setMeasuredDimension=>getDefaultSize
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)

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; //注意UNSPECIFIED返回的值是getSuggestedMinimumWidth
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize; //系統默認的規格
        break;
    }
    return result;
}

protected int getSuggestedMinimumWidth() {
    // 跟背景有關
    return (mBackground == null) ? mMinWidth :
                   max(mMinWidth, mBackground.getMinimumWidth());
    }

ViewGroup#measureChildren
ViewGroup#measureChild
ViewGroup#measureChildWithMargins

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
//靜態方法
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

注意:int size = Math.max(0, specSize - padding);

MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通過計算得出一個針對子View的測量結果。其中,子View的LayoutParams其實就是我們在xml寫的時候設置的layout_width和layout_height 轉化而來的。

實際的測量是在onMeasure方法進行,所以在View的子類需要重寫onMeasure方法,這是因為measure方法是final的,不允許重載,所以View子類只能通過重載onMeasure來實現自己的測量邏輯。

measure流程圖
image.png
measure注意事項
  1. 直接繼承View的控件需要重寫onMeasure方法并設置wrap_content時的自身大小,否則在布局中使用wrap_content就相當于使用match_parent。
  2. ViewGroup的子類就必須要求LayoutParams繼承子MarginLayoutParams,否則無法使用layout_margin參數。
  3. 使用View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onMeasure流程之后被調用才能返回有效值。而getWidth()與getHeight()方法必須在layout(int l, int t, int r, int b)執行之后才有效。

2. layout

重載onLayout的目的就是安排其children在父View的具體位置,重載onLayout通常做法就是寫一個for循環調用每一個子視圖的layout(l, t, r, b)函數,傳入不同的參數l, t, r, b來確定每個子視圖在父視圖中的顯示位置。View#layout=>setFrame(int left, int top, int right, int bottom)最終確定view的位置。一般情況下layout過程會參考measure過程中計算得到的mMeasuredWidth和mMeasuredHeight來安排子View在父View中顯示的位置。

整個layout過程比較容易理解,layout也是從頂層父View向子View的遞歸調用view.layout方法的過程,即父View根據上一步measure子View所得到的布局大小和布局參數,將子View放在合適的位置上。

layout注意事項
  1. View.layout方法可被重載,ViewGroup.layout為final的不可重載,ViewGroup.onLayout為abstract的,子類必須重載實現自己的位置邏輯。

  2. 凡是layout_XXX的布局屬性基本都針對的是包含子View的ViewGroup的,當對一個沒有父容器的View設置相關layout_XXX屬性是沒有任何意義。

  3. 使用View的getWidth()和getHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onLayout流程之后被調用才能返回有效值。

3. draw

draw的流程
  1. 背景繪制
  2. 對View的內容進行繪制
  3. 對當前View的所有子View進行繪制,ViewGroup #dispatchDraw(canvas)
  4. 對View的滾動條進行繪制

ViewGroup類已經為我們實現繪制子View的默認過程,這個實現基本能滿足大部分需求,所以ViewGroup類的子類(LinearLayout,FrameLayout)也基本沒有去重寫dispatchDraw方法,我們在實現自定義控件,除非比較特別,不然一般也不需要去重寫它, drawChild()的核心過程就是為子視圖分配合適的cavas剪切區,剪切區的大小正是由layout過程決定的,而剪切區的位置取決于滾動值以及子視圖當前的動畫。設置完剪切區后就會調用子視圖的draw()函數進行具體的繪制了。

ViewRootImpl中的代碼會創建一個Canvas對象。

final Rect dirty = mDirty;
......
canvas = mSurface.lockCanvas(dirty);
......
mView.draw(canvas);
......
draw注意事項
  1. 如果該View是一個ViewGroup,則需要遞歸繪制其所包含的所有子View。View默認不會繪制任何內容,真正的繪制都需要自己在子類中實現。View的繪制是借助onDraw方法傳入的Canvas類來進行的。

  2. 區分View動畫和ViewGroup布局動畫,前者指的是View自身的動畫,可以通過setAnimation添加,后者是專門針對ViewGroup顯示內部子視圖時設置的動畫,可以在xml布局文件中對ViewGroup設置layoutAnimation屬性(譬如對LinearLayout設置子View在顯示時出現逐行、隨機、下等顯示等不同動畫效果)。

  3. 默認情況下子View的ViewGroup.drawChild繪制順序和子View被添加的順序一致,但是你也可以重載ViewGroup.getChildDrawingOrder()方法提供不同順序。

4. invalidate

public void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) {
......
  // Propagate the damage rectangle to the parent view.
  final AttachInfo ai = mAttachInfo;
  final ViewParent p = mParent;
  if (p != null && ai != null && l < r && t < b) {
    final Rect damage = ai.mTmpInvalRect;
    //設置刷新區域
    damage.set(l, t, r, b);
    //傳遞調運Parent ViewGroup的invalidateChild方法
    p.invalidateChild(this, damage);
  }
  ......
}
public final void invalidateChild(View child, final Rect dirty) {
    ViewParent parent = this;
    final AttachInfo attachInfo = mAttachInfo;
    ......
    do {
        ......
        //循環層層上級調運,直到ViewRootImpl會返回null
        parent = parent.invalidateChildInParent(location, dirty);
        ......
    } while (parent != null);
}

View的invalidate(invalidateInternal)方法實質是將要刷新區域直接傳遞給了父ViewGroup的invalidateChild方法,在invalidate中,調用父View的invalidateChild,這是一個從當前View向上級父View回溯的過程,每一層的父View都將自己的顯示區域與傳入的刷新Rect做交集。最后傳遞到ViewRootImpl的invalidateChildInParent,方法結束。并且在ViewRootImpl#invalidateChildInParent中調用scheduleTraversals(),scheduleTraversals會通過Handler的Runnable發送一個異步消息,調用doTraversal方法,然后最終調用performTraversals()執行重繪。

注意:invalidate方法向上遍歷是在主線程。

invalidate注意事項
  1. 直接調用invalidate方法。請求重新draw,但只會繪制調用者本身。
  2. 觸發setSelection方法。請求重新draw,但只會繪制調用者本身。
  3. 觸發setVisibility方法。 當View可視狀態在INVISIBLE轉換VISIBLE時會間接調用invalidate方法,繼而繪制該View。當View的可視狀態在INVISIBLE\VISIBLE 轉換為GONE狀態時會間接調用requestLayout和invalidate方法,同時由于View樹大小發生了變化,所以會請求measure、layout過程以及draw過程,同樣只繪制需要【重新繪制】的視圖。
  4. 觸發setEnabled方法。請求重新draw,但不會重新繪制任何View包括該調用者本身。
  5. 觸發requestFocus方法。請求View樹的draw過程,只繪制【需要重繪】的View。

ViewRootImpl類的performTraversals()何時被調用?
Activity#setContentView=>mContentParent.addView(view, params);
ViewGroup#addView=>requestLayout()&invalidate(true);

5. requestLayout

public void requestLayout() {
  ......
  if (mParent != null && !mParent.isLayoutRequested()) {
    //由此向ViewParent請求布局
    //從這個View開始向上一直requestLayout,最終到達ViewRootImpl的requestLayout
    mParent.requestLayout();
 }
}

View的requestLayout時其實質就是層層向上傳遞,直到ViewRootImpl為止,然后觸發ViewRootImpl的requestLayout方法,其中調用 scheduleTraversals();

requestLayout()方法會調用measure過程和layout過程,不會調用draw過程,也不會重新繪制任何View包括該調用者本身。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容