Android减少布局层次--有关Activity根视图DecorView的思考
1Android应用图层 一直觉得有关DecorView还是有些问题没有搞清楚,今天在看了一点有关SurfaceFlinger的内容以后,顿时突发奇想,想到之前的问题,之前的思考是: 虽然可以将DecorView作为Activity布局的父View,也就是只存在 DecorView---->ActivityLayout两层,但是经过试验还是会存在Title Bar,或者说是现在的Action Bar,尝试如下: [html] view plain copy protectedvoidonCreate(BundlesavedInstanceState){ super.onCreate(savedInstanceState); ViewGroupgroup=(ViewGroup)getWindow().getDecorView(); LayoutInflater.from(this).inflate(R.layout.activity_main,group,true); ViewServer.get(this).addWindow(this); } 一个简单的APP运行出来看到的View结构图就是下面这样: 图中左右两边相同颜色的区域是对应的,下面是详细的View tree结构图: 可以看到即使像上面那样写,在DecorView下面有3个子View,放大看一下: DecorView里面包含: (1)ActionBarOverklayLayout :ActionBar带来的一个布局,遍布了除了第三层最上端View的所有区域 (2)RelativeLayout : Activity的Layout(上述的activity_main布局) (3)View : 屏幕最上端状态栏的背景 可以看到activity_main中的根布局RelativeLayout和ActionBarOverklayLayout并列的,虽然减少了Activity中布局的层次,但是还是存在ActionBarOverklayLayout 这个我们实际上可能不需要的布局。 2 改进--减少层次 上面的改法有很多的问题,首先DecorView是一个FrameLayout,里面的所有的内容简单的叠在了一起,上面的RelativeLayout里面其实放了一个TextVIew,显示“Hello world”,但是都被其余层重叠了,所以看不到(仔细看勉强能看到),顺理成章想到DecorView作为一个Viewgroup,自然能够进行View的add和remove操作,于是想把其余的都remove删掉,不就只剩下Activity的Layout,于是改了一下代码: [html] view plain copy protectedvoidonCreate(BundlesavedInstanceState){ super.onCreate(savedInstanceState); ViewGroupgroup=(ViewGroup)getWindow().getDecorView(); group.removeAllViews(); LayoutInflater.from(this).inflate(R.layout.activity_main,group,true); ViewServer.get(this).addWindow(this); } 结果崩溃了,信息如下: 看信息好像是和ActionBar有关,但是还是没有什么头绪,于是逐行加log看看到底是在onCreate的哪一行出现这样的问题,其实看上面的崩溃信息的调用栈就知道和onCreate没有关系,应该是在onCreate的后面哪一步调用的时候出现了问题,但是还是尝试了一下: [html] view plain copy protectedvoidonCreate(BundlesavedInstanceState){ super.onCreate(savedInstanceState); System.out.println("onCreate0"); ViewGroupgroup=(ViewGroup)getWindow().getDecorView(); System.out.println("onCreate1"); group.removeAllViews(); System.out.println("onCreate2"); LayoutInflater.from(this).inflate(R.layout.activity_main,group,true); System.out.println("onCreate3"); ViewServer.get(this).addWindow(this); System.out.println("onCreateover"); } 运行以后结果如下: onCreate已经完整调用结束,但是比较常规使用方法,就是少了一个setContentView函数,于是先看一下getWindow().getDecorView(),根据前面的学习知道这里的getWindow()返回的是一个PhoneWindow对象,于是看一下PhoneWindow.getDecorView(): [html] view plain copy publicfinalViewgetDecorView(){ if(mDecor==null){ installDecor(); } returnmDecor; } 这里就是调用installDecor(),这个函数会调用PhoneWindow.generateDecor()和PhoneWindow.generateLayout(DecorView)函数分别创建一个DecorView对象、为DecorView加载一个系统布局,再看一下Activity.setContentView函数: [html] view plain copy publicvoidsetContentView(intlayoutResID){ getWindow().setContentView(layoutResID); initWindowDecorActionBar(); } 先看一下这里的getWindow().setContentView(layoutResID)函数,同样这里的getWindow()返回的是一个PhoneWindow对象,看 PhoneWindow.setContentView: [html] view plain copy publicvoidsetContentView(intlayoutResID){ //Note:FEATURE_CONTENT_TRANSITIONSmaybesetintheprocessofinstallingthewindow //decor,whenthemeattributesandthelikearecrystalized.Donotcheckthefeature //beforethishappens. if(mContentParent==null){ installDecor(); }elseif(!hasFeature(FEATURE_CONTENT_TRANSITIONS)){ //FEATURE_CONTENT_TRANSITIONS这个属性以后可能会用到, //这里先标记一下,注意看上面的注释 mContentParent.removeAllViews(); } if(hasFeature(FEATURE_CONTENT_TRANSITIONS)){ finalScenenewScene= Scene.getSceneForLayout(mContentParent,layoutResID,getContext()); transitionTo(newScene); }else{ mLayoutInflater.inflate(layoutResID,mContentParent); } finalCallbackcb=getCallback(); if(cb!=null&&!isDestroyed()){ cb.onContentChanged(); } } 里面没有什么特别的流程,基本上也就只有installDecor()和实例化布局对象这两个调用,这里的installDecor调用和上面的效果完全是一样的,所以基本上可以排除问题不在这里。 再往回看,Acyivity.setContentView函数除了调用了PhoneWindow.setContentView以外还调用了Acyivity.initWindowDecorActionBar函数,看函数名称这个函数调用好像是和ActionBar有关系的,刚刚好刚才的问题也是和ActionBar有关系的,问题应该就是在这里,看一下Acyivity.initWindowDecorActionBar: 先看注释:创建一个新的ActionBar对象,定位和实例化ActionBar对应的View,并用这个View来初始化ActionBar对象,最后将ActionBar对象的引用存储在mActionBar中。同样这里的Window是一个PhoneWindow,于是再看一下下面的几个测试条件: (1)isChild(): 这个很简单就是判断当前的Activity有没有Parent,这个目前还没有遇到Activity有Parent,所以这一个应该是false,这个直接在Acticity.onCreate里面就可以测试 (2)window.hasFeature(Window.FEATURE_ACTION_BAR):判断当前的Activity有没有指定FEATURE_ACTION_BAR这个Feature,后面会再细说到这个,一般情况不做设置,这一个是true (3)mActionBar: 一开始自然是null 到这里基本上已经可以解决这个问题了,只要不创建这个WindowDecorActionBar,就不会出现与ActionBar有关的问题,只要让上面的三个判断的其中之一失效就可以直接return了,也就不会创建WindowDecorActionBar对象了,比较一下最合适的应该是window.hasFeature(Window.FEATURE_ACTION_BAR)这个判断,那现在要做的就是要让window.hasFeature(Window.FEATURE_ACTION_BAR)返回false即可,先分析一下这个Window.FEATURE_*****它的值是什么?先看一下Window.Java: [html] view plain copy /**Flagforthe"optionspanel"feature.Thisisenabledbydefault.*/ publicstaticfinalintFEATURE_OPTIONS_PANEL=0; /**Flagforthe"notitle"feature,turningoffthetitleatthetopofthescreen.*/ publicstaticfinalintFEATURE_NO_TITLE=1; /**Flagfortheprogressindicatorfeature*/ publicstaticfinalintFEATURE_PROGRESS=2; /**Flagforhavinganiconontheleftsideofthetitlebar*/ publicstaticfinalintFEATURE_LEFT_ICON=3; /**Flagforhavinganiconontherightsideofthetitlebar*/ publicstaticfinalintFEATURE_RIGHT_ICON=4; /**Flagforindeterminateprogress*/ publicstaticfinalintFEATURE_INDETERMINATE_PROGRESS=5; /**Flagforthecontextmenu.Thisisenabledbydefault.*/ publicstaticfinalintFEATURE_CONTEXT_MENU=6; /**Flagforcustomtitle.Youcannotcombinethisfeaturewithothertitlefeatures.*/ publicstaticfinalintFEATURE_CUSTOM_TITLE=7; /**FlagforenablingtheActionBar.Thisisenabledbydefaultforsomedevices.TheActionBarreplacesthetitlebar *andprovidesanalternatelocationforanon-screenmenubuttononsomedevices.*/ publicstaticfinalintFEATURE_ACTION_BAR=8; /** *FlagforrequestinganActionBarthatoverlayswindowcontent. *NormallyanActionBarwillsitinthespaceabovewindowcontent,butifthis *featureisrequestedalongwith{@link#FEATURE_ACTION_BAR}itwillbelayeredover *thewindowcontentitself.Thisisusefulifyouwouldlikeyourapptohavemorecontrol *overhowtheActionBarisdisplayed,suchaslettingapplicationcontentscrollbeneath *anActionBarwithatransparentbackgroundorotherwisedisplayingatransparent/translucent *ActionBaroverapplicationcontent. */ [html] view plain copy publicstaticfinalintFEATURE_ACTION_BAR_OVERLAY=9; publicstaticfinalintFEATURE_ACTION_MODE_OVERLAY=10; publicstaticfinalintFEATURE_SWIPE_TO_DISMISS=11; publicstaticfinalintFEATURE_CONTENT_TRANSITIONS=12; publicstaticfinalintFEATURE_ACTIVITY_TRANSITIONS=13; 目前一共是有13个FEATURE_**,各个FEATURE_**的作用可以在用到的时候看一下注释,这里面有2点很重要: (1)通过Window.getFeatures函数可知,实际上所有的FEATURE_**最终会集中在mFeatures这个变量里面; (2)再看下面的hasFeature函数,里面用的是位运算判断某一位是否为1,所以上述的FEATURE_**定义的值实际上是移位运算时候的位移量; [html] view plain copy publicbooleanhasFeature(intfeature){ return(getFeatures()&(1<<feature))!=0; } 在Activity中获取Activity对应的Window的mFeatures变量的值,也就是PhoneWindow的mFeatures值,但是这个值定义在Window里面,下面利用反射: [html] view plain copy intgetFeature(){ Stringname="android.view.Window"; try{ Fieldfield=Class.forName(name).getDeclaredField("mFeatures"); field.setAccessible(true); returnfield.getInt(getWindow()); }catch(Exceptione){ e.printStackTrace(); } return-1; } 这样就可以获取Window的mFeatures值了。找到了问题的原因,也找到了解决的办法,下面分析该怎么写代码。把上面的异常信息在分析一下,然后总结一下: 问题:Activity里面所有默认的创建的ActionBar的操作都是从initWindowDecorActionBar里面开始的,看上面的异常信息问题就是如何让initWindowDecorActionBar函数里面的 window.hasFeature(Window.FEATURE_ACTION_BAR)判断返回false; 常见有两种方法: 方法一: 调用一下requestWindowFeature(Window.FEATURE_NO_TITLE)就行了,为什么是这样?看一下Activity.requestWindowFeature: 然后跳转到PhoneWindow.requestWindowFeature,列出主要的部分: [html] view plain copy @Override publicbooleanrequestFeature(intfeatureId){ finalintfeatures=getFeatures();//获取mFeatures finalintnewFeatures=features|(1<<featureId); if((newFeatures&(1<<FEATURE_CUSTOM_TITLE))!=0&&(newFeatures&~CUSTOM_TITLE_COMPATIBLE_FEATURES)!=0){ thrownewAndroidRuntimeException("Youcannotcombinecustomtitleswithothertitlefeatures"); } /* 1(features&(1<<FEATURE_NO_TITLE))!=0:当前没有title 2(features&(1<<FEATURE_NO_TITLE))!=0 &&featureId==FEATURE_ACTION_BAR: 当前没有title,这时候指定ACTION_BAR没有任何用处 */ if((features&(1<<FEATURE_NO_TITLE))!=0&&featureId==FEATURE_ACTION_BAR){ returnfalse;//Ignore.Notitledominates. }/* 1(features&(1<<FEATURE_ACTION_BAR))!=0:当前有ACTION_BAR 2(features&(1<<FEATURE_ACTION_BAR))!=0 &&featureId==FEATURE_NO_TITLE: 当前有ACTION_BAR,这时候指定NO_TITLE会去除ACTION_BAR */ if((features&(1<<FEATURE_ACTION_BAR))!=0 &&featureId==FEATURE_NO_TITLE){ removeFeature(FEATURE_ACTION_BAR); } returnsuper.requestFeature(featureId); } 看上面的彩色部分,当原来指定了FEATURE_ACTION_BAR,并且当前指定FEATURE_NO_TITLE的话,就会从当前的mFeatures中的FEATURE_ACTION_BAR标志位去掉,所以下次调用到initWindowDecorActionBar来初始化ActionBar的时候都不会创建ActionBar。 方法二: 利用Theme主题,上网一搜一大把,这些主题可能满足不了我们的需求,有时候不能单单的为了一个配置项去设置一个完全没有关系的Theme,可以通过查找Theme中关于属性值的设置,然后加到自己的定义的Theme里面就可以了,下面的这些Theme,他们的关键配置属性都能在sdk里面找到,路径在: android sdk根路径\platforms\android-xx\data\res\values [html] view plain copy android:theme="@android:style/Theme.Dialog"将一个Activity显示为能话框模式 android:theme="@android:style/Theme.NoTitleBar"不显示应用程序标题栏 android:theme="@android:style/Theme.NoTitleBar.Fullscreen"不显示应用程序标题栏,并全屏 android:theme="Theme.Light"背景为白色 android:theme="Theme.Light.NoTitleBar"白色背景并无标题栏 android:theme="Theme.Light.NoTitleBar.Fullscreen"白色背景,无标题栏,全屏 android:theme="Theme.Black"背景黑色 android:theme="Theme.Black.NoTitleBar"黑色背景并无标题栏 android:theme="Theme.Black.NoTitleBar.Fullscreen"黑色背景,无标题栏,全屏 android:theme="Theme.Wallpaper"用系统桌面为应用程序背景 android:theme="Theme.Wallpaper.NoTitleBar"用系统桌面为应用程序背景,且无标题栏 android:theme="Theme.Wallpaper.NoTitleBar.Fullscreen"用系统桌面为应用程序背景,无标题栏,全屏 android:theme="Translucent"透明背景 android:theme="Theme.Translucent.NoTitleBar"透明背景并无标题 android:theme="Theme.Translucent.NoTitleBar.Fullscreen"透明背景并无标题,全屏 android:theme="Theme.Panel"面板风格显示 android:theme="Theme.Light.Panel"平板风格显示 找一下Theme.NoTitleBar需要的属性,android sdk根路径\platforms\android-xx\data\res\values\themes.xml: [html] view plain copy <!--Variantof{@link#Theme_Light}withnotitlebar--> <stylenamestylename="Theme.Light.NoTitleBar"> <itemnameitemname="android:windowNoTitle">true</item> </style> <!--Variantof{@link#Theme_Light}thathasnotitlebarand nostatusbar.Thistheme sets{@linkandroid.R.attr#windowFullscreen}totrue.--> <stylename="Theme.Light.NoTitleBar.Fullscreen"> <itemnameitemname="android:windowFullscreen">true</item> <itemnameitemname="android:windowContentOverlay">@null</item> </style> 这样就能知道: 去掉TitleBar的关键属性是<itemname="android:windowNoTitle">true</item> 全屏的关键属性是<item name="android:windowFullscreen">true</item> 3 再次思考 上面给出的是一个三层状态,DecorView里面包含: (1) ActionBarOverklayLayout:ActionBar带来的一个布局,遍布了除了第三层最上端View的所有区域 (2) RelativeLayout : Activity的Layout (3) View : 状态栏背景 上面直观的认为是因为ActionBar导致了ActionBarOverklayLayout的创建,根据上面的方法一再做一次尝试: [html] view plain copy protectedvoidonCreate(BundlesavedInstanceState){ requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); ViewGroupgroup=(ViewGroup)getWindow().getDecorView(); LayoutInflater.from(this).inflate(R.layout.activity_main,group,true); ViewServer.get(this).addWindow(this); } 再看一下布局图: 依旧还是3层,再想一想为什么?因为我们上面只是指定了当前的Activity不需要ActionBar,但是回忆一下最上面的Activity的View层次图,系统首先会在DecorView上面加载一个初始布局,然后把Activity的布局放到这个初始布局上id为content的位置上,上面ActionBarOverklayLayout实际上是一个自带了ActionBar布局的初始布局,现在设置了FEATURE_NO_TITLE以后,系统加载的初始布局就是这里的LinearLayout,所以这一层布局依旧还是存在的,再结合上面的removeView修改一下: [html] view plain copy protectedvoidonCreate(BundlesavedInstanceState){ requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); ViewGroupgroup=(ViewGroup)getWindow().getDecorView(); group.removeAllViews(); LayoutInflater.from(this).inflate(R.layout.activity_main,group,true); ViewServer.get(this).addWindow(this); } 再看一下这时候的布局图: 出来了,这下子少了一层并列的系统层了,下面的这个View是手机顶端的信号时间之类的状态信息,经过测试,在group.removeAllViews();的时候并没有这个View,具体什么时候创建的就不去深究了,但是借鉴 http://chaosleong.github.io/blog/2015/11/28/Android-%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD-layout-%E8%B5%84%E6%BA%90/ 这篇文章的内容,在inflate布局实例的时候,会调用ContextThemeWrapper.getResources()和ContextThemeWrapper.getTheme()函数(Activity继承了ContextThemeWrapper),根据当前的资源和主题来实例化对象,我们只要在Application的Theme中加一个全屏配置项: <itemname="android:windowFullscreen">true</item> 然后再看一下这时候的情况: 就只有这一层了,再来对比一下一般情况下设置全屏模式的布局: 复杂程度可见一斑,但是去掉前面这两层目前还不知道有没有什么问题,还需要经过一些测试。根据目前Android系统事件的传输机制来看,应该是 没有什么问题的,通过上述方式可以将我们的布局直接放到根View里面去,减少了中间的两层系统布局。 4 完整的demo https://github.com/houliang/Android-DecorView 里面用到了ViewServer类,该类的使用方法在注释里面说的很清楚了,加上这个类就可以利用sdk根路径\tools\hierarchyviewer.bat观察Activity的布局 最后注明一点:ActionBar和Activity里面放到content里面的View是相同的等级关系,互不隶属。 本文转自 一点点征服 博客园博客,原文链接:http://www.cnblogs.com/ldq2016/p/6835095.html,如需转载请自行联系原作者