《Android 群英傳》第三章 “ Android 控件架構(gòu)與自定義控件詳解 ” 學(xué)習(xí)筆記
Android控件架構(gòu)
在 Android 中,控件被分為兩類,即 ViewGroup 與 View。在界面上,控件其實(shí)是一個(gè)矩形。ViewGroup 可作為父控件包含一個(gè)或多個(gè)View。通過(guò)ViewGroup,整個(gè)界面上的控件形成了一個(gè)樹形結(jié)構(gòu)(控件樹)。上層控件負(fù)責(zé)下層子控件的測(cè)量與繪制,并傳遞交互事件。在控件樹頂部,有一個(gè) ViewParent 對(duì)象,它負(fù)責(zé)統(tǒng)一調(diào)度和分配所有的交互管理事件,對(duì)整個(gè)視圖進(jìn)行整體控制。
關(guān)于 setContentView()
每個(gè) Activity 都包含一個(gè) Window 對(duì)象,在 Android中Window 對(duì)象通常由 PhoneWindow 來(lái)實(shí)現(xiàn)。 PhoneWindow 將一個(gè) DecorView 設(shè)置為整個(gè)應(yīng)用的根 View。DecorView 作為窗口的頂層視圖,封裝了一些窗口操作的通用方法。DecorView 將要顯示的內(nèi)容呈現(xiàn)在 PhoneWindow 上,這里面的所有 View 的監(jiān)聽(tīng)事件,都通過(guò) WindowManagerService 來(lái)進(jìn)行接收。
DecorView 分為兩部分,TitleView 與 ContentView。ContentView 實(shí)際是一個(gè) ID 為content 的 FrameLayout,我們通過(guò) setContentView() 方法設(shè)置的布局就會(huì)顯示在這個(gè) FrameLayout 中。
當(dāng)onCreate() 方法中調(diào)用 setContentView() 方法后,ActivityManagerService 會(huì)回調(diào) onResume() 方法,此時(shí)系統(tǒng)才會(huì)把整個(gè) DecorView 添加到 PhoneWindow 中,并顯示出來(lái),從而完成界面繪制。
// 來(lái)自源碼
// 得到 Window 對(duì)象,設(shè)置布局(疑問(wèn):得到的Window對(duì)象是PhoneWindow對(duì)象嗎?)
// 初始化ActionBar
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
private void initWindowDecorActionBar() {
Window window = getWindow();
// Initializing the window decor can change window feature flags.
// Make sure that we have the correct set before performing the test below.
window.getDecorView();
// 判斷是否顯示ActionBar
if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) {
return;
}
mActionBar = new WindowDecorActionBar(this);
mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);
mWindow.setDefaultIcon(mActivityInfo.getIconResource());
mWindow.setDefaultLogo(mActivityInfo.getLogoResource());
}```
## View 的測(cè)量
繪制 View 需要知道它的大小和位置。這個(gè)過(guò)程在 onMeasure() 方法中進(jìn)行。同時(shí) MeasureSpec 類是幫助我們測(cè)量 View 的。MeasureSpec 中有三個(gè)int型常量。
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
看英文注釋,不是很明白。這里引用下書中的解釋:
* EXACTLY
精確值模式,當(dāng)我們將控件的layout_width 屬性或 layout_height 屬性指定為具體數(shù)值時(shí),比如設(shè)置寬為100dp,或者指定為match_parent 屬性時(shí),測(cè)量模式即為 EXACTLY 模式
* AT_MOST
最大值模式,當(dāng)控件的layout_width 屬性或 layout_height 屬性指定為 wrap_content 時(shí),這時(shí)候的控件的大小一般隨它的子控件或內(nèi)容的變化而變化。
* UNSPECIFIED
未指定模式(書中沒(méi)有中文解釋),不指定測(cè)量模式,View 想多大就多大。
View 類默認(rèn)的 onMeasure() 方法只支持 EXACTLY 模式。自定義的 View 需要重寫 onMeasure() 才能使用wrap_content 屬性。
// View 默認(rèn)的 onMeasure 方法
// 得到寬高后調(diào)用setMeasuredDimension
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
// 得到寬高的測(cè)量模式和大小(來(lái)自 TextView 的 onMeasure() 方法)
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// ...
自定義一個(gè)View,重寫onMeasure() 方法,測(cè)量寬高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getWidthSize(widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
// 通過(guò)測(cè)量模式返回寬度大小(疑問(wèn):為何 onMeasure() 方法會(huì)被多次執(zhí)行???)
private int getWidthSize(int widthMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int width;
if (mode == MeasureSpec.EXACTLY) {
width = size;
} else {
width = 200;
if (mode == MeasureSpec.AT_MOST) {
width = Math.min(width, size);
}
}
Log.i("size", size + "");
Log.i("width", width + "");
return width;
}
## View 的繪制
當(dāng)測(cè)量好 View 后,可以重寫 onDraw() 方法,在 Canvas 對(duì)象上繪制圖形。Canvas 就像一張畫布,使用 Paint 就可以在上面畫東西了。
## ViewGroup 的測(cè)量
當(dāng) ViewGroup 的大小為 wrap_content 時(shí),ViewGroup 就需要對(duì)子 View 進(jìn)行遍歷,以便獲取所有子 View 的大小,從而決定自身的大小。其他模式則通過(guò)具體的指定值來(lái)設(shè)置大小。
ViewGroup 通過(guò)遍歷子 View,從而調(diào)用子 View 的 onMeasure() 來(lái)獲取每一個(gè)子 View的測(cè)量結(jié)果。
當(dāng)子 View 測(cè)量完畢后,還需要放置 View 在界面的位置,這個(gè)過(guò)程是 View 的 Layout過(guò)程。ViewGroup 在執(zhí)行 Layout 過(guò)程中,同樣使用遍歷調(diào)用子 View 的Layout 方法,確定它的顯示位置。從而決定布局位置。
自定義 ViewGroup 是,一般要重寫 onLayout() 方法來(lái)控制子 View 顯示位置的邏輯。支持 wrap_content 屬性,還需要重寫 onMeasure 來(lái)決定自身的大小。
閱讀 LinearLayout 的部分源碼,理解測(cè)量過(guò)程
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 判斷布局方向,調(diào)用不同的測(cè)量方法
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
在 measureVertical(widthMeasureSpec, heightMeasureSpec) 發(fā)現(xiàn)如下代碼
final int count = getVirtualChildCount();
// .... 省略N行
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
// .... 省略N行
對(duì) Child View 進(jìn)行遍歷,去測(cè)量 weight 等。接著
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
通過(guò)方法名我們也能理解這是在 Layout 之前測(cè)量 子 View。接著跟下去
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);
}
發(fā)現(xiàn)這里正是在獲取子 View 寬高的 MeasureSpec,然后回調(diào)子 View 的measure() 方法,直接跳轉(zhuǎn)去看這個(gè)方法在干什么。(猜測(cè)應(yīng)該會(huì)去調(diào)用 onMeasure 方法了)
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
// 這里的確發(fā)生了 調(diào)用 子 View 的 onMeasure() 去測(cè)量大小
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;
}```