View的工作流程
View的工作流程,就是measure、layout和draw。measure用來測量View的寬高,layout用來確定View的位置,draw則用來繪製View。這裡measure較為複雜主要分析一下,measure流程分為View的measure流程和ViewGroup的measure流程,只不過ViewGroup的measure流程除了要完成自己的測量還要遍歷去呼叫子元素的measure()方法。
View的測量
先來看看onMeasure()方法(View.java):
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}複製程式碼
在這之前還有個measure()方法直接呼叫的上面的onMeasure()方法,這裡measure()唄final修飾所以無法重新所以主要看看onMeasure()裡的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);
}複製程式碼
大概意思是用來設定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;
}複製程式碼
specMode是View的測量模式,而specSize是View的測量大小,看到這裡我們有必要先說說MeasureSpec:
MeasureSpec類幫助我們來測量View,它是一個32位的int值,高兩位為specMode (測量的模式),低30位為specSize (測量的大小),測量模式分為三種:
UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用於系統內部的測量。
AT_MOST:最大模式,對應於wrap_comtent屬性,只要尺寸不超過父控制元件允許的最大尺寸就行。
EXACTLY:精確模式,對應於match_parent屬性和具體的數值,父容器測量出View所需要的大小,也就是specSize的值。
讓我們回頭看看getDefaultSize()方法,很顯然在AT_MOST和EXACTLY模式下,都返回specSize這個值,也就是View測量後的大小,而在UNSPECIFIED模式返回的是getDefaultSize()方法的第一次個引數的值,這第一個引數從onMeasure()方法來看是getSuggestedMinimumWidth()方法和getSuggestedMinimumHeight()得到的,那我們來看看getSuggestedMinimumWidth()方法做了什麼,我們只需要弄懂getSuggestedMinimumWidth()方法,因為這兩個方法原理是一樣的:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}複製程式碼
很明瞭,如果View沒有設定背景則取值為mMinWidth,如果View設定了背景在取值為max(mMinWidth,mBackground.getMinimumWidth()),取值mMinWidth和mBackground.getMinimumWidth()的最大值,mMinWidth是可以設定的,它對應於android:minWidth這個屬性設定的值或者View的setMinimumWidth的值,如果不指定的話則預設為0,mBackground.getMinimumWidth(),這個mBackground是Drawable型別的,看一下Drawable類的getMinimumWidth()方法(Drawable.java):
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}複製程式碼
intrinsicWidth得到的是這個Drawable的固有的寬度,如果固有寬度大於0則返回固有寬度,否則返回0。
綜上:getSuggestedMinimumWidth()方法就是:如果View沒有設定背景則返回mMinWidth ,如果設定了背景就返回mMinWidth 和Drawable最小寬度兩個值的最大值。
ViewGroup的測量
講完了View的measure流程,接下來看看ViewGroup的measure流程,對於ViewGroup,它不只要measure自己本身,還要遍歷的呼叫子元素的measure()方法,ViewGroup中沒有定義onMeasure()方,但他定義了measureChildren()方法,在我們自己實現onMeasure時可以呼叫它,也可以不呼叫(一般測量孩子都呼叫他),相當於一個模板。線上性佈局、相對佈局等中都有實現,稍後分析。(ViewGroup.java):
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);
}
}
}
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);
}複製程式碼
很簡單,遍歷孩子,呼叫measureChild,內部再讓孩子去measure,於是就到了View的測量。這裡getChildMeasureSpec()方法裡寫了什麼呢?點選去看看:
//三個引數分別是
//1.父View的measurespec
//2.父View已經佔用的尺寸,也就是孩子不能使用的(這個是父View的padding+孩子的margin)
//3.子view的width(MATCH_PARENT、WARP_CONTENT、具體數值)
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//父容器的可用尺寸(去掉了padding),如果是負的,那就是0
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:
//孩子的尺寸是具體數值(大於等於0就是具體數值)
if (childDimension >= 0) {
//如下
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//孩子是MATCH_PARENT
} 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 not be
// bigger than us.
//孩子是包裹內容,那麼孩子的測量模式就是AT_MOST,並且此時size的含義就是孩子最大可能的尺寸,而不是孩子的具體尺寸了
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
//父親是AT_MOST,說明父親的尺寸不確定,但是父親最大不能超過某個數值,這個數值是已知了
case MeasureSpec.AT_MOST:
//孩子是具體數值,那孩子就像下面那樣,精確模式、具體尺寸
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//孩子是MATCH_PARENT,那麼孩子不是精確的,但是孩子可以確定他最大尺寸,那就是父親的最大尺寸,模式是AT_MOST
} 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 not be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
//這種情況
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}複製程式碼
很顯然這是根據父容器的MeasureSpec的模式再結合子元素的LayoutParams屬性來得出子元素的MeasureSpec屬性。
LinearLayout的measure流程
ViewGroup並沒有提供onMeasure()方法,而是讓其子類來各自實現測量的方法,究其原因就是ViewGroup有不同的佈局的需要很難統一,接下來我們來簡單分析一下ViewGroup的子類LinearLayout的measure流程,先來看看它的onMeasure()方法(LinearLayout.java):
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}複製程式碼
兩個方法實現大同小異,這裡看下垂直measureVertical()方法的部分原始碼:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
mTotalLength = 0;
...
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// Optimization: don not bother measuring children who are going to use
// leftover space. These views will get measured again down below if
// there is any leftover space.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
// heightMode is either UNSPECIFIED or AT_MOST, and this
// child wanted to stretch to fill available space.
// Translate that to WRAP_CONTENT so that it does not end up
// with a height of 0
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
...
if (useLargestChild &&
(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
child.getLayoutParams();
// Account for negative margins
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height複製程式碼
大致意思就是定義了mTotalLength用來儲存LinearLayout在垂直方向的高度,然後遍歷子元素,根據子元素的MeasureSpec模式分別計算每個子元素的高度,如果是wrap_content則將每個子元素的高度和margin垂直高度等值相加並賦值給mTotalLength得出整個LinearLayout的高度。如果佈局高度設定為match_parent者具體數值則和View的測量方法一樣。
Layout
layout方法用來決定View自身的位置,在layout中呼叫了onLayout方法,這個方法沒有具體的實現,需要子類自己實現,主要是為了決定子View的位置
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//設定自身的位置
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
//呼叫onLayout,具體的實現都不一樣
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
if (mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>) 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 &= ~FORCE_LAYOUT;
}複製程式碼
在Linearlayout中,onLayout中主要就是遍歷孩子,然後呼叫setChildFrame方法,這個方法內部就是呼叫child的layout方法,所以又回到了上面那一步。
Draw
public void draw(Canvas canvas) {
/*
* 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
// 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);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
// we're done...
return;
}
// Step 2, save the canvas' layers
// 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
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
}複製程式碼
看註釋可知,View的draw過程主要有以下幾步:
畫背景
畫內容
畫孩子
畫裝飾
draw是通過dispatchDraw將繪畫分發給孩子的
有個方法是setWillNotDraw(),可以設定當前view不繪製內容,一般繼承自ViewGroup,並且確保自身不需要繪製,就設為true,可以優化。預設為false。
自定義view
自定義view這塊就大概說一下注意事項,具體不展開。如果你想深入瞭解這裡強烈推薦一下凱哥的自定義View系列 HenCoder:給高階 Android 工程師的進階手冊,如果還沒看過你就out了, 良心鉅作,現在好像都開始著手準備國際化了推向國外了,凱哥(扔物線)的“關注我就能達到大師級水平,這話我終於敢說了”可不是蓋的。
繼承自View的自定義View
在onMeasure中處理wrap_parent
在onDraw中處理padding
自定義xml屬性,檔名字不一定要交attrs。自定義屬性獲取完資料之後記得呼叫recycle。繼承自ViewGroup的自定義View
- 在onMeasure中呼叫measureChildren測量孩子(也可以自己寫邏輯),然後分析自己的measurespec,最後呼叫setMeasuredDimension
onLayout中根據測量寬高,遍歷孩子,為其佈局。
這裡最後放一張HenCoder:給高階 Android 工程師的進階手冊的微信公眾號的圖片,感謝大神的無私奉獻~~~