View的添加及绘制过程分析

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()进行保存。

发布了8 篇原创文章 · 获赞 1 · 访问量 2867

猜你喜欢

转载自blog.csdn.net/lneartao/article/details/104884491
今日推荐