引言
Android實際項目開發(fā)中,自定義View不可或缺,而作為自定義View的一種重要實現(xiàn)方式——繼承View重繪尤其重要,前面很多文章基本總結(jié)了繼承View的基本流程:自定義屬性和繼承View重寫onDraw方法——>實現(xiàn)構(gòu)造方法并完成相關(guān)初始化操作——>重寫onMeasure方法——>onSizeChanged()拿到view的寬高等數(shù)據(jù)——>重寫onLayout————>重寫onTouch實現(xiàn)交互——>定義交互回調(diào)接口,但是由于當時的具體的業(yè)務(wù)需求并沒有詳解總結(jié)下關(guān)于onMeasure和onLayout方法,相信很多初學者都是處于知其然而不知其所以然,這篇文章就專門總結(jié)下。
一、View的系統(tǒng)架構(gòu)
雖然前面已經(jīng)總結(jié)過了,但是這在里還是重申下,加深印象??偹苤?,在Android中每一個控件都會再界面中占據(jù)一塊矩形的區(qū)域,這和大多數(shù)系統(tǒng)的控件機制都差不多。Android中控件是通過構(gòu)造樹的形式來管理的(所謂控件樹如下圖所示),主要分為View和ViewGroup兩大類,其中ViewGroup直接繼承自View,View作為系統(tǒng)所有可視組件的基類,而通過控件樹,上層控件負責下層子控件的測量與繪制(即先執(zhí)行onMeasure——>onLayout——>onDraw的),并負責分發(fā)交互事件的即事件是先傳遞到ViewGroup的,再由ViewGroup決定是否傳遞給下層子View。而這顆樹的根節(jié)點ViewParent(其實質(zhì)是一個接口定義了一系列管理View的方法)對于該控件樹所有的交互事件驚喜統(tǒng)一管理和分發(fā),從而實現(xiàn)對整個樹進行整體控制。
二、View、ViewGroup的測量和繪制概述
Android中的GUI系統(tǒng)是客戶端和服務(wù)端配合的窗口系統(tǒng),即后臺運行了一個繪制服務(wù),每個應(yīng)用程序都是該服務(wù)端的一個客戶端,當客戶端需要繪制時,首先請求服務(wù)端創(chuàng)建一個窗口,然后在窗口中進行具體的視圖內(nèi)容繪制;對于每個客戶端而言,他們都感覺自己獨占了屏幕,而對于服務(wù)端而言,它會給每一個客戶端窗口分配不同的層值,并根據(jù)用戶的交互情況動態(tài)改變窗口的層值,這就給用戶造成了所謂的前臺窗口和后臺窗口的概念。當然這是屏幕繪制的原理簡要描述,繪制離不開測量,無論是系統(tǒng)控件和自定義的View要想展示于界面之上都離不開測量工作。簡而言之,當Activity獲得焦點時,Activity將被通知要求繪制自己的布局,從而Android framework接到Activity的消息將會處理繪制過程,而Activity只需提供它的布局的根節(jié)點即可。繪制過程是從布局的根節(jié)點開始,從根節(jié)點開始測量和繪制整個View tree。每一個父級ViewGroup 負責要求它的每一個孩子被繪制,每一個子View負責繪制自己。因為整個View tree是按順序遍歷的,所以父節(jié)點會先被繪制,而兄弟節(jié)點會按照它們在樹中出現(xiàn)的順序被繪制。完整的繪制是包含兩個過程:測量Measure 和布局Layout。測量過程(measuring pass)是在measure(int, int)中實現(xiàn)的,是從樹的頂端由上到下進行的(top-down)。在這個遞歸過程中,每一個View會把自己的dimension specifications傳遞下去。在測量Measure 完成之后,每一個View都存儲好了自己的測量結(jié)果。再者就是是布局Layout,它發(fā)生在 layout(int, int, int, int)中,仍然是從上到下進行,每一個父級都會負責用測量過程中得到的尺寸,把自己的所有孩子放在正確的地方。
1、View的測量
由父級ViewGroup負責要求子級View進行測量和繪制。我們都知道每一個控件都會占據(jù)一個矩形區(qū)域,但是Android系統(tǒng)在繪制前本身并不知道具體的大小和位置,所以它會先進行測量,主要是在View的onMeasure里去實現(xiàn)(這也是我們在自定義View里的構(gòu)造方法里,無論是調(diào)用getMeasureWidth抑或getWidth獲取寬度時得到的總是0的原因),而Android中還有一個功能類MeasureSpec(封裝了從父節(jié)點傳遞到子節(jié)點下的布局信息包括View的測量模式和大?。┯糜谳o助測量View,當我們重寫了onMeasure方法之后,系統(tǒng)通過super.onMeasure方法去調(diào)用setMeasuredDimension(width, height)將測量的大小設(shè)置進去完成測量。
2、View的繪制
完成測量工作之后,View的根據(jù)ViewGroup傳人的測量值和模式,對自己寬高進行確定(onMeasure中完成),然后在onDraw中在Canvas上完成對自己的繪制。
3、ViewGroup的測量
ViewGroup需要管理子View,所有其中一項重要的職責就是負責子View的大小,當ViewGroup大小設(shè)置為wrap_content,ViewGroup會對子View進行層級遍歷,來決定自己的大小,而其他模式下則會取設(shè)置的值來為自己的大小。ViewGroup在測量時遍歷所有子View,從調(diào)用子View對應(yīng)的onMeasure方法獲得子View的大小,完成測量之后再通過調(diào)用onLayout方法來決定子View的位置,同樣是通過遍歷調(diào)用子View的onLayout方法,最后在自己的onLayout中完成子View的位置布局工作。
4、ViewGroup的繪制
ViewGroup通常不需要繪制,因為它本身沒有需要繪制的東西,所以不會觸發(fā)自己的onDraw方法,但如果指定了background屬性則會觸發(fā)自身的onDraw完成背景的繪制。但ViewGroup會通過dispatchDraw方法來繪制其子View,原理也是一樣通過遍歷子View調(diào)用其子View對應(yīng)的onDraw方法來完成最終的繪制工作。
三、View.MeasureSpec和ViewGroup.LayoutParams
1、View.MeasureSpec
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* UNSPECIFIED 模式:
* 父View不對子View有任何限制,子View需要多大就多大
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* EXACTYLY 模式:
* 父View已經(jīng)測量出子Viwe所需要的精確大小,這時候View的最終大小
* 就是SpecSize所指定的值。對應(yīng)于match_parent和精確數(shù)值這兩種模式
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* AT_MOST 模式:
* 子View的最終大小是父View指定的SpecSize值,并且子View的大小不能大于這個值,
* 即對應(yīng)wrap_content這種模式
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
//將size和mode打包成一個32位的int型數(shù)值
//高2位表示SpecMode,測量模式,低30位表示SpecSize,某種測量模式下的規(guī)格大小
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//將32位的MeasureSpec解包,返回SpecMode,測量模式
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//將32位的MeasureSpec解包,返回SpecSize,某種測量模式下的規(guī)格大小
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
//...
}
View.MeasureSpec是View中的一個靜態(tài)內(nèi)部類,封裝了從父級節(jié)點傳遞下來給子級節(jié)點的布局需求信息,每一個MeasureSpec體現(xiàn)的是子類的布局的尺寸大小size(包括寬度或高度)和模式mode的需求,但是并不是子級的實際尺寸就必須是父級要求的,我們可以通過重寫onMeasure方法實現(xiàn)自己的規(guī)則,然后在子級中,而這里的模式來源于父ViewGroup去解析子View對應(yīng)的在布局文件中l(wèi)ayout_width和layout_height值來決定采用什么模式(至于怎么解析,這是后話),其中主要有三種模式:UNSPECIFIED、EXACTLY、AT_MOST:
UNSPECIFIED:說明父級沒有對子級強加任何限制,子級可以是它想要的任何尺寸。用得比較少,表示子布局被限制在一個最大值內(nèi),一般當childView設(shè)置其寬、高為wrap_content時,ViewGroup會將其設(shè)置為AT_MOST,換言之,表示子布局想要多大就多大,一般出現(xiàn)在AadapterView的item的heightMode中、ScrollView的childView的heightMode中
EXACTLY:父級為子級決定了一個確切的尺寸,子級將會被強制賦予這些邊界限制,不管子級自己想要多大(View類onMeasure方法中只支持EXACTLY),換言之,表示設(shè)置了精確的值,一般當childView設(shè)置其寬、高為精確值、match_parent時,ViewGroup會將其設(shè)置為EXACTLY,即在布局文件中可以解析指定的具體尺寸和match_parent,不支持wrap_content。
AT_MOST:子級可以是自己指定的任意大小,但是有個上限。比如說當MeasureSpec.EXACTLY的父容器為子級決定了一個大小,子級大小只能在這個父容器限制的范圍之內(nèi)。即在布局文件中可以解析wrap_content,換言之,表示子布局被限制在一個最大值內(nèi),一般當childView設(shè)置其寬、高為wrap_content時,ViewGroup會將其設(shè)置為AT_MOST。
| 方法 | 說明 |
|---|---|
| static int getMode(int measureSpec) | 獲取模式 |
| static int getSize(int measureSpec) | 獲取尺寸 |
| static int makeMeasureSpec(int size, int mode) | 根據(jù)指定的模式和尺寸創(chuàng)建對應(yīng)的測量規(guī)則 |
2、ViewGroup.LayoutParams
ViewGroup.LayoutParams直接繼承于Object作為位置參數(shù)信息的父類,是View用來告訴它的父容器它想要怎樣被放置的(包含高度、寬度、對齊方式、外邊距、內(nèi)邊距等等),Android中的布局信息ViewGroup.LayoutParams家族來決定的,常見包括AbsListView.LayoutParams, AbsoluteLayout.LayoutParams, Gallery.LayoutParams, ViewGroup.MarginLayoutParams, ViewPager.LayoutParams, WindowManager.LayoutParams、ActionBar.LayoutParams, ActionMenuView.LayoutParams, AppBarLayout.LayoutParams, BaseCardView.LayoutParams, BoxInsetLayout.LayoutParams,CollapsingToolbarLayout.LayoutParams,CoordinatorLayout.LayoutParams,DrawerLayout.LayoutParams,FrameLayout.LayoutParams,GridLayout.LayoutParams, GridLayoutManager.LayoutParams, LinearLayout.LayoutParams, LinearLayoutCompat.LayoutParams,PercentFrameLayout.LayoutParams、RelativeLayout.LayoutParams等。不同的Layout提供了不同LayoutParams,它們共同承擔起整個Android 的布局任務(wù)。
四、View中幾大重要的方法的意義和作用
繼承View/ViewGroup實現(xiàn)自定義View后,一般還需要復寫最基本的二、三個方法:onMeasure(),onSizeChanged()拿到view的寬高等數(shù)據(jù)、onLayout()、onDraw()。
onMeasure:用于本View寬高的測量,布局復雜時可能觸發(fā)多次。ViewGroup的onMeasure則負責處理它children的測量工作。由于View默認的onMeasure()僅僅支持EXACTLY模式,也就是說如果不重寫onMeasure()方法的話則無法在正確解析布局文件里的wrap_content,因為onMeasure()是Android提供給我們告訴系統(tǒng)自己定義的View的實際大?。ㄊ欠袷莾H僅依賴于父級要求的,也就是說自主定義View大小的)的機會,同時也是提供了我們自定義的解析規(guī)則的方法(如果你愿意,你可以完全實現(xiàn)match_parent和wrap_content和具體值一樣的效果),最終調(diào)用setMeasuredDimension(int ,int)完成最終的測量(因為onMeasure方法沒有返回值,所以測量的結(jié)果應(yīng)該通過setMeasuredDimension方法告知系統(tǒng))。
onSizeChanged():可拿到view的寬高等數(shù)據(jù)信息
onLayout:常復寫于viewGroup的自定義子類。它有負責對它內(nèi)部所有children進行處理,告知childrenView的位置,以正確擺放。ViewGroup中onLayout是抽象方法必須復寫,這是children位置能正確擺放的保證。依靠mLeft,mTop,mRight,mBottom這四個值,以坐上為原點,這四個值分別為對應(yīng)邊到原點的距離。最后和onMeasure一樣,記得調(diào)用child.layout()方法。
onDraw:UI最終呈現(xiàn)的過程,用戶使用Paint(What to draw)、Canvas(How to
draw)兩個類完成自定義畫面。繪制的時候需要考慮下padding,與margin不同,padding是屬于本View的屬性,不同于margin(不需要自定義時做處理系統(tǒng)就能很好的使用margin),所以要在測量繪圖時考慮它:測量時:desireSize=實際所需size+相應(yīng)方向的padding。
繪圖時:考慮padding,做相應(yīng)的位移。
五、onMeasure方法詳解及實現(xiàn)
onMeasure方法是測量View及其內(nèi)容的,決定measured width和measured height的,這個方法由 measure(int, int)方法喚起,子類可以重寫onMeasure來提供更加準確和有效的測量。(以前有一個約定:在重寫onMeasure方法的時候,必須調(diào)用 setMeasuredDimension(int,int)來存儲這個View經(jīng)過測量得到的measured width and height。否則,將會由measure(int, int)方法拋出一個IllegalStateException。)View類onMeasure方法中只支持EXACTLY,如果不重寫onMeasure的話就只支持EXACTLY模式。
1、onMeasure方法簽名
/**
*這兩個參數(shù)都是按趙View.MeasureSpec類來進行編碼的
*@param :widthMeasureSpec 父級提出的水平寬度要求
*@param :heightMeasureSpec 父級提出的垂直高度要求
*/
protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)
2、實現(xiàn)onMeasure方法的步驟
從父級傳遞過來的View.MeasureSpec對象里獲取測量模式和尺寸
然后根據(jù)不同的模式,給出不同的測量值,(即實際值)寬高都采用一樣的機制,一般mode為EXACTLY時,直接使用父類傳遞過來的測量值specValue;mode為UNSPECIFIED時,直接指定為默認的大?。ㄟ@個值需要我們自己定義);當mode為AT_MOST時也指定為默認的大小,但還需要我們拿指定的默認大小和測量值specValue比較取最小值。
調(diào)用父類測量方法setMeasuredDimension(測量寬度值,測量高度值)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measure(widthMeasureSpec);
measure(heightMeasureSpec);
Log.e("onMeasure", "realWidth: " + realWidth + "realHeiht: " + realHeiht + "widthMeasureSpec" + widthMeasureSpec + "heightMeasureSpec" + heightMeasureSpec);
setMeasuredDimension(realWidth, realHeiht);
}
private void measure(int measureValue) {
int defalueSize = 200;
int mode = View.MeasureSpec.getMode(measureValue);
int specValue = View.MeasureSpec.getSize(measureValue);
Log.e("onMeasure", "mode: " + mode + "specValue: " + specValue);
switch (mode) {
//指定一個默認值
case MeasureSpec.UNSPECIFIED:
realWidth = defalueSize;
realHeiht = defalueSize;
break;
//取測量值
case MeasureSpec.EXACTLY:
realHeiht = specValue;
realWidth = specValue;
break;
//取測量值和默認值中的最小值
case MeasureSpec.AT_MOST:
realWidth = Math.min(defalueSize, specValue);
realHeiht = Math.min(defalueSize, specValue);
break;
default:
break;
}
}
3、模仿谷歌官方寫法實現(xiàn)onMeasure
這里主要就是模仿View.resolveSizeAndState(int size, int measureSpec, int childMeasuredState),childMeasuredState其中 View.getMeasuredState()是由返回的,最終布局將結(jié)合childMeasuredState通過View.combineMeasuredStates()完成最終的測量結(jié)果,作用應(yīng)該是自定義viewGroup時才使用用于記錄children測量狀態(tài)的,一般自定義View傳0即可,特殊情況下可以傳遞1。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//third param. usually 0. http://stackoverflow.com/questions/13650903/whats-the-utility-of-the-third-argument-of-view-resolvesizeandstate
int w = resolveSizeAndState2(getDesireW(), widthMeasureSpec, 0);
int h = resolveSizeAndState2(300, heightMeasureSpec, 0);
setMeasuredDimension(MeasureSpec.getSize(w), MeasureSpec.getSize(h));
}
private int getDesireW(){
return 300;
}
/**
*
* @param size How big the view wants to be.即傳入你希望View的大小
* @param measureSpec Constraints imposed by the parent. 父級約束大小
* @param childMeasuredState 一般傳遞0即可,特殊情況還可以傳入1
* @return
*/
private int resolveSizeAndState2(int size, int measureSpec, int childMeasuredState) {
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:
//當specMode為AT_MOST,并且父控件指定的尺寸specSize小于View自己想要的尺寸時,
//我們就會用掩碼MEASURED_STATE_TOO_SMALL向量算結(jié)果加入尺寸太小的標記
//這樣其父ViewGroup就可以通過該標記其給子View的尺寸太小了,
//然后可能分配更大一點的尺寸給子View
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;//按味或
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result | (childMeasuredState&MEASURED_STATE_MASK);
}
五、一個簡單的例子
/**
* Auther: Crazy.Mo
* DateTime: 2017/5/3 15:52
* Summary:
*/
public class MeasuredView extends View {
private Context context;
private int realWidth, realHeiht;
public MeasuredView(Context context) {
this(context, null);
}
public MeasuredView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MeasuredView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measure(widthMeasureSpec);
measure(heightMeasureSpec);
Log.e("onMeasure", "realWidth: " + realWidth + "realHeiht: " + realHeiht + "widthMeasureSpec" + widthMeasureSpec + "heightMeasureSpec" + heightMeasureSpec);
setMeasuredDimension(realWidth, realHeiht);
}
private void measure(int measureValue) {
int defalueSize = 200;
int mode = View.MeasureSpec.getMode(measureValue);
int specValue = View.MeasureSpec.getSize(measureValue);
Log.e("onMeasure", "mode: " + mode + "specValue: " + specValue);
switch (mode) {
//指定一個默認值
case MeasureSpec.UNSPECIFIED:
Log.e("onMeasure", "mode: " + mode + "UNSPECIFIED " );
realWidth = defalueSize;
realHeiht = defalueSize;
break;
//取測量值
case MeasureSpec.EXACTLY:
Log.e("onMeasure", "mode: " + mode + "EXACTLY " );
realHeiht = specValue;
realWidth = specValue;
break;
//取測量值和默認值中的最小值
case MeasureSpec.AT_MOST:
Log.e("onMeasure", "mode: " + mode + "AT_MOST " );
realWidth = Math.min(defalueSize, specValue);
realHeiht = Math.min(defalueSize, specValue);
break;
default:
break;
}
}
}
此時在布局中使用的話,
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:orientation="vertical"
android:background="#0f8">
<!-- <com.ce.sesamecredit.ClockView
android:layout_width="match_parent"
android:layout_height="match_parent" />-->
<com.ce.sesamecredit.MeasuredView
android:background="@color/colorAccent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
2、運行結(jié)果分析:
-
未重寫onMeasure方法時,默認的onMeasure僅可以解析match_parent和指定的具體數(shù)值
這里寫圖片描述 -
重寫onMeasure方法時,可以解析match_parent、指定的具體數(shù)值和wrap_content
這里寫圖片描述