Android中的任何一個(gè)布局、控件都是直接或間接繼承自View的,要想開發(fā)一個(gè)Android App,就肯定少不了要和View打交道。盡管Android已經(jīng)提供了豐富的控件和布局,但是要想開發(fā)出有自己特色的App,自定義View是必須要掌握的。那么,今天我們就來聊聊View繪制的相關(guān)內(nèi)容。
本文的要點(diǎn)如下:
- View簡(jiǎn)介
- MeasureSpecs類
- onMeasure
- 單一View的測(cè)量
- ViewGroup的測(cè)量
- onLayout
- 單一View的layout過程
- ViewGroup的layout過程
- onDraw
- 總結(jié)
View簡(jiǎn)介
在Android系統(tǒng)中View是所有控件的基類,其中也包括ViewGroup在內(nèi),ViewGroup是代表著控件的集合,其中可以包含多個(gè)View控件。
從某種角度上來講Android中的控件可以分為兩大類:View與ViewGroup。通過ViewGroup,整個(gè)界面的控件形成了一個(gè)樹形結(jié)構(gòu),上層的控件要負(fù)責(zé)測(cè)量與繪制下層的控件,并傳遞交互事件。
在每棵控件樹的頂部都存在著一個(gè)ViewParent對(duì)象,它是整棵控件樹的核心所在,所有的交互管理事件都由它來統(tǒng)一調(diào)度和分配,從而對(duì)整個(gè)視圖進(jìn)行整體控制,如下圖所示:
繪制出整個(gè)界面肯定是要遍歷整個(gè)View樹,對(duì)這棵樹的所有節(jié)點(diǎn)分別進(jìn)行測(cè)量,布局和繪制。萬事皆有源頭,繪制得從根節(jié)點(diǎn)頂級(jí)View開始畫起,即DecorView。
系統(tǒng)內(nèi)部會(huì)依次調(diào)用DecorView的measure,layout和draw三大流程方法。measure方法又會(huì)調(diào)用onMeasure方法對(duì)它所有的子元素進(jìn)行測(cè)量,如此反復(fù)調(diào)用下去就能完成整個(gè)View樹的遍歷測(cè)量。同樣的,layout和draw兩個(gè)方法里也會(huì)調(diào)用相似的方法去對(duì)整個(gè)View樹進(jìn)行遍歷布局和繪制。
View的構(gòu)造函數(shù):共有4個(gè),具體如下:
// 如果View是在Java代碼里面new的,則調(diào)用第一個(gè)構(gòu)造函數(shù)
public CarsonView(Context context) {
super(context);
}
// 如果View是在.xml里聲明的,則調(diào)用第二個(gè)構(gòu)造函數(shù)
// 自定義屬性是從AttributeSet參數(shù)傳進(jìn)來的
public CarsonView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 不會(huì)自動(dòng)調(diào)用
// 一般是在第二個(gè)構(gòu)造函數(shù)里主動(dòng)調(diào)用
// 如View有style屬性時(shí)
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//API21之后才使用
// 不會(huì)自動(dòng)調(diào)用
// 一般是在第二個(gè)構(gòu)造函數(shù)里主動(dòng)調(diào)用
// 如View有style屬性時(shí)
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
View的位置參數(shù):
View的位置由4個(gè)頂點(diǎn)決定(View的位置是相對(duì)于父控件而言的)

- Top:子View上邊界到父view上邊界的距離
- Left:子View左邊界到父view左邊界的距離
- Bottom:子View下邊距到父View上邊界的距離
- Right:子View右邊界到父view左邊界的距離
MeasureSpecs類
MeasureSpecs類是View的內(nèi)部類,用一個(gè)變量封裝了兩個(gè)數(shù)據(jù)(size、mode),其目的是減少對(duì)象的內(nèi)存分配。
public static class MeasureSpec {
//省略了部分不關(guān)鍵代碼
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;
// 通過Mode 和 Size 生成新的SpecMode
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//獲取測(cè)量模式(Mode)
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
//原理:保留measureSpec的高2位(即測(cè)量模式)、使用0替換后30位
//例如10 00..00100 & 11 00..00(11后跟30個(gè)0) = 10 00..00(AT_MOST)
//這樣就得到了mode的值
}
//獲取測(cè)量大?。⊿ize)
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
// 原理類似上面,即 將MODE_MASK取反,也就是變成了00 111111(00后跟30個(gè)1),
//將32,31位替換成0也就是去掉mode,保留后30位的size
}
}
測(cè)量規(guī)格(MeasureSpec) = 測(cè)量模式(mode) + 測(cè)量大小(size)。
其中,測(cè)量模式占最高兩位,測(cè)量大小則是MeasureSpec的低30位。測(cè)量模式(Mode)的類型有3種:UNSPECIFIED、EXACTLY 和AT_MOST。具體如下:
UNSPECIFIED:父View不約束子View(即子View可以獲取任意尺寸),多用于系統(tǒng)內(nèi)部View(ListView,ScrollView等),自定義View一般用不到。(這個(gè)模式主要用于系統(tǒng)內(nèi)部多次Measure的情形,并不是真的說你想要多大最后就真有多大)
EXACTLY(精確模式):父View會(huì)為子View指定一個(gè)確切尺寸,子View必須在該尺寸之內(nèi)。對(duì)應(yīng)LayoutParams中的match_parent或具體數(shù)值。
AT_MOST(最大模式):父容器為子視圖指定一個(gè)最大尺寸SpecSize,View的大小不能大于這個(gè)值。對(duì)應(yīng)LayoutParams中的wrap_content。
onMeasure
View的繪制流程中,第一步就是測(cè)量,即onMeasure()方法。我們知道,自定義View的類型可以分為兩種,一種繼承View,一種繼承ViewGroup(繼承現(xiàn)有View的也可以根據(jù)繼承的View的不同歸入這兩種類型之中)。那么測(cè)量的過程也有兩種:
- 單一View的測(cè)量
- ViewGroup的測(cè)量
那么下面我們就分別來看看兩種流程的不同。
單一View的measure過程
這種情況相對(duì)而言比較簡(jiǎn)單,不用考慮子View,只有一個(gè)原始的View,通過measure()即可完成測(cè)量,具體過程如下:

那么我們來看看整個(gè)流程的源碼:
//measure是測(cè)量流程的開始,由于是final類型,因此不能被重寫,
//主要是用來進(jìn)行基本測(cè)量邏輯的判斷。
//里面調(diào)用onMeasure進(jìn)行測(cè)量邏輯。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);//具體的測(cè)量邏輯
} else {
...
}
measure方法是測(cè)量過程中最先調(diào)用的方法,View的這個(gè)方法是被它的父控件調(diào)用的。由于measure是final類型,不能被子類重寫,那么就只能重寫onMeasure方法來實(shí)現(xiàn)測(cè)量邏輯了。
//在onMeasure中就做了兩件事,
//1是根據(jù)View寬/高的測(cè)量規(guī)格用getDefaultSize()方法計(jì)算View的寬/高值,
//2是用setMeasuredDimension()方法存儲(chǔ)測(cè)量后的View寬 / 高。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
//如果有設(shè)置背景,則獲取背景的寬度,如果沒有設(shè)置背景,則取xml中android:minWidth的值。
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
onMeasure里面用到了getSuggestedMinimumWidth和getSuggestedMinimumHeight,這兩個(gè)方法差不多的,我們就以getSuggestedMinimumWidth為例。mMinWidth屬性對(duì)應(yīng)的就是xml布局里的android:minWidth屬性,設(shè)置最小寬度。mBackground.getMinimumWidth()方法返回的就是View背景Drawable的原始寬度,這個(gè)寬度跟背景的類型有關(guān)。比如我們給View的背景設(shè)置一張圖片,那這個(gè)方法返回的寬度就是圖片的寬度,而如果我們給View背景設(shè)置的是顏色,那么這個(gè)方法返回的寬度則是0。
所以,這個(gè)方法的返回的寬度是:如果View沒有設(shè)置背景,那就返回xml布局里的android:minWidth屬性定義的值,默認(rèn)為0;如果View設(shè)置了背景,就返回背景的寬度和mMinWidth中的最大值。
//存儲(chǔ)測(cè)量后的View寬 / 高,該方法即為我們重寫onMeasure()所要實(shí)現(xiàn)的最終目的。
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//判斷該view布局模式是否有一些特殊的邊界
boolean optical = isLayoutModeOptical(this);
////判斷view和該view的父view的布局模式情況,如果兩者不同步,則進(jìn)行子view的size大小的修改
if (optical != isLayoutModeOptical(mParent)) {
//有兩種情況會(huì)進(jìn)入到該if條件,
//一是子view有特殊的光學(xué)邊界,而父view沒有,此時(shí)optical為true,
//一種是父view有一個(gè)特殊的光學(xué)邊界,而子view沒有,此時(shí)optical為false
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
//存儲(chǔ)測(cè)量后的View寬 / 高的實(shí)際邏輯
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
//根據(jù)View寬/高的測(cè)量規(guī)格計(jì)算View的寬/高值
public static int getDefaultSize(int size, int measureSpec) {
int result = size;// 設(shè)置默認(rèn)大小
// 獲取寬/高測(cè)量規(guī)格的模式 & 測(cè)量大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
// 模式為UNSPECIFIED時(shí),使用提供的默認(rèn)大小 = 參數(shù)Size
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
//模式為AT_MOST,EXACTLY時(shí),
//使用View測(cè)量后的寬/高值 = measureSpec中的Size
result = specSize;
break;
}
return result;
}
可以看出,View在當(dāng)測(cè)量模式為UNSPECIFIED時(shí),返回的就是上面getSuggestedMinimumWidth/Height()方法里的大小。其實(shí)這對(duì)我們自定義控件并沒有什么影響,因?yàn)閁NSPECIFIED模式是給系統(tǒng)內(nèi)部用的。我們的重點(diǎn)還是應(yīng)該放在AT_MOST和EXACTLY兩種情況下。對(duì)于這兩種情況,getDefaultSize十分簡(jiǎn)單粗暴,直接返回了specSize,也就是View的測(cè)量規(guī)格里的測(cè)量尺寸。
這里就出現(xiàn)了一個(gè)問題在AT_MOST和EXACTLY兩種情況下返回的尺寸竟然都是specSize。
因此在自定義View控件時(shí),我們需要重寫onMeasure方法并設(shè)置wrap_content時(shí)自身的大小。否則在xml布局中使用wrap_content時(shí)與match_parent的效果將會(huì)是一樣。
至此,單一View的寬/高值已經(jīng)測(cè)量完成,即對(duì)于單一View的measure過程已經(jīng)完成。
小小的總結(jié)一下,其是前面的源碼只是為了對(duì)View的測(cè)量有個(gè)完整的概念,清楚整個(gè)流程,主要我們實(shí)現(xiàn)還是在onMeasure方法中,因此可以寫一個(gè)自定義View的onMeasure方法的通用模版,其實(shí)最關(guān)鍵的也就是在AT_MOST模式時(shí)進(jìn)行特殊處理,畢竟父類的onMeasure已經(jīng)實(shí)現(xiàn)了大部分邏輯:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 必須調(diào)用,因?yàn)楦割愡€是實(shí)現(xiàn)了很多東西的。
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 寬的測(cè)量模式
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
// 寬的測(cè)量尺寸
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
// 高度的測(cè)量模式
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
// 高度的測(cè)量尺寸
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//根據(jù)View的邏輯得到,比如TextView根據(jù)設(shè)置的文字計(jì)算wrap_content時(shí)的大小。
//這兩個(gè)數(shù)據(jù)變量要根據(jù)實(shí)現(xiàn)需求計(jì)算。
int wrapWidth,wrapHeight;
// 如果有測(cè)量模式是AT_MOST則需要進(jìn)行特殊處理
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
//長(zhǎng)寬都是AT_MOST,則都需要計(jì)算
setMeasuredDimension(wrapWidth, wrapHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
//只有寬是AT_MOST,則長(zhǎng)用測(cè)量尺寸
setMeasuredDimension(wrapWidth, heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
//只有長(zhǎng)是AT_MOST,則寬用測(cè)量尺寸
setMeasuredDimension(widthSpecSize, wrapHeight);
}
}
ViewGroup的measure過程
ViewGroup的情況就復(fù)雜一些了,畢竟它還有一堆子View要考慮,大多數(shù)時(shí)候要先確定子View的大小,再確定ViewGroup的的大小。不過原理也很簡(jiǎn)單,就是遍歷測(cè)量所有子View的尺寸然后將所有子View的尺寸進(jìn)行合并,最終得到ViewGroup父視圖的測(cè)量值。
看過源碼就知道ViewGroup并沒有重寫View的onMeasure方法,為什么呢?顯然這需要它的子類去根據(jù)相應(yīng)的邏輯去實(shí)現(xiàn),比如LinearLayout與RelativeLayout對(duì)child View的測(cè)量邏輯顯然是不同的。這個(gè)也是單一View的measure過程與ViewGroup過程最大的不同。
不過,ViewGroup倒是提供了一個(gè)measureChildren的方法,貌似可以用來測(cè)量child的樣子,看看源碼:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;//子View集合
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
這個(gè)方法的邏輯就很清晰嘛,就是遍歷子View,調(diào)用measureChild方法對(duì)其進(jìn)行測(cè)量,那么我們接著來看看measureChild方法:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//取出子View的LayoutParams
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方法里,會(huì)取出child的LayoutParams,再結(jié)合父控件的測(cè)量規(guī)格和已被占用的空間Padding,作為參數(shù)傳遞給getChildMeasureSpec方法,在getChildMeasureSpec里會(huì)組合生成child控件的測(cè)量規(guī)格。
getChildMeasureSpec是ViewGroup里提供的一個(gè)靜態(tài)方法,用來用來獲取子控件的測(cè)量規(guī)格。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//父view的測(cè)量模式
int specMode = MeasureSpec.getMode(spec);
//父view的測(cè)量大小
int specSize = MeasureSpec.getSize(spec);
//通過父view計(jì)算出的子view = 父大小-邊距(父要求的大小,但子view不一定用這個(gè)值)
int size = Math.max(0, specSize - padding);
//子view想要的實(shí)際大小和模式(需要計(jì)算)
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
/// 當(dāng)父控件的測(cè)量模式 是 精確模式,也就是有精確的尺寸了
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//如果child的布局參數(shù)有固定值(大于0),比如"layout_width" = "100dp"
//那么顯然child的測(cè)量規(guī)格也可以確定下來了,測(cè)量大小就是100dp,測(cè)量模式也是EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//當(dāng)子view的LayoutParams為MATCH_PARENT時(shí)(-1)
//此時(shí)父控件是精確模式,也就是能確定自己的尺寸了,那child也能確定自己大小了
//子view大小為父view大小,模式為EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//當(dāng)子view的LayoutParams為WRAP_CONTENT時(shí)(-2)
//比如TextView根據(jù)設(shè)置的字符串大小來決定自己的大小
//那就自己決定唄,不過你的大小肯定不能大于父控件的大小嘛
//所以測(cè)量模式就是AT_MOST,測(cè)量大小就是父控件的size
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當(dāng)父控件的測(cè)量模式是AT_MOST時(shí),父view強(qiáng)加給子view一個(gè)最大的值
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//同樣的,既然child能確定自己大小,盡管父控件自己還不知道自己大小,也會(huì)優(yōu)先滿足孩子的需求
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//child想要和父控件一樣大,但父控件自己也不確定自己大小,所以child也無法確定自己大小
//但同樣的,child的尺寸上限也是父控件的尺寸上限size
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//child想要根據(jù)自己邏輯決定大小,那就自己決定唄
//同樣的,child的尺寸上限也是父控件的尺寸上限size
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當(dāng)父view的模式為UNSPECIFIED時(shí),父容器不對(duì)view有任何限制,要多大給多大
// 多見于ListView、GridView
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);
}
總結(jié)下來邏輯如下:
!](https://upload-images.jianshu.io/upload_images/17755742-2a1a0809d563a88b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
至此,ViewGroup的測(cè)量流程也基本結(jié)束了,整體的流程如下圖:

不過還有一個(gè)問題不知道大家注意到?jīng)]有,measureChild方法只考慮了父View的padding,但是沒考慮到子View的margin。這就會(huì)導(dǎo)致子view在使用match_parent屬性的時(shí)候,margin屬性會(huì)有問題。
當(dāng)然,ViewGroup也考慮到了這個(gè)問題,為此也提供了另一個(gè)測(cè)量child的方法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);
}
measureChildWithMargins方法,根據(jù)名字也能看出來,比measureChild方法多考慮了個(gè)margin,源碼也跟前面的差不多,只是將margin考慮了進(jìn)去,所以一般情況下,這個(gè)方法使用的更多一些。
至此,自定義View的中最重要、最復(fù)雜的measure過程就全部總結(jié)完了。下面就該Layout過程了。
onLayout
類似measure過程,layout過程根據(jù)View的類型分為2種情況,單一View和ViewGroup。對(duì)于單身View來說,一人吃飽全家不餓,調(diào)用layout方法確定好自己的位置,設(shè)置好位置屬性的值(mLeft/mRgiht,mTop/mBottom)就行。而對(duì)于父母ViewGroup來說,還得通過調(diào)用onLayout方法幫助孩子們確定好位置。
單一View的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;
}
//當(dāng)前視圖的四個(gè)頂點(diǎn)
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//調(diào)用setFrame / setOpticalFrame方法來給View的四個(gè)頂點(diǎn)屬性賦值,
//即mLeft,mRight,mTop,mBottom四個(gè)值
//判斷當(dāng)前View大小和位置是否發(fā)生了變化 & 返回
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//調(diào)用onLayout方法
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
//監(jiān)聽View位置變化
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;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
盡管代碼有點(diǎn)長(zhǎng),不過不難看出layout方法首先會(huì)調(diào)用isLayoutModeOptical這個(gè)方法,判斷是否有光學(xué)邊界的(光學(xué)邊界這里暫時(shí)用不到,其實(shí)關(guān)鍵是我也不會(huì),想深入了解的請(qǐng)自行谷歌),之后調(diào)用setFrame或者setOpticalFrame方法來給View的四個(gè)頂點(diǎn)屬性賦值,即mLeft,mRight,mTop,mBottom四個(gè)值。那么我們?cè)賮砜纯磗etOpticalFrame方法:
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
可以看到,這個(gè)setOpticalFrame方法,最終也是調(diào)用了setFrame,那好我們可以直接繼續(xù)看setFrame方法了:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (DBG) {
Log.d("View", this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
//省略部分代碼
return changed;
}
不難看出,setFrame中先比較了新位置和老位置是否有差異,如果有差異則會(huì)調(diào)用sizechanged來更新View的位置。
setFrame后這個(gè)View的位置就確定了。之后我們也就能通過調(diào)用getWidth()和getHeight()方法來獲取View的實(shí)際寬高了。
然后,才會(huì)調(diào)用onLayout方法,由于單一View是沒有子View的,因此在View類里的onLayout方法是個(gè)空方法。
另外,在layout方法的最后我們能看到一個(gè)OnLayoutChangeListener的集合,光看名字我們也知道,這是View位置發(fā)生改變時(shí)的回調(diào)接口。所以我們可以通過addOnLayoutChangeListener方法可以監(jiān)聽一個(gè)View的位置變化,并做出想要的響應(yīng)。(不看源碼根本不知道還有這樣的方法。。。)
ViewGroup的layout過程
ViewGroup的layout過程就比View復(fù)雜一些了,大致分為兩步,首先用layout方法計(jì)算自身ViewGroup的位置,之后在onLayout中遍歷子View并且確定自身子View在ViewGroup的位置(調(diào)用子View 的 layout方法)。
其實(shí),ViewGroup的Layout的關(guān)鍵就是實(shí)現(xiàn)onLayout方法,在ViewGroup中onLayout方法被聲明成了抽象方法,這就強(qiáng)制繼承ViewGroup的類都得自己去實(shí)現(xiàn)自己定位子元素的邏輯。
由于onLayout方法之前的流程和View是一樣的,因此就不再贅述了。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// changed 當(dāng)前View的大小和位置改變了
// left 左部位置
// top 頂部位置
// right 右部位置
// bottom 底部位置
// 遍歷子View:循環(huán)所有子View
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i);
// 計(jì)算當(dāng)前子View的四個(gè)位置值
//位置的計(jì)算邏輯
//TODO
// 需自己實(shí)現(xiàn),也是自定義View的關(guān)鍵
// 對(duì)計(jì)算后的位置值進(jìn)行賦值
int mLeft = Left
int mTop = Top
int mRight = Right
int mBottom = Bottom
// 根據(jù)上述4個(gè)位置的計(jì)算值,設(shè)置子View的4個(gè)頂點(diǎn):調(diào)用子view的layout() & 傳遞計(jì)算過的參數(shù)
// 即確定了子View在父容器的位置
child.layout(mLeft, mTop, mRight, mBottom);
// 該過程類似于單一View的layout過程中的layout()和onLayout(),此處不作過多描述
}
}
}
onDraw
終于,在測(cè)量完畢,布局完成之后,我們來到了View繪制的最后一步,那就是將View繪制到屏幕上。不論是View還是ViewGroup,都是調(diào)用draw方法完成繪制,我們來看看draw的源碼:
public void draw(Canvas canvas) {
//省略部分代碼
int saveCount;
// 步驟1: 繪制本身View背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 步驟2:繪制本身View內(nèi)容
if (!dirtyOpaque)
onDraw(canvas);
// View 中:默認(rèn)為空實(shí)現(xiàn),需復(fù)寫
// ViewGroup中:需復(fù)寫
// 步驟3:繪制子View
// 由于單一View無子View,故View 中:默認(rèn)為空實(shí)現(xiàn)
// ViewGroup中:系統(tǒng)已經(jīng)復(fù)寫好對(duì)其子視圖進(jìn)行繪制我們不需要復(fù)寫
dispatchDraw(canvas);
// 步驟4:繪制裝飾,如滑動(dòng)條、前景色等等
onDrawScrollBars(canvas);
return;
}
//省略部分代碼
}
和Measure以及Layout過程一樣,View和ViewGroup是有些區(qū)別的,不過根據(jù)draw方法的源碼來看,整體流程都是下面幾個(gè)步驟:
- 繪制背景 -- drawBackground()
- 繪制自己 -- onDraw()
- 繪制孩子 -- dispatchDraw()
- 繪制裝飾 -- onDrawScrollbars()
View和ViewGroup最大的差別就是dispatchDraw()方法,由于View中不用考慮子View,那么dispatchDraw()就是一個(gè)空實(shí)現(xiàn),而ViewGroup則必須要實(shí)現(xiàn)dispatchDraw()。
那么我們?cè)賮硪徊揭徊娇纯丛创a中都做了什么:
1. 繪制背景 -- drawBackground()
private void drawBackground(Canvas canvas) {
//mBackground是該View的背景參數(shù),比如背景顏色
// 獲取背景 drawable
final Drawable background = mBackground;
//沒有背景則直接結(jié)束方法
if (background == null) {
return;
}
//根據(jù)在 layout 過程中獲取的 View 位置的四個(gè)參數(shù)來確定背景的邊界
setBackgroundBounds();
//省略部分代碼
//獲取當(dāng)前View的mScrollX和mScrollY值
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
// 調(diào)用 Drawable 的 draw 方法繪制背景
background.draw(canvas);
} else {
//如果scrollX和scrollY有值,則對(duì)canvas的坐標(biāo)進(jìn)行偏移,再繪制背景
canvas.translate(scrollX, scrollY);
// 調(diào)用 Drawable 的 draw 方法繪制背景
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
2.繪制自己 -- onDraw()
由于不同的控件都有自己不同的繪制實(shí)現(xiàn),所以View的onDraw方法肯定是空方法。在自定義繪制過程中,需由子類去實(shí)現(xiàn)復(fù)寫該方法,從而繪制自身的內(nèi)容。也就是說我們?cè)谧远xView的時(shí)候要根據(jù)實(shí)際需求對(duì)onDraw方法進(jìn)行實(shí)現(xiàn)。
3.繪制孩子 -- dispatchDraw()
protected void dispatchDraw(Canvas canvas) {
......
// 1. 遍歷子View
final int childrenCount = mChildrenCount;
......
for (int i = 0; i < childrenCount; i++) {
......
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
// 繪制子View視圖
more |= drawChild(canvas, transientChild, drawingTime);
}
....
}
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
不難看出,dispatchDraw的邏輯就是遍歷繪制子View,即遍歷調(diào)用drawChild方法,drawChild方法又調(diào)用了child的draw(canvas, this, drawingTime)方法,最后還是調(diào)用到了child的draw(canvas)方法,這樣繪制流程也就一層一層的傳遞下去了。
4.繪制裝飾 -- onDrawScrollbars()
public void onDrawForeground(Canvas canvas) {
onDrawScrollIndicators(canvas);
onDrawScrollBars(canvas);
final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (foreground != null) {
if (mForegroundInfo.mBoundsChanged) {
mForegroundInfo.mBoundsChanged = false;
final Rect selfBounds = mForegroundInfo.mSelfBounds;
final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
if (mForegroundInfo.mInsidePadding) {
selfBounds.set(0, 0, getWidth(), getHeight());
} else {
selfBounds.set(getPaddingLeft(), getPaddingTop(),
getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
}
final int ld = getLayoutDirection();
Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
foreground.setBounds(overlayBounds);
}
foreground.draw(canvas);
}
}
這一步的目的是繪制裝飾,如 滾動(dòng)指示器、滾動(dòng)條、和前景等。
至此,View的draw過程分析完畢。
總結(jié)
View的繪制流程可以總結(jié)為下圖:

從View的測(cè)量、布局和繪制原理來看,要實(shí)現(xiàn)自定義View,根據(jù)自定義View的種類不同,可能分別要自定義實(shí)現(xiàn)不同的方法。盡管源碼中調(diào)用的方法很多,但是這些方法其實(shí)不外乎:onMeasure()方法,onLayout()方法,onDraw()方法。
onMeasure()方法:?jiǎn)我籚iew,一般重寫此方法,針對(duì)wrap_content情況,規(guī)定View默認(rèn)的大小值,避免于match_parent情況一致。ViewGroup,若不重寫,就會(huì)執(zhí)行和單子View中相同邏輯,不會(huì)測(cè)量子View。一般會(huì)重寫onMeasure()方法,循環(huán)測(cè)量子View。
onLayout()方法:單一View,不需要實(shí)現(xiàn)該方法。ViewGroup必須實(shí)現(xiàn),該方法是個(gè)抽象方法,實(shí)現(xiàn)該方法,來對(duì)子View進(jìn)行布局。
onDraw()方法:無論單一View,或者ViewGroup都需要實(shí)現(xiàn)該方法。
圖片來源:Carson_Ho的自定義View
由于本人水平有限,若是文中有敘述不清晰或不準(zhǔn)確的地方,希望大家能夠指出,謝謝大家!
