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