《Android 群英传》第三章 “ Android 控件架构与自定义控件详解 ” 学习笔记
Android控件架构
在 Android 中,控件被分为两类,即 ViewGroup 与 View。在界面上,控件其实是一个矩形。ViewGroup 可作为父控件包含一个或多个View。通过ViewGroup,整个界面上的控件形成了一个树形结构(控件树)。上层控件负责下层子控件的测量与绘制,并传递交互事件。在控件树顶部,有一个 ViewParent 对象,它负责统一调度和分配所有的交互管理事件,对整个视图进行整体控制。
关于 setContentView()
每个 Activity 都包含一个 Window 对象,在 Android中Window 对象通常由 PhoneWindow 来实现。 PhoneWindow 将一个 DecorView 设置为整个应用的根 View。DecorView 作为窗口的顶层视图,封装了一些窗口操作的通用方法。DecorView 将要显示的内容呈现在 PhoneWindow 上,这里面的所有 View 的监听事件,都通过 WindowManagerService 来进行接收。
DecorView 分为两部分,TitleView 与 ContentView。ContentView 实际是一个 ID 为content 的 FrameLayout,我们通过 setContentView() 方法设置的布局就会显示在这个 FrameLayout 中。
当onCreate() 方法中调用 setContentView() 方法后,ActivityManagerService 会回调 onResume() 方法,此时系统才会把整个 DecorView 添加到 PhoneWindow 中,并显示出来,从而完成界面绘制。
// 来自源码
// 得到 Window 对象,设置布局(疑问:得到的Window对象是PhoneWindow对象吗?)
// 初始化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 的测量
绘制 View 需要知道它的大小和位置。这个过程在 onMeasure() 方法中进行。同时 MeasureSpec 类是帮助我们测量 View 的。MeasureSpec 中有三个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
精确值模式,当我们将控件的layout_width 属性或 layout_height 属性指定为具体数值时,比如设置宽为100dp,或者指定为match_parent 属性时,测量模式即为 EXACTLY 模式
* AT_MOST
最大值模式,当控件的layout_width 属性或 layout_height 属性指定为 wrap_content 时,这时候的控件的大小一般随它的子控件或内容的变化而变化。
* UNSPECIFIED
未指定模式(书中没有中文解释),不指定测量模式,View 想多大就多大。
View 类默认的 onMeasure() 方法只支持 EXACTLY 模式。自定义的 View 需要重写 onMeasure() 才能使用wrap_content 属性。
// View 默认的 onMeasure 方法
// 得到宽高后调用setMeasuredDimension
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
// 得到宽高的测量模式和大小(来自 TextView 的 onMeasure() 方法)
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// ...
自定义一个View,重写onMeasure() 方法,测量宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getWidthSize(widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
// 通过测量模式返回宽度大小(疑问:为何 onMeasure() 方法会被多次执行???)
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 的绘制
当测量好 View 后,可以重写 onDraw() 方法,在 Canvas 对象上绘制图形。Canvas 就像一张画布,使用 Paint 就可以在上面画东西了。
## ViewGroup 的测量
当 ViewGroup 的大小为 wrap_content 时,ViewGroup 就需要对子 View 进行遍历,以便获取所有子 View 的大小,从而决定自身的大小。其他模式则通过具体的指定值来设置大小。
ViewGroup 通过遍历子 View,从而调用子 View 的 onMeasure() 来获取每一个子 View的测量结果。
当子 View 测量完毕后,还需要放置 View 在界面的位置,这个过程是 View 的 Layout过程。ViewGroup 在执行 Layout 过程中,同样使用遍历调用子 View 的Layout 方法,确定它的显示位置。从而决定布局位置。
自定义 ViewGroup 是,一般要重写 onLayout() 方法来控制子 View 显示位置的逻辑。支持 wrap_content 属性,还需要重写 onMeasure 来决定自身的大小。
阅读 LinearLayout 的部分源码,理解测量过程
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 判断布局方向,调用不同的测量方法
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
在 measureVertical(widthMeasureSpec, heightMeasureSpec) 发现如下代码
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行
对 Child View 进行遍历,去测量 weight 等。接着
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
通过方法名我们也能理解这是在 Layout 之前测量 子 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);
}
发现这里正是在获取子 View 宽高的 MeasureSpec,然后回调子 View 的measure() 方法,直接跳转去看这个方法在干什么。(猜测应该会去调用 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
// 这里的确发生了 调用 子 View 的 onMeasure() 去测量大小
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;
}```