Android 的繪制過程可分為3個步驟即:measure(測量)、layout(布局)、draw(繪制)。
一、 measure過程
1.1 MeasureSpec
MeasureSpec 是 View 測量過程中的一個關鍵參數,很大程度上決定了 View 的寬高,父容器會影響 View 的MeasureSpec 的創建,MeasureSpec 不是唯一由 LayoutParams 決定的,LayoutParams 需要和父容器一起才能決定 View 的MeasureSpec,從而進一步確定 View 的寬高,在 View 測量過程中,系統會將該 View 的 LayoutParams 參數在父容器的約束下轉換成對應的 MeasureSpec ,然后再根據這個 measureSpec 來測量 View 的寬高。
MeasureSpec 代表一個32位 int 值,高2位代表 SpecMode(測量模式),低30位代表 SpecSize(在某個測量模式下的規格大小),MeasureSpec 通過將 SpecMode 和 SpecSize 打包成一個 int 值來避免過多的內存分配,為了方便操作,其提供了打包和解包方法源碼如下:
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
//通過將 SpecMode 和 SpecSize 打包,獲取 MeasureSpec
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//將 MeasureSpec 解包獲取 SpecMode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//將 MeasureSpec 解包獲取 SpecSize
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
SpecMode 有三類,每一類都表示特殊的含義:
UNSPECIFIED 父容器不對 View 有任何的限制,要多大給多大,這種情況下一般用于系統內部,表示一種測量的狀態。
EXACTLY 父容器已經檢測出 View 所需要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值,它對應于LayoutParams 中的 match_parent 和具體的數值這兩種模式
-
AT_MOST 父容器指定了一個可用大小即 SpecSize,View 的大小不能大于這個值,具體是什么值要看不同 View 的具體實現。它對應于 LayoutParams 中的 wrap_content。
?
1.2 測量過程
對于DecorView,它的 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 來決定;對于普通 View,它的MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來共同決定。
對普通的 View 的 measure 方法的調用,是由其父容器傳遞而來的,這里先看一下 ViewGroup 的 measureChildWithMargins 方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//第一步,獲取子 View 的 LayoutParams ,也就是我們在xml中設置的layout_width和layout_height。
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//第二步,獲取子 view 的 WidthMeasureSpec,根據父View的測量規格和父View自己的Padding,
//還有子View的Margin和已經用掉的空間大小(widthUsed),就能算出子View的MeasureSpec。
//其中傳入的幾個參數說明:
//parentWidthMeasureSpec 父容器的 WidthMeasureSpec
//mPaddingLeft + mPaddingRight view 本身的 Padding 值,即內邊距值
//lp.leftMargin + lp.rightMargin view 本身的 Margin 值,即外邊距值
//widthUsed 父容器已經被占用空間值
// lp.width view 本身期望的寬度 with 值
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);
// 第三步,根據獲取的子 veiw 的 WidthMeasureSpec 和 HeightMeasureSpec 對子 view 進行測量
//對子 view 進行測量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
看一下 getChildMeasureSpec 方法:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 獲取父容器的 specMode,父容器的測量模式影響子 View 的測量模式
int specMode = MeasureSpec.getMode(spec);
// 獲取父容器的 specSize 尺寸,這個尺寸是父容器用來約束子 View 大小的
int specSize = MeasureSpec.getSize(spec);
// 父容器尺寸減掉已經被用掉的尺寸,得到是子View的大小.
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
//父View是EXACTLY的類型.
case MeasureSpec.EXACTLY:
//如果子view是一個固定的值,即在xml中設置確定的值。
if (childDimension >= 0) {
//則大小為固定的值。
resultSize = childDimension;
//模式是EXACTLY
resultMode = MeasureSpec.EXACTLY;
}
//如果子View的類型是MATCH_PARENT。
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
//子view大小為父容器大小。
resultSize = size;
//模式是EXACTLY
resultMode = MeasureSpec.EXACTLY;
}
//如果子view的類型是WRAP_CONTENT。
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
//大小是最大值是size,具體由子view自己決定,最大不能超過size。
resultSize = size;
//模式是AT_MOST
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
// 父容器為 AT_MOST 最大測量模式
case MeasureSpec.AT_MOST:
//如果子view是一個固定的值,即在xml中設置確定的值。
if (childDimension >= 0) {
// Child wants a specific size... so be it
//則大小為固定的值。
resultSize = childDimension;
//模式是EXACTLY
resultMode = MeasureSpec.EXACTLY;
}
//如果子View的類型是MATCH_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.
//大小是最大值是size,具體由子view自己決定,最大不能超過size。
resultSize = size;
//模式是AT_MOST
resultMode = MeasureSpec.AT_MOST;
}
子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;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
//父View是UNSPECIFIED的
case MeasureSpec.UNSPECIFIED:
//如果子view是一個固定的值,即在xml中設置確定的值。
if (childDimension >= 0) {
// Child wants a specific size... let him have it
//則大小為固定的值。
resultSize = childDimension;
//模式是EXACTLY
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
//子 View 尺寸為 0,測量模式為 UNSPECIFIED
// 父容器不對 View 有任何的限制,要多大給多大
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
//子 View 尺寸為 0,測量模式為 UNSPECIFIED
// 父容器不對 View 有任何的限制,要多大給多大
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
以上代碼有點多,但是邏輯也很清晰:
-
如果父View傳進來的模式是EXACTLY類型,也就是父View的大小是確定的。
- 當當前的View是一個固定值,也就是我們在xml里面定義改View的width和height是固定的值的大小是的時候,它最后的大小就是View設置的值。模式是MeasureSpec.EXACTLY。
- 當當前View的大小是設置成match_parent的時候,則當前View 的大小是父VIew的大小size。模式是MeasureSpec.EXACTLY。
- 當當前的View設置成warp_content 內容包裹時,則View的大小由view自身決定,要多大就多大,但是不能超過父View的大小size,其實這時候是無法知道自己的大小,要等到child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 調用到時候才知道自身的大小。模式是MeasureSpec.AT_MOST。
-
如果父View傳進來的模式是AT_MOST類型, 最大測量模式.
- 當當前的View是一個固定值,也就是我們在xml里面定義改View的width和height是固定的值的大小是的時候,它最后的大小就是View設置的值。模式是MeasureSpec.EXACTLY。
- 當當前View的大小是設置成match_parent的時候,則當前View 的大小是父VIew的大小size。但是父View也不知道自己的大小,所以模式是MeasureSpec.AT_MOST。
- 當當前的View設置成warp_content 內容包裹時,父View的大小是不確定(只知道最大只能多大),子View又是WRAP_CONTENT,那么在子View的Content沒算出大小之前,子View的大小最大就是父View的大小,所以子View MeasureSpec mode的就是AT_MOST,而size 暫定父View的 size。
-
如果父View傳進來的模式是UNSPECIFIED類型
- 當當前的View是一個固定值,也就是我們在xml里面定義改View的width和height是固定的值的大小是的時候,它最后的大小就是View設置的值。模式是MeasureSpec.EXACTLY。
- 當當前View的大小是設置成match_parent的時候,子 View 尺寸為 0,測量模式為 UNSPECIFIED,父容器不對 View 有任何的限制,要多大給多大。
- 當當前的View設置成warp_content 內容包裹時,子 View 尺寸為 0,測量模式為 UNSPECIFIED,父容器不對 View 有任何的限制,要多大給多大。
1.3 View的measure過程
分兩種情況:
- 如果只是一個原始的 View,通過
measure
方法就完成了測量過程。 - 如果是一個 ViewGroup 除了完成自己的測量過程還會遍歷調用所有子 View 的
measure
方法,而且各個子 View 還會遞歸執行這個過程。
1.3.1 measure
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
。。。
//調用onMeasure()開始繪制.
onMeasure(widthMeasureSpec, heightMeasureSpec);
。。。
}
代碼有點長,主要看measure是一個final的方法,即不能被重寫,里面真正調用的是onMeasure()方法。所以我們在自定義View的時候可以重寫該方法。
1.3.2 onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//用于獲得View寬/高的測量值
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
onMeasure方法主要用來獲取View的寬和高的測量值。在往下看代碼前,先看一下默認值的獲取getDefaultSize().
1.3.3getDefaultSize
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;
}
當傳進來的模式是UNSPECIFIED的時候,默認值是size,看一下size的獲取getSuggestedMinimumWidth().
1.3.4 getSuggestedMinimumWidth
protected int getSuggestedMinimumWidth() {
//如果沒有設置背景,view的最小寬度是mMinWidth:
// 1、mMinWidth = android:minWidth屬性所指定的值,2、若android:minWidth沒指定,則默認為0
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
如果沒有設置背景則返回的值是設置的android:minWidth屬性指定的值,若android:minWidth沒指定,則默認為0。如果有背景,則取兩者的最大值。
回過頭來再看,在onMeasure()里面調用setMeasuredDimension方法。
1.3.5 setMeasuredDimension
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
在這里又調用setMeasuredDimensionRaw 將測量后的View的寬高進行存儲。
1.3.6 setMeasuredDimensionRaw
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
//測量后的view寬高的值.
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
1.4 實際獲取View寬高
在實際開發中,我們在Actiivty中獲取View的寬高往往都是0,這是由于我們無法保證在Activity執行生命周期中,View已經測量完成,如果還沒測量完成,這時候去獲取,那結果肯定是0.以下幾種方法可以獲取View的寬高。
-
Activity/View#onWindowsChanged 方法
public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if(hasWindowFocus){ int width=view.getMeasuredWidth(); int height=view.getMeasuredHeight(); } }
onWindowFocusChanged 方法表示 View 已經初始化完畢了,寬高已經準備好了,這個時候去獲取是沒問題的。這個方法會被調用多次,當 Activity 繼續執行或者暫停執行的時候,這個方法都會被調用。
-
View.post(runnable)
@Override protected void onStart() { super.onStart(); view.post(new Runnable() { @Override public void run() { int width=view.getMeasuredWidth(); int height=view.getMeasuredHeight(); } }); }
用post異步加入到消息隊列,這樣也是可以獲取到的。
-
ViewTreeObsever
@Override protected void onStart() { super.onStart(); ViewTreeObserver viewTreeObserver=view.getViewTreeObserver(); viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { view.getViewTreeObserver().removeOnGlobalLayoutListener(this); int width=view.getMeasuredWidth(); int height=view.getMeasuredHeight(); } }); }
開一個子線程,觀察ViewTreeObserver 的回調也可以獲取view的寬高。當 View 樹的狀態發生改變或者 View 樹內部的 View 的可見性發生改變時,onGlobalLayout 方法將被回調。伴隨著View樹的變化,這個方法也會被多次調用。
?
二、layout過程
layout 的作用是 ViewGroup 來確定子元素的位置,當 ViewGroup 的位置被確定后,在 layout 中會調用 onLayout ,在 onLayout 中會遍歷所有的子元素并調用子元素的 layout 方法,在子元素的 layout 方法中 onLayout 方法又會被調用,layout 方法是確定 View 本身在屏幕上顯示的具體位置。
2.1 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;
// 即初始化四個頂點的值,然后判斷當前View大小和位置是否發生了變化并返回
//第1步,調用 setFrame 方法 設置新的 mLeft、mTop、mBottom、mRight 值,
//設置 View 本身四個頂點位置,并返回 changed 用于判斷 view 布局是否改變
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//第二步,如果 view 位置改變那么調用 onLayout 方法設置子 view 位置
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//調用 onLayout,是一個空方法。
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;
}
即初始化四個頂點的值,然后判斷當前View大小和位置是否發生了變化并返回.
大致流程是,首先先調用setFrame(),設置View本身的4個點,其中setOpticalFrame本身內部也是調用setFrame(),所以最終都是通過調用setFrame()來設置view的4個點。View 的四個頂點一旦確定,那么 View 在父容器中的位置就確定了。然后第二步是onLayout,開始具體布局。其實onLayout是一個空方法,是要我們繼承重寫的。
2.1 onLayout
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
三、 draw過程
draw過程總共分6步,實際是4步。
- Draw the background 繪制view背景
- If necessary, save the canvas' layers to prepare for fading
- Draw view's content 繪制view內容
- Draw children 繪制子View
- If necessary, draw the fading edges and restore layers
- Draw decorations (scrollbars for instance) 繪制裝飾(漸變框,滑動條等等
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;
// 第一步
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;
}
。。。
// 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;
。。。。
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}