就用一部舞台剧的故事,带你揭开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; }
🎯 老王做了什么:
- 创建了舞台骨架
DecorView
。 - 在
generateLayout()
里,根据阿强要求的“无标题栏”特征 (FEATURE_NO_TITLE
),选择了一个更简洁的舞台布局模板 (通常是R.layout.screen_simple
,它本身不包含状态栏区域)。 - 更重要的是:他直接在窗口的属性上设置了一个魔法标记:
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做了什么:
- 看到了窗口属性中的
FLAG_FULLSCREEN
标志。 - 直接命令灯光师小张 (
StatusBarManagerService
) :隐藏状态栏! - 如果需要隐藏导航栏(通常在
FLAG_FULLSCREEN
基础上配合View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
使用),也会命令音响师小美 (NavigationBarController
) 隐藏导航栏! - 计算窗口的实际显示区域:现在这个区域等于整个屏幕大小了,因为状态栏/导航栏占用的空间被释放出来了。
- 通知图形系统 (
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. 同样会触发重新计算所有受影响窗口的显示区域 } }
🎯 动态全屏发生了什么:
-
View层 直接通过
setSystemUiVisibility()
发出请求,设置新的标志组合:View.SYSTEM_UI_FLAG_FULLSCREEN
:隐藏状态栏 (等价于FLAG_FULLSCREEN
的动态版)。View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
:隐藏导航栏。View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
:真正的沉浸模式,触摸屏幕边缘不会立即退出隐藏状态,而是短暂显示后自动隐藏。
-
请求通过
ViewRootImpl
传递给WMS
。 -
WMS 再次协调 StatusBarManagerService 和 NavigationBarController 进行显示/隐藏操作。
-
WMS 重新计算窗口的可用显示区域(再次变成全屏)。
-
ViewRootImpl 会收到
WMS
发来的新窗口尺寸 (relayoutWindow
),触发performTraversals()
,最终调用View.onSizeChanged()
,你的View就知道自己现在占据整个屏幕了!
-
📜 总结:全屏实现的“剧本”精髓
-
发起请求:
- 静态 (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
)。
- 静态 (Activity创建时) :在
-
传递指令:
- 无论静态标志还是动态可见性,最终都会汇集到窗口的
LayoutParams
属性或专门的SystemUiVisibility
状态。 - 舞台总监老李 (
ViewRootImpl
) 通过WindowSession
将关键信息(addWindow
,relayoutWindow
,setSystemUiVisibility
)传递给总控中心WMS
。
- 无论静态标志还是动态可见性,最终都会汇集到窗口的
-
执行隐藏:
-
总控中心
WMS
是核心决策者。它看到FLAG_FULLSCREEN
或特定的SystemUiVisibility
标志后,命令它的两个得力干将:StatusBarManagerService
:负责隐藏或显示状态栏。NavigationBarController
:负责隐藏或显示导航栏(尤其在有动态标志SYSTEM_UI_FLAG_HIDE_NAVIGATION
时)。
-
-
重绘窗口:
WMS
重新计算窗口的 最终显示区域 (DisplayFrame
) 。在全屏状态下,这个区域等于整个屏幕的物理尺寸,不再为状态栏/导航栏预留空间。- 这个新的区域信息会通过
ViewRootImpl
传递回应用层,触发 View 树的测量 (measure
)、布局 (layout
),最终你的ContentView
就知道自己可以“为所欲为”地填满整个屏幕了!SurfaceFlinger
也会收到指令绘制这块全屏的Surface
。
⚠️ 全屏模式的“坑”与注意事项
- 内容避开系统栏区域:老式方法
FLAG_FULLSCREEN
会让你的View直接延伸到状态栏后面,但状态栏突然弹出可能覆盖内容。用fitSystemWindows
属性或WindowInsets
处理更优雅。 - 沉浸模式交互:
SYSTEM_UI_FLAG_HIDE_NAVIGATION
隐藏导航栏后,用户任何触摸都会让它们重新出现。IMMERSIVE
或IMMERSIVE_STICKY
标志就是为了解决这个问题而生的。 - 输入法(软键盘) :全屏时弹出键盘也可能导致布局问题,需要处理好
Window
的SOFT_INPUT_ADJUST_RESIZE
等标志。 - 退出全屏:记得清除全屏标志 (
clearFlags()
) 或设置新的SystemUiVisibility
,并通知WMS
。
通过这部舞台剧,是不是感觉Android窗口系统实现全屏的过程就像一场精心编排的演出?每个角色 (PhoneWindow
, DecorView
, ViewRootImpl
, WMS
, *BarService
) 各司其职,通过标志 (Flags
, SystemUiVisibility
) 传递指令,最终由系统服务 (WMS
) 这个总导演协调灯光、音响(状态栏、导航栏),让主角 (Activity/View
) 得以占据整个舞台(屏幕)进行表演!这就是Android全屏背后的源码魔法!🎩✨