Android减少布局层次--有关Activity根视图DecorView的思考

1 前面文章里面总结的一张图

一直觉得有关DecorView还是有些问题没有搞清楚,今天在看了一点有关SurfaceFlinger的内容以后,顿时突发奇想,想到之前的问题,之前的思考是:

虽然可以将DecorView作为Activity布局的父View,也就是只存在
 DecorView---->Activity Layout两层,但是经过试验还是会存在Title Bar,或者说是现在的Action Bar,尝试如下:

protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       ViewGroup group = (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,于是改了一下代码:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ViewGroup group = (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的后面哪一步调用的时候出现了问题,但是还是尝试了一下:

protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       System.out.println("onCreate 0");
       ViewGroup group = (ViewGroup)getWindow().getDecorView();
       System.out.println("onCreate 1");
       group.removeAllViews();
       System.out.println("onCreate 2");
       LayoutInflater.from(this).inflate(R.layout.activity_main, group, true);
       System.out.println("onCreate 3");
       ViewServer.get(this).addWindow(this);
       System.out.println("onCreate over");
}

运行以后结果如下:

onCreate已经完整调用结束,但是比较常规使用方法,就是少了一个setContentView函数,于是先看一下getWindow().getDecorView(),根据前面

的学习知道这里的getWindow()返回的是一个PhoneWindow对象,于是看一下PhoneWindow.getDecorView():

    public final View getDecorView() {
        if (mDecor == null) {
            installDecor();
        }
        return mDecor;
    }

这里就是调用installDecor(),这个函数会调用PhoneWindow.generateDecor()和PhoneWindow.generateLayout(DecorView)函

数分别创建一个DecorView对象、为DecorView加载一个系统布局,再看一下Activity.setContentView函数:

    public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

先看一下这里的getWindow().setContentView(layoutResID)函数,同样这里的getWindow()返回的是一个PhoneWindow对象,看

PhoneWindow.setContentView:

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)) {  //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);
        }
        final Callback cb = 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:

    /** Flag for the "options panel" feature.  This is enabled by default. */
    public static final int FEATURE_OPTIONS_PANEL = 0;
    /** Flag for the "no title" feature, turning off the title at the top of the screen. */
    public static final int FEATURE_NO_TITLE = 1;
    /** Flag for the progress indicator feature */
    public static final int FEATURE_PROGRESS = 2;
    /** Flag for having an icon on the left side of the title bar */
    public static final int FEATURE_LEFT_ICON = 3;
    /** Flag for having an icon on the right side of the title bar */
    public static final int FEATURE_RIGHT_ICON = 4;
    /** Flag for indeterminate progress */
    public static final int FEATURE_INDETERMINATE_PROGRESS = 5;
    /** Flag for the context menu.  This is enabled by default. */
    public static final int FEATURE_CONTEXT_MENU = 6;
    /** Flag for custom title. You cannot combine this feature with other title features. */
    public static final int FEATURE_CUSTOM_TITLE = 7;
    /**Flag for enabling the Action Bar.This is enabled by default for some devices. The Action Bar replaces the title bar
     * and provides an alternate location for an on-screen menu button on some devices.*/
    public static final int FEATURE_ACTION_BAR = 8;
    /**
     * Flag for requesting an Action Bar that overlays window content.
     * Normally an Action Bar will sit in the space above window content, but if this
     * feature is requested along with {@link #FEATURE_ACTION_BAR} it will be layered over
     * the window content itself. This is useful if you would like your app to have more control
     * over how the Action Bar is displayed, such as letting application content scroll beneath
     * an Action Bar with a transparent background or otherwise displaying a transparent/translucent
     * Action Bar over application content.
     */
    public static final int FEATURE_ACTION_BAR_OVERLAY = 9;
    public static final int FEATURE_ACTION_MODE_OVERLAY = 10;
    public static final int FEATURE_SWIPE_TO_DISMISS = 11;
    public static final int FEATURE_CONTENT_TRANSITIONS = 12;
    public static final int FEATURE_ACTIVITY_TRANSITIONS = 13;

目前一共是有13个FEATURE_**,各个FEATURE_**的作用可以在用到的时候看一下注释,这里面有2点很重要:

(1)通过Window.getFeatures函数可知,实际上所有的FEATURE_**最终会集中在mFeatures这个变量里面;

(2)再看下面的hasFeature函数,里面用的是位运算判断某一位是否为1,所以上述的FEATURE_**定义的值实际上是移位运算时候的位移量;

    public boolean hasFeature(int feature) {
        return (getFeatures() & (1 << feature)) != 0;
    }

在Activity中获取Activity对应的Window的mFeatures变量的值,也就是PhoneWindow的mFeatures值,但是这个值定义在Window里面,下面利用反射:

int getFeature(){
       String name = "android.view.Window";
       try {
           Field field = Class.forName(name).getDeclaredField("mFeatures");
           field.setAccessible(true);
           returnfield.getInt(getWindow());
       } catch (Exception e) {
           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,列出主要的部分:

   @Override
    public boolean requestFeature(int featureId) {
        final int features = getFeatures();   //获取mFeatures
        final int newFeatures = features | (1 << featureId);
        if ((newFeatures & (1 << FEATURE_CUSTOM_TITLE)) != 0 &&(newFeatures & ~CUSTOM_TITLE_COMPATIBLE_FEATURES) != 0) {
            throw new AndroidRuntimeException("You cannot combine custom titles with other title features");
        }

       /*
           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) {
            return false; // Ignore. No title dominates.
        }
       /*
           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);
        }
        return super.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

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:
    <!-- Variant of {@link #Theme_Light} with no title bar -->
    <style name="Theme.Light.NoTitleBar">
        <item name="android:windowNoTitle">true</item>
    </style>

    <!-- Variant of {@link #Theme_Light} that has no title bar and
         no status bar.  This theme
         sets {@link android.R.attr#windowFullscreen} to true. -->
    <style name="Theme.Light.NoTitleBar.Fullscreen">
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowContentOverlay">@null</item>
    </style>

这样就能知道:

去掉TitleBar的关键属性是<item
name="android:windowNoTitle">true</item>

全屏的关键属性是<item name="android:windowFullscreen">true</item>

3 再次思考

上面给出的是一个三层状态,DecorView里面包含:

(1) ActionBarOverklayLayout : ActionBar带来的一个布局,遍布了除了第三层最上端View的所有区域

(2) RelativeLayout : Activity的Layout

(3) View : 状态栏背景

上面直观的认为是因为ActionBar导致了ActionBarOverklayLayout的创建,根据上面的方法一再做一次尝试:

<span style="font-family:SimSun;">    protected void onCreate(Bundle savedInstanceState) {
       requestWindowFeature(Window.FEATURE_NO_TITLE);
       super.onCreate(savedInstanceState);
       ViewGroup group = (ViewGroup)getWindow().getDecorView();
       LayoutInflater.from(this).inflate(R.layout.activity_main, group, true);
       ViewServer.get(this).addWindow(this);
    }</span>

再看一下布局图:

依旧还是3层,再想一想为什么?因为我们上面只是指定了当前的Activity不需要ActionBar,但是回忆一下最上面的Activity的View层次

图,系统首先会在DecorView上面加载一个初始布局,然后把Activity的布局放到这个初始布局上id为content的位置上,上面

ActionBarOverklayLayout实际上是一个自带了ActionBar布局的初始布局,现在设置了FEATURE_NO_TITLE以后,系统加载的初始布局就是

这里的LinearLayout,所以这一层布局依旧还是存在的,再结合上面的removeView修改一下:

    protected void onCreate(Bundle savedInstanceState) {
       requestWindowFeature(Window.FEATURE_NO_TITLE);
       super.onCreate(savedInstanceState);
       ViewGroup group = (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中加一个全屏配置项:

<item name="android:windowFullscreen">true</item>

然后再看一下这时候的情况:

就只有这一层了,再来对比一下一般情况下设置全屏模式的布局:

复杂程度可见一斑,但是去掉前面这两层目前还不知道有没有什么问题,还需要经过一些测试。

4 完整的demo

https://github.com/houliang/Android-DecorView

里面用到了ViewServer类,该类的使用方法在注释里面说的很清楚了,加上这个类就可以利用 sdk根路径\tools\hierarchyviewer.bat 观察Activity的布局

最后注明一点:ActionBar和Activity里面放到content里面的View是相同的等级关系,互不隶属。

时间: 2024-11-01 16:20:52

Android减少布局层次--有关Activity根视图DecorView的思考的相关文章

继承于Layout的自定义View减少布局层次

不管是为了封装也好,实现特殊的效果也好,大家或多或少都会进行自定义View的实践,这中间又主要有两种:一种是继承于View或ViewGroup,还有一个是继承于各种已存在的Layout使用XML来写. 今天要来讨论的是第二种,实践就不详细说了,这里主要是针对这种方式带来的布局层次过深的问题提出两个方案. 第一种,注意在布局xml中使用merge,千万不要误解这个只在FrameLayout时候才能用哦,这个的准确作用是在解析XML布局时由此标志位就不解析直接将这一层忽略,将下面层次的view直接添

Android应用程序窗口(Activity)的视图对象(View)的创建过程分析

从前文可知道,每一个Activity组件都有一个关联的Window对象,用来描述一个应用程序窗口.每一个应用程序窗口内部又包含有一个View对象,用来描述应用程序窗口的视图.应用程序窗口视图是真正用来实现UI内容和布局的,也就是说,每一个Activity组件的UI内容和布局都是通过与其所关联的一个Window对象的内部的一个View对象来实现的.在本文中,我们就详细分析应用程序窗口视图的创建过程. 在前面Android应用程序窗口(Activity)实现框架简要介绍和学习计划一文中提到,应用程序

Android应用程序窗口(Activity)的测量(Measure)、布局(Layout)和绘制(Draw)过程分析

文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/8372924 在前面一篇文章中,我们分析了Android应用程序窗口的绘图表面的创建过程.Android应用程序窗口的绘图表面在创建完成之后,我们就可以从上到下地绘制它里面的各个视图了,即各个UI元素了.不过在绘制这些UI元素之前,我们还需要从上到下地测量它们实际所需要的大小,以及对它们的位置进行合适的安排,即对它们进行合适的布局.在本文中,我们

三、Android学习第三天——Activity的布局初步介绍(转)

(转自:http://wenku.baidu.com/view/af39b3164431b90d6c85c72f.html) 三.Android学习第三天——Activity的布局初步介绍 今天总结下Activity相关布局的一些知识: Activity最简单跟常用的布局分为两种: ①LinearLayout -- 线性布局 ②TableLayout -- 表格布局 ③RelativeLayout -- 相对布局(今后将会频繁的使用到这个布局) 下面来简单总结下前两种(线性/表格)布局当中常用到

Android优化——UI优化(一)优化布局层次

优化布局层次 1.避免布局镶嵌过深(如下) <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent&quo

获取activity的根视图

Activity的根视图是什么? Activity所谓的根视图,就是Activity的最底层的View,也就是在Acitivty创建的时候setContentView的时候传入的View. 如何获取到Activity的根视图? ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView(); 怎样获取根视图中的子视图? View decorChild = decor.getChildAt(index);// index为位置编号

Android应用程序窗口(Activity)与WindowManagerService服务的连接过程分析

在前两文中,我们分析了Activity组件的窗口对象和视图对象的创建过程.Activity组件在其窗口对象和视图对象创建完成之后,就会请求与WindowManagerService建立一个连接,即请求WindowManagerService为其增加一个WindowState对象,用来描述它的窗口状态.在本文中,我们就详细分析Activity组件与WindowManagerService的连接过程. 我们从两方面来看Activity组件与WindowManagerService服务之间的连接.一方

Android应用程序窗口(Activity)的绘图表面(Surface)的创建过程分析

文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/8303098 在前文中,我们分析了应用程序窗口连接到WindowManagerService服务的过程.在这个过程中,WindowManagerService服务会为应用程序窗口创建过一个到SurfaceFlinger服务的连接.有了这个连接之后,WindowManagerService服务就可以为应用程序窗口创建绘图表面了,以便可以用来渲染窗口

Android开发学习笔记之Activity属性设置

麦子学院android开发老师说Activity是Android组件中最基本也是最为常见用的四大组件之一,在android开发中运用极为广泛,作为android开发初学者需要熟练掌握,麦子学院android开发老师分享了Activity属性的常用设置. android:allowTaskReparenting 是否允许activity更换从属的任务,比如从短信息任务 切换到浏览器任务. android:alwaysRetainTaskState 是否保留状态不变, 比如切换回home, 再从新打