一:自定義View繪制流程函數(shù)調(diào)用鏈
二.幾個(gè)重要的函數(shù)
構(gòu)造函數(shù)是View的入口灌侣,可以用于初始化一些的內(nèi)容辈赋,和獲取自定義屬性。
View的構(gòu)造函數(shù)有四種重載分別如下:
public void SloopView(Context context) {}
public void SloopView(Context context,AttributeSet attrs) {}
public void SloopView(Context context,AttributeSet attrs,int defStyleAttr) {}
public void SloopView(Context context,AttributeSet attrs,int defStyleAttr,int defStyleRes) {}
可以看出赦抖,關(guān)于View構(gòu)造函數(shù)的參數(shù)有多有少惧所,先排除幾個(gè)不常用的崖咨,留下常用的再研究锻拘。
有四個(gè)參數(shù)的構(gòu)造函數(shù)在API21的時(shí)候才添加上,暫不考慮击蹲。
有三個(gè)參數(shù)的構(gòu)造函數(shù)中第三個(gè)參數(shù)是默認(rèn)的Style署拟,這里的默認(rèn)的Style是指它在當(dāng)前Application或Activity所用的Theme中的默認(rèn)Style,且只有在明確調(diào)用的時(shí)候才會(huì)生效歌豺,以系統(tǒng)中的ImageButton為例說(shuō)明:
public ImageButton(Context context,AttributeSet attrs) {//調(diào)用了三個(gè)參數(shù)的構(gòu)造函數(shù)推穷,明確指定第三個(gè)參數(shù)this(context, attrs,com.android.internal.R.attr.imageButtonStyle);? ? }
publicImageButton(Context context,AttributeSet attrs, int defStyleAttr) {//此處調(diào)了四個(gè)參數(shù)的構(gòu)造函數(shù),無(wú)視即可this(context, attrs, defStyleAttr,0);? ? }
注意:即使你在View中使用了Style這個(gè)屬性也不會(huì)調(diào)用三個(gè)參數(shù)的構(gòu)造函數(shù)类咧,所調(diào)用的依舊是兩個(gè)參數(shù)的構(gòu)造函數(shù)馒铃。
由于三個(gè)參數(shù)的構(gòu)造函數(shù)第三個(gè)參數(shù)一般不用,暫不考慮痕惋,第三個(gè)參數(shù)的具體用法會(huì)在以后用到的時(shí)候詳細(xì)介紹区宇。
排除了兩個(gè)之后,只剩下一個(gè)參數(shù)和兩個(gè)參數(shù)的構(gòu)造函數(shù)值戳,他們的詳情如下:
//一般在直接New一個(gè)View的時(shí)候調(diào)用议谷。
public void SloopView(Context context) {}
//一般在layout文件中使用的時(shí)候會(huì)調(diào)用,關(guān)于它的所有屬性(包括自定義屬性)都會(huì)包含在attrs中傳遞進(jìn)來(lái)堕虹。
public void SloopView(Context context,AttributeSet attrs) {}
以下方法調(diào)用的是一個(gè)參數(shù)的構(gòu)造函數(shù):
//在Avtivity中SloopViewview=newSloopView(this);
以下方法調(diào)用的是兩個(gè)參數(shù)的構(gòu)造函數(shù):
//在layout文件中 - 格式為: <包名.View名 />
關(guān)于構(gòu)造函數(shù)先講這么多卧晓,關(guān)于如何自定義屬性和使用attrs中的內(nèi)容芬首,在后面會(huì)詳細(xì)講解,目前只需要知道這兩個(gè)構(gòu)造函數(shù)在何時(shí)調(diào)用即可逼裆。
2.onAttachedToWindow():
運(yùn)行在onResume()之后郁稍;此函數(shù)會(huì)調(diào)用Activity的onResume()生命周期,所以在onResume之后可以設(shè)置窗體尺寸胜宇。
3.Measure
具體分析
measure 過(guò)程由measure(int, int)方法發(fā)起艺晴,從上到下有序的測(cè)量 View,在 measure 過(guò)程的最后掸屡,每個(gè)視圖存儲(chǔ)了自己的尺寸大小和測(cè)量規(guī)格封寞。 layout 過(guò)程由layout(int, int, int, int)方法發(fā)起,也是自上而下進(jìn)行遍歷仅财。在該過(guò)程中狈究,每個(gè)父視圖會(huì)根據(jù) measure 過(guò)程得到的尺寸來(lái)擺放自己的子視圖。
measure 過(guò)程會(huì)為一個(gè) View 及所有子節(jié)點(diǎn)的 mMeasuredWidth 和 mMeasuredHeight 變量賦值盏求,該值可以通過(guò)getMeasuredWidth()和getMeasuredHeight()方法獲得抖锥。而且這兩個(gè)值必須在父視圖約束范圍之內(nèi),這樣才可以保證所有的父視圖都接收所有子視圖的測(cè)量碎罚。如果子視圖對(duì)于 Measure 得到的大小不滿意的時(shí)候磅废,父視圖會(huì)介入并設(shè)置測(cè)量規(guī)則進(jìn)行第二次 measure。比如荆烈,父視圖可以先根據(jù)未給定的 dimension 去測(cè)量每一個(gè)子視圖拯勉,如果最終子視圖的未約束尺寸太大或者太小的時(shí)候,父視圖就會(huì)使用一個(gè)確切的大小再次對(duì)子視圖進(jìn)行 measure憔购。
measure 過(guò)程傳遞尺寸的兩個(gè)類
ViewGroup.LayoutParams (View 自身的布局參數(shù))
MeasureSpecs 類(父視圖對(duì)子視圖的測(cè)量要求)
ViewGroup.LayoutParams
這個(gè)類我們很常見(jiàn)宫峦,就是用來(lái)指定視圖的高度和寬度等參數(shù)。對(duì)于每個(gè)視圖的 height 和 width玫鸟,你有以下選擇:
具體值
MATCH_PARENT 表示子視圖希望和父視圖一樣大(不包含 padding 值)
WRAP_CONTENT 表示視圖為正好能包裹其內(nèi)容大小(包含 padding 值)
ViewGroup 的子類有其對(duì)應(yīng)的 ViewGroup.LayoutParams 的子類导绷。比如 RelativeLayout 擁有的 ViewGroup.LayoutParams 的子類 RelativeLayoutParams。
有時(shí)我們需要使用 view.getLayoutParams() 方法獲取一個(gè)視圖 LayoutParams屎飘,然后進(jìn)行強(qiáng)轉(zhuǎn)妥曲,但由于不知道其具體類型,可能會(huì)導(dǎo)致強(qiáng)轉(zhuǎn)錯(cuò)誤钦购。其實(shí)該方法得到的就是其所在父視圖類型的 LayoutParams檐盟,比如 View 的父控件為 RelativeLayout,那么得到的 LayoutParams 類型就為 RelativeLayoutParams肮雨。
measure 核心方法
measure(int widthMeasureSpec, int heightMeasureSpec)
該方法定義在View.java類中遵堵,為 final 類型,不可被復(fù)寫,但 measure 調(diào)用鏈最終會(huì)回調(diào) View/ViewGroup 對(duì)象的onMeasure()方法陌宿,因此自定義視圖時(shí)锡足,只需要復(fù)寫onMeasure()方法即可。
4.onMeasure(int widthMeasureSpec, int heightMeasureSpec)
該方法就是我們自定義視圖中實(shí)現(xiàn)測(cè)量邏輯的方法壳坪,該方法的參數(shù)是父視圖對(duì)子視圖的 width 和 height 的測(cè)量要求舶得。在我們自身的自定義視圖中,要做的就是根據(jù)該 widthMeasureSpec 和 heightMeasureSpec 計(jì)算視圖的 width 和 height爽蝴,不同的模式處理方式不同沐批。
Q: 為什么要測(cè)量View大小蝎亚?
A: View的大小不僅由自身所決定九孩,同時(shí)也會(huì)受到父控件的影響,為了我們的控件能更好的適應(yīng)各種情況发框,一般會(huì)自己進(jìn)行測(cè)量躺彬。
測(cè)量View大小使用的是onMeasure函數(shù),我們可以從onMeasure的兩個(gè)參數(shù)中取出寬高的相關(guān)數(shù)據(jù):
@OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec{
int widthsize=MeasureSpec.getSize(widthMeasureSpec);//取出寬度的確切數(shù)值
int widthmode=MeasureSpec.getMode(widthMeasureSpec);//取出寬度的測(cè)量模式
int heightsize=MeasureSpec.getSize(heightMeasureSpec);//取出高度的確切數(shù)值
int heightmode=MeasureSpec.getMode(heightMeasureSpec);//取出高度的測(cè)量模式
}
setMeasuredDimension()
測(cè)量階段終極方法梅惯,在onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中調(diào)用宪拥,將計(jì)算得到的尺寸,傳遞給該方法铣减,測(cè)量階段即結(jié)束她君。該方法也是必須要調(diào)用的方法,否則會(huì)報(bào)異常葫哗。在我們?cè)谧远x視圖的時(shí)候缔刹,不需要關(guān)心系統(tǒng)復(fù)雜的 Measure 過(guò)程的,只需調(diào)用setMeasuredDimension()設(shè)置根據(jù) MeasureSpec 計(jì)算得到的尺寸即可魄梯,你可以參考ViewPagerIndicator的 onMeasure 方法桨螺。
View.MeasureSpec中:
模式二進(jìn)制數(shù)值描述
UNSPECIFIED00默認(rèn)值宾符,父控件沒(méi)有給子view任何限制酿秸,子View可以設(shè)置為任意大小。
EXACTLY01表示父控件已經(jīng)確切的指定了子View的大小魏烫。
AT_MOST10表示子View具體大小沒(méi)有尺寸限制辣苏,但是存在上限,上限一般為父View大小哄褒。
源碼解析:ANDROID自定義視圖——onMeasure稀蟋,MeasureSpec源碼 流程 思路詳解
5.確定View大小(onSizeChanged)
這個(gè)函數(shù)在視圖大小發(fā)生改變時(shí)調(diào)用。
Q: 在測(cè)量完View并使用setMeasuredDimension函數(shù)之后View的大小基本上已經(jīng)確定了呐赡,那么為什么還要再次確定View的大小呢退客?
A: 這是因?yàn)閂iew的大小不僅由View本身控制,而且受父控件的影響,所以我們?cè)诖_定View大小的時(shí)候最好使用系統(tǒng)提供的onSizeChanged回調(diào)函數(shù)萌狂。
onSizeChanged如下:
@OverrideprotectedvoidonSizeChanged(intw,inth,intoldw,intoldh) {super.onSizeChanged(w, h, oldw, oldh);? ? }
可以看出档玻,它又四個(gè)參數(shù),分別為 寬度茫藏,高度误趴,上一次寬度,上一次高度务傲。
這個(gè)函數(shù)比較簡(jiǎn)單凉当,我們只需關(guān)注 寬度(w), 高度(h) 即可,這兩個(gè)參數(shù)就是View最終的大小售葡。
6.確定子View布局位置(onLayout)
首先要明確的是看杭,子視圖的具體位置都是相對(duì)于父視圖而言的。View 的 onLayout 方法為空實(shí)現(xiàn)挟伙,而 ViewGroup 的 onLayout 為 abstract 的泊窘,因此,如果自定義的 View 要繼承 ViewGroup 時(shí)像寒,必須實(shí)現(xiàn) onLayout 函數(shù)烘豹。
在 layout 過(guò)程中,子視圖會(huì)調(diào)用getMeasuredWidth()和getMeasuredHeight()方法獲取到 measure 過(guò)程得到的 mMeasuredWidth 和 mMeasuredHeight诺祸,作為自己的 width 和 height携悯。然后調(diào)用每一個(gè)子視圖的layout(l, t, r, b)函數(shù),來(lái)確定每個(gè)子視圖在父視圖中的位置筷笨。
確定布局的函數(shù)是onLayout憔鬼,它用于確定子View的位置,在自定義ViewGroup中會(huì)用到胃夏,他調(diào)用的是子View的layout函數(shù)轴或。
在自定義ViewGroup中,onLayout一般是循環(huán)取出子View仰禀,然后經(jīng)過(guò)計(jì)算得出各個(gè)子View位置的坐標(biāo)值照雁,然后用以下函數(shù)設(shè)置子View位置。
child.layout(l, t, r, b);
四個(gè)參數(shù)分別為:
名稱說(shuō)明對(duì)應(yīng)的函數(shù)
lView左側(cè)距父View左側(cè)的距離getLeft();
tView頂部距父View頂部的距離getTop();
rView右側(cè)距父View左側(cè)的距離getRight();
bView底部距父View頂部的距離getBottom();
具體可以參考坐標(biāo)系這篇文章答恶。
7.draw繪制流程相關(guān)概念及核心方法
View.draw(Canvas canvas): 由于 ViewGroup 并沒(méi)有復(fù)寫此方法饺蚊,因此,所有的視圖最終都是調(diào)用 View 的 draw 方法進(jìn)行繪制的悬嗓。在自定義的視圖中污呼,也不應(yīng)該復(fù)寫該方法,而是復(fù)寫onDraw(Canvas)方法進(jìn)行繪制包竹,如果自定義的視圖確實(shí)要復(fù)寫該方法燕酷,那么請(qǐng)先調(diào)用super.draw(canvas)完成系統(tǒng)的繪制籍凝,然后再進(jìn)行自定義的繪制。
View.onDraw():
View 的onDraw(Canvas)默認(rèn)是空實(shí)現(xiàn)苗缩,自定義繪制過(guò)程需要復(fù)寫的方法静浴,繪制自身的內(nèi)容。
drawChild(canvas, this, drawingTime)
直接調(diào)用了 View 的child.draw(canvas, this,drawingTime)方法挤渐,文檔中也說(shuō)明了苹享,除了被ViewGroup.drawChild()方法外,你不應(yīng)該在其它任何地方去復(fù)寫或調(diào)用該方法浴麻,它屬于 ViewGroup得问。而View.draw(Canvas)方法是我們自定義控件中可以復(fù)寫的方法,具體可以參考上述對(duì)view.draw(Canvas)的說(shuō)明软免。從參數(shù)中可以看到宫纬,child.draw(canvas, this, drawingTime)肯定是處理了和父視圖相關(guān)的邏輯,但 View 的最終繪制膏萧,還是View.draw(Canvas)方法漓骚。
invalidate()
請(qǐng)求重繪 View 樹,即 draw 過(guò)程榛泛,假如視圖發(fā)生大小沒(méi)有變化就不會(huì)調(diào)用layout()過(guò)程蝌蹂,并且只繪制那些調(diào)用了invalidate()方法的 View。
requestLayout()
當(dāng)布局變化的時(shí)候曹锨,比如方向變化孤个,尺寸的變化,會(huì)調(diào)用該方法沛简,在自定義的視圖中齐鲤,如果某些情況下希望重新測(cè)量尺寸大小,應(yīng)該手動(dòng)去調(diào)用該方法椒楣,它會(huì)觸發(fā)measure()和layout()過(guò)程给郊,但不會(huì)進(jìn)行 draw。
dispatchDraw() 發(fā)起對(duì)子視圖的繪制捧灰。View 中默認(rèn)是空實(shí)現(xiàn)淆九,ViewGroup 復(fù)寫了dispatchDraw()來(lái)對(duì)其子視圖進(jìn)行繪制。該方法我們不用去管凤壁,自定義的 ViewGroup 不應(yīng)該對(duì)dispatchDraw()進(jìn)行復(fù)寫吩屹。
Android的view組件顯示主要經(jīng)過(guò)mesure, layout和draw這三個(gè)過(guò)程。在mesure階段里調(diào)用mesure(int widthSpec, int heightSpec)方法拧抖,這個(gè)方法是final不能被重寫,在這個(gè)過(guò)程里會(huì)調(diào)用onMesure(int widthSpec, int heightSpec)方法免绿。當(dāng)組件設(shè)置好大小后唧席,調(diào)用final layout(int l, int t, int r, int b)方法進(jìn)行布局,在這個(gè)過(guò)程里會(huì)調(diào)用onLayout(boolean changed, int l, int t, int r, int b)方法,所以處理組件的布局通常要重寫onMesure和onLayout這兩個(gè)方法淌哟。
View組件的繪制會(huì)調(diào)用draw(Canvas canvas)方法迹卢,這個(gè)方法在源代碼里看不到在哪里調(diào)用...draw過(guò)程中主要是先畫Drawable背景,對(duì)drawable調(diào)用setBounds()然后是draw(Canvas c)方法.有點(diǎn)注意的是背景drawable的實(shí)際大小會(huì)影響view組件的大小徒仓,drawable的實(shí)際大小通過(guò)getIntrinsicWidth()和getIntrinsicHeight()獲取腐碱,當(dāng)背景比較大時(shí)view組件大小等于背景drawable的大小,不過(guò)俺沒(méi)有在源代碼里找到布局時(shí)調(diào)用過(guò)getIntrinsicWidth()和getIntrinsicHeight()方法...
畫完背景后掉弛,draw過(guò)程會(huì)調(diào)用onDraw(Canvas canvas)方法症见,然后就是dispatchDraw(Canvas canvas)方法, dispatchDraw()主要是分發(fā)給子組件進(jìn)行繪制,我們通常定制組件的時(shí)候重寫的是onDraw()方法殃饿。值得注意的是ViewGroup容器組件的繪制谋作,當(dāng)它沒(méi)有背景時(shí)直接調(diào)用的是dispatchDraw()方法, 而繞過(guò)了draw()方法督暂,當(dāng)它有背景的時(shí)候就調(diào)用draw()方法乔遮,而draw()方法里包含了dispatchDraw()方法的調(diào)用。因此要在ViewGroup上繪制東西的時(shí)候往往重寫的是dispatchDraw()方法而不是onDraw()方法寞钥,或者自定制一個(gè)Drawable奈惑,重寫它的draw(Canvas c)和getIntrinsicWidth(),
getIntrinsicHeight()方法吭净,然后設(shè)為背景。
參考文檔: