從凱哥Blog copy 過來 HenCoder Android UI 部分 2-1 布局基礎(chǔ)
自定義分三部分繪制、布局和觸摸反饋,本篇主要講的布局過程的自定義
布局過程的含義
布局過程,就是程序在運行時利用布局文件的代碼來計算出實際尺寸的過程。
布局過程的工作內(nèi)容
兩個階段:測量階段和布局階段。
測量階段:從上到下遞歸地調(diào)用每個 View 或者 ViewGroup 的 measure() 方法,
測量他們的尺寸并計算它們的位置;
布局階段:從上到下遞歸地調(diào)用每個 View 或者 ViewGroup 的 layout() 方法,
把測得的它們的尺寸和位置賦值給它們。
View 或 ViewGroup 的布局過程
測量階段,measure() 方法被父 View 調(diào)用,在 measure() 中做一些準(zhǔn)備和優(yōu)化工作后,
調(diào)用 onMeasure() 來進(jìn)行實際的自我測量。
onMeasure() 做的事,View 和 ViewGroup 不一樣:
View:View 在 onMeasure() 中會計算出自己的尺寸然后保存;
ViewGroup:ViewGroup 在 onMeasure() 中會調(diào)用所有子 View 的 measure() 讓它們進(jìn)行自我測量,
并根據(jù)子 View 計算出的期望尺寸來計算出它們的實際尺寸和位置(實際上 99.99% 的父 View 都會
使用子 View 給出的期望尺寸來作為實際尺寸,原因在下期或下下期會講到)然后保存。
同時,它也會根據(jù)子 View 的尺寸和位置來計算出自己的尺寸然后保存;
布局階段,layout() 方法被父 View 調(diào)用,在 layout() 中它會保存父 View 傳進(jìn)來的自己的位置和
尺寸,并且調(diào)用 onLayout() 來進(jìn)行實際的內(nèi)部布局。
onLayout() 做的事, View 和 ViewGroup也不一樣:
View:由于沒有子 View,所以 View 的 onLayout() 什么也不做。
ViewGroup:ViewGroup 在 onLayout() 中會調(diào)用自己的所有子 View 的 layout() 方法,
把它們的尺寸和位置傳給它們,讓它們完成自我的內(nèi)部布局。
布局過程自定義的方式
三類:
1.重寫 onMeasure() 來修改已有的 View 的尺寸;
2.重寫 onMeasure() 來全新定制自定義 View 的尺寸;
3.重寫 onMeasure() 和 onLayout() 來全新定制自定義 ViewGroup 的內(nèi)部布局。
第一類自定義--修改已有的 View 的尺寸
重寫 onMeasure() 來修改已有的 View 的尺寸的具體做法:
重寫 onMeasure() 方法,并在里面調(diào)用 super.onMeasure(),觸發(fā)原有的自我測量;
在 super.onMeasure() 的下面用 getMeasuredWidth() 和getMeasuredHeight()
來獲取到之前的測量結(jié)果,
并使用自己的算法,根據(jù)測量結(jié)果計算出新的結(jié)果;
調(diào)用 setMeasuredDimension() 來保存新的結(jié)果。
//重寫onMeasure()方法,自定義正方形ImageView
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = getMeasuredWidth();
int measuredHeight = getMeasuredHeight();
if (measuredWidth > measuredHeight) {
measuredWidth = measuredHeight;
} else {
measuredHeight = measuredWidth;
}
setMeasuredDimension(measuredWidth, measuredHeight);
}
第二類自定義--全新定制自定義 View 的尺寸
全新定制尺寸和修改尺寸的最重要區(qū)別:
1.不需要調(diào)用super.onMeasure(widthMeasureSpec, heightMeasureSpec);
2.需要在計算的同時,保證計算結(jié)果滿足父 View 給出的的尺寸限制
父 View 的尺寸限制
由來:開發(fā)者的要求(布局文件中 layout_ 打頭的屬性)經(jīng)過父 View 處理計算后的更精確的要求;
限制的分類:
UNSPECIFIED:不限制(0)
AT_MOST:限制上限(-2147483648)
EXACTLY:限制固定值(1073741824)
全新定制自定義 View 尺寸的方式:
1.重寫 onMeasure(),并計算出 View 的尺寸;
2.使用 resolveSize() 來讓子 View 的計算結(jié)果符合父 View 的限制(當(dāng)然,如果你想用自己的方式
來滿足父 View 的限制也行。)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = ---;
int height = ---;
width = resolveSize(width, widthMeasureSpec);
height = resolveSize(height, heightMeasureSpec);
setMeasuredDimension(width, height);
}
public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
//View中的靜態(tài)方法
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
第三類自定義--全新定制 ViewGroup 的內(nèi)部布局
重寫 onMeasure() 和 onLayout() 來定制 Layout 的內(nèi)部布局。
定制 Layout 內(nèi)部布局的方式
1.重寫 onMeasure() 來計算內(nèi)部布局
2.重寫 onLayout() 來擺放子 View
重寫 onMeasure() 的三個步驟:
1.調(diào)用每個子 View 的 measure() 來計算子 View 的尺寸
2.計算子 View 的位置并保存子 View 的位置和尺寸
3.計算自己的尺寸并用 setMeasuredDimension() 保存
計算子 View 尺寸的關(guān)鍵
計算子 View 的尺寸,關(guān)鍵在于 measure() 方法的兩個參數(shù)——也就是子 View 的兩個 MeasureSpec 的計算。
子 View 的 MeasureSpec 的計算方式:
1.結(jié)合開發(fā)者的要求(xml 中 layout_ 打頭的屬性)和自己的可用空間(自己的尺寸上限 - 已用尺寸)
2.尺寸上限根據(jù)自己的 MeasureSpec 中的 mode 而定
2.1 EXACTLY / AT_MOST:尺寸上限為 MeasureSpec 中的 size
2.2 UNSPECIFIED:尺寸無上限
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//調(diào)用ViewGroup類中測量子類的方法
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
/**
* 遍歷所有的子view去測量(跳過GONE類型View)
* @param widthMeasureSpec 父視圖的寬可用空間測量值
* @param heightMeasureSpec 父視圖的高可用空間測量值
*/
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);
}
}
}
/**
* 測量單個子View,將寬高和padding加在一起后交給getChildMeasureSpec去獲得最終的測量值
* @param child 需要測量的子視圖
* @param parentWidthMeasureSpec 父視圖的寬可用空間測量值
* @param parentHeightMeasureSpec 父視圖的高可用空間測量值
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 取得子視圖的布局參數(shù)
final LayoutParams lp = child.getLayoutParams();
// 通過getChildMeasureSpec獲取最終的寬高詳細(xì)測量值
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 將計算好的寬高詳細(xì)測量值傳入measure方法,完成最后的測量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
重寫 onLayout() 的方式
在 onLayout() 里調(diào)用每個子 View 的 layout() ,讓它們保存自己的位置和尺寸。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View childAt = getChildAt(i);
childAt.layout(childLeft[i], childTop[i], childRight[i], childBottom[i]);
}
}