android窗口之《全屏大作战》

19 阅读8分钟

就用一部舞台剧的故事,带你揭开Android全屏背后的源码魔法。放心,咱们不用硬啃代码,而是把代码变成“演员”和“剧本”来演出!


🎭 舞台剧:《全屏大作战》

角色介绍:

  • Activity (主角阿强) :想要实现全屏效果的APP界面。
  • PhoneWindow (舞台设计师老王) :每个Activity专属的窗口设计师,负责设计窗口的“框架”和“装饰”。
  • DecorView (舞台本身) :老王设计出来的最底层的“舞台”,包含标题栏、内容区等区域。
  • ViewRootImpl (舞台总监老李) :连接DecorView和系统窗口管理服务的核心人物,负责执行最终操作。
  • WindowManager (舞台经理) :负责和系统窗口管理服务沟通,安排舞台的摆放、灯光(状态栏)、音响(导航栏)等。
  • WindowManagerService (WMS, 总控中心) :Android系统的核心服务,真正管理所有窗口的位置、大小、层级和系统UI显示。
  • StatusBarManagerService (灯光师小张) :WMS手下,专门管状态栏(顶部通知栏)的显示/隐藏。
  • NavigationBarController (音响师小美) :WMS手下,专门管导航栏(底部虚拟按键栏)的显示/隐藏。

🎬 第一幕:阿强的全屏梦想 & 老王的初步设计

  • 阿强 (Activity) : “老王啊,我这个界面要放电影,得搞个全屏!一点多余的边角料都不要!”

  • 老王 (PhoneWindow) : “明白!全屏是吧?我先在‘设计图纸’上标注一下!” (onCreate() 方法里)

    java

    // PhoneWindow.java - 设计师老王的“设计图纸”标注处
    public void setContentView(int layoutResID) {
        // ... 初始化DecorView等 ...
        if (mDecorContentParent == null) { // 检查是否已经有DecorView了
            installDecor(); // 重点!准备搭建舞台框架
        }
        // ... 设置用户布局 ...
    }
    
    private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor(-1); // 1. 创建舞台(DecorView)骨架
            // ... 设置DecorView属性 ...
        }
        if (mContentParent == null) {
            // 2. 根据Window的特征(Feature)生成具体的舞台布局
            mContentParent = generateLayout(mDecor);
            // 关键点:generateLayout() 内部会根据我们设置的标志(flags)选择不同的舞台模板!
        }
    }
    
    protected ViewGroup generateLayout(DecorView decor) {
        // ... 读取Window的属性 ...
        int layoutResource;
        int features = getLocalFeatures(); // 获取窗口特征(比如是否要标题栏、全屏等)
    
        // 3. 判断是否要全屏!(检查特征标志)
        if ((features & (1 << FEATURE_NO_TITLE)) != 0) {
            if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
                layoutResource = R.layout.screen_simple; // 可能是带ActionBar但无Title的
            } else {
                layoutResource = R.layout.screen_simple; // 或者更简单的无标题布局
            }
            // 4. 关键一步:设置全屏标志位 (FLAG_FULLSCREEN)
            setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                     WindowManager.LayoutParams.FLAG_FULLSCREEN & ~getForcedWindowFlags());
        } else {
            // ... 其他布局 ...
        }
    
        // 5. 把选定的布局模板“安装”到DecorView这个舞台上
        View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, ...);
        return contentParent;
    }
    

    🎯 老王做了什么:

    1. 创建了舞台骨架 DecorView
    2. 在 generateLayout() 里,根据阿强要求的“无标题栏”特征 (FEATURE_NO_TITLE),选择了一个更简洁的舞台布局模板 (通常是 R.layout.screen_simple,它本身不包含状态栏区域)。
    3. 更重要的是:他直接在窗口的属性上设置了一个魔法标记FLAG_FULLSCREEN!这个标记相当于告诉后面的工作人员:“这个窗口要求占据整个屏幕空间,系统UI(状态栏)应该被隐藏”。

🎬 第二幕:舞台总监老李的沟通与执行

  • 老王 (PhoneWindow) : “老李啊,舞台框架我搭好了 (DecorView),这是窗口的属性参数 (LayoutParams),上面标了 FLAG_FULLSCREEN,你赶紧去安排一下!”

  • 老李 (ViewRootImpl) : “收到!(setView() 方法里) 我这就去和总控中心 (WMS) 沟通,让他们把灯光(状态栏)和音响(导航栏)都给我关了!”

    java

    // ViewRootImpl.java - 舞台总监老李的沟通手册
    public void setView(View view, WindowManager.LayoutParams attrs, ...) {
        // ... 各种准备工作 ...
        // 关键点:请求重新布局并最终将窗口参数传递给WMS
        requestLayout();
        // 通过WindowSession (通信代理) 调用WMS的addToDisplay()方法
        res = mWindowSession.addToDisplay(mWindow, ..., attrs, ...);
        // ...
    }
    
  • 老李 -> WMS (总控中心) : “报告!新窗口阿强 (WindowToken) 申请显示,它的属性里带了 FLAG_FULLSCREEN,要求隐藏状态栏!”

  • WMS (总控中心) : (收到 addToDisplay 请求)

    java

    // WindowManagerService.java - 总控中心的决策逻辑
    public int addWindow(...) {
        // ... 权限检查、窗口类型检查等 ...
        // 关键点:应用窗口的标志位 (FLAG_FULLSCREEN)
        if ((attrs.flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0) {
            // 1. 告诉灯光师小张(StatusBarManagerService):这个窗口要求全屏,隐藏状态栏!
            mPolicy.altersSystemUiVisibilityLw(visibility, ...);
            // 或者更直接地调用:
            mStatusBarController.hide(...);
            // 2. 同样,如果需要隐藏导航栏(比如结合了View.SYSTEM_UI_FLAG_HIDE_NAVIGATION):
            mNavigationBarController.hide(...);
        }
        // ... 其他处理,如计算窗口大小、位置、层级等 ...
        // 3. 最终,WMS会通知SurfaceFlinger绘制这个没有状态栏覆盖的窗口区域
    }
    

    🎯 WMS做了什么:

    1. 看到了窗口属性中的 FLAG_FULLSCREEN 标志。
    2. 直接命令灯光师小张 (StatusBarManagerService)隐藏状态栏!
    3. 如果需要隐藏导航栏(通常在 FLAG_FULLSCREEN 基础上配合 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 使用),也会命令音响师小美 (NavigationBarController隐藏导航栏!
    4. 计算窗口的实际显示区域:现在这个区域等于整个屏幕大小了,因为状态栏/导航栏占用的空间被释放出来了。
    5. 通知图形系统 (SurfaceFlinger) 绘制这个全屏的区域。

🎬 第三幕:动态全屏与沉浸模式(加演剧情)

阿强可能想在电影播放中动态进入/退出全屏,或者在游戏中完全沉浸。这需要舞台上的“演员”(内容View)直接和舞台总监老李沟通!

  • 阿强 (Activity) : “老李!现在立刻马上给我全屏!连虚拟按键都藏起来!要最沉浸那种!”

  • 老李 (ViewRootImpl) : “好的!(View.setSystemUiVisibility() 方法)”

    java

    // View.java - 舞台上的“演员”可以直接喊话总监
    public void setSystemUiVisibility(int visibility) {
        if (visibility != mSystemUiVisibility) {
            mSystemUiVisibility = visibility;
            // 关键点:通知父View,最终会传到ViewRootImpl
            if (mParent != null) {
                mParent.recomputeViewAttributes(this);
            }
        }
    }
    
    // ViewRootImpl.java - 老李再次行动
    void updateSystemUiVisibility(int seq, int globalVisibility, int localValue, int localChanges) {
        // ... 计算最终的可见性标志 ...
        // 关键点:再次通过WindowSession调用WMS的relayoutWindow() 或 专门的setSystemUiVisibility()方法
        try {
            mWindowSession.setSystemUiVisibility(..., mSystemUiVisibility);
            // 或者通过relayoutWindow更新窗口参数
        } catch (...) { ... }
    }
    
  • 老李 -> WMS (总控中心) : “报告!窗口阿强内部演员要求改变系统UI可见性!新标志是 SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_HIDE_NAVIGATION | SYSTEM_UI_FLAG_IMMERSIVE_STICKY!”

  • WMS (总控中心) : “收到!(setSystemUiVisibility 处理)”

    java

    // WindowManagerService.java - 处理动态UI可见性变化
    public void setSystemUiVisibility(int vis, int fullscreenStackVis, int dockedStackVis, int mask, ...) {
        // ... 权限检查 ...
        // 关键点:更新系统UI状态
        synchronized(mWindowMap) {
            // 1. 计算新的全局系统UI可见性
            // 2. 通知StatusBarManagerService/NavigationBarController:
            mPolicy.updateSystemUiVisibilityLw(newVis, ...);
            // 3. 同样会触发重新计算所有受影响窗口的显示区域
        }
    }
    

    🎯 动态全屏发生了什么:

    1. View层 直接通过 setSystemUiVisibility() 发出请求,设置新的标志组合:

      • View.SYSTEM_UI_FLAG_FULLSCREEN:隐藏状态栏 (等价于 FLAG_FULLSCREEN 的动态版)。
      • View.SYSTEM_UI_FLAG_HIDE_NAVIGATION:隐藏导航栏。
      • View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY:真正的沉浸模式,触摸屏幕边缘不会立即退出隐藏状态,而是短暂显示后自动隐藏。
    2. 请求通过 ViewRootImpl 传递给 WMS

    3. WMS 再次协调 StatusBarManagerService 和 NavigationBarController 进行显示/隐藏操作。

    4. WMS 重新计算窗口的可用显示区域(再次变成全屏)。

    5. ViewRootImpl 会收到 WMS 发来的新窗口尺寸 (relayoutWindow),触发 performTraversals(),最终调用 View.onSizeChanged(),你的View就知道自己现在占据整个屏幕了!


📜 总结:全屏实现的“剧本”精髓

  1. 发起请求

    • 静态 (Activity创建时) :在 Activity.onCreate() 中 requestWindowFeature(Window.FEATURE_NO_TITLE) + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, ...)。设计师老王 (PhoneWindow) 在搭建舞台框架 (installDecor) 时,根据标志选择无标题模板并设置 FLAG_FULLSCREEN
    • 动态 (运行时) :在任意View上调用 setSystemUiVisibility(View.SYSTEM_UI_FLAG_XXX)。演员直接喊话总监老李 (ViewRootImpl)。
  2. 传递指令

    • 无论静态标志还是动态可见性,最终都会汇集到窗口的 LayoutParams 属性或专门的 SystemUiVisibility 状态。
    • 舞台总监老李 (ViewRootImpl) 通过 WindowSession 将关键信息(addWindowrelayoutWindowsetSystemUiVisibility)传递给总控中心 WMS
  3. 执行隐藏

    • 总控中心 WMS 是核心决策者。它看到 FLAG_FULLSCREEN 或特定的 SystemUiVisibility 标志后,命令它的两个得力干将:

      • StatusBarManagerService:负责隐藏或显示状态栏。
      • NavigationBarController:负责隐藏或显示导航栏(尤其在有动态标志 SYSTEM_UI_FLAG_HIDE_NAVIGATION 时)。
  4. 重绘窗口

    • WMS 重新计算窗口的 最终显示区域 (DisplayFrame) 。在全屏状态下,这个区域等于整个屏幕的物理尺寸,不再为状态栏/导航栏预留空间。
    • 这个新的区域信息会通过 ViewRootImpl 传递回应用层,触发 View 树的测量 (measure)、布局 (layout),最终你的 ContentView 就知道自己可以“为所欲为”地填满整个屏幕了!SurfaceFlinger 也会收到指令绘制这块全屏的 Surface

⚠️ 全屏模式的“坑”与注意事项

  1. 内容避开系统栏区域:老式方法 FLAG_FULLSCREEN 会让你的View直接延伸到状态栏后面,但状态栏突然弹出可能覆盖内容。用 fitSystemWindows 属性或 WindowInsets 处理更优雅。
  2. 沉浸模式交互SYSTEM_UI_FLAG_HIDE_NAVIGATION 隐藏导航栏后,用户任何触摸都会让它们重新出现。IMMERSIVE 或 IMMERSIVE_STICKY 标志就是为了解决这个问题而生的。
  3. 输入法(软键盘) :全屏时弹出键盘也可能导致布局问题,需要处理好 Window 的 SOFT_INPUT_ADJUST_RESIZE 等标志。
  4. 退出全屏:记得清除全屏标志 (clearFlags()) 或设置新的 SystemUiVisibility,并通知 WMS

通过这部舞台剧,是不是感觉Android窗口系统实现全屏的过程就像一场精心编排的演出?每个角色 (PhoneWindowDecorViewViewRootImplWMS*BarService) 各司其职,通过标志 (FlagsSystemUiVisibility) 传递指令,最终由系统服务 (WMS) 这个总导演协调灯光、音响(状态栏、导航栏),让主角 (Activity/View) 得以占据整个舞台(屏幕)进行表演!这就是Android全屏背后的源码魔法!🎩✨