ViewTree的创建
在分析Activity启动过程中:
Activity启动流程会执行startSpecificActivityLocked
ActivityThread.performLaunchActivity //onCreate -> onStart
SetContentView创建DecorView(DecorView = ContentView(用户需要显示的组件) + 系统其它组件)
ActivityThread.handleResumeActivity //onResume
WindowManager.addView
WindowManagerImpl.addView (将DecorView保存到WindowManagerImpl.mViews,将ViewRootImpl保存到mRoots)
ViewRootImpl.setView (将DecorView保存到ViewRootImpl.mView)
ViewTree的创建过程首先从onCreate开始分析:
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
}
class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
implements MenuBuilder.Callback, LayoutInflater.Factory2 {
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
}
1.1 分析ensureSubDecor
class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
implements MenuBuilder.Callback, LayoutInflater.Factory2 {
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
}
private ViewGroup createSubDecor() {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
if (!mWindowNoTitle) {
if (mIsFloating) {
// If we're floating, inflate the dialog title decor
subDecor = (ViewGroup) inflater.inflate( //根据R.styleable属性加载不同的布局文件
R.layout.abc_dialog_title_material, null);
// Floating windows can never have an action bar, reset the flags
mHasActionBar = mOverlayActionBar = false;
} else if (mHasActionBar) {
...
}
} else {
...
}
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) {
// There might be Views already added to the Window's content view so we need to
// migrate them to our content view
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
// Change our content FrameLayout to use the android.R.id.content id.
// Useful for fragments.
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content); //设置subDecor的contentView的id为android.R.id.content
}
// Now set the Window's content view with the decor
mWindow.setContentView(subDecor); //根据加载的布局文件创建的view - subDecor设置为PhoneWindow的contentView
return subDecor;
}
}
根据R.styleable属性加载不同的布局文件,根据布局文件创建ViewGroup并保存到mSubDecor
将加载的布局文件创建的view - subDecor设置为PhoneWindow的contentView,即mWindow.setContentView(subDecor);
其中subDecor的contentView的id为android.R.id.content,因此创建的subDecor为ViewGroup,subDecor中真正的内容是id为android.R.id.content的用户自定义View
1.2 分析inflate
public abstract class LayoutInflater {
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
final XmlResourceParser parser = res.getLayout(resource); //resource = R.layout.activity_main
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser); //attrs为布局文件的attr参数
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName(); //获取根节点的标签名
if (TAG_MERGE.equals(name)) { //如果是merge标签,一般走else
...
} else {
// Temp is the root view that was found in the xml
//根据根节点的标签名生成root view --- temp
final View temp = createViewFromTag(root, name, inflaterContext, attrs); //root是id为android.R.id.content的contentView,name为布局文件R.layout.activity_main的根节点的标签名,attrs为布局文件的attr参数
ViewGroup.LayoutParams params = null;
if (root != null) {
// Create layout params that match root, if supplied
//创建root view的布局属性params,例如宽高、字体大小
params = root.generateLayoutParams(attrs);
if (!attachToRoot) { //如果root不为null且attachToRoot设为false
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params); //将布局文件最外层的所有layout属性设置到布局文件的根节点temp
}
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true); //parser为根节点,temp为根节点对应的view,attrs为根节点对应的属性
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) { //如果id为android.R.id.content的root为空,或者attachToRoot为false
result = temp; //直接返回布局文件的根节点对应的view --- temp
}
}
}
return result;
}
}
}
public abstract class LayoutInflater {
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth(); //获取子节点的深度
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG || //使用while循环
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
final String name = parser.getName(); //获取子节点的name
final View view = createViewFromTag(parent, name, context, attrs); //创建子节点对应的view
final ViewGroup viewGroup = (ViewGroup) parent; //viewGroup即为root view
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true); //递归调用
viewGroup.addView(view, params); //将创建的子节点对应的view添加到root view中
}
}
}
总结:
1> ViewTree创建总结 — 即创建DecorView的过程
a. 在Activity的onCreate中调用setContentView,传入自定义布局文件对应的id
b. 调用createSubDecor根据R.styleable属性加载不同的布局文件创建ViewGroup — subDecor,并将subDecor设置为PhoneWindow的contentView,subDecor的contentView对应的id设置为android.R.id.content
c. 调用inflate首先找到自定义布局文件的根节点对应的View — temp,然后在rInflateChildren中的while循环递归创建子节点对应的View,将创建的子View添加到temp中,最终将temp添加到android.R.id.content对应的contentView中
2> inflate参数总结
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
//parser对应R.layout.activity_main
//root对应android.R.id.content
//attachToRoot:true表示将R.layout.activity_main中的根节点对应的view添加到android.R.id.content对应的contentView中,false表示直接将parser中的根节点对应的view直接作为android.R.id.content对应的contentView,而不是添加进去
a. 如果root为null,attachToRoot将失去作用,设置任何值都没有意义
b. 如果root不为null,attachToRoot参数默认为true
b.1 attachToRoot设为true,则会给加载的布局文件的指定一个父布局,即id为android.R.id.content的root
b.2 attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效
View绘制的三大流程
1.measure
1.1.View的measure过程
View的measure过程由其measure方法来完成,measure方法是一个final类型方法(意味着子类不能复写)。在View的measure方法中会调用View的onMeasure方法,onMeasure方法也是我们在自定义View时重点需要复写的方法。
onMeasure方法有两个参数:widthMeasureSpec和heightMeasureSpec,分别表示宽度测量规格和高度测量规格,它们都对应着同一个类——MeasureSpec。
MeasureSpec表示View的尺寸规格,其代表着一个32位的int值。
其中高2位代表SpecMode,即测量模式;低30位代表SpecSize,即该模式下的大小。
SpecMode有三类:UNSPECIFIED、EXACTLY和AT_MOST。其中UNSPECIFIED主要用于系统内部,此处可以不做考虑。EXACTLY表示View的MeasureSpec所提供的大小是精确值,也是View需要展现的大小;AT_MOST则表示View的MeasureSpec所提供的的大小是一个最大上限值,View的实际大小不能大于这个值。
一个View的MeasureSpec是由其自身的LayoutParms和父容器的MeasureSpec来共同决定的。
秘诀:
①当View的布局参数是明确的大小(指定dp值),该View的SpecMode一律为EXACTLY,并且SpecSize值为View自身布局参数中设置的大小。
②当View的布局参数是match_parent,则其SpecMode和父容器的SpecMode相同,SpecSize为父容器的大小。
③当View的布局参数是wrap_content,该View的SpecMode一律为AT_MOST,且其SpecSize为其父容器的大小。
在View的onMeasure方法中,通过调用setMeasuredDimension方法,即完成了measure过程。
1.2.ViewGroup的measure过程
ViewGroup除了要完成自己的measure过程,还会遍历调用所有子元素的measure方法。
由于不同的ViewGroup有不同的布局特性,这导致它们的测量细节各不相同。ViewGroup是个抽象类,它自身并未重写View的onMeasure方法,它的测量过程需要其各个子类去具体实现,如LinearLayout、RelativeLayout等。
1.3.在Activity中获取View的的测量宽高
由于View的measure过程和Activity的生命周期方法不是同步执行的,所以无法保证Activity执行onCreate、onStart、onResume时View已经测量完毕了。如果View没有测量完毕,那么获得的宽高大小就是0。
这里主要介绍两种常用的方法。
①View.post(Runnable)
通过post方法可以将runnable投递到主线程消息队列的尾部,等Looper调用此runnable时,View已经完成初始化了。因此可以在Runnable中获取View的宽高。
②ViewTreeObserver
为ViewTreeObserver添加监听器OnGlobalLayoutListener,当View树的状态发生改变或者View树内部的View可见性发生变化时,onGlobalLayout方法将会被回调,因此这是获取此View的宽高的一个好时机。
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
2.layout
Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout方法中会遍历所有的子元素并调用其layout方法,在子元素的layout方法中子元素的onLayout方法又会接着被调用。
在layout方法中,首先通过setFrame方法来设定View的四个顶点的位置,View的四个顶点一旦确定,其在父容器中的位置也就确定了。接着会调用onLayout方法,用来确定子元素的位置,这个方法在View和ViewGroup中都没有实现,需要根据子类的需求去具体实现。
总之,layout方法用来确定自身位置,onLayout方法用来确定所有子元素的位置。
View的测量宽高和最终宽高的区别?
View的测量宽高对应View的getMeasuredWidth/Height方法,最终宽高则对应View的getWidth/Height方法。
在View的默认实现中,View的测量宽高和最终宽高是相等的,只不过测量宽高形成于View的measure过程,最终宽高形成于View的layout过程,两者赋值时机不同而已。
例外:可以通过重写View的layout方法来强行改变View的最终宽高。
3.draw
Draw的作用是将View绘制到屏幕上。
通过浏览View的draw方法,可以看出View的绘制过程遵循如下几步:
- 绘制背景
- 绘制自己(onDraw)
- 绘制子元素
- 绘制装饰
其中第二步,通过调用onDraw方法绘制自己的内容,这也是在自定义View时非常重要的需要复写的方法。
onDraw方法中的内容需要根据视图的特征去具体实现,这方面内容在自定义View中再做详细说明。
以上是Android开发中的ViewTree的创建与绘制;更多Android核心技术学习前往:《Android核心技术手册》参考文档学习,里面记载了30多个Android开发技术点,供大家浏览学习。
文末
Activity启动流程会执行startSpecificActivityLocked
- 首先调用ActivityThread.performLaunchActivity,此函数最终调用onCreate -> onStart,在onCreate中调用SetContentView创建DecorView
- 然后调用ActivityThread.handleResumeActivity,此函数首先调用onResume,然后调用ViewRootImpl.setView将创建的DecorView保存到ViewRootImpl.mView,在ViewRootImpl.performTraversals中分别调用performMeasure、performLayout、performDraw进行View的测量、布局、绘制
网友评论