Android Theme XML 属性如何影响 PhoneWindow:从 TypedArray 到 LayoutParams 的完整链路
每天写布局、配主题,但你有没有想过------
android:windowIsTranslucent="true"这行 XML 到底是怎么一路传递,最终影响了 Window 的像素格式?为什么statusBarColor有时候怎么设都不生效?FLAG_LAYOUT_IN_SCREEN到底是干什么的?本文将带你深入 Android Framework 源码,逐属性、逐 Flag 拆解这条完整链路。
目录
- [架构概览:Theme → PhoneWindow 的数据流](#架构概览:Theme → PhoneWindow 的数据流 "#1-%E6%9E%B6%E6%9E%84%E6%A6%82%E8%A7%88theme--phonewindow-%E7%9A%84%E6%95%B0%E6%8D%AE%E6%B5%81")
- 属性的定义:R.styleable.Window
- [PhoneWindow 读取属性的两个入口](#PhoneWindow 读取属性的两个入口 "#3-phonewindow-%E8%AF%BB%E5%8F%96%E5%B1%9E%E6%80%A7%E7%9A%84%E4%B8%A4%E4%B8%AA%E5%85%A5%E5%8F%A3")
- [深入理解 WindowManager.LayoutParams 的核心 Flags](#深入理解 WindowManager.LayoutParams 的核心 Flags "#4-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3-windowmanagerlayoutparams-%E7%9A%84%E6%A0%B8%E5%BF%83-flags")
- [4.1 什么是 Flags?](#4.1 什么是 Flags? "#41-%E4%BB%80%E4%B9%88%E6%98%AF-flags")
- [4.2 布局相关 Flags(Layout Flags)](#4.2 布局相关 Flags(Layout Flags) "#42-%E5%B8%83%E5%B1%80%E7%9B%B8%E5%85%B3-flagslayout-flags")
- [4.3 触摸与焦点 Flags(Input Flags)](#4.3 触摸与焦点 Flags(Input Flags) "#43-%E8%A7%A6%E6%91%B8%E4%B8%8E%E7%84%A6%E7%82%B9-flagsinput-flags")
- [4.4 系统栏相关 Flags(System Bar Flags)](#4.4 系统栏相关 Flags(System Bar Flags) "#44-%E7%B3%BB%E7%BB%9F%E6%A0%8F%E7%9B%B8%E5%85%B3-flags%E7%B3%BB%E7%BB%9F-bar-flags")
- [4.5 安全与显示 Flags(Security & Display Flags)](#4.5 安全与显示 Flags(Security & Display Flags) "#45-%E5%AE%89%E5%85%A8%E4%B8%8E%E6%98%BE%E7%A4%BA-flags%E5%AE%89%E5%85%A8%E4%B8%8E%E6%98%BE%E7%A4%BA-flags")
- [4.6 锁屏相关 Flags(Keyguard Flags)](#4.6 锁屏相关 Flags(Keyguard Flags) "#46-%E9%94%81%E5%B1%8F%E7%9B%B8%E5%85%B3-flags%E9%94%81%E5%B1%8F%E7%9B%B8%E5%85%B3-flags")
- [4.7 Flags 组合使用矩阵](#4.7 Flags 组合使用矩阵 "#47-flags-%E7%BB%84%E5%90%88%E4%BD%BF%E7%94%A8%E7%9F%A9%E9%98%B5")
- [Theme 属性分类详解](#Theme 属性分类详解 "#5-theme-%E5%B1%9E%E6%80%A7%E5%88%86%E7%B1%BB%E8%AF%A6%E8%A7%A3")
- [5.1 窗口 Flags 类](#5.1 窗口 Flags 类 "#51-%E7%AA%97%E5%8F%A3-flags-%E7%B1%BB")
- [5.2 窗口 Type 类](#5.2 窗口 Type 类 "#52-%E7%AA%97%E5%8F%A3-type-%E7%B1%BB")
- [5.3 尺寸与布局类](#5.3 尺寸与布局类 "#53-%E5%B0%BA%E5%AF%B8%E4%B8%8E%E5%B8%83%E5%B1%80%E7%B1%BB")
- [5.4 背景与样式类](#5.4 背景与样式类 "#54-%E8%83%8C%E6%99%AF%E4%B8%8E%E6%A0%B7%E5%BC%8F%E7%B1%BB")
- [5.5 系统状态栏/导航栏类](#5.5 系统状态栏/导航栏类 "#55-%E7%B3%BB%E7%BB%9F%E7%8A%B6%E6%80%81%E6%A0%8F%E5%AF%BC%E8%88%AA%E6%A0%8F%E7%B1%BB")
- [5.6 软键盘(SoftInput)类](#5.6 软键盘(SoftInput)类 "#56-%E8%BD%AF%E9%94%AE%E7%9B%98softinput%E7%B1%BB")
- [5.7 转场动画类](#5.7 转场动画类 "#57-%E8%BD%AC%E5%9C%BA%E5%8A%A8%E7%94%BB%E7%B1%BB")
- [5.8 像素格式与绘制类](#5.8 像素格式与绘制类 "#58-%E5%83%8F%E7%B4%A0%E6%A0%BC%E5%BC%8F%E4%B8%8E%E7%BB%98%E5%88%B6%E7%B1%BB")
- [5.9 FitSystemWindows 与 Edge-to-Edge 类](#5.9 FitSystemWindows 与 Edge-to-Edge 类 "#59-fitsystemwindows-%E4%B8%8E-edge-to-edge-%E7%B1%BB")
- [5.10 其他行为属性](#5.10 其他行为属性 "#510-%E5%85%B6%E4%BB%96%E8%A1%8C%E4%B8%BA%E5%B1%9E%E6%80%A7")
- 完整映射表速查
- 实战场景
- [7.1 全屏 Activity](#7.1 全屏 Activity "#71-%E5%85%A8%E5%B1%8F-activity")
- [7.2 透明背景 Activity / Dialog 样式 Activity](#7.2 透明背景 Activity / Dialog 样式 Activity "#72-%E9%80%8F%E6%98%8E%E8%83%8C%E6%99%AF-activity--dialog-%E6%A0%B7%E5%BC%8F-activity")
- [7.3 自定义状态栏颜色(Material 风格)](#7.3 自定义状态栏颜色(Material 风格) "#73-%E8%87%AA%E5%AE%9A%E4%B9%89%E7%8A%B6%E6%80%81%E6%A0%8F%E9%A2%9C%E8%89%B2material-%E9%A3%8E%E6%A0%BC")
- [7.4 图片查看器(沉浸式)](#7.4 图片查看器(沉浸式) "#74-%E5%9B%BE%E7%89%87%E6%9F%A5%E7%9C%8B%E5%99%A8%E6%B2%89%E6%B5%B8%E5%BC%8F")
- [7.5 锁屏上显示窗口](#7.5 锁屏上显示窗口 "#75-%E9%94%81%E5%B1%8F%E4%B8%8A%E6%98%BE%E7%A4%BA%E7%AA%97%E5%8F%A3")
- [7.6 禁止截屏的敏感页面](#7.6 禁止截屏的敏感页面 "#76-%E7%A6%81%E6%AD%A2%E6%88%AA%E5%B1%8F%E7%9A%84%E6%95%8F%E6%84%9F%E9%A1%B5%E9%9D%A2")
- [7.7 软键盘适配表单页面](#7.7 软键盘适配表单页面 "#77-%E8%BD%AF%E9%94%AE%E7%9B%98%E9%80%82%E9%85%8D%E8%A1%A8%E5%8D%95%E9%A1%B5%E9%9D%A2")
- 最佳实践与常见陷阱
- 总结
1. 架构概览:Theme → PhoneWindow 的数据流
先上一张整体数据流图,建立心智模型:
scss
AndroidManifest.xml 中指定的 Theme / setTheme()
│
▼
Resources.Theme (持有所有属性的键值对)
│
│ context.obtainStyledAttributes(R.styleable.Window)
▼
TypedArray (内存中的属性值集合,按索引访问)
│
├──▶ PhoneWindow() 构造函数 ──▶ 读取约 15 个属性
│ ├── mIsFloating
│ ├── 部分 flags (windowNoDisplay, windowSecure...)
│ └── 初始 LayoutParams 设置
│
└──▶ PhoneWindow.generateLayout() ──▶ 读取约 80+ 个属性(核心分发)
│
├──▶ requestFeature() ──▶ FEATURE_NO_TITLE, FEATURE_ACTION_BAR 等
├──▶ addFlags() / setFlags()──▶ WindowManager.LayoutParams.flags
├──▶ setBackgroundDrawable()──▶ DecorView 背景
├──▶ setStatusBarColor() ──▶ 通过 InsetsController 控制 SystemUI
├──▶ setNavigationBarColor()──▶ 同上
├──▶ setSoftInputMode() ──▶ WindowManager.LayoutParams.softInputMode
├──▶ setElevation() ──▶ WindowManager.LayoutParams.elevation
├──▶ getAttributes() ──▶ 直接修改 WindowManager.LayoutParams 字段:
│ ├── width / height / minWidth / minHeight
│ ├── format (OPAQUE / TRANSLUCENT)
│ ├── windowAnimations
│ ├── layoutInDisplayCutoutMode
│ └── ...
│
└──▶ 构建 DecorView 布局 ──▶ 决定 mContentParent 的结构
│
▼
最终通过 WindowManager.addView()
将 LayoutParams 传递给 WMS (WindowManagerService)
│
▼
WMS 根据 LayoutParams 决定:
- Z-order(层级顺序)
- 窗口尺寸与位置
- Surface 像素格式
- 输入事件分发策略
- 系统栏行为
- 转场动画
关键类说明:
| 类 | 角色 | 所在进程 |
|---|---|---|
Window (android.view.Window) |
抽象基类,定义 flags、type、softInputMode、features 等抽象方法 | App 进程 |
PhoneWindow (com.android.internal.policy.PhoneWindow) |
唯一的 Window 实现(系统进程外),所有 Activity/Dialog 的 Window 都是它 | App 进程 |
WindowManager.LayoutParams |
描述 Window 的布局参数的结构体,约 80+ 字段 | App 进程创建,IPC 传递 |
WindowManagerService (WMS) |
系统服务,管理所有窗口的 Z-order、尺寸、输入事件分发 | system_server 进程 |
SurfaceFlinger |
系统服务,消费 WMS 创建的 Surface,做 GPU 合成 | surfaceflinger 进程 |
关键认知:Theme 属性 → PhoneWindow → LayoutParams → WMS 这条链路中,真正的"消费者"是 WMS。你在 Theme 里设置的属性,本质是在向 WMS 描述"我希望我的窗口在系统中如何表现",而 PhoneWindow 是中间的翻译官。
2. 属性的定义:R.styleable.Window
所有 Window 相关的 theme 属性定义在 AOSP 的 frameworks/base/core/res/res/values/attrs.xml 中,属于 <declare-styleable name="Window">。截至 Android 14 (API 34),这个 styleable 包含约 120+ 个属性。
xml
<!-- frameworks/base/core/res/res/values/attrs.xml -->
<declare-styleable name="Window">
<!-- Flags 类 -->
<attr name="windowNoDisplay" format="boolean" />
<attr name="windowDisablePreview" format="boolean" />
<attr name="windowEnableSplitTouch" format="boolean" />
<attr name="windowShowWallpaper" format="boolean" />
<attr name="windowBlurBehind" format="boolean" />
<attr name="windowSecure" format="boolean" />
<attr name="windowNotTouchable" format="boolean" />
<!-- 系统栏类 -->
<attr name="windowTranslucentStatus" format="boolean" />
<attr name="windowTranslucentNavigation" format="boolean" />
<attr name="windowDrawsSystemBarBackgrounds" format="boolean" />
<attr name="statusBarColor" format="color" />
<attr name="navigationBarColor" format="color" />
<attr name="windowLightStatusBar" format="boolean" />
<attr name="windowLightNavigationBar" format="boolean" />
<!-- 尺寸类 -->
<attr name="windowFixedWidth" format="dimension" />
<attr name="windowFixedHeight" format="dimension" />
<attr name="windowMinWidthMajor" format="dimension" />
<attr name="windowMinWidthMinor" format="dimension" />
<attr name="windowElevation" format="dimension" />
<!-- 样式/背景类 -->
<attr name="windowBackground" format="reference|color" />
<attr name="windowIsTranslucent" format="boolean" />
<attr name="windowIsFloating" format="boolean" />
<attr name="windowNoTitle" format="boolean" />
<attr name="windowFullscreen" format="boolean" />
<attr name="windowTitleSize" format="dimension" />
<!-- 软键盘 -->
<attr name="windowSoftInputMode" />
<!-- 转场动画 -->
<attr name="windowActivityTransitions" format="boolean" />
<attr name="windowEnterTransition" format="reference" />
<attr name="windowExitTransition" format="reference" />
<!-- ...还有 100+ 个 -->
</declare-styleable>
这些属性被 Android 系统的预置 Theme(Theme.Material3.*、Theme.Material.*、Theme.Holo.* 等)设置了默认值,应用层通过自定义 Theme 覆写。
默认值的来源链 (以 Theme.Material3.DayNight.NoActionBar 为例):
yaml
Theme.Material3.DayNight.NoActionBar
└─ parent: Theme.Material3.DayNight
└─ parent: Theme.Material3.Light
└─ parent: Theme.Material3 (或 Theme.Material)
└─ parent: Theme.Overlay.Material3 (部分属性)
└─ parent: Platform (系统内置,android: 前缀的属性在这里)
在你的 Theme 中设置的属性,会逐级向上覆盖父 Theme 的同名属性。
3. PhoneWindow 读取属性的两个入口
PhoneWindow 在两个时机读取 Window styleable:
入口一:构造函数
java
// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public PhoneWindow(Context context, Window preservedWindow,
ActivityConfigCallback activityConfigCallback) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
// ★ 第一次读取 Window styleable
TypedArray a = context.obtainStyledAttributes(
null, // 不传 AttributeSet(不从 XML layout 读)
R.styleable.Window, // 目标 styleable
0, // defStyleAttr=0,不设中间层
R.style.PhoneWindow); // defStyleRes:兜底默认样式
关键参数解读:
set = null:不从 XML layout 节点读取属性,只从 Theme 读取(这正是我们关心的)defStyleAttr = 0:没有中间层默认 attr。如果传了(比如R.attr.windowStyle),系统会先从 Theme 中取这个 attr 指向的 style 作为默认值defStyleRes = R.style.PhoneWindow:如果 Theme 完全没设置某属性,回退到PhoneWindow这个 style 取值
构造阶段消费的属性(约 15 个):
| 属性 | 消费方式 |
|---|---|
windowNoDisplay |
直接 setFlags |
windowDisablePreview |
直接 addFlags |
windowEnableSplitTouch |
直接 addFlags |
windowShowWallpaper |
直接 addFlags |
windowBlurBehind |
直接 addFlags |
windowSecure |
直接 addFlags + 设置输入特性 |
windowIsFloating |
存入 mIsFloating,延后到 generateLayout 使用 |
windowIsTranslucent |
存入标记,延后到 generateLayout 设置 format |
windowSwipeToDismiss |
设置 dismiss 监听器 |
windowBackgroundBlurRadius |
存入标记 |
入口二:generateLayout()
java
// PhoneWindow.java
protected ViewGroup generateLayout(DecorView decor) {
// ★ 第二次读取 Window styleable
TypedArray a = getWindowStyle();
// ... 约 80+ 个属性的读取和应用 ...
}
generateLayout() 是核心分发函数,大部分属性在这里被消费。为什么分两次?
因为在 generateLayout() 之前,以下 API 可能已被调用:
java
// Activity.onCreate() 中很常见的代码:
getWindow().requestFeature(Window.FEATURE_NO_TITLE);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().setSoftInputMode(...);
generateLayout() 的策略是:如果某个状态已经被代码显式设置了,就不再被 Theme 覆盖;如果没被设置,就用 Theme 的值作为默认值。
实现方式是"标记位"模式:
java
// Window.java 基类中的标记
protected boolean mHasSoftInputMode = false; // 代码调过 setSoftInputMode()?
protected boolean mIsFloating = false; // Theme 或代码设了浮动窗口?
// generateLayout() 中:
if (!mHasSoftInputMode) {
// 代码没设过 → 从 Theme 读取默认值
final int softInputMode = a.getInt(R.styleable.Window_windowSoftInputMode, 0);
if (softInputMode != 0) {
getAttributes().softInputMode = softInputMode;
}
}
4. 深入理解 WindowManager.LayoutParams 的核心 Flags
在具体拆解每个 Theme 属性之前,我们必须先彻底理解它们最终写入的目标------LayoutParams.flags。不理解 flags 的含义,就无法理解这些 Theme 属性到底做了什么。
4.1 什么是 Flags?
WindowManager.LayoutParams.flags 是一个 int 类型的位掩码(bitmask)。每个 bit 代表一个独立的行为开关。WMS 在收到 addView() 请求时读取这些 flags,决定窗口的布局方式、输入事件分发策略、系统栏行为等。
目前定义了约 40+ 个 flags,按功能可以分为几类:
arduino
flags 字段 (32-bit int)
├── bit 0-7: 布局相关 (LAYOUT_*)
├── bit 8-15: 输入/触摸相关 (NOT_TOUCHABLE, NOT_FOCUSABLE, WATCH_OUTSIDE_TOUCH...)
├── bit 16-23: 系统栏相关 (FULLSCREEN, TRANSLUCENT_*, DRAWS_SYSTEM_BAR_BACKGROUNDS...)
├── bit 24-27: 显示/安全 (SECURE, SHOW_WALLPAPER, DIM_BEHIND...)
└── bit 28-31: 锁屏/屏幕 (SHOW_WHEN_LOCKED, KEEP_SCREEN_ON, TURN_SCREEN_ON...)
4.2 布局相关 Flags(Layout Flags)
这些 flags 控制窗口内容的布局边界------你的 View 能画到屏幕的哪个区域。
FLAG_LAYOUT_IN_SCREEN (0x00000100)
作用:允许窗口内容延伸到整个屏幕(包括状态栏和导航栏后面的区域)。
WMS 的效果:
- 正常窗口的 content 区域被限制在
(0, statusBarHeight, screenWidth, screenHeight - navBarHeight)范围内 - 设置此 flag 后,content 区域变为
(0, 0, screenWidth, screenHeight)------即全屏幕 - 但不改变状态栏/导航栏的可见性------系统栏仍然是可见的,只是窗口内容可以画到它们后面
- WMS 仍然会通过
WindowInsets告知应用系统栏的位置,由应用自行决定是否避开
使用场景:
- 需要在状态栏后面绘制背景(Material Design 的 AppBar 延伸到状态栏后面的效果)
- 配合
FLAG_TRANSLUCENT_STATUS使用,让内容延伸到透明的状态栏后面 - 全屏视频播放器,视频画面覆盖整个屏幕但保留状态栏可见
与 FLAG_LAYOUT_NO_LIMITS 的区别:
| FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_NO_LIMITS | |
|---|---|---|
| 布局边界 | 屏幕物理边界内 | 允许超出屏幕(用于 overscan 等场景) |
| 状态栏 | 可延伸到状态栏后面 | 可延伸到状态栏后面 |
| 超出屏幕 | 不允许 | 允许 |
| 常用程度 | ⭐⭐⭐⭐⭐ | ⭐(已废弃) |
FLAG_LAYOUT_NO_LIMITS (0x00000200)
作用:允许窗口布局超出屏幕物理边界。
WMS 的效果:
- 窗口的 content 区域不再被限制在屏幕边界内
- 主要用于旧的 overscan 机制(CRT 电视时代的过扫描概念,Android 早期用于处理不同 TV 的过扫描差异)
- API 18-30 期间配合
FLAG_LAYOUT_IN_OVERSCAN使用,现已基本废弃
与 FLAG_LAYOUT_IN_SCREEN 的关系:
FLAG_LAYOUT_IN_SCREEN通常会被 WMS 自动附加 到FLAG_LAYOUT_NO_LIMITS上,因为超出屏幕必然要先允许覆盖全屏- 在
windowClipToOutline=true场景下也会隐式设置此 flag
FLAG_LAYOUT_IN_OVERSCAN (0x00080000)
作用:允许窗口内容延伸到屏幕的 overscan 区域(已废弃,API 30+ 无效)。
这是 Android TV 时代的遗留物。老式电视机有 5-10% 的画面区域不可见(过扫描),Android 通过 overscan insets 来处理。现代显示器(LCD/OLED)没有过扫描概念,所以 API 30 起已废弃。
FLAG_LAYOUT_ATTACHED_IN_DECOR (0x40000000)
作用:内部使用的标志,表示 LayoutParams 已被附加到 DecorView。
这是一个平台内部标志 ,不应由应用设置。当 WindowManager.addView() 被调用时自动添加,用于防止同一个 LayoutParams 被复用。
4.3 触摸与焦点 Flags(Input Flags)
这些 flags 控制窗口是否接收/如何处理输入事件。
FLAG_NOT_TOUCHABLE (0x00000010)
作用:窗口不接收任何触摸事件,所有触摸事件直接穿透到下层窗口。
WMS 的效果:
- 在 WMS 的输入事件分发中,该窗口被标记为 "untouchable"
- InputDispatcher 跳过该窗口,将触摸事件发给 Z-order 中下一个可触摸的窗口
- 即使窗口可见,用户也无法与之交互
使用场景:
- 悬浮窗 + 穿透:如悬浮歌词、悬浮球在不可交互状态时
- Loading 遮罩 :显示一个全屏的半透明遮罩阻止用户操作(需要设置该 flag 配合
FLAG_NOT_FOCUSABLE) - Starting Window(启动窗口):系统在真正 Activity 启动前显示的预览窗口,不应接收触摸
- Toast / 短暂提示:不应拦截触摸事件
与 FLAG_NOT_FOCUSABLE 的组合关系:
| FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE | 效果 |
|---|---|---|
| ✓ | ✓ | 窗口完全透明于人机交互------不可触摸、不可获焦 |
| ✓ | ✗ | 窗口不可触摸但可以获焦(少见,通常无实际意义) |
| ✗ | ✓ | 窗口可触摸,但不能成为输入焦点窗口(常用------如输入法窗口上面的工具栏) |
| ✗ | ✗ | 正常窗口,可触摸、可获焦 |
FLAG_NOT_FOCUSABLE (0x00000008)
作用:窗口不能成为输入焦点窗口(即键盘事件的目标窗口)。
WMS 的效果:
- 该窗口不会接收 KeyEvent(按键事件)
- InputDispatcher 不会将焦点设给该窗口
- 如果同时设置了
FLAG_NOT_TOUCHABLE,相当于该窗口对输入系统完全不可见
使用场景:
- 系统级悬浮窗(如 Chat Heads、悬浮球):可见、可点击但不能抢键盘焦点
- 输入法窗口:IME 自身不应该是焦点窗口,但它上面的 EditText 所在的窗口是
- Toast / 短暂提示:不需要焦点
- 窗口管理器中的非交互式 Layer:如壁纸窗口
注意 :如果窗口需要接收 KeyEvent(如返回键、音量键等),必须取消此 flag。
FLAG_WATCH_OUTSIDE_TOUCH (0x00040000)
作用 :当用户触摸窗口外部 (在窗口边界之外)时,WMS 向该窗口发送 ACTION_OUTSIDE 事件。
WMS 的效果:
- 正常情况下,窗口外部的触摸事件不会发送给该窗口(只发给被触摸到的下层窗口)
- 设置该 flag 后,WMS 会将外部触摸事件打包成一个特殊的
MotionEvent.ACTION_OUTSIDE发给该窗口 - 该 flag 必须配合
FLAG_NOT_TOUCH_MODAL使用
使用场景:
- Dialog 点击外部关闭 :这是
windowCloseOnTouchOutside的底层实现机制 - PopupWindow 点击外部关闭
- 下拉菜单 / 上下文菜单:点击菜单外部区域关闭菜单
FLAG_NOT_TOUCH_MODAL (0x00000020)
作用:允许触摸事件穿透到该窗口后面的窗口(即 WMS 不会因为该窗口存在而阻止下层窗口接收触摸)。
WMS 的效果:
- 正常情况下(不设此 flag),即使窗口 B 在窗口 A 上面且窗口 B 较小,窗口 A 中未被遮挡的区域也不能接收触摸------因为窗口 B 是 "touch modal" 的
- "touch modal" = 该窗口的 "触摸区域" 是整个屏幕,即使窗口本身很小
- 设置该 flag 后,窗口 B 外的触摸事件会传给下层窗口 A
使用场景:
- 系统级悬浮窗:悬浮球在屏幕上,但下面的 App 仍然可以正常操作
- 输入法窗口:键盘弹出时,键盘上面的内容区域仍可滑动
- Side Bar / Edge Panel:侧边栏出现时,其余区域仍可操作
典型的触摸行为矩阵:
css
┌──────────────────────────┐
│ Window A (全屏 App) │
│ │
│ ┌──────────┐ │
│ │ Window B │ │ ← 一个 300dp×200dp 的 Dialog
│ └──────────┘ │
│ │
│ ★ 用户点击这个区域 ★ │ ← Dialog 外部、App 内部
└──────────────────────────┘
不设 FLAG_NOT_TOUCH_MODAL:事件发给 Window B(ACTION_OUTSIDE),App A 收不到
设置 FLAG_NOT_TOUCH_MODAL:事件发给 Window A(正常处理),同时给 B 发 ACTION_OUTSIDE
FLAG_SPLIT_TOUCH (0x00800000)
作用:允许多个手指的触摸事件分别发送给不同的窗口。
WMS 的效果:
- 如果没有此 flag,多点触摸的第二根手指不会触发新的窗口命中测试
- 设置后,每个手指的 DOWN 事件都独立进行窗口命中,可以分发到不同的窗口
使用场景:
- 多窗口 / 分屏模式:用户可以同时用两根手指操作两个窗口
- 系统任务栏 + 应用:一根手指滑动任务栏,另一根手指操作应用
- 游戏手柄映射窗口:虚拟按键和游戏画面需要同时响应不同的触摸
4.4 系统栏相关 Flags(System Bar Flags)
这些 flags 是日常开发中最常用的,控制状态栏和导航栏的外观。
FLAG_FULLSCREEN (0x00000400)
作用:隐藏系统状态栏和导航栏,窗口占据全部屏幕空间。
WMS 的效果:
- WMS 通知 SystemUI 隐藏状态栏和导航栏
- 屏幕只显示该窗口的内容(完全的"全屏模式")
- 与
FLAG_LAYOUT_IN_SCREEN的区别:后者只改变布局边界但不隐藏系统栏
使用场景:
- 视频播放器全屏模式
- 游戏(几乎所有手游都是全屏的)
- 图片浏览器全屏
- 演示 / 幻灯片模式
与 FLAG_FORCE_NOT_FULLSCREEN 的关系:
java
// Window.java 中定义 FULLSCREEN:
// 设置此 flag 实际上 = 设置 FLAG_FULLSCREEN + 清除 FLAG_FORCE_NOT_FULLSCREEN
public static final int FLAG_FULLSCREEN = 0x00000400;
// FLAG_FORCE_NOT_FULLSCREEN 是内部标志,表示"有东西强制非全屏",
// 系统用来平衡多个全屏请求
FLAG_TRANSLUCENT_STATUS (0x04000000)
作用:状态栏变为半透明/透明,窗口内容可以延伸到状态栏后面。
WMS 的效果:
- SystemUI 绘制状态栏时使用半透明混合(而非完全不透明)
- WMS 隐式添加
FLAG_LAYOUT_IN_SCREEN+FLAG_LAYOUT_NO_LIMITS,使窗口布局延伸到状态栏后面 - 状态栏仍占据空间,但下方的窗口内容透过它可见
- 此 flag 自动暗示
SYSTEM_UI_FLAG_LAYOUT_STABLE和SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
重要 :单独设置此 flag 只会让状态栏半透明 (约 40% 不透明度的黑色 scrim)。如果你想要完全自定义状态栏颜色,必须同时设置:
markdown
FLAG_TRANSLUCENT_STATUS --- 状态栏透明
+
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS --- 由窗口负责绘制状态栏背景色
+
statusBarColor --- 你自定义的颜色
三者缺一不可。
使用场景:
- Material Design 彩色状态栏:让状态栏颜色与 ActionBar 统一
- 图片延伸到状态栏:如个人主页的头部大图
- 地图 / 相机等全屏应用:地图内容充满整个屏幕,状态栏浮在地图上方
- Edge-to-Edge 设计:Android 15+ 的默认行为
历史变更:
- API 19 (KitKat):引入
FLAG_TRANSLUCENT_STATUS,但 window 可获得半透明的渐变状态栏 - API 21 (Lollipop):引入
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS和statusBarColor,实现真正的自定义颜色 - API 23 (Marshmallow):引入
SYSTEM_UI_FLAG_LIGHT_STATUS_BAR(Android 30+ 改用WindowInsetsController),浅色状态栏图标 - API 35 (Android 15):强制 Edge-to-Edge(自动设置这些 flags),除非显式 opt-out
FLAG_TRANSLUCENT_NAVIGATION (0x08000000)
作用 :与 FLAG_TRANSLUCENT_STATUS 完全对称,作用于导航栏。
所有关于状态栏的讨论同样适用于导航栏。唯一需要注意的是:导航栏通常是屏幕底部(手势导航条区域),在全面屏设备上可能非常窄(甚至与内容重叠)。
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS (0x80000000)
作用:声明"系统栏的背景由本窗口负责绘制",WMS 不再绘制默认的黑色半透明 scrim。
WMS 的效果:
- 只有此 flag 存在时,
statusBarColor和navigationBarColor才会生效 - WMS 在绘制系统栏时采用该窗口指定的颜色(通过
WindowInsetsController传递) - 此 flag 必须 配合
FLAG_TRANSLUCENT_STATUS/FLAG_TRANSLUCENT_NAVIGATION使用
底层原理:
bash
没有 FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 时:
┌────────────────────────────┐
│ statusBarColor (不生效) │
├────────────────────────────┤ ← 这里还是黑色 scrim(WMS 自行绘制)
│ │
│ 窗口内容 │
│ │
└────────────────────────────┘
有 FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 时:
┌────────────────────────────┐
│ statusBarColor = #2196F3 │ ← 这里是你设的颜色
├────────────────────────────┤
│ │
│ 窗口内容 │
│ │
└────────────────────────────┘
FLAG_LAYOUT_HIDE_NAV (deprecated)
此 flag 是旧式全屏 API 的残留,功能已被 FLAG_TRANSLUCENT_STATUS / FLAG_TRANSLUCENT_NAVIGATION 替代。
4.5 安全与显示 Flags(Security & Display Flags)
FLAG_SECURE (0x00002000)
作用:窗口内容被视为"安全内容"------禁止截屏、录屏,且禁止在非安全显示器上显示。
WMS 的效果:
- WMS 标记该窗口所在的 Surface 为 "secure"
- SurfaceFlinger 拒绝在截图/录屏中包含该 Surface 的像素
- 如果设备连接了非安全的虚拟显示器(如录屏软件创建的虚拟屏),该窗口的内容不渲染到虚拟显示器,在录屏中显示为黑屏
- 注意:这不影响物理屏幕上的显示------用户在设备上仍然能看到正常内容
使用场景:
- 银行 App / 支付页面
- 密码输入 / 设置页面
- 企业内部敏感信息页面
- DRM 保护的视频播放(如 Netflix 的截屏保护)
限制:
- Android 截图 API 被阻止(
MediaProjection的截图中该窗口为黑色) - ADB screencap / screenrecord 中该窗口为黑色
- 第三方录屏 App 中该窗口为黑色
- 系统截图(电源键 + 音量键)中该窗口为黑色
- 无法阻止物理拍照(用另一部手机拍屏幕)
FLAG_SHOW_WALLPAPER (0x00100000)
作用:窗口内容置于壁纸之上,壁纸作为窗口的背景(而非桌面背景)。
WMS 的效果:
- WMS 将该窗口的 Z-order 调整到壁纸窗口之上、其他窗口之下
- 壁纸作为该窗口的"背景层"
- 必须配合
windowIsTranslucent或半透明背景使用,否则看不到壁纸
使用场景:
- 半透明主题的 App:如天气 App 的主界面透过半透明面板看到壁纸
- Launcher:桌面本身
- 壁纸选择器 / 预览
- 锁屏界面
FLAG_DIM_BEHIND (0x00000002)
作用:使该窗口后面的所有窗口变暗。
WMS 的效果:
- WMS 在该窗口后面插入一个半透明黑色 Layer
- 变暗的程度由
LayoutParams.dimAmount控制(范围 0.0~1.0,默认 0.0 = 不变暗) - 用于实现"模态对话框"的遮罩效果
使用场景:
- AlertDialog:系统默认 AlertDialog 使用此 flag(dimAmount = 0.5)实现背景变暗效果
- BottomSheet:半透明遮罩
- Loading 弹窗:遮罩 + 加载动画
注意 :dimAmount 的值在 Theme 中通过 android:backgroundDimAmount 属性设置:
xml
<item name="android:backgroundDimAmount">0.6</item>
FLAG_BLUR_BEHIND (0x00000004)
作用:使该窗口后面的所有窗口内容模糊化。(API 31+ / Android 12+)
使用场景:
- 毛玻璃效果:iOS 风格的模糊背景
- 通知面板 / 控制中心:下拉面板后面的模糊
- Dialog 背景模糊
性能注意 :实时模糊是 GPU 密集操作,在低端设备上可能导致掉帧。通常配合 windowBackgroundBlurRadius 控制模糊半径。
4.6 锁屏相关 Flags(Keyguard Flags)
FLAG_SHOW_WHEN_LOCKED (0x00080000)
作用:窗口可以显示在锁屏(Keyguard)之上。
使用场景:
- 来电界面:锁屏时接听电话的 UI
- 闹钟:闹铃响时的关闭/贪睡界面
- 音乐播放器锁屏控件
- 紧急拨号器
安全注意:此 flag 不会绕过锁屏安全------用户仍然需要解锁才能访问 App 的其他部分。
FLAG_DISMISS_KEYGUARD (0x00400000)
作用:当窗口被显示时,自动解除普通锁屏(非安全锁屏------即无密码/图案的滑动解锁)。
使用场景:
- 来电界面:自动解除滑动锁屏,让用户直接看到接听按钮
- 闹钟:闹铃时解除锁屏
注意:如果用户设置了密码/图案/PIN/生物识别锁屏,此 flag 无效------需要用户主动解锁。
FLAG_TURN_SCREEN_ON (0x00200000)
作用:当窗口被添加到 WMS 时,自动点亮屏幕。
使用场景:
- 来电界面:手机在口袋里,来电时自动亮屏
- 闹钟:到时间自动亮屏
- 视频通话请求:收到请求时自动亮屏
FLAG_KEEP_SCREEN_ON (0x00000080)
作用:只要该窗口可见,屏幕不自动熄灭。
使用场景:
- 视频播放器
- 游戏
- 地图导航
- 阅读器(可选)
- 相机
等效代码:
java
// 在 Activity 的 View 中设置(不需要 Window flag 权限):
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// 或者在 layout XML 中:
<View android:keepScreenOn="true" />
FLAG_ALLOW_LOCK_WHILE_SCREEN_ON (0x00000001)
作用 :即使屏幕亮着,也允许设备进入锁屏状态(与 FLAG_KEEP_SCREEN_ON 相反但互不冲突)。
使用场景:极少直接使用。通常由系统管理。
4.7 Flags 组合使用矩阵
以下是一些常见场景的 flags 组合,方便你快速查阅。
全屏沉浸式(视频/游戏)
FLAG_FULLSCREEN --- 隐藏所有系统栏
FLAG_KEEP_SCREEN_ON --- 不自动灭屏
FLAG_LAYOUT_IN_SCREEN --- 布局充满屏幕(可选,视需求)
Material Design 自定义状态栏颜色
bash
FLAG_TRANSLUCENT_STATUS --- 状态栏透明
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS --- 窗口绘制状态栏背景
+ statusBarColor = #2196F3 --- 自定义颜色
透明/半透明 Activity(Dialog 样式)
ini
FLAG_TRANSLUCENT_STATUS --- 状态栏透明(可选)
+ LP.format = PixelFormat.TRANSLUCENT --- Surface 支持 alpha
+ windowBackground = transparent --- DecorView 背景透明
悬浮窗(System Alert Window)
ini
FLAG_NOT_FOCUSABLE --- 不抢键盘焦点
FLAG_NOT_TOUCH_MODAL --- 允许触摸穿透
FLAG_WATCH_OUTSIDE_TOUCH --- 监听外部触摸(可选)
FLAG_SPLIT_TOUCH --- 多点触摸(可选)
+ type = TYPE_APPLICATION_OVERLAY --- Z-order 在上层
锁屏上显示(来电/闹钟)
FLAG_SHOW_WHEN_LOCKED --- 显示在锁屏上方
FLAG_DISMISS_KEYGUARD --- 解除滑动锁屏
FLAG_TURN_SCREEN_ON --- 自动亮屏
FLAG_KEEP_SCREEN_ON --- 不自动灭屏(来电场景)
敏感内容保护(银行 App)
arduino
FLAG_SECURE --- 禁止截屏/录屏
// 不需要其他特殊 flag,单独使用即可
5. Theme 属性分类详解
有了上面的 flags 知识储备,现在逐个分析每个 Theme XML 属性的 Java 消费路径就非常清晰了。
5.1 窗口 Flags 类
这些属性直接映射到 LayoutParams flags。
android:windowNoDisplay
xml
<item name="android:windowNoDisplay">true</item>
PhoneWindow 源码消费:
java
// PhoneWindow 构造函数
if (a.getBoolean(R.styleable.Window_windowNoDisplay, false)) {
setFlags(FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE,
FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE);
// 额外设置输入特性,使窗口完全不参与输入系统
final WindowManager.LayoutParams params = getAttributes();
params.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL;
}
→ LayoutParams 影响 :LP.flags |= FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE,同时禁用 InputChannel。
使用场景:
- 后台 Service 持有的 Window(如不需要 UI 的后台任务)
- 纯逻辑 Activity(如 URL Scheme 中转页,finish 前不显示任何 UI)
- 极少直接在 Theme 中使用,通常在特定业务场景中由代码设置
android:windowDisablePreview
xml
<item name="android:windowDisablePreview">true</item>
PhoneWindow 源码消费:
java
// PhoneWindow 构造函数
if (a.getBoolean(R.styleable.Window_windowDisablePreview, false)) {
addFlags(FLAG_DISABLE_PREVIEW);
}
→ LayoutParams 影响 :LP.flags |= FLAG_DISABLE_PREVIEW
什么是 Starting Window(启动预览)?
当用户点击图标启动一个冷启动(进程不存在)的 Activity 时,在真正的 Activity 渲染出第一帧之前,AMS 会指示 WMS 显示一个"Starting Window"。Starting Window 根据 Theme 中的 windowBackground 和 statusBarColor 生成一个简单的纯色/渐变窗口,让用户感知到"应用正在启动"。
css
冷启动时序:
[点击图标] → [进程创建] → [Application.onCreate] → [Activity.onCreate]
│
在此时机,屏幕显示 Starting Window(如果没被 Disable)
│
[setContentView] → [首帧渲染]
│
此时 Starting Window 被移除,显示真正的 UI
使用场景:
- 自定义 Splash Screen:应用有自己的 SplashActivity(带品牌 Logo 动画),不希望系统再插入一个默认预览窗口
- 透明背景 Activity:Starting Window 默认是纯色的,在半透明 Activity 下会先看到一个纯色闪烁再变透明,体验很差
- Android 12+ (API 31) 引入了新的 SplashScreen API(
SplashScreen类),推荐使用它替代自定义 Splash Activity。此时系统会自动处理FLAG_DISABLE_PREVIEW。
android:windowEnableSplitTouch
xml
<item name="android:windowEnableSplitTouch">true</item>
PhoneWindow 源码消费:
java
// PhoneWindow 构造函数
if (a.getBoolean(R.styleable.Window_windowEnableSplitTouch, false)) {
addFlags(FLAG_SPLIT_TOUCH);
}
→ LayoutParams 影响 :LP.flags |= FLAG_SPLIT_TOUCH
使用场景:
- 分屏 / 多窗口:需要多个窗口同时响应不同的手指触摸
- 游戏:双摇杆控制需要独立处理两个手指
- 支持分屏的应用:Android 7.0+ 的分屏模式需要此 flag 才能正确分发多点触摸
android:windowShowWallpaper
xml
<item name="android:windowShowWallpaper">true</item>
PhoneWindow 源码消费:
java
// PhoneWindow 构造函数
if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) {
addFlags(FLAG_SHOW_WALLPAPER);
}
→ LayoutParams 影响 :LP.flags |= FLAG_SHOW_WALLPAPER
使用场景:
- Launcher / 桌面:在壁纸之上显示图标和 Widget
- 半透明主题:天气 App 的半透明面板让壁纸透出
- 锁屏界面:在壁纸上叠加锁屏控件
- 壁纸选择器:预览壁纸效果
注意:必须配合透明或半透明背景使用,否则不透明的窗口背景会完全遮挡壁纸:
xml
<item name="android:windowShowWallpaper">true</item>
<item name="android:windowBackground">#80000000</item> <!-- 半透明背景 -->
android:windowBlurBehind
xml
<item name="android:windowBlurBehind">true</item>
→ LayoutParams 影响 :LP.flags |= FLAG_BLUR_BEHIND(API 31+)
使用场景:
- 毛玻璃效果 Dialog:iOS 风格的对话框
- 通知中心 / 控制中心:下拉面板的背景模糊
- 半透明 Activity:内容后面的模糊背景
实现原理 :WMS 请求 SurfaceFlinger 对下层窗口的 Surface 做 GPU 模糊渲染,再将结果作为本窗口的背景。性能开销较大,建议通过 windowBackgroundBlurRadius 控制模糊半径以平衡效果与性能:
xml
<item name="android:windowBlurBehind">true</item>
<item name="android:windowBackgroundBlurRadius">25dp</item>
android:windowSecure
xml
<item name="android:windowSecure">true</item>
PhoneWindow 源码消费:
java
// PhoneWindow 构造函数
if (a.getBoolean(R.styleable.Window_windowSecure, false)) {
addFlags(FLAG_SECURE);
// 同时在 InputChannel 层面标记 secure
params.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL;
}
→ LayoutParams 影响 :LP.flags |= FLAG_SECURE
使用场景:
- 银行 / 金融 App 所有页面
- 密码输入页面
- 支付确认页面
- 企业 VPN / 内部工具
- 医学 / 健康隐私数据页面
- 加密货币钱包
代码中动态启用(更灵活,在特定页面才启用):
java
// 在敏感页面的 onResume 中启用
@Override
protected void onResume() {
super.onResume();
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
// 在离开敏感页面时关闭(可选)
@Override
protected void onPause() {
super.onPause();
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
坑点:
- SurfaceView / TextureView 中的内容同样被保护(整个 Surface 都被标记 secure)
- 系统截图快捷键(电源+音量)也会触发黑屏
- Android 15 引入的私有空间(Private Space)可能会与此 flag 有交互
android:windowNotTouchable
xml
<item name="android:windowNotTouchable">true</item>
→ LayoutParams 影响 :LP.flags |= FLAG_NOT_TOUCHABLE(在 generateLayout() 中处理)
使用场景:
- 纯展示页面(如广告展示页、信息大屏):不需要用户交互
- 悬浮 Loading/进度指示器 :展示加载状态但不阻止用户操作(需配合
FLAG_NOT_FOCUSABLE) - AOD(常亮显示)或屏保
5.2 窗口 Type 类
android:windowIsFloating
xml
<item name="android:windowIsFloating">true</item>
PhoneWindow 源码消费:
java
// 构造函数中保存标记
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
// generateLayout() 中使用
if (mIsFloating) {
LayoutParams lp = getAttributes();
lp.width = LayoutParams.WRAP_CONTENT; // Dialog 风格:宽度自适应内容
lp.height = LayoutParams.WRAP_CONTENT; // 高度自适应内容
lp.gravity = Gravity.CENTER; // 居中显示
// 额外:如果窗口有边框/阴影设置,会在 DecorView 层面处理
}
→ LayoutParams 影响:
LP.width = WRAP_CONTENTLP.height = WRAP_CONTENTLP.gravity = CENTER
更深远的影响 :mIsFloating 还会影响 DecorView 的布局选择。PhoneWindow 根据不同 Feature 和 mIsFloating 的组合选择不同的 DecorView 布局文件:
| Feature 组合 | DecorView 布局 |
|---|---|
| 标准 Activity(无特殊 Feature) | R.layout.screen_simple |
标准 Activity + FEATURE_NO_TITLE |
R.layout.screen_simple(无标题区域) |
mIsFloating + FEATURE_NO_TITLE |
R.layout.dialog_custom_title 或简化布局 |
mIsFloating + ActionBar |
R.layout.screen_toolbar |
使用场景:
- Dialog 样式 Activity :在
AndroidManifest.xml中设置android:theme="@style/Theme.AppCompat.Dialog",其父 Theme 中就设置了windowIsFloating=true - 浮动工具栏:类似悬浮窗的小窗口
- 画中画(PiP)相关 :PiP 窗口本质上就是
mIsFloating=true的特例
完整的 Dialog 样式 Theme(Android 系统内置的 Dialog Theme 大体上是这样的):
xml
<style name="Theme.AppCompat.Dialog" parent="Theme.AppCompat">
<item name="android:windowIsFloating">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowCloseOnTouchOutside">true</item>
</style>
5.3 尺寸与布局类
android:windowFixedWidth / android:windowFixedHeight
xml
<item name="android:windowFixedWidth">320dp</item>
<item name="android:windowFixedHeight">480dp</item>
PhoneWindow 源码消费:
java
// generateLayout()
if (a.hasValue(R.styleable.Window_windowFixedWidth)) {
lp.width = a.getDimensionPixelSize(
R.styleable.Window_windowFixedWidth, lp.width);
}
if (a.hasValue(R.styleable.Window_windowFixedHeight)) {
lp.height = a.getDimensionPixelSize(
R.styleable.Window_windowFixedHeight, lp.height);
}
→ LayoutParams 影响 :覆写 LP.width / LP.height 为固定 dp 值。
使用场景:
- 特殊尺寸的 Activity:如电视 App 的遥控器提示面板(不需要全屏)
- 自定义 Dialog:固定宽度的对话框
- 多窗口 / 自由窗口模式(Freeform):指定窗口的初始尺寸
与 windowIsFloating 的配合:
xml
<item name="android:windowIsFloating">true</item> <!-- WRAP_CONTENT 兜底 -->
<item name="android:windowFixedWidth">360dp</item> <!-- 覆写为固定宽度 -->
android:windowFixedWidthMajor / android:windowFixedWidthMinor
这两个属性配合横竖屏使用:
Major= 长边(landscape 下是宽度,portrait 下是高度)Minor= 短边(landscape 下是高度,portrait 下是宽度)
示例:在平板上,竖屏时窗口宽度设为 360dp,横屏时设为 600dp:
xml
<item name="android:windowFixedWidthMajor">600dp</item> <!-- 横屏时生效 -->
<item name="android:windowFixedWidthMinor">360dp</item> <!-- 竖屏时生效 -->
使用场景:
- 平板 / 折叠屏适配:不同屏幕方向下窗口尺寸不同
- 多窗口 / Freeform 模式:在大屏设备上指定窗口初始大小
android:windowMinWidthMajor / android:windowMinWidthMinor
最小宽度/高度约束,与固定尺寸不同,它们是下限而非精确值。
java
lp.minWidth = a.getDimensionPixelSize(
R.styleable.Window_windowMinWidthMajor, lp.minWidth);
→ LayoutParams 影响 :LP.minWidth / LP.minHeight
使用场景:
- 防止窗口过小:在多窗口模式下,窗口可能被缩得很小,设置 minWidth 可以防止内容被挤压到不可用
- 响应式设计:窗口可以选择更大的尺寸,但不能小于某个阈值
android:windowElevation
xml
<item name="android:windowElevation">4dp</item>
PhoneWindow 源码消费:
java
final float elevation = a.getDimension(R.styleable.Window_windowElevation, 0);
if (elevation != 0 && lp.setElevation(elevation)) {
// elevation 发生了变化,触发 relayout
}
→ LayoutParams 影响 :LP.elevation
使用场景:
- Activity 间层级:后启动的 Activity 默认在更高的 Z 层,设置 elevation 可以微调
- Dialog 相对于 Activity 的 Z 轴偏移:确保 Dialog 的阴影正确投射在下层 Activity 上
- Material Design 阴影:配合背景的 elevation 阴影效果
注意 :elevation 影响的是 Z-order 和阴影投射,不是窗口在屏幕上的 x/y 位置。
android:windowClipToOutline
xml
<item name="android:windowClipToOutline">true</item>
java
final boolean clipToOutline = a.getBoolean(
R.styleable.Window_windowClipToOutline, false);
if (clipToOutline) {
lp.flags |= FLAG_LAYOUT_NO_LIMITS;
}
使用场景:
- 圆角窗口 :配合
windowBackground的圆角 shape,将窗口裁剪为圆角矩形 - 自定义形状窗口:如圆形头像悬浮窗
为什么 clipToOutline 会设置 FLAG_LAYOUT_NO_LIMITS?
因为裁剪到 outline 意味着窗口的可见区域由 outline path 决定,layout 可以超出屏幕(反正会被裁剪掉)。这个 flag 确保内容可以渲染到 outline 内的所有区域,即使部分在屏幕外。
5.4 背景与样式类
android:windowBackground
xml
<!-- 纯色 -->
<item name="android:windowBackground">@color/window_bg</item>
<!-- Drawable -->
<item name="android:windowBackground">@drawable/my_bg</item>
<!-- 透明 -->
<item name="android:windowBackground">@android:color/transparent</item>
PhoneWindow 源码消费:
java
// generateLayout()
final Drawable background;
if (a.hasValue(R.styleable.Window_windowBackground)) {
// 优先作为 Drawable 读取
background = a.getDrawable(R.styleable.Window_windowBackground);
} else {
// 其次作为 Color 读取
int color = a.getColor(R.styleable.Window_windowBackground, 0xFF000000);
background = new ColorDrawable(color);
}
// 设置给 DecorView
mDecor.setWindowBackground(background);
→ LayoutParams 影响 :不直接影响 LP,但有两个重要间接影响:
- Starting Window(启动窗口)的颜色 :AMS 读取
windowBackground作为启动窗口的背景色(在 API 31 之前的实现中) - 窗口是否为 "半透明" 的判定:系统会根据背景的 alpha 来判断窗口是否需要特殊处理
使用场景:
- 应用主色调背景:避免每个 Activity 的 root view 都要设置背景(系统级默认背景)
- Splash Screen :启动窗口的背景色(API 31 后由
windowSplashScreenBackground替代) - 防止启动白屏/黑屏 :这是
windowBackground最实用的场景之一:
xml
<!-- 在 Theme 中设置与你的启动页相同的背景 -->
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowBackground">@color/splash_bg</item>
</style>
这样在 Activity 渲染出第一帧之前,用户看到的就是 splash_bg 颜色,而不是刺眼的白屏。
android:windowBackgroundFallback
xml
<item name="android:windowBackgroundFallback">@color/fallback_bg</item>
作用 :当同时满足以下条件时,使用此背景替代 windowBackground:
windowBackground未设置或为 nullwindowActivityTransitions为 true
原因 :Transition 框架需要一个背景来做转场动画的渐变/淡出。如果 windowBackground 为空,转场动画没有"底"可画,此时 fallback 登场。
使用场景:通常不需要手动设置,系统有合理的默认值。只有在精细化控制转场体验时才需要。
android:windowContentOverlay
xml
<item name="android:windowContentOverlay">@drawable/content_shadow</item>
作用 :在 mContentParent(即 android.R.id.content)上方绘制一个遮罩层。
历史意义:在 Holo 时代,ActionBar 和 content 之间有一个阴影分隔线,这个属性就是用来设置那个阴影的。Material Design 已不再使用。
使用场景 :几乎不再使用。如果你想要 content 区域的阴影/边框效果,建议用 elevation 或 View 自身的 background 实现。
android:windowTitleSize
xml
<item name="android:windowTitleSize">24sp</item>
→ 设置传统 Title Bar 的文字大小。在 ActionBar / Toolbar 时代基本废弃。
android:windowNoTitle
xml
<item name="android:windowNoTitle">true</item>
java
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
}
→ 调用 requestFeature(FEATURE_NO_TITLE),移除传统标题栏,精简 DecorView 布局。
使用场景:
- 所有使用 Toolbar 的 App :老式 TitleBar 与 Toolbar 冲突,必须先
requestFeature(FEATURE_NO_TITLE)才能正常使用 Toolbar - NoActionBar 主题 :
Theme.Material3.DayNight.NoActionBar的父主题里默认设置了此属性 - 全屏 / 沉浸式页面
android:windowActionBar
xml
<item name="android:windowActionBar">true</item>
<!-- 或设为 false 移除 ActionBar -->
<item name="android:windowActionBar">false</item>
→ requestFeature(FEATURE_ACTION_BAR)
使用场景:
- 设为
false→ 使用自定义 Toolbar 代替系统的 ActionBar - 设为
true→ 使用系统 ActionBar
android:windowActionModeOverlay
xml
<item name="android:windowActionModeOverlay">true</item>
→ ActionMode(上下文操作栏,如文字选择时的"复制/粘贴/剪切"条)覆盖在 ActionBar 上方而不是推挤 ActionBar。
5.5 系统状态栏/导航栏类
这是实际开发中最常需要自定义的一类属性。
android:statusBarColor / android:navigationBarColor
xml
<item name="android:statusBarColor">@color/primary_dark</item>
<item name="android:navigationBarColor">@color/nav_bar</item>
java
mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000);
mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000);
→ 颜色值存储到 PhoneWindow 字段,之后通过 WindowInsetsController(API 30+)或 View.setSystemUiVisibility()(API 21-29)发送给 SystemUI。
这两个颜色生效的前提条件(极其重要):
ini
statusBarColor 生效需要:
FLAG_TRANSLUCENT_STATUS = true (windowTranslucentStatus)
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = true (windowDrawsSystemBarBackgrounds)
navigationBarColor 生效需要:
FLAG_TRANSLUCENT_NAVIGATION = true (windowTranslucentNavigation)
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = true (windowDrawsSystemBarBackgrounds)
使用场景:
- 品牌色状态栏:状态栏颜色与 Toolbar/ActionBar 颜色一致
- 深色导航栏:配合深色主题,导航栏设为深色
- 沉浸式页面:状态栏颜色与页面顶部内容融合
android:windowTranslucentStatus
xml
<item name="android:windowTranslucentStatus">true</item>
java
if (a.getBoolean(R.styleable.Window_windowTranslucentStatus, false)) {
setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS);
}
→ LP.flags |= FLAG_TRANSLUCENT_STATUS
使用场景:
- 图片/视频延伸到状态栏:如个人主页头部大图
- 自定义状态栏颜色 :配合
windowDrawsSystemBarBackgrounds+statusBarColor - Material Design edge-to-edge:让状态栏成为 UI 的一部分
单独设置此属性时的视觉效果:
scss
没有 FLAG_TRANSLUCENT_STATUS 时:
┌─ 纯黑状态栏 ──────────────┐ ← 不透明,挡住内容
├──────────────────────────┤
│ │
│ Window Content │ ← 内容被限制在状态栏之下
│ │
└──────────────────────────┘
仅 FLAG_TRANSLUCENT_STATUS(未设 DRAWS_SYSTEM_BAR_BACKGROUNDS):
┌─ 40% 黑色半透明状态栏 ────┐ ← 半透明 scrim
├──────────────────────────┤
│ Window Content │ ← 内容延伸到状态栏后面
│ (在状态栏后面可见) │
│ │
└──────────────────────────┘
FLAG_TRANSLUCENT_STATUS + FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS:
┌─ statusBarColor = blue ──┐ ← 自定义颜色
├──────────────────────────┤
│ Window Content │ ← 内容延伸到状态栏后面
│ (在蓝色状态栏后面可见) │
│ │
└──────────────────────────┘
android:windowTranslucentNavigation
xml
<item name="android:windowTranslucentNavigation">true</item>
→ LP.flags |= FLAG_TRANSLUCENT_NAVIGATION
与上面完全对称,作用于底部导航栏。对于全面屏手势设备(Android 10+),导航栏非常窄(通常只有一个横条),这个属性影响的是导航手势区域的颜色。
android:windowDrawsSystemBarBackgrounds
xml
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
java
if (a.getBoolean(R.styleable.Window_windowDrawsSystemBarBackgrounds, false)) {
setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
}
→ LP.flags |= FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
核心作用 :告诉 WMS「系统栏的背景色由我的 statusBarColor / navigationBarColor 决定,你不要画默认的半透明黑色背景了」。
这三个属性必须成组使用:
xml
<!-- 设置状态栏颜色的"正确姿势"三部曲 -->
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@color/primary_dark</item>
为什么 Theme.Material3.DayNight.NoActionBar 不需要手动设前两个?
因为 Google 在 Theme.Material3.DayNight 中已经预设了这些值为 true(至少在 API 21+ 的版本中)。如果你的 Theme 没有继承 Material3,或基于旧主题,就需要手动添加。
android:windowLightStatusBar
xml
<item name="android:windowLightStatusBar">true</item>
→ 在 Android 11+ (API 30+) 中通过 WindowInsetsController.setSystemBarsAppearance() 设置 APPEARANCE_LIGHT_STATUS_BARS:
java
// API 30+ 的实现路径
final WindowInsetsController controller = getWindowInsetsController();
if (controller != null) {
controller.setSystemBarsAppearance(
APPEARANCE_LIGHT_STATUS_BARS, // 外观值
APPEARANCE_LIGHT_STATUS_BARS); // mask
}
作用:状态栏的背景是浅色(如白色)时,状态栏图标(时间、电池、信号等)变为深色。
使用场景:
- 浅色主题:如果 statusBarColor 是浅色(如白色、浅灰),必须设置此属性使图标可读
- 白色状态栏:iOS 风格的白色顶部栏
- 透明背景 + 浅色内容:图片延伸到状态栏后面且图片顶部是浅色
API 差异:
| API 级别 | 实现方式 |
|---|---|
| API 23-29 | View.setSystemUiVisibility(SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) |
| API 30+ | WindowInsetsController.setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS) |
| Theme 属性 | 上述两个路径的声明式封装 |
android:windowLightNavigationBar
xml
<item name="android:windowLightNavigationBar">true</item>
→ 浅色导航栏背景 + 深色导航栏图标(三条横线或药丸按钮)。
使用场景:浅色底部的 UI(如浅色底部导航栏 + 浅色系统导航栏)。
android:enforceNavigationBarContrast / android:enforceStatusBarContrast
xml
<item name="android:enforceNavigationBarContrast">true</item>
→ API 29+,当系统栏内容(图标/文字)与你的窗口内容颜色对比度不足时,强制在系统栏后面添加一层半透明 scrim 确保可读性。
使用场景:当你的状态栏/导航栏颜色与背景图片/内容颜色相近,导致图标不可读时,开启此属性可自动添加保护层。Android 15 的 Edge-to-Edge 模式默认启用此行为。
android:windowFullscreen
xml
<item name="android:windowFullscreen">true</item>
→ 传统全屏模式(隐藏所有系统栏)。在现代开发中,推荐使用 FLAG_TRANSLUCENT_STATUS + FLAG_TRANSLUCENT_NAVIGATION + FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS 的方式实现全屏(因为用户可以随时下滑唤出系统栏),或者使用 WindowInsetsController.hide() / WindowInsetsController.show()。
5.6 软键盘(SoftInput)类
android:windowSoftInputMode
xml
<item name="android:windowSoftInputMode">stateVisible|adjustResize</item>
这是最复杂但也最常用的属性之一。整个值是一个位掩码,分为状态 (state) 和调整 (adjust) 两部分,用 | 组合。
完整取值表:
| XML 值 | 常量 | 位值 | 含义 |
|---|---|---|---|
| 状态部分(低 4 bits) | |||
stateUnspecified |
STATE_UNSPECIFIED |
0 | 系统决定(通常是 hide) |
stateUnchanged |
STATE_UNCHANGED |
1 | 保持上一 Activity 的键盘状态 |
stateHidden |
STATE_HIDDEN |
2 | 进入时键盘隐藏 |
stateAlwaysHidden |
STATE_ALWAYS_HIDDEN |
3 | 窗口获得焦点时隐藏键盘 |
stateVisible |
STATE_VISIBLE |
4 | 进入时键盘显示 |
stateAlwaysVisible |
STATE_ALWAYS_VISIBLE |
5 | 窗口获得焦点时显示键盘 |
| 调整部分(高 4 bits) | |||
adjustUnspecified |
ADJUST_UNSPECIFIED |
0x00 | 系统决定(通常 = resize) |
adjustResize |
ADJUST_RESIZE |
0x10 | 键盘弹出时压缩窗口 |
adjustPan |
ADJUST_PAN |
0x20 | 键盘弹出时平移窗口 |
adjustNothing |
ADJUST_NOTHING |
0x30 | 键盘弹出时不做任何调整 |
PhoneWindow 源码消费:
java
// generateLayout() → 实际在 Window.java 基类中
final int softInputMode = a.getInt(
R.styleable.Window_windowSoftInputMode, 0);
if (softInputMode != 0) {
getAttributes().softInputMode = softInputMode; // 直接赋值
mHasSoftInputMode = true; // 标记为"已设置",防止回退
}
→ LayoutParams 影响 :LP.softInputMode 直接赋值
adjustResize vs adjustPan 的深入对比:
adjustResize:
┌──────────────────┐
│ Toolbar │ ← 保持可见
│ ┌────────────┐ │
│ │ ScrollView │ │ ← 窗口被压缩,可以滚动
│ │ EditText │ │ ← 焦点 EditText 自动可见(在可见区域内)
│ └────────────┘ │
├──────────────────┤
│ 键盘区域 │
└──────────────────┘
适用:列表/表单(内容可滚动)
adjustPan:
┌──────────────────┐
│ │
│ │ ← 整个窗口向上平移
│ EditText │ ← 焦点 EditText 保持可见
├──────────────────┤
│ 键盘区域 │
├──────────────────┤
│ Toolbar │ ← Toolbar 可能被顶出屏幕!
└──────────────────┘
适用:简单表单(少量输入框在页面底部时)
使用场景:
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 聊天页面(底部输入框 + 消息列表) | adjustResize |
列表内容随键盘弹出自动上移 |
| 登录页面(顶部 Logo + 底部输入框) | adjustResize |
整体缩放,Logo 可见但变小 |
| 简单表单(少量底部输入框) | adjustPan |
平移即可露出输入框 |
| 全屏视频(键盘用于弹幕) | adjustNothing |
键盘弹出不应移动视频画面 |
| 搜索页面(搜索框在顶部的列表) | adjustResize |
列表需要重新布局 |
| 沉浸式游戏(虚拟键盘) | adjustNothing |
游戏画面不应被键盘影响 |
stateAlwaysHidden 的妙用:
xml
<item name="android:windowSoftInputMode">stateAlwaysHidden</item>
→ 用户点击 EditText 时键盘不会自动弹出,除非用户主动点击才会弹出。最适合搜索页(用户不想进入页面就看到键盘)。
默认值 :如果没有设置 windowSoftInputMode,默认值是 STATE_UNCHANGED | ADJUST_RESIZE。
5.7 转场动画类
API 21+ 引入的 Activity Transition 框架,所有属性在 generateLayout() 读取。
转场属性全景图
css
Activity A → Activity B 的过程涉及的转场:
windowExitTransition ← Activity A 内容退出(如 Fade)
windowEnterTransition ← Activity B 内容进入(如 Explode)
用户按返回键 (B → A):
windowReturnTransition ← Activity B 内容退出
windowReenterTransition ← Activity A 内容重新进入
Shared Element(如头像从列表页飞入详情页):
windowSharedElementExitTransition ← A 的共享元素退出
windowSharedElementEnterTransition ← B 的共享元素进入
windowSharedElementReenterTransition← 返回时重新进入
windowSharedElementReturnTransition ← 返回时退出
android:windowActivityTransitions
xml
<item name="android:windowActivityTransitions">true</item>
→ 总开关。必须设为 true,后续的所有 Transition 设置才会生效。
android:windowEnterTransition / android:windowExitTransition
xml
<item name="android:windowEnterTransition">@transition/explode</item>
<item name="android:windowExitTransition">@transition/fade</item>
使用场景:
- 页面切换动画 :替代
overridePendingTransition() - Material Motion:Material Design 的转场系统
- Shared Element Transition:必须配合这些属性才能工作
android:windowReturnTransition / android:windowReenterTransition
返回时的转场动画(按返回键时触发)。默认与 exit/enter 相同,但可以设置为不同:
xml
<item name="android:windowEnterTransition">@transition/slide_right</item> <!-- 从右侧滑入 -->
<item name="android:windowReturnTransition">@transition/slide_left</item> <!-- 从左侧滑出 -->
android:windowSharedElementEnterTransition / android:windowSharedElementExitTransition
Shared Element 的内容转场。默认为 @transition/move(即系统内置的 ChangeBounds + ChangeTransform + ChangeClipBounds 组合)。
xml
<item name="android:windowSharedElementEnterTransition">@transition/change_image_transform</item>
android:windowAllowEnterTransitionOverlap / android:windowAllowReturnTransitionOverlap
xml
<item name="android:windowAllowEnterTransitionOverlap">true</item>
→ 设为 true 时,调用 startActivity() 后,当前 Activity 的退出动画和下一个 Activity 的进入动画并行执行 ;设为 false 时,串行执行(先退出完再进入)。
使用场景:
- 流畅体验 :通常建议
allowEnterTransitionOverlap=true(两个动画叠加更流畅) - 避免闪烁:如果两个动画有重叠冲突,设为 false
android:windowTransitionBackgroundFadeDuration
xml
<item name="android:windowTransitionBackgroundFadeDuration">300</item>
→ 转场过程中,windowBackground 的淡出/淡入动画时长(毫秒)。这对于避免 Shared Element Transition 期间出现"白色闪烁"非常有用------设置一个合理的 fade duration 让背景渐变过渡。
代码中的等价实现
java
// 在 Activity 中代码设置 Transition(与 Theme 属性等价)
getWindow().requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
getWindow().setEnterTransition(new Explode());
getWindow().setExitTransition(new Fade());
getWindow().setAllowEnterTransitionOverlap(true);
5.8 像素格式与绘制类
android:windowIsTranslucent
xml
<item name="android:windowIsTranslucent">true</item>
这是影响最深远的属性之一。它直接改变了 Window 所在 Surface 的像素格式。
PhoneWindow 源码全链路:
java
// 第 1 步:构造函数中标记
if (a.getBoolean(R.styleable.Window_windowIsTranslucent, false)) {
// 先清除一些 flags(这些 flags 与 translucent 不兼容)
setFlags(0,
FLAG_NOT_TOUCHABLE // translucent 意味着需要触摸
| FLAG_NOT_FOCUSABLE // translucent 意味着可以获得焦点
);
// 设置输入特性
params.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL;
}
// 第 2 步:generateLayout() 中应用
if (mTranslucent) {
lp.format = PixelFormat.TRANSLUCENT; // ← 核心!
}
// 第 3 步:后续初始化
if (mTranslucent) {
// translucent 窗口不设置 windowBackground 的前景 scrim
// 因为窗口本身就是半透明的
}
→ LayoutParams 影响:
| 字段 | 变化 |
|---|---|
LP.format |
OPAQUE → TRANSLUCENT |
PixelFormat.TRANSLUCENT vs OPAQUE 对 SurfaceFlinger 的影响:
makefile
OPAQUE (默认):
SurfaceFlinger 将该 Layer 标记为 "不透明"
合成时不需要读取下层 Layer 的像素
性能:快(跳过混合运算)
TRANSLUCENT:
SurfaceFlinger 将该 Layer 标记为 "带 alpha 通道"
合成时读取每个像素的 alpha 值,与下层做逐像素混合
性能:慢(增加 GPU 负载和带宽消耗)
完整的半透明 Activity Theme:
xml
<style name="Theme.Transparent" parent="Theme.Material3.DayNight.NoActionBar">
<!-- 这 3 个缺一不可 -->
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<!-- 可选优化 -->
<item name="android:windowDisablePreview">true</item> <!-- 避免启动闪烁 -->
<item name="android:windowAnimationStyle">@null</item> <!-- 移除默认动画 -->
</style>
常见问题诊断:
| 症状 | 原因 |
|---|---|
| 背景是黑色不是透明的 | 只设了 windowIsTranslucent=true,没设 windowBackground=transparent |
| 状态栏区域是黑色的 | 状态栏没设 translucent,半透明只对 content 生效 |
| 触摸穿透到了下层 App | windowIsTranslucent 清理了 FLAG_NOT_TOUCHABLE,确认是否在其他地方又被设置了 |
| 启动时短暂白屏 | 设置了 windowBackground=transparent 但没设置 windowDisablePreview=true |
android:windowSwipeToDismiss
xml
<item name="android:windowSwipeToDismiss">true</item>
→ 启用滑动关闭手势(iOS 风格的侧滑返回)。
使用场景:
- 图片预览 / 大图浏览:下滑关闭
- 视频播放:下滑缩小/关闭
- 模态页面:iOS 风格的下滑 dismiss
5.9 FitSystemWindows 与 Edge-to-Edge 类
android:fitsSystemWindows
这是 View 属性而非 Window 属性,但在 Theme 中设置时会作用于 DecorView:
xml
<item name="android:fitsSystemWindows">true</item>
→ 设置 DecorView 的 fitsSystemWindows = true。DecorView 在 dispatchApplyWindowInsets() 时会先消费掉状态栏/导航栏的 insets(为其 padding),然后将剩余的 insets 分发给子 View。
使用场景:
- 需要内容自动避开状态栏:如标准的列表页面
- 与 CoordinatorLayout 配合:CoordinatorLayout 自身会处理 insets
fitsSystemWindows = true 的效果:
ini
状态栏 24dp ─┐
│ ← DecorView 消费了此 inset,给自身加 paddingTop=24dp
│ 子 View 收到调整后的 insets(状态栏部分已消耗)
内容区域 │
│ 效果:内容自动在状态栏下方开始,不会被遮挡
导航栏 48dp ─┘
android:windowOptOutEdgeToEdgeEnforcement
xml
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
→ Android 15+ (API 35) 新增。从 API 35 开始,targetSdk=35 的应用默认 强制启用 Edge-to-Edge 模式------即系统自动设置 FLAG_LAYOUT_IN_SCREEN、FLAG_TRANSLUCENT_STATUS、FLAG_TRANSLUCENT_NAVIGATION 等标志,使窗口内容延伸到系统栏后面。
如果你需要退出这个行为(例如你的旧 UI 大量依赖 fitsSystemWindows 而没有使用 WindowInsets),设置此属性为 true:
xml
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
建议:尽量适配 Edge-to-Edge 而非 opt-out。Google 的长期方向是全面 Edge-to-Edge。
android:windowLayoutInDisplayCutoutMode
xml
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
可选值:
| XML 值 | 常量 | 行为 |
|---|---|---|
default (0) |
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT |
竖屏:不允许伸入刘海;横屏:允许 |
shortEdges (1) |
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES |
允许内容延伸到短边的刘海区域 |
never (2) |
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER |
永远不允许延伸到刘海 |
always (3) |
LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS |
系统栏透明时允许延伸到任何方向的刘海 |
java
int cutoutMode = a.getInt(
R.styleable.Window_windowLayoutInDisplayCutoutMode, 0);
if (cutoutMode != 0) {
lp.layoutInDisplayCutoutMode = cutoutMode;
}
→ LayoutParams 影响 :LP.layoutInDisplayCutoutMode
使用场景:
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 普通列表页面 | default |
竖屏不伸入刘海,安全稳妥 |
| 全屏视频/游戏 | shortEdges |
利用顶部刘海两侧区域显示内容 |
| 横屏全屏 | always |
让内容充满整个屏幕(包括刘海后面) |
| 刘海区域有重要 UI 元素 | never |
确保 UI 不被刘海遮挡 |
5.10 其他行为属性
android:windowCloseOnTouchOutside
xml
<item name="android:windowCloseOnTouchOutside">true</item>
java
if (a.getBoolean(R.styleable.Window_windowCloseOnTouchOutside, false)) {
setCloseOnTouchOutsideIfNotSet(true);
}
→ LayoutParams 影响 :LP.flags |= FLAG_NOT_TOUCH_MODAL | FLAG_WATCH_OUTSIDE_TOUCH
完整实现链路:
scss
用户触摸 Dialog 外部区域
→ WMS 发送 ACTION_OUTSIDE 到该 Dialog
→ PhoneWindow 收到 ACTION_OUTSIDE
→ 回调 to Window.onTouchEvent()
→ 调用 dispatchTouchEvent()
→ 因为 closeOnTouchOutside=true
→ 判断触摸坐标在 DecorView 外部
→ 调用 Activity.finish() (如果 attached 到 Activity)
使用场景:
- Dialog 样式 Activity :
Theme.AppCompat.Dialog默认开启 - 底部菜单 / ActionSheet
- Dropdown 菜单
- PopupWindow
android:windowAnimationStyle
xml
<item name="android:windowAnimationStyle">@style/MyWindowAnimation</item>
java
mAnimationStyle = a.getResourceId(
R.styleable.Window_windowAnimationStyle, 0);
→ LayoutParams 影响 :LP.windowAnimations = mAnimationStyle
这个资源 ID 传给 WMS,WMS 在窗口出现/消失时查找对应的 WindowAnimation 资源并播放。
自定义窗口动画:
xml
<!-- res/values/styles.xml -->
<style name="MyWindowAnimation">
<!-- 进入动画 -->
<item name="android:activityOpenEnterAnimation">@anim/slide_in_right</item>
<!-- 退出动画 -->
<item name="android:activityOpenExitAnimation">@anim/slide_out_left</item>
<!-- 返回时的进入动画 -->
<item name="android:activityCloseEnterAnimation">@anim/slide_in_left</item>
<!-- 返回时的退出动画 -->
<item name="android:activityCloseExitAnimation">@anim/slide_out_right</item>
</style>
使用场景:任何需要自定义窗口转场动画的场景------做预设动画、淡入淡出、从底部弹出等。
android:windowBackgroundBlurRadius
xml
<item name="android:windowBackgroundBlurRadius">25dp</item>
→ API 31+,配合 windowBlurBehind 使用,设置背景模糊半径。值越大越模糊,但 GPU 开销越大。
android:backgroundDimAmount
xml
<item name="android:backgroundDimAmount">0.6</item>
→ 配合 FLAG_DIM_BEHIND 使用,设置下层窗口变暗的程度(0.0=不变暗,1.0=全黑)。Dialog 的默认值大约是 0.5。
android:backgroundDimEnabled
xml
<item name="android:backgroundDimEnabled">true</item>
→ 是否启用背景变暗(通常与 FLAG_DIM_BEHIND 联动)。
android:windowSplashScreenBackground (API 31+)
xml
<item name="android:windowSplashScreenBackground">@color/splash_bg</item>
→ Android 12+ SplashScreen API 使用的启动窗口背景色。等价于在代码中:
java
SplashScreen splashScreen = SplashScreen.installSplashScreen(this);
// 背景色通过 Theme 属性设置,代码中不需要额外调用
android:windowSplashScreenAnimationDuration (API 31+)
xml
<item name="android:windowSplashScreenAnimationDuration">300</item>
→ SplashScreen 动画持续时间(毫秒)。注意:系统会根据应用冷启动耗时决定实际显示时长,此值只是一个参考上限。
android:windowSplashScreenIcon (API 31+)
→ SplashScreen 中心图标(通常不需要手动设置,系统用应用图标)。
android:windowContentTransitions
→ 允许窗口内容在布局变化时启用 Transition(如 TransitionManager.beginDelayedTransition)。
android:windowHideAnimation / android:windowShowAnimation
→ 已废弃,被 windowAnimationStyle 和 Transition 框架取代。
6. 完整映射表速查
| Theme XML 属性 | PhoneWindow 读取入口 | 目标字段 | 设置的 Flag/值 |
|---|---|---|---|
| Flags 映射 | |||
windowNoDisplay |
构造函数 | LP.flags |
`FLAG_NOT_TOUCHABLE |
windowDisablePreview |
构造函数 | LP.flags |
FLAG_DISABLE_PREVIEW |
windowEnableSplitTouch |
构造函数 | LP.flags |
FLAG_SPLIT_TOUCH |
windowShowWallpaper |
构造函数 | LP.flags |
FLAG_SHOW_WALLPAPER |
windowBlurBehind |
构造函数 | LP.flags |
FLAG_BLUR_BEHIND (API 31+) |
windowSecure |
构造函数 | LP.flags + InputChannel |
FLAG_SECURE |
windowNotTouchable |
generateLayout | LP.flags |
FLAG_NOT_TOUCHABLE |
windowFullscreen |
generateLayout | LP.flags |
FLAG_FULLSCREEN |
windowTranslucentStatus |
generateLayout | LP.flags |
FLAG_TRANSLUCENT_STATUS (+ 隐式 LAYOUT_IN_SCREEN) |
windowTranslucentNavigation |
generateLayout | LP.flags |
FLAG_TRANSLUCENT_NAVIGATION |
windowDrawsSystemBarBackgrounds |
generateLayout | LP.flags |
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS |
windowDimBehind |
generateLayout | LP.flags |
FLAG_DIM_BEHIND |
windowCloseOnTouchOutside |
generateLayout | LP.flags |
`FLAG_NOT_TOUCH_MODAL |
| 尺寸映射 | |||
windowFixedWidth |
generateLayout | LP.width |
固定像素值 |
windowFixedHeight |
generateLayout | LP.height |
固定像素值 |
windowFixedWidthMajor |
generateLayout | LP.width |
条件覆写(长边) |
windowFixedWidthMinor |
generateLayout | LP.width |
条件覆写(短边) |
windowMinWidthMajor |
generateLayout | LP.minWidth |
最小宽度(长边) |
windowMinWidthMinor |
generateLayout | LP.minWidth |
最小宽度(短边) |
windowElevation |
generateLayout | LP.elevation |
float (px) |
windowIsFloating |
构造函数 | mIsFloating → LP.width/height/gravity |
WRAP_CONTENT + CENTER |
| 系统栏映射 | |||
statusBarColor |
generateLayout | mStatusBarColor → InsetsController |
Color int |
navigationBarColor |
generateLayout | mNavigationBarColor → InsetsController |
Color int |
windowLightStatusBar |
generateLayout | InsetsController | APPEARANCE_LIGHT_STATUS_BARS |
windowLightNavigationBar |
generateLayout | InsetsController | APPEARANCE_LIGHT_NAVIGATION_BARS |
enforceNavigationBarContrast |
generateLayout | InsetsController | Boolean |
enforceStatusBarContrast |
generateLayout | InsetsController | Boolean |
| 软键盘映射 | |||
windowSoftInputMode |
generateLayout | LP.softInputMode |
位掩码组合 |
| 像素与绘制映射 | |||
windowIsTranslucent |
构造函数 + generateLayout | LP.format |
PixelFormat.TRANSLUCENT |
windowClipToOutline |
generateLayout | LP.flags |
FLAG_LAYOUT_NO_LIMITS |
| 转场动画映射 | |||
windowActivityTransitions |
generateLayout | mActivityTransitionsAllowed |
boolean |
windowEnterTransition |
generateLayout | mEnterTransition |
Transition ref |
windowExitTransition |
generateLayout | mExitTransition |
Transition ref |
windowReenterTransition |
generateLayout | mReenterTransition |
Transition ref |
windowReturnTransition |
generateLayout | mReturnTransition |
Transition ref |
windowSharedElementEnterTransition |
generateLayout | mSharedElementEnterTransition |
Transition ref |
windowSharedElementExitTransition |
generateLayout | mSharedElementExitTransition |
Transition ref |
windowAllowEnterTransitionOverlap |
generateLayout | mAllowEnterTransitionOverlap |
boolean |
windowAllowReturnTransitionOverlap |
generateLayout | mAllowReturnTransitionOverlap |
boolean |
windowTransitionBackgroundFadeDuration |
generateLayout | mBackgroundFadeDurationMillis |
long (ms) |
| 背景映射 | |||
windowBackground |
generateLayout | mBackgroundDrawable → DecorView |
Drawable/Color |
windowBackgroundFallback |
generateLayout | mBackgroundFallback |
Drawable/Color |
windowBackgroundBlurBehind |
generateLayout | mBackgroundBlurBehind |
boolean |
windowBackgroundBlurRadius |
generateLayout | mBackgroundBlurRadius |
int |
backgroundDimAmount |
generateLayout | LP.dimAmount |
float |
| Feature 映射 | |||
windowNoTitle |
generateLayout | requestFeature(FEATURE_NO_TITLE) |
--- |
windowActionBar |
generateLayout | requestFeature(FEATURE_ACTION_BAR) |
--- |
windowActionModeOverlay |
generateLayout | requestFeature(FEATURE_ACTION_MODE_OVERLAY) |
--- |
| 动画映射 | |||
windowAnimationStyle |
构造函数 | LP.windowAnimations |
style resource id |
| Cutout 映射 | |||
windowLayoutInDisplayCutoutMode |
generateLayout | LP.layoutInDisplayCutoutMode |
int enum |
| Edge-to-Edge 映射 | |||
windowOptOutEdgeToEdgeEnforcement |
构造函数 (API 35+) | 退出强制 Edge-to-Edge | boolean |
| 其他 | |||
windowContentOverlay |
generateLayout | mContentOverlay → DecorView |
Drawable |
windowTitleSize |
generateLayout | TitleView textSize | px |
windowSwipeToDismiss |
generateLayout | 注册滑动监听器 | boolean |
windowSplashScreenBackground |
--- (SplashScreen API) | 启动画面背景 | Color |
windowSplashScreenAnimationDuration |
--- (SplashScreen API) | 启动画面动画时长 | int (ms) |
windowContentTransitions |
generateLayout | mContentTransitionsAllowed |
boolean |
7. 实战场景
以下每个场景都给出了完整的 Theme XML 配置 和等价的 Java 代码,以及关键属性的解释。
7.1 全屏 Activity
效果:隐藏状态栏和导航栏,Window 占据全部屏幕。
xml
<!-- res/values/themes.xml -->
<style name="Theme.Fullscreen" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowFullscreen">true</item>
<item name="android:windowNoTitle">true</item>
</style>
等价 Java 代码:
java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_fullscreen);
}
现代推荐方式(Android 11+,允许用户下滑唤出系统栏):
java
// 使用 WindowInsetsController 而非 FLAG_FULLSCREEN
getWindow().getInsetsController().hide(WindowInsets.Type.systemBars());
// 或让系统栏以半透明方式浮在内容上方
getWindow().getInsetsController().setSystemBarsBehavior(
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
7.2 透明背景 Activity / Dialog 样式 Activity
效果:Activity 的背景完全透明,可以看到下层窗口内容。
xml
<style name="Theme.Transparent" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:windowAnimationStyle">@null</item>
</style>
在 AndroidManifest 中使用:
xml
<activity
android:name=".TransparentActivity"
android:theme="@style/Theme.Transparent" />
使用范例:
java
public class TransparentActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transparent);
// 整个 Window 背景透明,只有 R.layout.activity_transparent
// 中的非透明 View 可见
}
}
Dialog 样式 Activity(与上面类似,但加上了浮动窗口行为):
xml
<style name="Theme.DialogStyle" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowCloseOnTouchOutside">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:backgroundDimAmount">0.5</item>
</style>
7.3 自定义状态栏颜色(Material 风格)
效果:状态栏颜色与 ActionBar/Toolbar 颜色统一。
xml
<style name="Theme.CustomStatusBar" parent="Theme.Material3.DayNight.NoActionBar">
<!-- 三部曲:缺一不可 -->
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
<!-- 如果是浅色状态栏,加上这个 -->
<item name="android:windowLightStatusBar">true</item>
<!-- 同样处理导航栏 -->
<item name="android:windowTranslucentNavigation">true</item>
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
</style>
Java 等价代码:
java
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.colorPrimaryDark));
// Android 11+
getWindow().getInsetsController().setSystemBarsAppearance(
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS);
7.4 图片查看器(沉浸式)
效果:图片充满屏幕,状态栏和导航栏以半透明浮层方式覆盖,点击可切换。
xml
<style name="Theme.ImageViewer" parent="Theme.Material3.DayNight.NoActionBar">
<!-- 系统栏透明,内容延伸到后面 -->
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<!-- 状态栏和导航栏:半透明深色 -->
<item name="android:statusBarColor">#40000000</item>
<item name="android:navigationBarColor">#40000000</item>
<!-- 深色系统栏图标(因为半透明深色背景上暗色图标看不清) -->
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
<!-- 背景黑色,适合图片查看 -->
<item name="android:windowBackground">@android:color/black</item>
</style>
点击切换显示/隐藏系统栏的代码:
java
private boolean isSystemBarsVisible = true;
private void toggleSystemBars() {
if (isSystemBarsVisible) {
// 隐藏系统栏
getWindow().getInsetsController().hide(
WindowInsets.Type.systemBars());
} else {
// 显示系统栏
getWindow().getInsetsController().show(
WindowInsets.Type.systemBars());
}
isSystemBarsVisible = !isSystemBarsVisible;
}
7.5 锁屏上显示窗口
效果:在锁屏上方显示一个窗口(如来电界面、闹钟)。
xml
<style name="Theme.LockScreenOverlay" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowShowWhenLocked">true</item>
<item name="android:windowTurnScreenOn">true</item>
<item name="android:windowDismissKeyguard">true</item>
<item name="android:windowKeepScreenOn">true</item>
</style>
⚠️ 注意 :
windowShowWhenLocked、windowTurnScreenOn、windowDismissKeyguard等属性不能直接在 Theme XML 中设置 ,因为它们需要特殊权限或必须在代码中使用setFlags()动态设置。上述 XML 仅是示意对应关系。实际实现为:
java
// 在 Activity.onCreate() 中(必须在 setContentView 之前)
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
7.6 禁止截屏的敏感页面
效果:阻止系统截屏和录屏。
xml
<style name="Theme.Secure" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowSecure">true</item>
</style>
Java 等价代码:
java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
setContentView(R.layout.activity_secure);
}
7.7 软键盘适配表单页面
场景一:聊天页面(底部输入框 + 消息列表)
xml
<style name="Theme.Chat" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowSoftInputMode">stateHidden|adjustResize</item>
</style>
stateHidden:进入页面时不弹出键盘adjustResize:键盘弹出时压缩消息列表,使最新消息可见
场景二:登录页面(顶部 Logo + 底部输入框)
xml
<style name="Theme.Login" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowSoftInputMode">stateHidden|adjustResize</item>
</style>
- 同样使用
adjustResize,但需要对布局做处理------如使用ConstraintLayout让 Logo 随窗口压缩而缩放或上移
场景三:搜索页面
xml
<style name="Theme.Search" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowSoftInputMode">stateAlwaysHidden|adjustResize</item>
</style>
stateAlwaysHidden:即使用户上次在别的页面打开了键盘,进入搜索页也不自动弹出
8. 最佳实践与常见陷阱
8.1 statusBarColor 不生效的 4 种可能
xml
<!-- 正确配置(缺一不可) -->
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@color/custom</item>
检查清单:
- ☐
windowTranslucentStatus是否为true? - ☐
windowDrawsSystemBarBackgrounds是否为true? - ☐
statusBarColor是否被父 Theme 覆盖?(使用styles.xml的parent链追查) - ☐ 代码中是否调用了
getWindow().setStatusBarColor(Color.TRANSPARENT)或其他 setter 覆盖了 Theme 值?
8.2 透明 Activity 背景显示为黑色
xml
<!-- 错误 -->
<item name="android:windowIsTranslucent">true</item>
<!-- 正确:同时设置透明背景 -->
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
原因 :windowIsTranslucent 只改变了 Surface 的像素格式(TRANSLUCENT),但 DecorView 的背景仍然从 Theme 继承为默认的白色(或主题默认颜色)。如果不覆写 windowBackground 为透明,DecorView 会用不透明的默认背景填满整个窗口。
8.3 windowSoftInputMode Theme vs 代码的优先级
Theme 默认值 → 代码覆写 :代码中的 getWindow().setSoftInputMode() 会完全覆盖 Theme 中的 windowSoftInputMode。
java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ★ 必须在 super.onCreate() 之后,
// 在 setContentView() 之前调用
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
setContentView(R.layout.my_layout);
}
为什么必须在 setContentView 之前? 因为 setContentView 触发了 generateLayout(),而 generateLayout() 中会检查 mHasSoftInputMode。如果代码中已经调用过 setSoftInputMode(),则 mHasSoftInputMode = true,Theme 中的值被跳过。
8.4 Flags 设置的正确姿势
java
// addFlags:添加 flags(OR 操作,不影响其他 bit)
getWindow().addFlags(FLAG_SECURE);
// 等价于:flags |= FLAG_SECURE
// clearFlags:清除 flags(AND NOT 操作)
getWindow().clearFlags(FLAG_SECURE);
// 等价于:flags &= ~FLAG_SECURE
// setFlags(mask, value):
// mask 中为 1 的 bit → 设为 value 中对应 bit 的值
// mask 中为 0 的 bit → 保持不变
getWindow().setFlags(FLAG_SECURE, FLAG_SECURE); // 打开
getWindow().setFlags(0, FLAG_SECURE); // 关闭
// 等价于:flags = (flags & ~FLAG_SECURE) | (FLAG_SECURE & FLAG_SECURE)
// getAttributes() 后直接修改(需要额外调用 setAttributes)
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.flags |= FLAG_SECURE;
getWindow().setAttributes(lp); // 触发 relayout
8.5 Android 15 Edge-to-Edge 迁移
targetSdk = 35 时必须面对的迁移:
老代码 (依赖 fitsSystemWindows):
xml
<item name="android:fitsSystemWindows">true</item>
新代码(适配 Edge-to-Edge):
kotlin
// 方案 1:在 root View 中处理 WindowInsets
ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, insets ->
val statusBars = insets.getInsets(WindowInsetsCompat.Type.statusBars())
val navBars = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
view.setPadding(
view.paddingLeft,
statusBars.top,
view.paddingRight,
navBars.bottom
)
WindowInsetsCompat.CONSUMED
}
// 方案 2(不想适配):退出强制模式
// 在 Theme 中:
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
8.6 windowClipToOutline 不生效?
windowClipToOutline 需要同时满足以下条件:
windowClipToOutline = truewindowBackground设置了带有圆角/自定义形状的 DrawablewindowElevation已设置(需要 Z 轴高度来触发阴影/裁剪)
8.7 Shared Element Transition 闪烁
如果 Shared Element Transition 期间出现白色闪烁:
xml
<!-- 增加背景淡出时长,平滑过渡 -->
<item name="android:windowTransitionBackgroundFadeDuration">300</item>
<!-- 确保背景不是纯白 -->
<item name="android:windowBackground">@color/window_background</item>
9. 总结
Android Theme 属性到 PhoneWindow 的映射链路核心归纳为四层:
scss
第 1 层:定义层
R.styleable.Window(~120 个属性),在 attrs.xml 中声明
↓
第 2 层:读取层
PhoneWindow 构造函数(~15 个属性)+ generateLayout()(~80 个属性)
通过 TypedArray 读取
↓
第 3 层:分发层
根据属性类型分发到不同目标:
- requestFeature() → Feature flags(影响 DecorView 结构)
- addFlags()/setFlags() → LayoutParams.flags
- getAttributes() → LayoutParams 各字段
- setStatusBarColor() → InsetsController → SystemUI
- requestFitSystemWindows() → View insets 分发
↓
第 4 层:消费层
WindowManager.LayoutParams → IPC 传递 → WMS → SurfaceFlinger
最终决定了窗口的 Z-order、尺寸、像素格式、输入事件策略、系统栏行为
核心要点:
- flags 是 Bitmask,正确理解每个 flag 的语义才能正确配置 Theme
- 系统栏三件套 (
windowTranslucentStatus+windowDrawsSystemBarBackgrounds+statusBarColor)是最常见的组合,缺一不可 windowIsTranslucent改变了 Surface 像素格式,不仅是"背景透明",更是向下层窗口承诺了 alpha 通道windowSoftInputMode要在setContentView之前设置,否则 Theme 覆盖无效- Android 15 Edge-to-Edge 是未来趋势,尽早适配
理解这条链路,可以帮助你:
- 精准定位 "为什么我的 window 样式不生效"
- 在代码中精确覆写 Theme 默认值
- 设计自定义 Dialog / 半透明 / 全屏 / 沉浸式等特殊窗口效果
- 调试 WMS 相关的窗口行为异常
参考资料
- AOSP
frameworks/base/core/res/res/values/attrs.xml--- Window styleable 定义(120+ 属性)- AOSP
frameworks/base/core/java/android/view/Window.java--- Window 抽象基类,flags/type 定义- AOSP
frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java--- 唯一实现,所有消费逻辑- AOSP
frameworks/base/core/java/android/view/WindowManager.java--- LayoutParams 类(flags 常量、字段定义)- AOSP
frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java--- WMS,flags 的真正消费者- AOSP
frameworks/native/services/surfaceflinger/--- SurfaceFlinger,Surface 合成- Android Developer --- Window
- Android Developer --- 系统栏
- Android Developer --- Edge-to-Edge
本文基于 Android 14 (API 34) 源码分析,涵盖 Android 15 (API 35) 新增属性。
本文约 15000 字,阅读时间约 40 分钟。建议收藏,作为随手查阅的速查手册。