在自定義控件這個學(xué)習(xí)系列里劳曹,首先寫篇文章記錄一下View的繪制流程,壓壓驚:-P欠气。也為以后的自定義控件實(shí)踐打個基礎(chǔ)。雖然講解View工作流程的文章很多镜撩,其中不乏很多精品文章预柒,不過自己能從中理清思路,以自己之言總結(jié)出來袁梗,也是十分必要的宜鸯。好的,我要開始裝...不遮怜,總結(jié)了淋袖。
1. 前言
當(dāng)我們打開手機(jī),開始看朋友圈锯梁,刷微博的時候即碗,我們有考慮過在我們眼前的一個個View是如何從無到有的展示在我們眼前的么?有考慮過它們的感受么陌凳?(神經(jīng)病才去考慮(ノ??)ノ彡┻━┻......)剥懒。
當(dāng)我們在一張紙上畫畫的時候,哪怕是簡單的一只小雞合敦,我們也不得不考慮下面幾點(diǎn):
- 這只雞得畫多大呀初橘?多寬,多高蛤肌?不能大的超過紙的范圍吧壁却?
- 這只雞畫在紙的哪里呢?是紙的中間還是靠下面一點(diǎn)呢裸准?
- 確定好大小和位置了展东,該怎么畫呢?公雞母雞炒俱?這只雞是什么形狀(當(dāng)然是雞形)盐肃?什么顏色爪膊?
其實(shí)在屏幕上“畫”一個View跟上述的流程也很相似。同樣是經(jīng)過了測量流程砸王、布局流程以及繪制流程推盛。我們都知道,Android界面布局是以一棵樹的結(jié)構(gòu)形式展現(xiàn)的谦铃,看我們的xml布局文件也看的出來耘成。而繪制出整個界面肯定是要遍歷整個View樹,對這棵樹的所有節(jié)點(diǎn)分別進(jìn)行測量驹闰,布局和繪制瘪菌。萬事皆有源頭,繪制這棵樹得從根節(jié)點(diǎn)頂級View開始畫起嘹朗,也就是DecorView师妙。至于啥是DecorView,大家可以自行去查閱資料屹培。
系統(tǒng)內(nèi)部會依次調(diào)用DecorView的measure
默穴,layout
和draw
三大流程方法。measure
方法又會調(diào)用onMeasure
方法對它所有的子元素進(jìn)行測量褪秀,如此反復(fù)調(diào)用下去就能完成整個View樹的遍歷測量蓄诽。同樣的,layout
和draw
兩個方法里也會調(diào)用相似的方法去對整個View樹進(jìn)行遍歷布局和繪制溜歪。
下面就以這三個流程來了解一下View從無到有的不容易若专。
2. 測量流程-measure
測量流程得分情況來看蝴猪,如果是單身View调衰,那自然是沒話說,自己照顧好自己自阱,本分的測量好自己就行嚎莉。而如果是為人父母的ViewGroup,那就得顧家了沛豌,除了測量好自己趋箩,還得去調(diào)用孩子們的measure
方法讓孩子們都測量好自己。甚至很多時候加派,ViewGroup
得先測量好孩子們叫确,最后才能確定自己的測量大小。一把辛酸淚...(ノへ ̄芍锦、)
下面分別來看看View和ViewGroup的測量過程:
2.1 View的measure過程
View類的measure
方法的簽名如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
看到這個方法竹勉,我得提出兩個問題:
- 形參
widthMeasureSpec
和heightMeasureSpec
是幾個意思?是用來測量自身大小的寬高么娄琉? - measure方法是final修飾的次乓,那怎么通過重寫此方法來實(shí)現(xiàn)自定義控件的測量方式呢吓歇?
要回答第1個問題,首先得弄清楚:在界面的繪制過程中票腰,View的這個方法是被它的父控件調(diào)用的城看,也就是說widthMeasureSpec
和heightMeasureSpec
是通過父控件傳遞進(jìn)來的,如果這兩個參數(shù)是完全用來決定孩子View的大小杏慰,那孩子們也太沒主動權(quán)了测柠。
事實(shí)上,這兩個參數(shù)在很大程度上是決定了一個View的尺寸的逃默,只不過孩子View可能各有各的特點(diǎn)鹃愤,它們是能根據(jù)自身的特點(diǎn)來進(jìn)行調(diào)整的,具體的呢以后再說完域。先來具體的看看MeasureSpec:
測量規(guī)格MeasureSpec
像widthMeasureSpec
這樣的32位的int類型的數(shù)肯定是有自己的故事滴,它的高2位代表測量模式Mode瘩将,低30位代表測量大小Size吟税。系統(tǒng)提供了一個MeasureSpec類來對這個參數(shù)進(jìn)行操作,代碼如下:
public static class MeasureSpec {
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;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
上面的代碼也不復(fù)雜姿现,都是通過位運(yùn)算來進(jìn)行操作的肠仪。(我在平時位運(yùn)算用的少,所以我還得慢慢捋一捋才看的明白备典。╥﹏╥...)不過异旧,這樣做的好處就是更省內(nèi)存,因?yàn)橐俏襾碜龅脑捥嵊叮隙ㄊ菫檫@樣的測量規(guī)格定義一個類吮蛹,里面有mode和size兩個屬性,這樣每次就會new很多測量規(guī)格的對象了拌屏。
好了潮针,喝口水,接著往下說倚喂。既然測量規(guī)格是由測量模式mode和測量大小size組成的每篷,size好說,那測量模式mode代表什么含義呢端圈。由上面的代碼可知焦读,測量模式有三類:
-
UNSPECIFIED
父控件不對你有任何限制,你想要多大給你多大舱权,想上天就上天矗晃。這種情況一般用于系統(tǒng)內(nèi)部,表示一種測量狀態(tài)刑巧。(這個模式主要用于系統(tǒng)內(nèi)部多次Measure的情形喧兄,并不是真的說你想要多大最后就真有多大)
-
EXACTLY
父控件已經(jīng)知道你所需的精確大小无畔,你的最終大小應(yīng)該就是這么大。
-
AT_MOST
你的大小不能大于父控件給你指定的size吠冤,但具體是多少浑彰,得看你自己的實(shí)現(xiàn)。
上面的三種模式的區(qū)別我們弄清楚了拯辙,但是父控件是怎樣給它的孩子們構(gòu)建好測量大小和測量模式的呢郭变?這其中必有蹊蹺。好吧涯保,冤有頭債有主诉濒,我們得去ViewGroup類里去找找看。ViewGroup里提供了一個靜態(tài)方法getChildMeasureSpec
用來獲取子控件的測量規(guī)格夕春,下面是代碼和詳細(xì)注釋:
/**
*
* 目標(biāo)是將父控件的測量規(guī)格和child view的布局參數(shù)LayoutParams相結(jié)合未荒,得到一個
* 最可能符合條件的child view的測量規(guī)格。
* @param spec 父控件的測量規(guī)格
* @param padding 父控件里已經(jīng)占用的大小
* @param childDimension child view布局LayoutParams里的尺寸
* @return child view 的測量規(guī)格
*/
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) {
// 當(dāng)父控件的測量模式 是 精確模式及志,也就是有精確的尺寸了
case MeasureSpec.EXACTLY:
//如果child的布局參數(shù)有固定值片排,比如"layout_width" = "100dp"
//那么顯然child的測量規(guī)格也可以確定下來了,測量大小就是100dp速侈,測量模式也是EXACTLY
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//如果child的布局參數(shù)是"match_parent"率寡,也就是想要占滿父控件
//而此時父控件是精確模式,也就是能確定自己的尺寸了倚搬,那child也能確定自己大小了
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
//如果child的布局參數(shù)是"wrap_content"冶共,也就是想要根據(jù)自己的邏輯決定自己大小,
//比如TextView根據(jù)設(shè)置的字符串大小來決定自己的大小
//那就自己決定唄每界,不過你的大小肯定不能大于父控件的大小嘛
//所以測量模式就是AT_MOST捅僵,測量大小就是父控件的size
else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當(dāng)父控件的測量模式 是 最大模式,也就是說父控件自己還不知道自己的尺寸盆犁,但是大小不能超過size
case MeasureSpec.AT_MOST:
//同樣的命咐,既然child能確定自己大小,盡管父控件自己還不知道自己大小谐岁,也優(yōu)先滿足孩子的需求
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//child想要和父控件一樣大醋奠,但父控件自己也不確定自己大小,所以child也無法確定自己大小
//但同樣的伊佃,child的尺寸上限也是父控件的尺寸上限size
else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
//child想要根據(jù)自己邏輯決定大小窜司,那就自己決定唄
else if (childDimension == LayoutParams.WRAP_CONTENT) {
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
根據(jù)上面的代碼,可以列出默認(rèn)情況下航揉,View的測量規(guī)格的生成規(guī)則:
(注:圖片來自任玉剛博客 任玉剛:Android View系統(tǒng)解析(下))
現(xiàn)在我們知道了塞祈,View的測量規(guī)格是由父控件的測量規(guī)格和自身的LayoutParams共同決定的。并且在普通情況下帅涂,會滿足上面表格里的規(guī)則议薪。但是那是在普通情況下尤蛮,而在我們自定義控件中,有時候是根據(jù)特有的邏輯去得到測量規(guī)格的斯议。所以产捞,掌握好原理,以不變應(yīng)萬變才是上策哼御。
解釋完MeasureSpec坯临,就讓我們回到一開始提出的第2個問題:
- measure方法是final修飾的,那怎么通過重寫此方法來實(shí)現(xiàn)自定義控件的測量方式呢恋昼?
我們來看看measure方法的實(shí)現(xiàn):
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
...
}
看到了我們熟悉的onMeasure
方法啦看靠,所以我們想要實(shí)現(xiàn)自己自定義控件的測量方式,就得重寫onMeasure
方法液肌。再來跟進(jìn)看看onMeasure
方法的實(shí)現(xiàn):
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
這方法一層嵌一層的挟炬,還是從里往外接著看吧,對于getSuggestedMinimumWidth
和getSuggestedMinimumHeight
方法矩屁,顧名思義辟宗,就是得到建議的最小的寬/高。什么意思呢吝秕?以getSuggestedMinimumWidth
為例:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
mMinWidth
屬性對應(yīng)的就是xml布局里的android:minWidth
屬性,設(shè)置最小寬度空幻。mBackground.getMinimumWidth()
方法返回的就是View背景Drawable的原始寬度烁峭,這個寬度跟背景的類型有關(guān)。比如我們給View的背景設(shè)置一張圖片秕铛,那這個方法返回的寬度就是圖片的寬度约郁,而如果我們給View背景設(shè)置的是顏色,那么這個方法返回的寬度則是0但两。具體的大家可以自行查閱Drawable尺寸的相關(guān)資料鬓梅。所以,這個方法的返回的寬度是:如果View沒有設(shè)置背景谨湘,那就返回xml布局里的android:minWidth
屬性定義的值绽快,默認(rèn)為0;如果View設(shè)置了背景紧阔,就返回背景的寬度和mMinWidth
中的最大值坊罢。
再來看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;//這里的size就是上面getSuggestedMinimumWidth/height的返回值
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;//測量規(guī)格里的尺寸
break;
}
return result;
}
可以看出,View在當(dāng)測量模式為UNSPECIFIED
時擅耽,返回的就是上面getSuggestedMinimumWidth/Height()
方法里的大小活孩。其實(shí)這對我們自定義控件并沒有什么影響,因?yàn)樯衔挠刑岬竭^乖仇,UNSPECIFIED
一般用于系統(tǒng)內(nèi)部的測量過程憾儒,對我們正常邏輯沒什么影響询兴。我們的重點(diǎn)還是應(yīng)該放在AT_MOST
和EXACTLY
兩種情況下。對于這兩種情況起趾,getDefaultSize
十分簡單粗暴诗舰,直接返回了specSize,也就是View的測量規(guī)格里的測量尺寸阳掐。
AT_MOST
和EXACTLY
兩種情況下返回的尺寸竟然都是specSize汛闸,這意味著什么呢?
自定義View控件時艺骂,我們需要重寫onMeasure方法并設(shè)置wrap_content時自身的大小诸老。否則在xml布局中使用wrap_content時與match_parent的效果一樣。
為什么呢钳恕?如果View在xml布局中使用wrap_content别伏,根據(jù)上面提到的規(guī)則表格,它的測量模式是AT_MOST模式忧额,測量尺寸specSize是parentSize厘肮,而getDefaultSize
方法在AT_MOST里直接返回specSize,也就是等于父容器的剩余空間大小睦番,這和match_parent是一樣的类茂。所以我們需要自己來處理AT_MOST模式下的寬高。
一個重寫onMeasure
方法來支持wrap_content屬性的模版如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int wrapWidth,wrapHeight;//根據(jù)View的邏輯得到托嚣,比如TextView根據(jù)設(shè)置的文字計算wrap_content時的大小
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(wrapWidth, wrapHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(wrapWidth, heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, wrapHeight);
}
}
以上代碼可以直接應(yīng)用到我們的自定義控件里去巩检,當(dāng)然最重要的還是大家得對AT_MOST模式留點(diǎn)心,記得對它特別對待就行示启。
好的兢哭,我們再看最外層的方法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);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
setMeasuredDimension
方法里調(diào)用了setMeasuredDimensionRaw
方法,在這個方法里面夫嗓,終于看到了我們熟悉的mMeasuredWidth
和measuredHeight
的賦值語句迟螺。從此以后,我們就可以安心的調(diào)用View的getMeasureWidth()
和getMeasureHeight()
方法了啤月!(≧?≦)?
2.2 ViewGroup的measure過程
ViewGroup并沒有重寫View的onMeasure
方法煮仇,這需要它的子類去根據(jù)相應(yīng)的邏輯去實(shí)現(xiàn),比如LinearLayout與RelativeLayout對child view的測量邏輯顯然是不同的谎仲。不過浙垫,ViewGroup倒是提供了一個measureChildren
的方法,貌似可以用來測量child的樣子,看看源碼:
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);
}
}
}
上面的代碼邏輯很清晰夹姥,就是遍歷每個孩子杉武,調(diào)用measureChild
方法對其進(jìn)行測量,接著來看看measureChild
:
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
方法里辙售,會取出child的LayoutParams轻抱,再結(jié)合父控件的測量規(guī)格和已被占用的空間Padding
,作為參數(shù)傳遞給getChildMeasureSpec
方法旦部,在getChildMeasureSpec
里會組合生成child控件的測量規(guī)格祈搜。getChildMeasureSpec
方法的邏輯在上面的MeasureSpec部分有詳細(xì)說明。最后士八,當(dāng)然還是得調(diào)用child的measure
方法啦容燕,讓孩子根據(jù)父母的指引去測量自己。
在我看來婚度,我們在自己自定義控件時蘸秘,上面的這兩個方法幾乎不會用到。因?yàn)?code>measureChildren太過簡單粗暴蝗茁,我們一般都會考慮孩子們之間的邏輯關(guān)系(順序醋虏、間隔等),再計算他們的測量規(guī)格哮翘。不過這個方法也給我們一點(diǎn)啟示颈嚼,就是:
測量子元素時,對可見性為GONE的View要做特殊處理饭寺,一般來說就是跳過對它們的測量粘舟,來優(yōu)化布局。
而measureChild
方法只考慮了父控件的padding佩研,但是沒考慮到child view的margin,這就會導(dǎo)致child view在使用match_parent
屬性的時候霞揉,margin屬性會有問題旬薯。(什么?你說你自定義的ViewGroup對孩子不支持margin屬性不就不會有問題了么...是是是适秩,那當(dāng)我沒說....)當(dāng)然绊序,ViewGroup里為此也提供了另一個測量child的方法:
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
方法,顧名思義秽荞,比measureChild
方法多考慮了個margin骤公。看源碼也看得出來扬跋,的確是這樣阶捆。所以一般情況下,這個方法使用的更多一些。
3.布局流程-layout
布局的流程就沒有測量流程那么“蜿蜒曲折”了洒试。對于單身View來說倍奢,調(diào)用layout
方法確定好自己的位置,設(shè)置好位置屬性的值(mLeft/mRgiht
,mTop/mBottom
)就行垒棋。而對于父母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;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
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;
}
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
...
// Invalidate our old position
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
...
}
return changed;
}
從上面的代碼叼架,能看到layout
方法首先會調(diào)用setFrame
方法來給View的四個頂點(diǎn)屬性賦值畔裕,即mLeft,mRight乖订,mTop扮饶,mBottom四個值,此時這個View的位置就確定了垢粮。同時我們也就能通過調(diào)用getWidth()
和getHeight()
方法來獲取View的實(shí)際寬高了贴届。
接下來醉锅,onLayout
方法才會被調(diào)用卒密,這也意味著我們在自定義ViewGroup時瞭吃,想要重寫onLayout
方法給我們的子元素定位轿亮,是可以直接調(diào)用getWidth()
和getHeight()
方法來獲取ViewGroup的真實(shí)寬高的攻走。在View類里的onLayout
方法是個空方法尉辑,而在ViewGroup方法里聲明成了抽象方法殿如,所以繼承ViewGroup的類都得自己去實(shí)現(xiàn)自己定位子元素的邏輯柴墩。
最后君仆,在layout
方法的最后我們能看到一個OnLayoutChangeListener
的集合翩概,看名字我們也猜得出,這是View位置發(fā)生改變時的回調(diào)接口返咱。所以我們可以通過addOnLayoutChangeListener
方法可以監(jiān)聽一個View的位置變化钥庇,并做出想要的響應(yīng)。(看源碼的時候才發(fā)現(xiàn)這個回調(diào)接口的咖摹,以前都不知道评姨。新技能get!︿( ̄︶ ̄)︿)
4.繪制流程-draw
繪制的流程也就是通過調(diào)用View的draw
方法實(shí)現(xiàn)的萤晴。draw
方法里的邏輯看起來更清晰吐句,我就不貼源碼了。一般是遵循下面幾個步驟:
- 繪制背景 -- drawBackground()
- 繪制自己 -- onDraw()
- 繪制孩子 -- dispatchDraw()
- 繪制裝飾 -- onDrawScrollbars()
由于不同的控件都有自己不同的繪制實(shí)現(xiàn)店读,所以View的onDraw
方法肯定是空方法嗦枢。而ViewGroup由于需要照顧孩子們的繪制,所以肯定在dispatchDraw
方法里遍歷調(diào)用了child的draw
方法屯断。不信文虏?不信咱來看看ViewGroup里重寫的dispatchDraw
方法:
protected void dispatchDraw(Canvas canvas) {
...
for (int i = 0; i < childrenCount; i++) {
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
ViewGroup里的dispatchDraw
方法遍歷調(diào)用drawChild
方法侣诺,drawChild
方法又調(diào)用了child的draw(canvas, this, drawingTime)
方法,最后還是調(diào)用到了child的draw(canvas)
方法择葡。如此這般紧武,繪制流程也就一層一層的傳遞下去了。
好的敏储,說完了...(*???).........................................
5.總結(jié)
我已經(jīng)無力總結(jié)了阻星,沒想到一篇總結(jié)的文章寫了我兩天半...。不過已添,自己在總結(jié)的過程中確實(shí)也學(xué)到了蠻多妥箕,加深了對View的繪制流程的理解,也弄清楚了一些模糊的知識點(diǎn)更舞。當(dāng)然了畦幢,也希望我的文章能對正在學(xué)Android開發(fā)的小伙伴們有所幫助。
當(dāng)然了缆蝉,這些都屬于自定義控件的基本功宇葱,還需要在實(shí)踐中多積累一些相關(guān)的經(jīng)驗(yàn),并逐漸做到融會貫通刊头,這樣才能提高自己的水平黍瞧。keep going!
6.預(yù)告
下一篇文章打算記錄LayoutParams相關(guān)的一些知識。敬請關(guān)注......
我是蘑菇君原杂,我為自己帶鹽
7.參考資料
- 《Android開發(fā)藝術(shù)探索》
- 郭霖:Android視圖繪制流程完全解析印颤,帶你一步步深入了解View(二)
- 任玉剛:Android View系統(tǒng)解析(下)