View添加到屏幕窗口
入口在Activity的setContentView方法,关键代码如下:
AppCompatActivity.setContentView();
AppCompatDelegate.setContentView();
AppCompatDelegateImpl.setContentView(){
// 确保SubDecor
ensureSubDecor();
// 找到subDecor中的content id
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
// 移除content中的所有view
contentParent.removeAllViews();
// 通过LayoutInflater加载用户定义的布局,内部实现是通过获得类名反射出每一个View,我们可以通过setFactory2()接管创建View的过程来自定义LayoutInflater
LayoutInflater.from(mContext).inflate(resId, contentParent);
// 内容改变回调
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
// AppCompatDelegateImpl.java
private void ensureSubDecor() {
// 如果没有创建mSubDecor,那么创建它
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
}
}
// AppCompatDelegateImpl.java
private ViewGroup createSubDecor(){
// 1. 设置App主题,比如
requestWindowFeature(Window.FEATURE_NO_TITLE);
// 2. 获取DecorView (下面会详细分析)
ensureWindow();
mWindow.getDecorView();
// 3. 通过LayoutInflater inflate subDecor的视图,这里有好几种,我挑了其中一个继续分析
LayoutInflater inflater = LayoutInflater.from(mContext);
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_dialog_title_material, null);
// 4. 通过第2步,我们得到了DecorView,而DecorView里面已经有了content内容,现在这里新建了subDecor,所以需要把content的视图migrate,之后再重新设置 content id
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
// 也许已经有Views加载到windowContentView中,需要把这些视图重新添加到contentView中,并设置id
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
}
// 5.用subDecor填充Window的内容视图
mWindow.setContentView(subDecor);
}
继续分析获取DecorView,即上面注释的第二步。
ensureWindow()是为了保证AppCompatDelegate获取到Activity attach()时,new 出来的 PhoneWindow,接着再调用mWindow.getDecorView(),这个时候mWindow就是PhoneWindow,具体可以跟踪ensureWindow()这个方法,会有点绕。
接着 mWindow.getDecorView()就调用了installDecor()
// PhoneWindow.java
private void installDecor(){
if (mDecor == null) {
// 直接 new 一个 DecorView,继承自 FrameLayout
mDecor = generateDecor(-1);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
// 下面还有一些FEATURE_ACTIVITY_TRANSITIONS的动画初始化
// ...
}
// PhoneWindow.java
protected ViewGroup generateLayout(DecorView decor) {
// 1. 设置app主题
// 2. 主要是inflate布局得到root根布局,比如R.layout.screen_simple,其他所有布局都有一个关键id Window.ID_ANDROID_CONTENT,接着把root根布局添加到DecorView中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// 3. 返回content view
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
return contentParent;
}
总结:
- 创建顶层布局SubDecor
- 创建次级顶层布局DecorView
- 在次级顶层布局DecorView中加载基础布局ViewGroup(根布局root),关键有个ContentView
- 将ContentView添加到基础布局中的FrameLayout中
- 在顶层布局SubDecor中migrate迁移DecorView的ContentView
- 最后,用户的View被加载到ContentView中
View的绘制流程
入口在ActivityThread.handleResumeActivity方法,关键代码如下:
// View的绘制在onResume()之后
ActivityThread.handleResumeActivity();
// WindowManagerImpl是在Activity attach的的时候new出来赋值给 Window.mWindowManager
WindowManagerImpl.addView();
// 这是一个单例实现
WindowManagerGlobal.addView();
// WindowManagerGlobal.java
public void addView(){
ViewRootImpl root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
}
// ViewRootImpl.java
public void setView(){
requestLayout();
}
public void requestLayout(){
// 检查当前线程是否为UI主线程,否则抛出异常
checkThread();
scheduleTraversals();
}
public void scheduleTraversals(){
// 得到VSYNC信号,执行绘制任务
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
void doTraversal() {
performTraversals();
}
pirvate void performTraversals(){
// 调用View的measure(),接着会调用onMeasure(),测量结束后通过setMeasuredDimension()将mMeasureWidth和mMeasureHeight保存,下面会详细分析这一块
performMeasure();
// 调用View的layout(),确定自身的位置,即mLeft, mTop, mRight, mBottom,接着调用onLayout(),如果是ViewGroup类型,还需要确定子View的位置,可以参考FrameLayout
performLayout();
// 调用draw(),接着调用drawSoftware(),再调用View的draw(),
// 这里面主要完成的步骤有:
// 1. 绘制背景 drawBackground() 2. 绘制自己 onDraw() 3. 绘制子View dispatchDraw() 4. 绘制前景、滚动条等装饰 onDrawForeground(canvas)
performDraw();
}
MeasureSpec相关
MeasureSpec表示View的模式和尺寸,是一个32位的int值,前2位是SpecMode,后30位是SpecSize,有对应的方法可以获取一个Spec的mode或size。
Mode有三种,分别是
- UNSPECIFIED:父View不限制子View,子View想要多大就多大
- EXACTLY:精确模式,父View已经决定了子View的大小,子View必须遵循
- AT_MOST:最大模式,父View限制了一个最大值,子View可以取小于或等于该值的大小
顶层DecorView宽高测量遵循的规则
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// 精确模式,窗口大小
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// 最大模式,最大为窗口大小
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// 精确模式,大小为LayoutParams的大小
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
ViewGroup宽高测量的规则
因为DecorView继承自FrameLayout,在measure()之后,会调用onMeasure()方法,然后遍历测量子View的宽高,即measureChildWithMargins(),在这个方法里面通过getChildMeasureSpec()获得子View的测量规格,再把规格给子View,通过子View的measure()方法,让它去测量自己需要的宽高。
getChildMeasureSpec()解释
- EXACTLY
- 子View写死大小,类似50dp,那么子View的测量规格是 size=childSize, mode=EXACTLY
- MATCH_PARENT, size = parentSize, mode = EXACTLY
- WRAP_CONTENT, size = parentSize, mode = AT_MOST
- AT_MOST
- dp, size = childSize, mode = EXACTLY
- MATCH_PARENT or WRAP_CONTENT, size = parentSize, mode= AT_MOST
- UNSPECIFIED 系统用,平时用得比较少,自己细看
底层View的宽高测量规则
从getDefaultSize()可以观察到,无论是AT_MOST还是EXACTLY,View的大小都直接等于测量规格里面的大小,所以在自定义View的过程中,如果不重写onMeasure(),那么MATCH_PARENT和WRAP_CONTENT的效果是一样的。如果重写了onMeasure(),可以先计算出自己的尺寸,然后通过resolveSize()修正结果,接着通过setMeasuredDimension()进行保存。