深入解析Android中的setContentView加载布局原理
前言
对于android的开发者来说,setcontentview大家再熟悉不过了,在我们的activity中首先就是要用它加载我们的布局,但是应该有一部分人是不知道加载布局的原理,也包括我,今天就从源码的角度分析setcontentview加载布局原理。
准备工作
由于我们使用的android api部分源码是隐藏的,当我们在androidstudio中是不能找到源码的,我们可以去官网下载相应源码去查看,当然在github下载相应版本的api替换我们sdk下platforms相应api的android.jar。这样我们就可以在androidstudio查看到隐藏的api了,可以断点调试帮助我们阅读源码。
本篇文章分析源码是android7.1(api25)。
activiy setcontentview源码分析
/** * set the activity content from a layout resource. the resource will be * inflated, adding all top-level views to the activity. */ public void setcontentview(@layoutres int layoutresid) { getwindow().setcontentview(layoutresid); initwindowdecoractionbar(); }
在activity中setcontentview最终调用了getwindow()
的setcontentview·方法,getwindow()
返回的是一个window类,它表示一个窗口的概念,我们的activity就是一个window,dialog和toast也都是通过window来展示的,这很好理解,它是一个抽象类,具体的实现是phonewindow,加载布局的相关逻辑都几乎都是它处理的。
@override public void setcontentview(int layoutresid) { // note: feature_content_transitions may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. do not check the feature // before this happens. 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 { mlayoutinflater.inflate(layoutresid, mcontentparent); } mcontentparent.requestapplyinsets(); final callback cb = getcallback(); if (cb != null && !isdestroyed()) { cb.oncontentchanged(); } mcontentparentexplicitlyset = true; }
先判断mcontentparent 是否为空,当然第一次启动时mcontentparent 时为空的,然后执行installdecor();
方法。
mcontentparent不为空是通过hasfeature(feature_content_transitions)
判断是否有转场动画,当没有的时候就把通过mcontentparent.removeallviews();
移除mcontentparent节点下的所有view.再通过inflate将我们的把布局填充到mcontentparent,最后就是内容变化的回调。至于mcontentparent 是什么东东,先留个悬念,稍后再说。
private void installdecor() { mforcedecorinstall = false; if (mdecor == null) { mdecor = generatedecor(-1); mdecor.setdescendantfocusability(viewgroup.focus_after_descendants); mdecor.setisrootnamespace(true); if (!minvalidatepanelmenuposted && minvalidatepanelmenufeatures != 0) { mdecor.postonanimation(minvalidatepanelmenurunnable); } } else { mdecor.setwindow(this); } if (mcontentparent == null) { mcontentparent = generatelayout(mdecor); // set up decor part of ui to ignore fitssystemwindows if appropriate. mdecor.makeoptionalfitssystemwindows(); final decorcontentparent decorcontentparent = (decorcontentparent) mdecor.findviewbyid( r.id.decor_content_parent); if (decorcontentparent != null) { mdecorcontentparent = decorcontentparent; mdecorcontentparent.setwindowcallback(getcallback()); if (mdecorcontentparent.gettitle() == null) { mdecorcontentparent.setwindowtitle(mtitle); } final int localfeatures = getlocalfeatures(); for (int i = 0; i < feature_max; i++) { if ((localfeatures & (1 << i)) != 0) { mdecorcontentparent.initfeature(i); } } mdecorcontentparent.setuioptions(muioptions); if ((mresourcessetflags & flag_resource_set_icon) != 0 || (miconres != 0 && !mdecorcontentparent.hasicon())) { mdecorcontentparent.seticon(miconres); } else if ((mresourcessetflags & flag_resource_set_icon) == 0 && miconres == 0 && !mdecorcontentparent.hasicon()) { mdecorcontentparent.seticon( getcontext().getpackagemanager().getdefaultactivityicon()); mresourcessetflags |= flag_resource_set_icon_fallback; } if ((mresourcessetflags & flag_resource_set_logo) != 0 || (mlogores != 0 && !mdecorcontentparent.haslogo())) { mdecorcontentparent.setlogo(mlogores); } // invalidate if the panel menu hasn't been created before this. // panel menu invalidation is deferred avoiding application oncreateoptionsmenu // being called in the middle of oncreate or similar. // a pending invalidation will typically be resolved before the posted message // would run normally in order to satisfy instance state restoration. panelfeaturestate st = getpanelstate(feature_options_panel, false); if (!isdestroyed() && (st == null || st.menu == null) && !misstartingwindow) { invalidatepanelmenu(feature_action_bar); } } else { //设置标题 mtitleview = (textview) findviewbyid(r.id.title); if (mtitleview != null) { if ((getlocalfeatures() & (1 << feature_no_title)) != 0) { final view titlecontainer = findviewbyid(r.id.title_container); if (titlecontainer != null) { titlecontainer.setvisibility(view.gone); } else { mtitleview.setvisibility(view.gone); } mcontentparent.setforeground(null); } else { mtitleview.settext(mtitle); } } } //......初始化属性变量 } }
在上面的方法中主要工作就是初始化mdecor和mcontentparent ,以及一些属性的初始化
protected decorview generatedecor(int featureid) { // system process doesn't have application context and in that case we need to directly use // the context we have. otherwise we want the application context, so we don't cling to the // activity. context context; if (musedecorcontext) { context applicationcontext = getcontext().getapplicationcontext(); if (applicationcontext == null) { context = getcontext(); } else { context = new decorcontext(applicationcontext, getcontext().getresources()); if (mtheme != -1) { context.settheme(mtheme); } } } else { context = getcontext(); } return new decorview(context, featureid, this, getattributes()); }
generatedecor初始化一个decorview对象,decorview继承了framelayout,是我们要显示布局的*view,我们看到的布局,标题栏都是它里面。
然后将mdecor作为参数调用generatelayout初始化mcontetparent
protected viewgroup generatelayout(decorview decor) { // apply data from current theme. //获取主题样式 typedarray a = getwindowstyle(); //......省略样式的设置 // inflate the window decor. int layoutresource; //获取feature并根据其来加载对应的xml布局文件 int features = getlocalfeatures(); if ((features & (1 << feature_swipe_to_dismiss)) != 0) { layoutresource = r.layout.screen_swipe_dismiss; } else if ((features & ((1 << feature_left_icon) | (1 << feature_right_icon))) != 0) { if (misfloating) { typedvalue res = new typedvalue(); getcontext().gettheme().resolveattribute( r.attr.dialogtitleiconsdecorlayout, res, true); layoutresource = res.resourceid; } else { layoutresource = r.layout.screen_title_icons; } // xxx remove this once action bar supports these features. removefeature(feature_action_bar); // system.out.println("title icons!"); } else if ((features & ((1 << feature_progress) | (1 << feature_indeterminate_progress))) != 0 && (features & (1 << feature_action_bar)) == 0) { // special case for a window with only a progress bar (and title). // xxx need to have a no-title version of embedded windows. layoutresource = r.layout.screen_progress; // system.out.println("progress!"); } else if ((features & (1 << feature_custom_title)) != 0) { // special case for a window with a custom title. // if the window is floating, we need a dialog layout if (misfloating) { typedvalue res = new typedvalue(); getcontext().gettheme().resolveattribute( r.attr.dialogcustomtitledecorlayout, res, true); layoutresource = res.resourceid; } else { layoutresource = r.layout.screen_custom_title; } // xxx remove this once action bar supports these features. removefeature(feature_action_bar); } else if ((features & (1 << feature_no_title)) == 0) { // if no other features and not embedded, only need a title. // if the window is floating, we need a dialog layout if (misfloating) { typedvalue res = new typedvalue(); getcontext().gettheme().resolveattribute( r.attr.dialogtitledecorlayout, res, true); layoutresource = res.resourceid; } else if ((features & (1 << feature_action_bar)) != 0) { layoutresource = a.getresourceid( r.styleable.window_windowactionbarfullscreendecorlayout, r.layout.screen_action_bar); } else { layoutresource = r.layout.screen_title; } // system.out.println("title!"); } else if ((features & (1 << feature_action_mode_overlay)) != 0) { layoutresource = r.layout.screen_simple_overlay_action_mode; } else { // embedded, so no decoration is needed. layoutresource = r.layout.screen_simple; // system.out.println("simple!"); } mdecor.startchanging(); mdecor.onresourcesloaded(mlayoutinflater, layoutresource); viewgroup contentparent = (viewgroup)findviewbyid(id_android_content); if (contentparent == null) { throw new runtimeexception("window couldn't find content container view"); } if ((features & (1 << feature_indeterminate_progress)) != 0) { progressbar progress = getcircularprogressbar(false); if (progress != null) { progress.setindeterminate(true); } } if ((features & (1 << feature_swipe_to_dismiss)) != 0) { registerswipecallbacks(); } // 给顶层窗口设置标题和背景 if (getcontainer() == null) { final drawable background; if (mbackgroundresource != 0) { background = getcontext().getdrawable(mbackgroundresource); } else { background = mbackgrounddrawable; } mdecor.setwindowbackground(background); final drawable frame; if (mframeresource != 0) { frame = getcontext().getdrawable(mframeresource); } else { frame = null; } mdecor.setwindowframe(frame); mdecor.setelevation(melevation); mdecor.setcliptooutline(mcliptooutline); if (mtitle != null) { settitle(mtitle); } if (mtitlecolor == 0) { mtitlecolor = mtextcolor; } settitlecolor(mtitlecolor); } mdecor.finishchanging(); return contentparent; }
代码较多,先通过getwindowstyle获取主题样式进行初始化,然后通过getlocalfeatures获取设置的不同features加载不同的布局,例如我们通常在activity 加入requestwindowfeature(window.feature_no_title);
来隐藏标题栏,不管根据feature最终使用的是哪一种布局,里面都有一个android:id="@android:id/content"
的framelayout,我们的布局文件就添加到这个framelayout中了。我们看一下一个简单的布局
<?xml version="1.0" encoding="utf-8"?> <linearlayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:fitssystemwindows="true"> <!-- popout bar for action modes --> <viewstub android:id="@+id/action_mode_bar_stub" android:inflatedid="@+id/action_mode_bar" android:layout="@layout/action_mode_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="?attr/actionbartheme" /> <framelayout android:layout_width="match_parent" android:layout_height="?android:attr/windowtitlesize" style="?android:attr/windowtitlebackgroundstyle"> <textview android:id="@android:id/title" style="?android:attr/windowtitlestyle" android:background="@null" android:fadingedge="horizontal" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="match_parent" /> </framelayout> <framelayout android:id="@android:id/content" android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="1" android:foregroundgravity="fill_horizontal|top" android:foreground="?android:attr/windowcontentoverlay" /> </linearlayout>
通过上面的分析,你应该明白了requestwindowfeature为什么必须在setcontentview之前设置了,如果在之后设置,那么通过上面的分析在setcontentview执行时已经从本地读取features,而此时还没有设置,当然就无效了。
viewgroup contentparent = (viewgroup)findviewbyid(id_android_content); public static final int id_android_content = com.android.internal.r.id.content;
通过上面findviewbyid获取该对象。不过在获取viewgroup之前还有一个重要的方法
void onresourcesloaded(layoutinflater inflater, int layoutresource) { mstackid = getstackid(); if (mbackdropframerenderer != null) { loadbackgrounddrawablesifneeded(); mbackdropframerenderer.onresourcesloaded( this, mresizingbackgrounddrawable, mcaptionbackgrounddrawable, musercaptionbackgrounddrawable, getcurrentcolor(mstatuscolorviewstate), getcurrentcolor(mnavigationcolorviewstate)); } mdecorcaptionview = createdecorcaptionview(inflater); final view root = inflater.inflate(layoutresource, null); if (mdecorcaptionview != null) { if (mdecorcaptionview.getparent() == null) { addview(mdecorcaptionview, new viewgroup.layoutparams(match_parent, match_parent)); } mdecorcaptionview.addview(root, new viewgroup.marginlayoutparams(match_parent, match_parent)); } else { // put it below the color views. addview(root, 0, new viewgroup.layoutparams(match_parent, match_parent)); } mcontentroot = (viewgroup) root; initializeelevation(); }
这个比较好理解,root就是在上面判断的根据不同的features,加载的布局,然后将该布局通过addview添加到decorview.到这里初始都成功了.
mlayoutinflater.inflate(layoutresid, mcontentparent);
在回到最初setcontentview中的一句代码,如上,我们也就好理解了,它就是将我们的布局文件inflate到mcontentparent中。到这里activity的加载布局文件就完毕了。
appcompatactivity的setcontentview分析
由于appcompatactivity的setcontentview加载布局的与activity有很多不同的地方,而且相对activity稍微复杂点,在这里也简单分析一下。
@override public void setcontentview(@layoutres int layoutresid) { getdelegate().setcontentview(layoutresid); }
通过名字也就知道把加载布局交给了一个委托对象。
@nonnull public appcompatdelegate getdelegate() { if (mdelegate == null) { mdelegate = appcompatdelegate.create(this, this); } return mdelegate; }
appcompatdelegate时一个抽象类,如下图他有几个子类实现
为啥有那么多子类呢,其实通过名字我们也能猜到,是为了兼容。为了证明这点,我们看看create方法
private static appcompatdelegate create(context context, window window, appcompatcallback callback) { final int sdk = build.version.sdk_int; if (buildcompat.isatleastn()) { return new appcompatdelegateimpln(context, window, callback); } else if (sdk >= 23) { return new appcompatdelegateimplv23(context, window, callback); } else if (sdk >= 14) { return new appcompatdelegateimplv14(context, window, callback); } else if (sdk >= 11) { return new appcompatdelegateimplv11(context, window, callback); } else { return new appcompatdelegateimplv9(context, window, callback); } }
这里就很明显了,根据不同的api版本初始化不同的delegate。通过查看代码setcontentview方法的实现是在appcompatdelegateimplv9中
@override 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(); }
有了分析activity的加载经验,我们就很容易明白contentparent和activity中的mcontentparent是一个东东,ensuresubdecor就是初始msubdecor,然后removeallviews,再将我们的布局填充到contentparent中。最后执行回调。
private void ensuresubdecor() { if (!msubdecorinstalled) { msubdecor = createsubdecor(); //省略部分代码 onsubdecorinstalled(msubdecor); } } private viewgroup createsubdecor() { typedarray a = mcontext.obtainstyledattributes(r.styleable.appcompattheme); //如果哦们不设置置appcompat主题会报错,就是在这个地方 if (!a.hasvalue(r.styleable.appcompattheme_windowactionbar)) { a.recycle(); throw new illegalstateexception( "you need to use a theme.appcompat theme (or descendant) with this activity."); } //省略..... 初始化一下属性 viewgroup subdecor = null; //phtowindowgetdecorview会调用installdecor,在activity已经介绍过,主要工作就是初始化mdecor,mcontentparent。 mwindow.getdecorview(); //省略 //根据设置加载不同的布局 if (!mwindownotitle) { if (misfloating) { // if we're floating, inflate the dialog title decor subdecor = (viewgroup) inflater.inflate( r.layout.abc_dialog_title_material, null); // floating windows can never have an action bar, reset the flags mhasactionbar = moverlayactionbar = false; } else if (mhasactionbar) { /** * this needs some explanation. as we can not use the android:theme attribute * pre-l, we emulate it by manually creating a layoutinflater using a * contextthemewrapper pointing to actionbartheme. */ typedvalue outvalue = new typedvalue(); mcontext.gettheme().resolveattribute(r.attr.actionbartheme, outvalue, true); context themedcontext; if (outvalue.resourceid != 0) { themedcontext = new contextthemewrapper(mcontext, outvalue.resourceid); } else { themedcontext = mcontext; } // now inflate the view using the themed context and set it as the content view subdecor = (viewgroup) layoutinflater.from(themedcontext) .inflate(r.layout.abc_screen_toolbar, null); mdecorcontentparent = (decorcontentparent) subdecor .findviewbyid(r.id.decor_content_parent); mdecorcontentparent.setwindowcallback(getwindowcallback()); /** * propagate features to decorcontentparent */ if (moverlayactionbar) { mdecorcontentparent.initfeature(feature_support_action_bar_overlay); } if (mfeatureprogress) { mdecorcontentparent.initfeature(window.feature_progress); } if (mfeatureindeterminateprogress) { mdecorcontentparent.initfeature(window.feature_indeterminate_progress); } } } else { if (moverlayactionmode) { subdecor = (viewgroup) inflater.inflate( r.layout.abc_screen_simple_overlay_action_mode, null); } else { subdecor = (viewgroup) inflater.inflate(r.layout.abc_screen_simple, null); } if (build.version.sdk_int >= 21) { // if we're running on l or above, we can rely on viewcompat's // setonapplywindowinsetslistener viewcompat.setonapplywindowinsetslistener(subdecor, new onapplywindowinsetslistener() { @override public windowinsetscompat onapplywindowinsets(view v, windowinsetscompat insets) { final int top = insets.getsystemwindowinsettop(); final int newtop = updatestatusguard(top); if (top != newtop) { insets = insets.replacesystemwindowinsets( insets.getsystemwindowinsetleft(), newtop, insets.getsystemwindowinsetright(), insets.getsystemwindowinsetbottom()); } // now apply the insets on our view return viewcompat.onapplywindowinsets(v, insets); } }); } else { // else, we need to use our own fitwindowsviewgroup handling ((fitwindowsviewgroup) subdecor).setonfitsystemwindowslistener( new fitwindowsviewgroup.onfitsystemwindowslistener() { @override public void onfitsystemwindows(rect insets) { insets.top = updatestatusguard(insets.top); } }); } } if (subdecor == null) { throw new illegalargumentexception( "appcompat does not support the current theme features: { " + "windowactionbar: " + mhasactionbar + ", windowactionbaroverlay: "+ moverlayactionbar + ", android:windowisfloating: " + misfloating + ", windowactionmodeoverlay: " + moverlayactionmode + ", windownotitle: " + mwindownotitle + " }"); } if (mdecorcontentparent == null) { mtitleview = (textview) subdecor.findviewbyid(r.id.title); } // make the decor optionally fit system windows, like the window's decor viewutils.makeoptionalfitssystemwindows(subdecor); //contentview 是我们布局填充的地方 final contentframelayout contentview = (contentframelayout) subdecor.findviewbyid( r.id.action_bar_activity_content); //这个就是和我们activity中的介绍的mdecor层级中的mcontentparent是一个东西, 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的id windowcontentview.setid(view.no_id); //将contentview的id设置成android.r.id.content,在此我们应该明白了,contentview 就成为了activity中的mcontentparent,我们的布局加载到这个view中。 contentview.setid(android.r.id.content); // the decorcontent may have a foreground drawable set (windowcontentoverlay). // remove this as we handle it ourselves if (windowcontentview instanceof framelayout) { ((framelayout) windowcontentview).setforeground(null); } } // now set the window's content view with the decor //将subdecor 填充到decorview中 mwindow.setcontentview(subdecor); //省略部分代码 return subdecor; }
上面的处理逻辑就是先初始化一些主题样式,然后通过mwindow.getdecorview()
初始化decorview.和布局,然后createsubdecor根据主题加载不同的布局subdecor,通过findviewbyid获取contentview( appcompat根据不同主题加载的布局中的view r.id.action_bar_activity_content)
和windowcontentview (
decorview中的view android.r.id.content
)控件。获取控件后将windowcontentview 的id清空,并将 contentview的id由r.id.action_bar_activity_content更改为android.r.id.content。最后通过 mwindow.setcontentview(subdecor);
将subdecor添加到decorview中。
//调用两个参数方法 @override public void setcontentview(view view) { setcontentview(view, new viewgroup.layoutparams(match_parent, match_parent)); } //此处处理和在activity中分析的setcontentview传资源id进行加载布局是一样的,不同的是此时mcontentparent 不为空,先removeallviews(无转场动画情况)后再直接执行mcontentparent.addview(view, params);即将subdecor添加到mcontentparent @override public void setcontentview(view view, viewgroup.layoutparams params) { // note: feature_content_transitions may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. do not check the feature // before this happens. if (mcontentparent == null) { installdecor(); } else if (!hasfeature(feature_content_transitions)) { mcontentparent.removeallviews(); } if (hasfeature(feature_content_transitions)) { view.setlayoutparams(params); final scene newscene = new scene(mcontentparent, view); transitionto(newscene); } else { mcontentparent.addview(view, params); } mcontentparent.requestapplyinsets(); final callback cb = getcallback(); if (cb != null && !isdestroyed()) { cb.oncontentchanged(); } mcontentparentexplicitlyset = true; }
关于subdecor到底是什么布局,我们随便看一个布局r.layout.abc_screen_toolbar
,有标题(mwindownotitle为false)并且有actionbar(mhasactionbar 为true)的情况加载的布局。
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.actionbaroverlaylayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/decor_content_parent" android:layout_width="match_parent" android:layout_height="match_parent" android:fitssystemwindows="true"> <include layout="@layout/abc_screen_content_include"/> <android.support.v7.widget.actionbarcontainer android:id="@+id/action_bar_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignparenttop="true" style="?attr/actionbarstyle" android:touchscreenblocksfocus="true" android:gravity="top"> <android.support.v7.widget.toolbar android:id="@+id/action_bar" android:layout_width="match_parent" android:layout_height="wrap_content" app:navigationcontentdescription="@string/abc_action_bar_up_description" style="?attr/toolbarstyle"/> <android.support.v7.widget.actionbarcontextview android:id="@+id/action_context_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone" android:theme="?attr/actionbartheme" style="?attr/actionmodestyle"/> </android.support.v7.widget.actionbarcontainer> </android.support.v7.widget.actionbaroverlaylayout>
不管哪个主题下的布局,都会有一个id 为 abc_screen_content_include最好将id更改为androd.r,content,然后添加到mdecor中的mcontentparent中。我们可以同sdk中tools下hierarchyviewer工具查看我们的布局层级结构。例如我们appcompatactivity中setcontentview传入的布局文件,是一个线程布局,该布局下有一个button,则查看到层级结构
参考链接:http://www.weyye.me/detail/framework-appcompatactivity-setcontentview/
总结
以上就是这篇文章的全部内容了,到这里setcontentview已经分析完毕,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,由于水平有限,难免有错误,若在阅读时发现不妥或者错误的地方留言指正,谢谢大家对的支持。