Android 应用层开发中绕不开自定义 View 这个话题,虽然现在 Github 上有形形色色的开源库供大家使用,
但是作为一名开发者而言,虽然不提倡重复造轮子,但是轮子都是造出来的。碰到一些新鲜的 UI 效果时,
如果现有的控件无法完成任务,那么我们就应该想到要自定义一个 View 了。
我们知道,在 Android 中 View 绘制流程有测量、布局、绘制三个步骤,它们分别对应 3 个 API :onMeasure()、onLayout()、onDraw()。
- 测量 onMeasure() :测量View的尺寸,决定View的大小
- 布局 onLayout() :通过设置l,t,r,b确定view在父容器中的位置
- 绘制 onDraw():通过canvas绘制我们想要展示的内容
没有办法说这三个阶段,哪个阶段最重要,只是相对而言,测量阶段对于开发者而言难度相对其它两个要大,处理的细节也要多得多,
自定义一个 View,正确的测量是第一步,正因为如此今天我将从源码的角度来学习View的测量过程.
View在本质上是一个Rect矩形的区域.
在Android中View的测量是从View树的根节点开始的,一步一步的往下测量而成的.
那么,首先我们来了解下View树的结构:
我们在Activity中一般通过setContentView()方法来设置我们的View视图:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
在setContentView()方法中,我们调用了getWindow().setContentView()方法,而这里的
getWindow()就是PhoneWindow,因此我们转到PhoneWindow的setContentView()方法:
public void setContentView(int layoutResID) {
//如果顶层容器FrameLayout为空的话,需要从xml加载顶层容器
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//将我们通过setContentView设置的布局资源加载到顶层容器mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
在PhoneWindow的setContentView()中会调用installDecor()方法(部分代码省略):
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//生成DecorView
mDecor = generateDecor(-1);
} else {
//将DecorView关联到PhoneWindow上
mDecor.setWindow(this);
}
if (mContentParent == null) {
//同时生成Activity的setContentView方法需要加载的View的顶层容器,并添加到DecorView中去
mContentParent = generateLayout(mDecor);
}
}
分别调用generateDecor()方法生成DecorView:
protected DecorView generateDecor(int featureId) {
//生成顶层的DecorView
return new DecorView(context, featureId, this, getAttributes());
}
同时我们会看到在installDecor()方法中,也会通过generateLayout方法生成mContentParent对象:
protected ViewGroup generateLayout(DecorView decor) {
int layoutResource;
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
//在该方法中最终将mContentParent顶层view添加到DecorView中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//mContentParent顶层容器View的布局id为com.android.internal.R.id.content
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
return contentParent;
}
以上的流程中最终会将资源id为:R.layout.screen_simple的布局加载DecorView中:
<LinearLayout xmlns:android="http