1 概述
對上圖做出簡單解釋:DecorView是一個應用窗口的根容器,它本質上是一個FrameLayout。DecorView有唯一一個子View,它是一個垂直LinearLayout,包含兩個子元素,一個是TitleView(ActionBar的容器),另一個是ContentView(窗口內容的容器)。關于ContentView,它是一個FrameLayout(android.R.id.content),我們平常用的setContentView就是設置它的子View。上圖還表達了每個Activity都與一個Window(具體來說是PhoneWindow)相關聯,用戶界面則由Window所承載。
2 view繪制流程
2.1 view繪制的三個階段
- measure: 判斷是否需要重新計算View的大小,需要的話則計算;
- layout: 判斷是否需要重新計算View的位置,需要的話則計算;
- draw: 判斷是否需要重新繪制View,需要的話則重繪制。
這三個子階段可以用下圖來描述:
2.2 measure階段
此階段的目的是計算出控件樹中各個控件顯示所需要的尺寸。
measure方法是為了確定View的大小,父容器會提供寬度和高度參數的約束信息。該方法是一個final類型的方法,意味著子類不能重寫它。真正的測量工作是在View的onMeasure方法中進行,因此關注onMeasure方法的實現即可。
2.2.1測量參數
- MeasureSpec.UNSPECIFIED:父容器不對View有任何限制,要多大給多大。這種情況一般用于系統內部,表示一種測量的狀態。
- MeasureSpec.EXACTLY:父容器已經為子容器設置了尺寸,子容器應當服從這些邊界,不論子容器想要多大的空間。它對應于LayoutParams中的match_parent和具體數值這兩種模式
- MeasureSpec.AT_MOST:父容器指定了一個可用大小,View的大小不能超過這個值。它對應于LayoutParams中的wrap_content。
父View的measure的過程會先測量子View,等子View測量結果出來后,再來測量自己,上面的measureChildWithMargins就是用來測量某個子View的,我們來分析是怎樣測量的,具體看注釋:
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
// 子View的LayoutParams,你在xml的layout_width和layout_height,
// layout_xxx的值最后都會封裝到這個個LayoutParams。
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//根據父View的測量規格和父View自己的Padding,
//還有子View的Margin和已經用掉的空間大小(widthUsed),就能算出子View的MeasureSpec,具體計算過程看getChildMeasureSpec方法。
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);
//通過父View的MeasureSpec和子View的自己LayoutParams的計算,算出子View的MeasureSpec,然后父容器傳遞給子容器的
// 然后讓子View用這個MeasureSpec(一個測量要求,比如不能超過多大)去測量自己,如果子View是ViewGroup 那還會遞歸往下測量。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// spec參數 表示父View的MeasureSpec
// padding參數 父View的Padding+子View的Margin,父View的大小減去這些邊距,才能精確算出
// 子View的MeasureSpec的size
// childDimension參數 表示該子View內部LayoutParams屬性的值(lp.width或者lp.height)
// 可以是wrap_content、match_parent、一個精確指(an exactly size),
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec); //獲得父View的mode
int specSize = MeasureSpec.getSize(spec); //獲得父View的大小
//父View的大小-自己的Padding+子View的Margin,得到值才是子View的大小。
int size = Math.max(0, specSize - padding);
int resultSize = 0; //初始化值,最后通過這個兩個值生成子View的MeasureSpec
int resultMode = 0; //初始化值,最后通過這個兩個值生成子View的MeasureSpec
switch (specMode) {
// Parent has imposed an exact size on us
//1、父View是EXACTLY的 !
case MeasureSpec.EXACTLY:
//1.1、子View的width或height是個精確值 (an exactly size)
if (childDimension >= 0) {
resultSize = childDimension; //size為精確值
resultMode = MeasureSpec.EXACTLY; //mode為 EXACTLY 。
}
//1.2、子View的width或height為 MATCH_PARENT/FILL_PARENT
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size; //size為父視圖大小
resultMode = MeasureSpec.EXACTLY; //mode為 EXACTLY 。
}
//1.3、子View的width或height為 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //size為父視圖大小
resultMode = MeasureSpec.AT_MOST; //mode為AT_MOST 。
}
break;
// Parent has imposed a maximum size on us
//2、父View是AT_MOST的 !
case MeasureSpec.AT_MOST:
//2.1、子View的width或height是個精確值 (an exactly size)
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension; //size為精確值
resultMode = MeasureSpec.EXACTLY; //mode為 EXACTLY 。
}
//2.2、子View的width或height為 MATCH_PARENT/FILL_PARENT
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; //size為父視圖大小
resultMode = MeasureSpec.AT_MOST; //mode為AT_MOST
}
//2.3、子View的width或height為 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //size為父視圖大小
resultMode = MeasureSpec.AT_MOST; //mode為AT_MOST
}
break;
// Parent asked to see how big we want to be
//3、父View是UNSPECIFIED的 !
case MeasureSpec.UNSPECIFIED:
//3.1、子View的width或height是個精確值 (an exactly size)
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension; //size為精確值
resultMode = MeasureSpec.EXACTLY; //mode為 EXACTLY
}
//3.2、子View的width或height為 MATCH_PARENT/FILL_PARENT
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0; //size為0! ,其值未定
resultMode = MeasureSpec.UNSPECIFIED; //mode為 UNSPECIFIED
}
//3.3、子View的width或height為 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0; //size為0! ,其值未定
resultMode = MeasureSpec.UNSPECIFIED; //mode為 UNSPECIFIED
}
break;
}
//根據上面邏輯條件獲取的mode和size構建MeasureSpec對象。
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
如果父View的MeasureSpec 是EXACTLY,說明父View的大小是確切的,(確切的意思很好理解,如果一個View的MeasureSpec 是EXACTLY,那么它的size 是多大,最后展示到屏幕就一定是那么大)
- 如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是確切,子View的大小又MATCH_PARENT(充滿整個父View),那么子View的大小肯定是確切的,而且大小值就是父View的size。所以子View的size=父View的size,mode=EXACTLY
- 如果子View 的layout_xxxx是WRAP_CONTENT,也就是子View的大小是根據自己的content 來決定的,但是子View的畢竟是子View,大小不能超過父View的大小,但是子View的是WRAP_CONTENT,我們還不知道具體子View的大小是多少,要等到child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 調用的時候才去真正測量子View 自己content的大?。ū热鏣extView wrap_content 的時候你要測量TextView content 的大小,也就是字符占用的大小,這個測量就是在child.measure(childWidthMeasureSpec, childHeightMeasureSpec)的時候,才能測出字符的大小,MeasureSpec 的意思就是假設你字符100px,但是MeasureSpec 要求最大的只能50px,這時候就要截掉了)。通過上述描述,子View MeasureSpec mode的應該是AT_MOST,而size 暫定父View的 size,表示的意思就是子View的大小沒有不確切的值,子View的大小最大為父View的大小,不能超過父View的大小(這就是AT_MOST 的意思),然后這個MeasureSpec 做為子View measure方法 的參數,做為子View的大小的約束或者說是要求,有了這個MeasureSpec子View再實現自己的測量。
- 如果如果子View 的layout_xxxx是確定的值(200dp),那么就更簡單了,不管你父View的mode和size是什么,我都寫死了就是200dp,那么控件最后展示就是就是200dp,不管我的父View有多大,也不管我自己的content 有多大,反正我就是這么大,所以這種情況MeasureSpec 的mode = EXACTLY 大小size=你在layout_xxxx 填的那個值
如果父View的MeasureSpec 是AT_MOST,說明父View的大小是不確定,最大的大小是MeasureSpec 的size值,不能超過這個值。
- 如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是不確定(只知道最大只能多大),子View的大小MATCH_PARENT(充滿整個父View),那么子View你即使充滿父容器,你的大小也是不確定的,父View自己都確定不了自己的大小,你MATCH_PARENT你的大小肯定也不能確定的,所以子View的mode=AT_MOST,size=父View的size,也就是你在布局雖然寫的是MATCH_PARENT,但是由于你的父容器自己的大小不確定,導致子View的大小也不確定,只知道最大就是父View的大小。
- 如果子View 的layout_xxxx是WRAP_CONTENT,父View的大小是不確定(只知道最大只能多大),子View又是WRAP_CONTENT,那么在子View的Content沒算出大小之前,子View的大小最大就是父View的大小,所以子View MeasureSpec mode的就是AT_MOST,而size 暫定父View的 size。
- 如果如果子View 的layout_xxxx是確定的值(200dp),同上,寫多少就是多少,改變不了的。
如果父View的MeasureSpec 是UNSPECIFIED(未指定),表示沒有任何束縛和約束,不像AT_MOST表示最大只能多大,也不像EXACTLY表示父View確定的大小,子View可以得到任意想要的大小,不受約束
- 如果子View 的layout_xxxx是MATCH_PARENT,因為父View的MeasureSpec是UNSPECIFIED,父View自己的大小并沒有任何約束和要求,那么對于子View來說無論是WRAP_CONTENT還是MATCH_PARENT,子View也是沒有任何束縛的,想多大就多大,沒有不能超過多少的要求,一旦沒有任何要求和約束,size的值就沒有任何意義了,所以一般都直接設置成0
- 同上
- 如果如果子View 的layout_xxxx是確定的值(200dp),同上,寫多少就是多少,改變不了的(記住,只有設置的確切的值,那么無論怎么測量,大小都是不變的,都是你寫的那個值)
2.2.2 測量過程
View的測量過程主要是在onMeasure()方法中,measure方法是final,所以這個方法不可重寫,如果想自定義View的測量,應該去重寫onMeasure()方法。在onMeasure方法的最后需要調用setMeasuredDimension方法,不然會拋異常
基本思想:父View把自己的MeasureSpec,傳給子View結合子View自己的LayoutParams 算出子View 的MeasureSpec,然后繼續往下傳,傳遞葉子節點,葉子節點沒有子View,根據傳下來的這個MeasureSpec測量自己就好了。所有的孩子測量之后,經過一系列的計算之后通過setMeasuredDimension設置自己的寬高,對于FrameLayout 可能用最大的子View的大小,對于LinearLayout,可能是高度的累加,具體測量的原理去看看源碼。總的來說,父View是等所有的子View測量結束之后,再來測量自己。通過setMeasuredDimension進行測量。
3 layout過程
Layout的作用是ViewGroup用來確定子元素的位置,當ViewGroup的位置被確定后,它在onLayout中會遍歷所有子元素并調用其layout方法,在layout方法中又會調用onLayout方法。
layout方法的大致流程:首先會通過setFrame方法來設定View的四個頂點的位置,即初始化mLeft、mRight、mTop和mBottom,View的四個頂點一旦確定,那么View在父容器中的位置也就確定了;接著會調用onLayout方法,這個方法的用途是父容器確定子元素的位置,因為它的具體實現和具體的布局有關,所以View和ViewGroup都沒有真正實現它,而是由子類重寫。
注意:ViewGroup的layout方法是final類型的,它會調用onLayout方法(ViewGroup中是抽象方法,子類必須重寫),所以子類根據布局需要重寫onLayout方法。View的layout方法是public類型的,但是最終的位置確定是在onLayout方法(View中是public的空方法)中進行的,所以重寫onLayout方法即可。
4 draw過程
draw的繪制過程主要分為6步
- 繪制背景
- 如有必要,保存畫布的圖層以準備淡化
- 對view內容進行繪制
- 對當前view的所有子view進行繪制
- 如有必要,繪制淡化邊緣并恢復圖層
- 對view的滾動條進行繪制
接下來看View的一個方法:
/**
* 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以后,系統會進行相應的優化。默認情況下,View沒有起用這個優化標記位,但是ViewGroup會默認啟用。這個標記位對實際開發的意義是:當我們的自定義控件繼承于ViewGroup并且本身不具備繪制功能時,就可以開始這個標記位便于系統進行優化。如果明確知道一個ViewGroup需要通過onDraw來繪制內容,就需要顯式的關閉WILL_NOT_DRAW 這個標記位。
相關問答
- 在Activity啟動的時候獲取某個View的寬高
答:在onCreate、onStart、onResume中均無法獲取到某個View的正確寬高,因為View的measure過程和Activity的生命周期方法并不是同步執行的,無法保證Activity執行了onCreate、onStart、onResume時某個View已經測量完畢,獲取到的寬高可能為0。可以通過下列四種方法來解決這個問題:
- Activity重寫onWindowFocusChanged
onWindowFocusChanged這個方法的含義是:View已經初始化完畢,寬高已經準備好了,所以這時候獲取的寬高是沒問題的。這個方法會被調用多次,當Activity窗口得到焦點和失去焦點的時候都會被調用。- view.post(runnable)
通過post可以講一個runnable投遞到消息隊列的尾部,然后等待Looper調用此runnable的時候,View已經初始化好了。- ViewTreeObserver
使用ViewTreeObserver的眾多回調可以完成這個功能,比如OnGlobalLayoutListener接口:當View樹的狀態發生改變或者View樹內部的View的可見性發生改變時,onGlobalLayout方法將會被回調,這是獲取View寬高的一個好時機。注意該方法會被調用多次。- view.measure(int widthMeasureSpec, int heightMeasureSpec)
通過手動對View進行measure來得到View的寬高,個人不推薦該方法,有局限性,而且容易出錯,就不介紹了。
public class TestActivity extends Activity {
@BindView(R.id.btn)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
ButterKnife.bind(this);
textView.post(new Runnable() {
@Override
public void run() {
int width = textView.getMeasuredWidth();
int height = textView.getMeasuredHeight();
Log.d("==========", "onCreate view.post width = " + width + " ; height = " + height);
}
});
ViewTreeObserver viewTreeObserver = textView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//特別注意:此處不能直接使用viewTreeObserver.removeGlobalOnLayoutListener(this); 會有如下異常:
//java.lang.IllegalStateException: This ViewTreeObserver is not alive, call getViewTreeObserver() again
textView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = textView.getMeasuredWidth();
int height = textView.getMeasuredHeight();
Log.d("==========", "onCreate viewTreeObserver.addOnGlobalLayoutListener width = " + width + " ; height = " + height);
}
});
}
@Override
protected void onStart() {
super.onStart();
textView.post(new Runnable() {
@Override
public void run() {
int width = textView.getMeasuredWidth();
int height = textView.getMeasuredHeight();
Log.d("==========", "onStart view.post width = " + width + " ; height = " + height);
}
});
}
@Override
protected void onResume() {
super.onResume();
int width = textView.getMeasuredWidth();
int height = textView.getMeasuredHeight();
Log.d("==========", "onResume width = " + width + " ; height = " + height);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus) {
int width = textView.getMeasuredWidth();
int height = textView.getMeasuredHeight();
Log.d("==========", "onWindowFocusChanged width = " + width + " ; height = " + height);
}
}
}
//測試結果:
//onResume width = 0 ; height = 0
//onCreate viewTreeObserver.addOnGlobalLayoutListener width = 391 ; height = 57
//onCreate view.post width = 391 ; height = 57
//onStart view.post width = 391 ; height = 57
//onWindowFocusChanged width = 391 ; height = 57
- Activity顯示流程
ViewRoot對應于ViewRootImpl類,它是連接WindowManager和DecorView的紐帶,View的三大流程均是通過viewRoot來完成的。在ActivityThread中,當Activity對象創建完畢后,會將decorView添加到window中,同時會創建ViewRootImpl對象viewRoot,并將viewRoot和decorView建立關聯。
View的繪制流程從viewRoot的performTraversals方法開始的。 - View的測量寬高和顯示的最終寬高區別
答:測量寬高是在measure過程中確定的,為onMeasure方法中通過setMeasuredDimension設置的值;最終寬高是在layout過程中確定的,寬為mRight - mLeft,高為mBottom - mTop。
正常情況下,它兩是相等的,某些特殊情況會不一致,比如:
//重寫View的layout方法
public void layout(int l, int t, int r, int b) {
//最終寬高會比測量寬高大100px
super.layout(l, t, r + 100, b + 100);
}
- MeasureSpec的賦值原理
答:MeasureSpec代表一個32位的int值,高2位代表SpecMode,低30位代表SpecSize。
DecorView:MeasureSpec由窗口的尺寸和自身的LayoutParams共同決定;
普通View:MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定 - 當一個TextView的實例調用setText()方法后執行了什么
答:setText最后會調用requestLayout()和invalidate()。requestLayout()會調用父容器的requestLayout方法,直至頂層View。requestLayout()會調用measure過程和layout過程,invalidate()會調用draw過程。 - 幾個常用方法介紹
requestLayout():調用measure過程和layout過程;
invalidate():必須要在UI線程調用;View可見的話,會調用onDraw方法;
postInvalidate():可以在非UI線程調用,通過handler發送一個MSG_INVALIDATE消息,然后在主線程處理消息,執行invalidate()方法。
參考文章
https://www.cnblogs.com/jycboy/p/6219915.html
http://www.lxweimin.com/p/c4412f878508