自定義View繪制流程:
概述
自定義View的基本方法
自定義 View 的最基本的三個方法分別是: onMeasure()、onLayout()、onDraw(); View 在 Activity 中顯示出來,要經(jīng)歷測量、布局和繪制三個步驟,分別對應(yīng)三個動作:measure、layout 和 draw。
- 測量:onMeasure() 決定 View 的大小;
- 布局:onLayout() 決定 View 在 ViewGroup 中的位置;
- 繪制:onDraw() 決定繪制這個 View。
自定義 View 控件分類
- 自定義 View: 只需要重寫 onMeasure() 和 onDraw()
- 自定義 ViewGroup: 則只需要重寫 onMeasure() 和 onLayout()
1. 自定義ViewGroup
自定義ViewGroup一般是利用現(xiàn)有的組件根據(jù)特定的布局方式來組成新的組件,大多繼承自ViewGroup或各種Layout,包含有子View。
例如:應(yīng)用底部導(dǎo)航條中的條目,一般都是上面圖標(biāo)(ImageView),下面文字(TextView),那么這兩個就可以用自定義ViewGroup組合成為一個Veiw,提供兩個屬性分別用來設(shè)置文字和圖片,使用起來會更加方便。
2. 自定義View
在沒有現(xiàn)成的View,需要自己實現(xiàn)的時候,就使用自定義View,一般繼承自View,SurfaceView或其他的View,不包含子View。
例如:制作一個支持自動加載網(wǎng)絡(luò)圖片的ImageView,制作圖表等
PS: 自定義View在大多數(shù)情況下都有替代方案,利用圖片或者組合動畫來實現(xiàn),但是使用后者可能會面臨內(nèi)存耗費過大,制作麻煩等諸多問題。
自定義View基礎(chǔ)
View 類簡介
- View 類是Android中各種組件的基類,如View是ViewGroup基類
- View表現(xiàn)為顯示在屏幕上的各種視圖
Android中的UI組件都由View、ViewGroup組成。
- View的構(gòu)造函數(shù):共有4個
構(gòu)造函數(shù)是View的入口,可以用于初始化一些內(nèi)容,和獲取自定義屬性
View的構(gòu)造函數(shù)有四種重載分別如下:
// 如果View是在Java代碼里面new的,則調(diào)用第一個構(gòu)造函數(shù)
public void SloopView(Context context) {}
// 如果View是在.xml里聲明的,則調(diào)用第二個構(gòu)造函數(shù)
// 自定義屬性是從AttributeSet參數(shù)傳進(jìn)來的 public
public void SloopView(Context context, AttributeSet attrs) {}
// 不會自動調(diào)用
// 一般是在第二個構(gòu)造函數(shù)里主動調(diào)用
// 如 view 有 style 屬性時
public void SloopView(Context context, AttributeSet attrs, int defStyleAttr) {}
// API 21 之后才使用
// 不會自動調(diào)用
// 一般是在第二個構(gòu)造函數(shù)里主動調(diào)用
// 如View有style屬性時
public void SloopView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {}
可以看出,關(guān)于View構(gòu)造函數(shù)的參數(shù)有多有少。有四個參數(shù)的構(gòu)造函數(shù)在API21的時候才添加上,暫不考慮
有三個參數(shù)的構(gòu)造函數(shù)中第三個參數(shù)是默認(rèn)的Style,這里的默認(rèn)的Style是指它在當(dāng)前Application或Activity所用的Theme中的默認(rèn)Style,且只有在明確調(diào)用的時候才會生效,以系統(tǒng)中的ImageButton為例說明:
public ImageButton(Context context, AttributeSet attrs) {
//調(diào)用了三個參數(shù)的構(gòu)造函數(shù),明確指定第三個參數(shù)
this(context, attrs, com.android.internal.R.attr.imageButtonStyle);
}
public ImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
//此處調(diào)了四個參數(shù)的構(gòu)造函數(shù),無視即可
this(context, attrs, defStyleAttr, 0);
}
注意:即使你在View中使用了Style這個屬性也不會調(diào)用三個參數(shù)的構(gòu)造函數(shù),所調(diào)用的依舊是兩個參數(shù)的構(gòu)造函數(shù)。
由于三個參數(shù)的構(gòu)造函數(shù)第三個參數(shù)一般不用,暫不考慮,第三個參數(shù)的具體用法會在以后用到的時候詳細(xì)介紹。
排除了兩個之后,只剩下一個參數(shù)和兩個參數(shù)的構(gòu)造函數(shù),他們的詳情如下:
//一般在直接New一個View的時候調(diào)用。
public void SloopView(Context context) {}
//一般在layout文件中使用的時候會調(diào)用,關(guān)于它的所有屬性(包括自定義屬性)都會包含在attrs中傳遞進(jìn)來。
public void SloopView(Context context, AttributeSet attrs) {}
以下方法調(diào)用的是一個參數(shù)的構(gòu)造函數(shù):
//在Avtivity中
SloopView view = new SloopView(this);
以下方法調(diào)用的是兩個參數(shù)的構(gòu)造函數(shù):
//在layout文件中 - 格式為: 包名.View名
<com.sloop.study.SloopView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
AttributeSet 與自定義屬性
系統(tǒng)自帶的 View 可以在 xml 中配置屬性,對于寫的好的自定義 View 同樣可以在 xml 中配置屬性,為了使自定義的 View 的屬性可以在 xml 中配置,需要以下4個步驟:
- 通過
<declare-styleable>
為自定義 View 添加屬性 - 在 xml 中為相應(yīng)的屬性聲明屬性值
- 在運行時(一般為構(gòu)造函數(shù))獲取屬性值
- 將獲取到的屬性值應(yīng)用到 View
View 視圖結(jié)構(gòu)
- PhoneWindow 是 Android 系統(tǒng)中最基本的窗口系統(tǒng),繼承自 Windows 類,負(fù)責(zé)管理界面顯示以及事件響應(yīng)。它是 Activity 與 View 系統(tǒng)交互的接口
- DecorView 是 PhoneWindow 中的起始節(jié)點 View,繼承于 View 類,作為整個視圖容器來使用。用于設(shè)置窗口屬性。它本質(zhì)上是一個 FrameLayout。
- ViewRoot 在 Activity 啟動時創(chuàng)建,負(fù)責(zé)管理、布局、渲染窗口 UI 等。
對于多 View 的視圖,結(jié)構(gòu)是樹形結(jié)構(gòu):最頂層是 ViewGroup,ViewGroup下可能有多個 ViewGroup 或 View,如下圖:
無論是 measure 過程、layout 過程還是 draw 過程,永遠(yuǎn)都是從 View 樹的根節(jié)點開始測量或計算(即從樹的頂端開始),一層一層、一個分支一個分支地進(jìn)行(即樹形遞歸),最終計算整個 View 樹中各個 View,最終確定整個 View 樹的相關(guān)屬性。
Android 坐標(biāo)系及位置獲取方式
Android 中顏色相關(guān)內(nèi)容
Android 支持的顏色模式:
以 ARGB8888 為例介紹顏色定義:
測量View大小(onMeasure)
View 的大小不僅由自身所決定,同時也會受到父控件的影響,為了我們的控件能更好的適應(yīng)各種情況,一般會自己進(jìn)行測量。
測量View大小使用的是onMeasure函數(shù),我們可以從onMeasure的兩個參數(shù)中取出寬高的相關(guān)數(shù)據(jù):
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//取出寬度的確切數(shù)值
int widthsize = MeasureSpec.getSize(widthMeasureSpec);
//取出寬度的測量模式
int widthmode = MeasureSpec.getMode(widthMeasureSpec);
//取出高度的確切數(shù)值
int heightsize = MeasureSpec.getSize(heightMeasureSpec);
//取出高度的測量模式
int heightmode = MeasureSpec.getMode(heightMeasureSpec);
}
從上面可以看出 onMeasure 函數(shù)中有 widthMeasureSpec 和 heightMeasureSpec 這兩個 int 類型的參數(shù), 毫無疑問他們是和寬高相關(guān)的, 但它們其實不是寬和高, 而是由寬、高和各自方向上對應(yīng)的測量模式來合成的一個值:
測量模式一共有三種, 被定義在 Android 中的 View 類的一個內(nèi)部類View.MeasureSpec中:
模式 | 二進(jìn)制數(shù)值 | 描述 |
---|---|---|
UNSPECIFIED | 00 | 默認(rèn)值,父控件沒有給子view任何限制,子View可以設(shè)置為任意大小。 |
EXACTLY | 01 | 表示父控件已經(jīng)確切的指定了子View的大小。 |
AT_MOST | 10 | 表示子View具體大小沒有尺寸限制,但是存在上限,上限一般為父View大小。 |
在int類型的32位二進(jìn)制位中,31-30這兩位表示測量模式,29~0這三十位表示寬和高的實際值
用 MeasureSpec 的 getSize是獲取數(shù)值, getMode是獲取模式即可。
注意:
如果對View的寬高進(jìn)行修改了,不要調(diào)用super.onMeasure(widthMeasureSpec,heightMeasureSpec);要調(diào)用setMeasuredDimension(widthsize,heightsize); 這個函數(shù)。
確定View大小(onSizeChanged)
這個函數(shù)在視圖大小發(fā)生改變時調(diào)用。
Q: 在測量完View并使用setMeasuredDimension函數(shù)之后View的大小基本上已經(jīng)確定了,那么為什么還要再次確定View的大小呢?
A: 這是因為View的大小不僅由View本身控制,而且受父控件的影響,所以我們在確定View大小的時候最好使用系統(tǒng)提供的onSizeChanged回調(diào)函數(shù)。
onSizeChanged如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
可以看出,它又四個參數(shù),分別為 寬度,高度,上一次寬度,上一次高度。
我們只需關(guān)注 寬度(w), 高度(h) 即可,這兩個參數(shù)就是View最終的大小。
確定子View布局位置(onLayout)
確定布局的函數(shù)是onLayout,它用于確定子View的位置,在自定義ViewGroup中會用到,他調(diào)用的是子View的layout函數(shù)。
在自定義ViewGroup中,onLayout一般是循環(huán)取出子View,然后經(jīng)過計算得出各個子View位置的坐標(biāo)值,然后用以下函數(shù)設(shè)置子View位置。
child.layout(l, t, r, b);
四個參數(shù)分別為:
名稱 | 說明 | 對應(yīng)的函數(shù) |
---|---|---|
l | View左側(cè)距父View左側(cè)的距離 | getLeft(); |
t | View頂部距父View頂部的距離 | getTop(); |
r | View右側(cè)距父View左側(cè)的距離 | getRight(); |
b | View底部距父View頂部的距離 | getBottom(); |
具體可以參考 坐標(biāo)系 這篇文章。
5.繪制內(nèi)容(onDraw)
onDraw是實際繪制的部分,也就是我們真正關(guān)心的部分,使用的是Canvas繪圖。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
getMeasureWidth 與 getWidth 的區(qū)別
- getWidth 在layout()過程結(jié)束后才能獲取到;通過視圖右邊的坐標(biāo)減去左邊的坐標(biāo)計算出來的.
- getMeasuredWidth 在measure()過程結(jié)束后就可以獲取到對應(yīng)的值;通過setMeasuredDimension()方法來進(jìn)行設(shè)置的.
LayoutParams
LayoutParams 翻譯過來就是布局參數(shù),子 View 通過 LayoutParams 告訴父容器(ViewGroup)應(yīng)該如何放置自己。從這個定義中也可以看出來 LayoutParams 與 ViewGroup 是息息相關(guān)的,因此脫離 ViewGroup 談 LayoutParams 是沒有意義的。
事實上,每個 ViewGroup 的子類都有自己對應(yīng)的 LayoutParams 類,典型的如 LinearLayout.LayoutParams 和 FrameLayout.LayoutParams 等,可以看出來 LayoutParams 都是對應(yīng) ViewGroup 子類的內(nèi)部類
MarginLayoutParams
MarginLayoutParams 是和外間距有關(guān)的。事實也確實如此,和 LayoutParams 相比,MarginLayoutParams 只是增加了對上下左右外間距的支持。實際上大部分 LayoutParams 的實現(xiàn)類都是繼承自 MarginLayoutParams,因為基本所有的父容器都是支持子 View 設(shè)置外間距的。
- 屬性優(yōu)先級問題
MarginLayoutParams 主要就是增加了上下左右4種外間距。在構(gòu)造方法中,先是獲取了 margin 屬性;如果該值不合法,就獲取 horizontalMargin;如果該值不合法,再去獲取 leftMargin 和 rightMargin 屬性(verticalMargin、topMargin和bottomMargin同理)。我們可以據(jù)此總結(jié)出這幾種屬性的優(yōu)先級
margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin
- 屬性覆蓋問題
優(yōu)先級更高的屬性會覆蓋掉優(yōu)先級較低的屬性。此外,還要注意一下這幾種屬性上的注釋
Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
LayoutParams 與 View 如何建立聯(lián)系
- 在XML中定義 View
- 在 Java 代碼中直接生成 View 對應(yīng)的實例對象
addView
/**
* 重載方法1:添加一個子View
* 如果這個子View還沒有LayoutParams,就為子View設(shè)置當(dāng)前ViewGroup默認(rèn)的LayoutParams
*/
public void addView(View child) {
addView(child, -1);
}
/**
* 重載方法2:在指定位置添加一個子View
* 如果這個子View還沒有LayoutParams,就為子View設(shè)置當(dāng)前ViewGroup默認(rèn)的LayoutParams
* @param index View將在ViewGroup中被添加的位置(-1代表添加到末尾)
*/
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();// 生成當(dāng)前ViewGroup默認(rèn)的LayoutParams
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
/**
* 重載方法3:添加一個子View
* 使用當(dāng)前ViewGroup默認(rèn)的LayoutParams,并以傳入?yún)?shù)作為LayoutParams的width和height
*/
public void addView(View child, int width, int height) {
final LayoutParams params = generateDefaultLayoutParams(); // 生成當(dāng)前ViewGroup默認(rèn)的LayoutParams
params.width = width;
params.height = height;
addView(child, -1, params);
}
/**
* 重載方法4:添加一個子View,并使用傳入的LayoutParams
*/
@Override
public void addView(View child, LayoutParams params) {
addView(child, -1, params);
}
/**
* 重載方法4:在指定位置添加一個子View,并使用傳入的LayoutParams
*/
public void addView(View child, int index, LayoutParams params) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
.....
if (mTransition != null) {
mTransition.addChild(this, child);
}
if (!checkLayoutParams(params)) { // ① 檢查傳入的LayoutParams是否合法
params = generateLayoutParams(params); // 如果傳入的LayoutParams不合法,將進(jìn)行轉(zhuǎn)化操作
}
if (preventRequestLayout) { // ② 是否需要阻止重新執(zhí)行布局流程
child.mLayoutParams = params; // 這不會引起子View重新布局(onMeasure->onLayout->onDraw)
} else {
child.setLayoutParams(params); // 這會引起子View重新布局(onMeasure->onLayout->onDraw)
}
if (index < 0) {
index = mChildrenCount;
}
addInArray(child, index);
// tell our children
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this;
}
.....
}
自定義LayoutParams
- 創(chuàng)建自定義屬性
<resources>
<declare-styleable name="xxxViewGroup_Layout">
<!-- 自定義的屬性 -->
<attr name="layout_simple_attr" format="integer"/>
<!-- 使用系統(tǒng)預(yù)置的屬性 -->
<attr name="android:layout_gravity"/>
</declare-styleable>
</resources>
- 繼承MarginLayout
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public int simpleAttr;
public int gravity;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
// 解析布局屬性
TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.SimpleViewGroup_Layout);
simpleAttr = typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity, -1);
typedArray.recycle();//釋放資源
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
- 重寫ViewGroup中幾個與LayoutParams相關(guān)的方法
// 檢查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof SimpleViewGroup.LayoutParams;
}
// 生成默認(rèn)的LayoutParams
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
// 對傳入的LayoutParams進(jìn)行轉(zhuǎn)化
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new SimpleViewGroup.LayoutParams(p);
}
// 對傳入的LayoutParams進(jìn)行轉(zhuǎn)化
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new SimpleViewGroup.LayoutParams(getContext(), attrs);
}
LayoutParams常見的子類
在為View設(shè)置LayoutParams的時候需要根據(jù)它的父容器選擇對應(yīng)的LayoutParams,否則結(jié)果可能與預(yù)期不一致,這里簡單羅列一些常見的LayoutParams子類:
- ViewGroup.MarginLayoutParams
- FrameLayout.LayoutParams
- LinearLayout.LayoutParams
- RelativeLayout.LayoutParams
- RecyclerView.LayoutParams
- GridLayoutManager.LayoutParams
- StaggeredGridLayoutManager.LayoutParams
- ViewPager.LayoutParams
- WindowManager.LayoutParams
MeasureSpec
定義
測量規(guī)格,封裝了父容器對 view 的布局上的限制,內(nèi)部提供了寬高的信息( SpecMode 、 SpecSize ),SpecSize 是指在某種 SpecMode 下的參考尺寸,其中 SpecMode 有如下三種:
- UNSPECIFIED
父控件不對你有任何限制,你想要多大給你多大,想上天就上天。這種情況一般用于系統(tǒng)內(nèi)部,表示一種測量狀態(tài)。(這個模式主要用于系統(tǒng)內(nèi)部多次Measure的情形,并不是真的說你想要多大最后就真有多大) - EXACTLY
父控件已經(jīng)知道你所需的精確大小,你的最終大小應(yīng)該就是這么大。 - AT_MOST
你的大小不能大于父控件給你指定的size,但具體是多少,得看你自己的實現(xiàn)。
MeasureSpecs 的意義
通過將 SpecMode 和 SpecSize 打包成一個 int 值可以避免過多的對象內(nèi)存分配,為了方便操作,其提供了打包 / 解包方法
MeasureSpec值的確定
MeasureSpec值到底是如何計算得來的呢?
子 View 的 MeasureSpec 值是根據(jù)子 View 的布局參數(shù)(LayoutParams)和父容器的 MeasureSpec 值計算得來的,具體計算邏輯封裝在 getChildMeasureSpec()
里
/**
*
* 目標(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);
}
針對上表,這里再做一下具體的說明
- 對于應(yīng)用層 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來共同決定
- 對于不同的父容器和view本身不同的LayoutParams,view就可以有多種MeasureSpec。
1. 當(dāng)view采用固定寬高的時候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精確模式并且其大小遵循Layoutparams中的大小;
2. 當(dāng)view的寬高是match_parent時,這個時候如果父容器的模式是精準(zhǔn)模式,那么view也是精準(zhǔn)模式并且其大小是父容器的剩余空間,如果父容器是最大模式,那么view也是最大模式并且其大小不會超過父容器的剩余空間;
3. 當(dāng)view的寬高是wrap_content時,不管父容器的模式是精準(zhǔn)還是最大化,view的模式總是最大化并且大小不能超過父容器的剩余空間。
4. Unspecified模式,這個模式主要用于系統(tǒng)內(nèi)部多次measure的情況下,一般來說,我們不需要關(guān)注此模式(這里注意自定義View放到ScrollView的情況 需要處理)。
實例 流式布局
public class FlowLayout extends ViewGroup {
/**
* 每個item 橫向間距
*/
private final int mHorizontalSpacing = dp2px(16);
/**
* 每個item 豎向間距
*/
private final int mVerticallSpacing = dp2px(16);
/**
* 記錄所有的行,一行一行的存儲,用于layout
*/
private List<List<View>> allLines = new ArrayList<>();
/**
* 記錄每一行的行高,用于layout
*/
private List<Integer> lineHeights = new ArrayList<>();
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private void clearMeasureParams() {
allLines.clear();
lineHeights.clear();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 內(nèi)存 抖動
clearMeasureParams();
// 先測量孩子
int childCount = getChildCount();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
int paddingTop = getPaddingTop();
//記錄這行已經(jīng)使用了多寬的size
int lineWidthUsed = 0;
// ViewGroup解析的父親給我的寬度
int selfWidth = MeasureSpec.getSize(widthMeasureSpec);
// ViewGroup解析的父親給我的高度
int selfHeight = MeasureSpec.getSize(heightMeasureSpec);
// 保存一行中的所有的view
List<View> lineView = new ArrayList<>();
// 一行的行高
int lineHeight = 0;
// measure過程中,子View要求的父ViewGroup的寬
int parentNeededWidth = 0;
// measure過程中,子View要求的父ViewGroup的高
int parentNeededHeight = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
LayoutParams childLP = childView.getLayoutParams();
if (childView.getVisibility() != View.GONE) {
// 將layoutParams轉(zhuǎn)變成為 measureSpec
// 作用:根據(jù)父視圖的MeasureSpec & 布局參數(shù)LayoutParams,計算單個子View的MeasureSpec
// 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams屬性 共同決定
// 參數(shù)說明
// * @param spec 父view的詳細(xì)測量值(MeasureSpec)
// * @param padding view當(dāng)前尺寸的的內(nèi)邊距和外邊距(padding,margin)
// * @param childDimension 子視圖的布局參數(shù)(寬/高)
int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width);
int childHeigthMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height);
// 測量子view的方法,就把孩子測量完了
childView.measure(childWidthMeasureSpec, childHeigthMeasureSpec);
// 獲取子view的測量寬高
int childMesauredWidth = childView.getMeasuredWidth();
int childMeasuredHeight = childView.getMeasuredHeight();
// 這里需要換行,等于說 接下來要放置的控件放不下了,需要換行
if (childMesauredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {
// 一旦換行,我們就可以判斷當(dāng)前行需要的寬和高了,所以此時要記錄下來
allLines.add(lineView);
lineHeights.add(lineHeight);
// 一旦換行,我們就可以判斷當(dāng)前需要的寬和高了,所以要記錄起來
parentNeededHeight = parentNeededHeight + lineHeight + mVerticallSpacing;
parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
lineView = new ArrayList<>();
lineHeight = 0;
lineWidthUsed = 0;
}
// view 是分行l(wèi)ayout的,所以要記錄每一行有哪些view,這樣可以方便layout布局
lineView.add(childView);
// 每行也需要加上空格
lineWidthUsed = lineWidthUsed + childMesauredWidth + mHorizontalSpacing;
// 獲取每行最高的高度
lineHeight = Math.max(lineHeight, childMeasuredHeight);
//處理最后一行數(shù)據(jù)
if (i == childCount - 1) {
allLines.add(lineView);
lineHeights.add(lineHeight);
// 一旦換行,我們就可以判斷當(dāng)前需要的寬和高了,所以要記錄起來
parentNeededHeight = parentNeededHeight + lineHeight + mVerticallSpacing;
parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
}
}
}
// setMeasuredDimension 此接口是設(shè)置自己的大小,并且保存起來
// 在度量自己,并保存,父親想要獲取的時候,直接調(diào)用孩子的 child.getMeasureWidth就行
// setMeasuredDimension(width,height);
// 再測量自己,保存
// 根據(jù)子View的度量結(jié)果,來重新度量自己ViewGroup
// 作為一個ViewGroup,它自己也是一個View,它的大小也需要根據(jù)它的父親給它提供的寬高來度量
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int realWidth = widthMode == MeasureSpec.EXACTLY ? selfWidth : parentNeededWidth;
int realHeight = heightMode == MeasureSpec.EXACTLY ? selfHeight : parentNeededHeight;
// 這個傳遞的是具體的size,不是MeasureSpec
setMeasuredDimension(realWidth, realHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 總共的行數(shù)
int lineCount = allLines.size();
int curT = getPaddingTop();
int curL = getPaddingLeft();
for (int i = 0; i < lineCount; i++) {
List<View> lineViews = allLines.get(i);
int lineHeight = lineHeights.get(i);
// 每行的view進(jìn)行布局
for (int j = 0; j < lineViews.size(); j++) {
View view = lineViews.get(j);
int left = curL;
int top = curT;
// getWidth 在layout()過程結(jié)束后才能獲取到;通過視圖右邊的坐標(biāo)減去左邊的坐標(biāo)計算出來的.
// int right = left + view.getWidth();
// int bottom = top + view.getHeight();
// getMeasuredWidth 在measure()過程結(jié)束后就可以獲取到對應(yīng)的值;通過setMeasuredDimension()方法來進(jìn)行設(shè)置的.
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
view.layout(left, top, right, bottom);
curL = right + mHorizontalSpacing;
}
curL = getPaddingLeft();
curT = curT + lineHeight + mVerticallSpacing;
}
}
public static int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}
}
效果圖: