繪制流程

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

前言

View在Activity的onCreate()方法中通過setContentView()方法添加到Activity的DecorView上。此時(shí)ViewRootImpl和DecorView沒有關(guān)聯(lián)上奉瘤,不會(huì)繪制View勾拉。在 Activity的onResume()方法執(zhí)行后,通過最終執(zhí)行ViewRootImpl#setView盗温,將DecorView會(huì)被添加帶ViewRootImpl中藕赞。然后執(zhí)行requestlayout(),requestLayout()和invalidate()都會(huì)觸發(fā)ViewRootImpl繪制View => performTraversals()卖局。


Dialog和PopupWindow中View的繪制過程也是一樣的斧蜕,只是觸發(fā)的方式不同。例如Dialog中砚偶,是調(diào)用dialog.show()時(shí)批销,觸發(fā)了WindowManagerImpl的addView()(上圖步驟2),后面的流程就一樣了染坯。

可以通過Activity類的requestWindowFeature()方法來定制Activity關(guān)聯(lián)PhoneWindow的外觀均芽,這個(gè)方法實(shí)際上做的是把我們所請求的窗口外觀特性存儲(chǔ)到了PhoneWindow的mFeatures成員中,在窗口繪制生成外觀模板時(shí)单鹿,根據(jù)mFeatures的值繪制特定外觀骡技。

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

ViewRootImpl#performTraversals()

View的繪制從ViewRootImp#performTraversals()開始,該方法在另一個(gè)線程中被執(zhí)行昼窗。該函數(shù)做的執(zhí)行過程主要是根據(jù)之前設(shè)置的狀態(tài)是趴,判斷是否重新計(jì)算視圖大小(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的兩個(gè)參數(shù)(mWidth, lp.width)mWith和mHeight 是屏幕的寬度和高度掸驱, lp是WindowManager.LayoutParams肛搬,它的lp.width和lp.height的默認(rèn)值是MATCH_PARENT,所以通過getRootMeasureSpec 生成的測量規(guī)格MeasureSpec 的mode是MATCH_PARENT 毕贼,size是屏幕的高寬温赔,即DecorView的MeasureSpec。

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

MeasureSpec.EXACTLY     //確定模式,父View希望子View的大小是確定的待秃,由specSize決定拜秧;
MeasureSpec.AT_MOST     //最多模式,父View希望子View的大小最多是specSize指定的值章郁;
MeasureSpec.UNSPECIFIED //未指定模式枉氮,父View完全依據(jù)子View的設(shè)計(jì)值來決定; 

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

1. measure

父View的measure的過程會(huì)先測量子View佃牛,等子View測量結(jié)果出來后,再來測量自己医舆。

View#onMeasure

onMeasure默認(rèn)的實(shí)現(xiàn)僅僅調(diào)用了setMeasuredDimension俘侠,setMeasuredDimension函數(shù)是一個(gè)很關(guān)鍵的函數(shù),它對View的成員變量mMeasuredWidth和mMeasuredHeight變量賦值蔬将,measure的主要目的就是對View樹中的每個(gè)View的mMeasuredWidth和mMeasuredHeight進(jìn)行賦值爷速,所以一旦這兩個(gè)變量被賦值意味著該View的測量工作結(jié)束。

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; //系統(tǒng)默認(rèn)的規(guī)格
        break;
    }
    return result;
}

protected int getSuggestedMinimumWidth() {
    // 跟背景有關(guān)
    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);
}
//靜態(tài)方法
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通過計(jì)算得出一個(gè)針對子View的測量結(jié)果霞怀。其中惫东,子View的LayoutParams其實(shí)就是我們在xml寫的時(shí)候設(shè)置的layout_width和layout_height 轉(zhuǎn)化而來的。

實(shí)際的測量是在onMeasure方法進(jìn)行,所以在View的子類需要重寫onMeasure方法廉沮,這是因?yàn)閙easure方法是final的颓遏,不允許重載,所以View子類只能通過重載onMeasure來實(shí)現(xiàn)自己的測量邏輯滞时。

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

2. layout

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

整個(gè)layout過程比較容易理解窟她,layout也是從頂層父View向子View的遞歸調(diào)用view.layout方法的過程,即父View根據(jù)上一步measure子View所得到的布局大小和布局參數(shù)蔼水,將子View放在合適的位置上震糖。

layout注意事項(xiàng)
  1. View.layout方法可被重載,ViewGroup.layout為final的不可重載趴腋,ViewGroup.onLayout為abstract的吊说,子類必須重載實(shí)現(xiàn)自己的位置邏輯。

  2. 凡是layout_XXX的布局屬性基本都針對的是包含子View的ViewGroup的优炬,當(dāng)對一個(gè)沒有父容器的View設(shè)置相關(guān)layout_XXX屬性是沒有任何意義颁井。

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

3. draw

draw的流程
  1. 背景繪制
  2. 對View的內(nèi)容進(jìn)行繪制
  3. 對當(dāng)前View的所有子View進(jìn)行繪制雅宾,ViewGroup #dispatchDraw(canvas)
  4. 對View的滾動(dòng)條進(jìn)行繪制

ViewGroup類已經(jīng)為我們實(shí)現(xiàn)繪制子View的默認(rèn)過程,這個(gè)實(shí)現(xiàn)基本能滿足大部分需求葵硕,所以ViewGroup類的子類(LinearLayout,FrameLayout)也基本沒有去重寫dispatchDraw方法眉抬,我們在實(shí)現(xiàn)自定義控件,除非比較特別懈凹,不然一般也不需要去重寫它蜀变, drawChild()的核心過程就是為子視圖分配合適的cavas剪切區(qū),剪切區(qū)的大小正是由layout過程決定的介评,而剪切區(qū)的位置取決于滾動(dòng)值以及子視圖當(dāng)前的動(dòng)畫库北。設(shè)置完剪切區(qū)后就會(huì)調(diào)用子視圖的draw()函數(shù)進(jìn)行具體的繪制了爬舰。

ViewRootImpl中的代碼會(huì)創(chuàng)建一個(gè)Canvas對象。

final Rect dirty = mDirty;
......
canvas = mSurface.lockCanvas(dirty);
......
mView.draw(canvas);
......
draw注意事項(xiàng)
  1. 如果該View是一個(gè)ViewGroup寒瓦,則需要遞歸繪制其所包含的所有子View情屹。View默認(rèn)不會(huì)繪制任何內(nèi)容,真正的繪制都需要自己在子類中實(shí)現(xiàn)杂腰。View的繪制是借助onDraw方法傳入的Canvas類來進(jìn)行的屁商。

  2. 區(qū)分View動(dòng)畫和ViewGroup布局動(dòng)畫,前者指的是View自身的動(dòng)畫颈墅,可以通過setAnimation添加,后者是專門針對ViewGroup顯示內(nèi)部子視圖時(shí)設(shè)置的動(dòng)畫雾袱,可以在xml布局文件中對ViewGroup設(shè)置layoutAnimation屬性(譬如對LinearLayout設(shè)置子View在顯示時(shí)出現(xiàn)逐行恤筛、隨機(jī)、下等顯示等不同動(dòng)畫效果)芹橡。

  3. 默認(rèn)情況下子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;
    //設(shè)置刷新區(qū)域
    damage.set(l, t, r, b);
    //傳遞調(diào)運(yùn)Parent ViewGroup的invalidateChild方法
    p.invalidateChild(this, damage);
  }
  ......
}
public final void invalidateChild(View child, final Rect dirty) {
    ViewParent parent = this;
    final AttachInfo attachInfo = mAttachInfo;
    ......
    do {
        ......
        //循環(huán)層層上級調(diào)運(yùn)林说,直到ViewRootImpl會(huì)返回null
        parent = parent.invalidateChildInParent(location, dirty);
        ......
    } while (parent != null);
}

View的invalidate(invalidateInternal)方法實(shí)質(zhì)是將要刷新區(qū)域直接傳遞給了父ViewGroup的invalidateChild方法煎殷,在invalidate中,調(diào)用父View的invalidateChild腿箩,這是一個(gè)從當(dāng)前View向上級父View回溯的過程豪直,每一層的父View都將自己的顯示區(qū)域與傳入的刷新Rect做交集。最后傳遞到ViewRootImpl的invalidateChildInParent珠移,方法結(jié)束弓乙。并且在ViewRootImpl#invalidateChildInParent中調(diào)用scheduleTraversals(),scheduleTraversals會(huì)通過Handler的Runnable發(fā)送一個(gè)異步消息钧惧,調(diào)用doTraversal方法击狮,然后最終調(diào)用performTraversals()執(zhí)行重繪绽快。

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

invalidate注意事項(xiàng)
  1. 直接調(diào)用invalidate方法。請求重新draw披坏,但只會(huì)繪制調(diào)用者本身。
  2. 觸發(fā)setSelection方法缓呛。請求重新draw蛉迹,但只會(huì)繪制調(diào)用者本身。
  3. 觸發(fā)setVisibility方法钮孵。 當(dāng)View可視狀態(tài)在INVISIBLE轉(zhuǎn)換VISIBLE時(shí)會(huì)間接調(diào)用invalidate方法骂倘,繼而繪制該View。當(dāng)View的可視狀態(tài)在INVISIBLE\VISIBLE 轉(zhuǎn)換為GONE狀態(tài)時(shí)會(huì)間接調(diào)用requestLayout和invalidate方法巴席,同時(shí)由于View樹大小發(fā)生了變化历涝,所以會(huì)請求measure、layout過程以及draw過程,同樣只繪制需要【重新繪制】的視圖荧库。
  4. 觸發(fā)setEnabled方法堰塌。請求重新draw,但不會(huì)重新繪制任何View包括該調(diào)用者本身分衫。
  5. 觸發(fā)requestFocus方法场刑。請求View樹的draw過程,只繪制【需要重繪】的View蚪战。

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

5. requestLayout

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

View的requestLayout時(shí)其實(shí)質(zhì)就是層層向上傳遞邀桑,直到ViewRootImpl為止瞎疼,然后觸發(fā)ViewRootImpl的requestLayout方法,其中調(diào)用 scheduleTraversals();

requestLayout()方法會(huì)調(diào)用measure過程和layout過程壁畸,不會(huì)調(diào)用draw過程贼急,也不會(huì)重新繪制任何View包括該調(diào)用者本身。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末捏萍,一起剝皮案震驚了整個(gè)濱河市太抓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌令杈,老刑警劉巖走敌,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異这揣,居然都是意外死亡悔常,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門给赞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來机打,“玉大人,你說我怎么就攤上這事片迅〔醒” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵柑蛇,是天一觀的道長芥挣。 經(jīng)常有香客問我,道長耻台,這世上最難降的妖魔是什么空免? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮盆耽,結(jié)果婚禮上蹋砚,老公的妹妹穿的比我還像新娘扼菠。我一直安慰自己,他們只是感情好坝咐,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布循榆。 她就那樣靜靜地躺著,像睡著了一般墨坚。 火紅的嫁衣襯著肌膚如雪秧饮。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天泽篮,我揣著相機(jī)與錄音盗尸,去河邊找鬼。 笑死帽撑,一個(gè)胖子當(dāng)著我的面吹牛振劳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播油狂,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼寸癌!你這毒婦竟也來了专筷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤蒸苇,失蹤者是張志新(化名)和其女友劉穎磷蛹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溪烤,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡味咳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了檬嘀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片槽驶。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖鸳兽,靈堂內(nèi)的尸體忽然破棺而出掂铐,到底是詐尸還是另有隱情,我是刑警寧澤揍异,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布全陨,位于F島的核電站,受9級特大地震影響衷掷,放射性物質(zhì)發(fā)生泄漏辱姨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一戚嗅、第九天 我趴在偏房一處隱蔽的房頂上張望雨涛。 院中可真熱鬧枢舶,春花似錦、人聲如沸镜悉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽侣肄。三九已至旧困,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間稼锅,已是汗流浹背吼具。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留矩距,地道東北人拗盒。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像锥债,于是被迫代替她去往敵國和親陡蝇。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

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