Android window属性全解析

13 阅读48分钟

Android Theme XML 属性如何影响 PhoneWindow:从 TypedArray 到 LayoutParams 的完整链路

每天写布局、配主题,但你有没有想过——android:windowIsTranslucent="true" 这行 XML 到底是怎么一路传递,最终影响了 Window 的像素格式?为什么 statusBarColor 有时候怎么设都不生效?FLAG_LAYOUT_IN_SCREEN 到底是干什么的?本文将带你深入 Android Framework 源码,逐属性、逐 Flag 拆解这条完整链路。


目录

  1. 架构概览:Theme → PhoneWindow 的数据流
  2. 属性的定义:R.styleable.Window
  3. PhoneWindow 读取属性的两个入口
  4. 深入理解 WindowManager.LayoutParams 的核心 Flags
  5. Theme 属性分类详解
  6. 完整映射表速查
  7. 实战场景
  8. 最佳实践与常见陷阱
  9. 总结

1. 架构概览:Theme → PhoneWindow 的数据流

先上一张整体数据流图,建立心智模型:

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+ 个属性

<!-- 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 为例):

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:

入口一:构造函数

// 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()

// PhoneWindow.java
protected ViewGroup generateLayout(DecorView decor) {
    // ★ 第二次读取 Window styleable
    TypedArray a = getWindowStyle();

    // ... 约 80+ 个属性的读取和应用 ...
}

generateLayout()核心分发函数,大部分属性在这里被消费。为什么分两次?

因为在 generateLayout() 之前,以下 API 可能已被调用:

// Activity.onCreate() 中很常见的代码:
getWindow().requestFeature(Window.FEATURE_NO_TITLE);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().setSoftInputMode(…);

generateLayout() 的策略是:如果某个状态已经被代码显式设置了,就不再被 Theme 覆盖;如果没被设置,就用 Theme 的值作为默认值。

实现方式是"标记位"模式:

// 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,按功能可以分为几类:

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_SCREENFLAG_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_TOUCHABLEFLAG_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:侧边栏出现时,其余区域仍可操作

典型的触摸行为矩阵

┌──────────────────────────┐
│  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 的关系

// 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_STABLESYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

重要:单独设置此 flag 只会让状态栏半透明(约 40% 不透明度的黑色 scrim)。如果你想要完全自定义状态栏颜色,必须同时设置:

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_BACKGROUNDSstatusBarColor,实现真正的自定义颜色
  • 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 存在时,statusBarColornavigationBarColor 才会生效
  • WMS 在绘制系统栏时采用该窗口指定的颜色(通过 WindowInsetsController 传递)
  • 此 flag 必须配合 FLAG_TRANSLUCENT_STATUS / FLAG_TRANSLUCENT_NAVIGATION 使用

底层原理

没有 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 属性设置:

<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)

作用:只要该窗口可见,屏幕不自动熄灭。

使用场景

  • 视频播放器
  • 游戏
  • 地图导航
  • 阅读器(可选)
  • 相机

等效代码

// 在 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 自定义状态栏颜色
FLAG_TRANSLUCENT_STATUS                  — 状态栏透明
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS        — 窗口绘制状态栏背景
+ statusBarColor = #2196F3               — 自定义颜色
透明/半透明 Activity(Dialog 样式)
FLAG_TRANSLUCENT_STATUS                  — 状态栏透明(可选)
+ LP.format = PixelFormat.TRANSLUCENT    — Surface 支持 alpha
+ windowBackground = transparent         — DecorView 背景透明
悬浮窗(System Alert Window)
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)
FLAG_SECURE                              — 禁止截屏/录屏
// 不需要其他特殊 flag,单独使用即可

5. Theme 属性分类详解

有了上面的 flags 知识储备,现在逐个分析每个 Theme XML 属性的 Java 消费路径就非常清晰了。

5.1 窗口 Flags 类

这些属性直接映射到 LayoutParams flags。

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

PhoneWindow 源码消费

// 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
<item name="android:windowDisablePreview">true</item>

PhoneWindow 源码消费

// 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 中的 windowBackgroundstatusBarColor 生成一个简单的纯色/渐变窗口,让用户感知到"应用正在启动"。

冷启动时序:
[点击图标][进程创建][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
<item name="android:windowEnableSplitTouch">true</item>

PhoneWindow 源码消费

// 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
<item name="android:windowShowWallpaper">true</item>

PhoneWindow 源码消费

// PhoneWindow 构造函数
if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) {
    addFlags(FLAG_SHOW_WALLPAPER);
}

LayoutParams 影响LP.flags |= FLAG_SHOW_WALLPAPER

使用场景

  • Launcher / 桌面:在壁纸之上显示图标和 Widget
  • 半透明主题:天气 App 的半透明面板让壁纸透出
  • 锁屏界面:在壁纸上叠加锁屏控件
  • 壁纸选择器:预览壁纸效果

注意:必须配合透明或半透明背景使用,否则不透明的窗口背景会完全遮挡壁纸:

<item name="android:windowShowWallpaper">true</item>
<item name="android:windowBackground">#80000000</item>  <!-- 半透明背景 -->
android:windowBlurBehind
<item name="android:windowBlurBehind">true</item>

LayoutParams 影响LP.flags |= FLAG_BLUR_BEHIND(API 31+)

使用场景

  • 毛玻璃效果 Dialog:iOS 风格的对话框
  • 通知中心 / 控制中心:下拉面板的背景模糊
  • 半透明 Activity:内容后面的模糊背景

实现原理:WMS 请求 SurfaceFlinger 对下层窗口的 Surface 做 GPU 模糊渲染,再将结果作为本窗口的背景。性能开销较大,建议通过 windowBackgroundBlurRadius 控制模糊半径以平衡效果与性能:

<item name="android:windowBlurBehind">true</item>
<item name="android:windowBackgroundBlurRadius">25dp</item>
android:windowSecure
<item name="android:windowSecure">true</item>

PhoneWindow 源码消费

// 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 / 内部工具
  • 医学 / 健康隐私数据页面
  • 加密货币钱包

代码中动态启用(更灵活,在特定页面才启用):

// 在敏感页面的 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
<item name="android:windowNotTouchable">true</item>

LayoutParams 影响LP.flags |= FLAG_NOT_TOUCHABLE(在 generateLayout() 中处理)

使用场景

  • 纯展示页面(如广告展示页、信息大屏):不需要用户交互
  • 悬浮 Loading/进度指示器:展示加载状态但不阻止用户操作(需配合 FLAG_NOT_FOCUSABLE
  • AOD(常亮显示)或屏保

5.2 窗口 Type 类

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

PhoneWindow 源码消费

// 构造函数中保存标记
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_CONTENT
  • LP.height = WRAP_CONTENT
  • LP.gravity = CENTER

更深远的影响mIsFloating 还会影响 DecorView 的布局选择。PhoneWindow 根据不同 Feature 和 mIsFloating 的组合选择不同的 DecorView 布局文件:

Feature 组合DecorView 布局
标准 Activity(无特殊 Feature)R.layout.screen_simple
标准 Activity + FEATURE_NO_TITLER.layout.screen_simple(无标题区域)
mIsFloating + FEATURE_NO_TITLER.layout.dialog_custom_title 或简化布局
mIsFloating + ActionBarR.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 大体上是这样的):

<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
<item name="android:windowFixedWidth">320dp</item>
<item name="android:windowFixedHeight">480dp</item>

PhoneWindow 源码消费

// 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 的配合

<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:

<item name="android:windowFixedWidthMajor">600dp</item>  <!-- 横屏时生效 -->
<item name="android:windowFixedWidthMinor">360dp</item>  <!-- 竖屏时生效 -->

使用场景

  • 平板 / 折叠屏适配:不同屏幕方向下窗口尺寸不同
  • 多窗口 / Freeform 模式:在大屏设备上指定窗口初始大小
android:windowMinWidthMajor / android:windowMinWidthMinor

最小宽度/高度约束,与固定尺寸不同,它们是下限而非精确值。

lp.minWidth = a.getDimensionPixelSize(
    R.styleable.Window_windowMinWidthMajor, lp.minWidth);

LayoutParams 影响LP.minWidth / LP.minHeight

使用场景

  • 防止窗口过小:在多窗口模式下,窗口可能被缩得很小,设置 minWidth 可以防止内容被挤压到不可用
  • 响应式设计:窗口可以选择更大的尺寸,但不能小于某个阈值
android:windowElevation
<item name="android:windowElevation">4dp</item>

PhoneWindow 源码消费

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
<item name="android:windowClipToOutline">true</item>
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
<!-- 纯色 -->
<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 源码消费

// 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,但有两个重要间接影响:

  1. Starting Window(启动窗口)的颜色:AMS 读取 windowBackground 作为启动窗口的背景色(在 API 31 之前的实现中)
  2. 窗口是否为 "半透明" 的判定:系统会根据背景的 alpha 来判断窗口是否需要特殊处理

使用场景

  • 应用主色调背景:避免每个 Activity 的 root view 都要设置背景(系统级默认背景)
  • Splash Screen:启动窗口的背景色(API 31 后由 windowSplashScreenBackground 替代)
  • 防止启动白屏/黑屏:这是 windowBackground 最实用的场景之一:
<!-- 在 Theme 中设置与你的启动页相同的背景 -->
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
    <item name="android:windowBackground">@color/splash_bg</item>
</style>

这样在 Activity 渲染出第一帧之前,用户看到的就是 splash_bg 颜色,而不是刺眼的白屏。

android:windowBackgroundFallback
<item name="android:windowBackgroundFallback">@color/fallback_bg</item>

作用:当同时满足以下条件时,使用此背景替代 windowBackground

  1. windowBackground 未设置或为 null
  2. windowActivityTransitions 为 true

原因:Transition 框架需要一个背景来做转场动画的渐变/淡出。如果 windowBackground 为空,转场动画没有"底"可画,此时 fallback 登场。

使用场景:通常不需要手动设置,系统有合理的默认值。只有在精细化控制转场体验时才需要。

android:windowContentOverlay
<item name="android:windowContentOverlay">@drawable/content_shadow</item>

作用:在 mContentParent(即 android.R.id.content)上方绘制一个遮罩层。

历史意义:在 Holo 时代,ActionBar 和 content 之间有一个阴影分隔线,这个属性就是用来设置那个阴影的。Material Design 已不再使用。

使用场景:几乎不再使用。如果你想要 content 区域的阴影/边框效果,建议用 elevation 或 View 自身的 background 实现。

android:windowTitleSize
<item name="android:windowTitleSize">24sp</item>

→ 设置传统 Title Bar 的文字大小。在 ActionBar / Toolbar 时代基本废弃。

android:windowNoTitle
<item name="android:windowNoTitle">true</item>
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
<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
<item name="android:windowActionModeOverlay">true</item>

→ ActionMode(上下文操作栏,如文字选择时的"复制/粘贴/剪切"条)覆盖在 ActionBar 上方而不是推挤 ActionBar。


5.5 系统状态栏/导航栏类

这是实际开发中最常需要自定义的一类属性。

android:statusBarColor / android:navigationBarColor
<item name="android:statusBarColor">@color/primary_dark</item>
<item name="android:navigationBarColor">@color/nav_bar</item>
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。

这两个颜色生效的前提条件(极其重要):

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
<item name="android:windowTranslucentStatus">true</item>
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 的一部分

单独设置此属性时的视觉效果

没有 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
<item name="android:windowTranslucentNavigation">true</item>

LP.flags |= FLAG_TRANSLUCENT_NAVIGATION

与上面完全对称,作用于底部导航栏。对于全面屏手势设备(Android 10+),导航栏非常窄(通常只有一个横条),这个属性影响的是导航手势区域的颜色。

android:windowDrawsSystemBarBackgrounds
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
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 决定,你不要画默认的半透明黑色背景了」。

这三个属性必须成组使用

<!-- 设置状态栏颜色的"正确姿势"三部曲 -->
<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
<item name="android:windowLightStatusBar">true</item>

→ 在 Android 11+ (API 30+) 中通过 WindowInsetsController.setSystemBarsAppearance() 设置 APPEARANCE_LIGHT_STATUS_BARS

// 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-29View.setSystemUiVisibility(SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
API 30+WindowInsetsController.setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS)
Theme 属性上述两个路径的声明式封装
android:windowLightNavigationBar
<item name="android:windowLightNavigationBar">true</item>

→ 浅色导航栏背景 + 深色导航栏图标(三条横线或药丸按钮)。

使用场景:浅色底部的 UI(如浅色底部导航栏 + 浅色系统导航栏)。

android:enforceNavigationBarContrast / android:enforceStatusBarContrast
<item name="android:enforceNavigationBarContrast">true</item>

→ API 29+,当系统栏内容(图标/文字)与你的窗口内容颜色对比度不足时,强制在系统栏后面添加一层半透明 scrim 确保可读性。

使用场景:当你的状态栏/导航栏颜色与背景图片/内容颜色相近,导致图标不可读时,开启此属性可自动添加保护层。Android 15 的 Edge-to-Edge 模式默认启用此行为。

android:windowFullscreen
<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
<item name="android:windowSoftInputMode">stateVisible|adjustResize</item>

这是最复杂但也最常用的属性之一。整个值是一个位掩码,分为状态 (state) 和调整 (adjust) 两部分,用 | 组合。

完整取值表

XML 值常量位值含义
状态部分(低 4 bits)
stateUnspecifiedSTATE_UNSPECIFIED0系统决定(通常是 hide)
stateUnchangedSTATE_UNCHANGED1保持上一 Activity 的键盘状态
stateHiddenSTATE_HIDDEN2进入时键盘隐藏
stateAlwaysHiddenSTATE_ALWAYS_HIDDEN3窗口获得焦点时隐藏键盘
stateVisibleSTATE_VISIBLE4进入时键盘显示
stateAlwaysVisibleSTATE_ALWAYS_VISIBLE5窗口获得焦点时显示键盘
调整部分(高 4 bits)
adjustUnspecifiedADJUST_UNSPECIFIED0x00系统决定(通常 = resize)
adjustResizeADJUST_RESIZE0x10键盘弹出时压缩窗口
adjustPanADJUST_PAN0x20键盘弹出时平移窗口
adjustNothingADJUST_NOTHING0x30键盘弹出时不做任何调整

PhoneWindow 源码消费

// 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 的妙用

<item name="android:windowSoftInputMode">stateAlwaysHidden</item>

→ 用户点击 EditText 时键盘不会自动弹出,除非用户主动点击才会弹出。最适合搜索页(用户不想进入页面就看到键盘)。

默认值:如果没有设置 windowSoftInputMode,默认值是 STATE_UNCHANGED | ADJUST_RESIZE


5.7 转场动画类

API 21+ 引入的 Activity Transition 框架,所有属性在 generateLayout() 读取。

转场属性全景图
Activity A  →  Activity B  的过程涉及的转场:

windowExitTransition       ← Activity A 内容退出(如 Fade)
windowEnterTransition      ← Activity B 内容进入(如 Explode)

用户按返回键 (BA):

windowReturnTransition     ← Activity B 内容退出
windowReenterTransition    ← Activity A 内容重新进入

Shared Element(如头像从列表页飞入详情页):

windowSharedElementExitTransition   ← A 的共享元素退出
windowSharedElementEnterTransition  ← B 的共享元素进入
windowSharedElementReenterTransition← 返回时重新进入
windowSharedElementReturnTransition ← 返回时退出
android:windowActivityTransitions
<item name="android:windowActivityTransitions">true</item>

→ 总开关。必须设为 true,后续的所有 Transition 设置才会生效。

android:windowEnterTransition / android:windowExitTransition
<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 相同,但可以设置为不同:

<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 组合)。

<item name="android:windowSharedElementEnterTransition">@transition/change_image_transform</item>
android:windowAllowEnterTransitionOverlap / android:windowAllowReturnTransitionOverlap
<item name="android:windowAllowEnterTransitionOverlap">true</item>

→ 设为 true 时,调用 startActivity() 后,当前 Activity 的退出动画和下一个 Activity 的进入动画并行执行;设为 false 时,串行执行(先退出完再进入)。

使用场景

  • 流畅体验:通常建议 allowEnterTransitionOverlap=true(两个动画叠加更流畅)
  • 避免闪烁:如果两个动画有重叠冲突,设为 false
android:windowTransitionBackgroundFadeDuration
<item name="android:windowTransitionBackgroundFadeDuration">300</item>

→ 转场过程中,windowBackground 的淡出/淡入动画时长(毫秒)。这对于避免 Shared Element Transition 期间出现"白色闪烁"非常有用——设置一个合理的 fade duration 让背景渐变过渡。

代码中的等价实现
// 在 Activity 中代码设置 Transition(与 Theme 属性等价)
getWindow().requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
getWindow().setEnterTransition(new Explode());
getWindow().setExitTransition(new Fade());
getWindow().setAllowEnterTransitionOverlap(true);

5.8 像素格式与绘制类

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

这是影响最深远的属性之一。它直接改变了 Window 所在 Surface 的像素格式。

PhoneWindow 源码全链路

// 第 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.formatOPAQUETRANSLUCENT

PixelFormat.TRANSLUCENT vs OPAQUE 对 SurfaceFlinger 的影响

OPAQUE (默认):
  SurfaceFlinger 将该 Layer 标记为 "不透明"
  合成时不需要读取下层 Layer 的像素
  性能:快(跳过混合运算)

TRANSLUCENT:
  SurfaceFlinger 将该 Layer 标记为 "带 alpha 通道"
  合成时读取每个像素的 alpha 值,与下层做逐像素混合
  性能:慢(增加 GPU 负载和带宽消耗)

完整的半透明 Activity Theme

<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 生效
触摸穿透到了下层 AppwindowIsTranslucent 清理了 FLAG_NOT_TOUCHABLE,确认是否在其他地方又被设置了
启动时短暂白屏设置了 windowBackground=transparent 但没设置 windowDisablePreview=true
android:windowSwipeToDismiss
<item name="android:windowSwipeToDismiss">true</item>

→ 启用滑动关闭手势(iOS 风格的侧滑返回)。

使用场景

  • 图片预览 / 大图浏览:下滑关闭
  • 视频播放:下滑缩小/关闭
  • 模态页面:iOS 风格的下滑 dismiss

5.9 FitSystemWindows 与 Edge-to-Edge 类

android:fitsSystemWindows

这是 View 属性而非 Window 属性,但在 Theme 中设置时会作用于 DecorView:

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

→ 设置 DecorView 的 fitsSystemWindows = true。DecorView 在 dispatchApplyWindowInsets() 时会先消费掉状态栏/导航栏的 insets(为其 padding),然后将剩余的 insets 分发给子 View。

使用场景

  • 需要内容自动避开状态栏:如标准的列表页面
  • 与 CoordinatorLayout 配合:CoordinatorLayout 自身会处理 insets

fitsSystemWindows = true 的效果

状态栏 24dp ─┐
            │ ← DecorView 消费了此 inset,给自身加 paddingTop=24dp
            │   子 View 收到调整后的 insets(状态栏部分已消耗)
内容区域    │
            │  效果:内容自动在状态栏下方开始,不会被遮挡
导航栏 48dp ─┘
android:windowOptOutEdgeToEdgeEnforcement
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>

Android 15+ (API 35) 新增。从 API 35 开始,targetSdk=35 的应用默认强制启用 Edge-to-Edge 模式——即系统自动设置 FLAG_LAYOUT_IN_SCREENFLAG_TRANSLUCENT_STATUSFLAG_TRANSLUCENT_NAVIGATION 等标志,使窗口内容延伸到系统栏后面。

如果你需要退出这个行为(例如你的旧 UI 大量依赖 fitsSystemWindows 而没有使用 WindowInsets),设置此属性为 true:

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

建议:尽量适配 Edge-to-Edge 而非 opt-out。Google 的长期方向是全面 Edge-to-Edge。

android:windowLayoutInDisplayCutoutMode
<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系统栏透明时允许延伸到任何方向的刘海
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
<item name="android:windowCloseOnTouchOutside">true</item>
if (a.getBoolean(R.styleable.Window_windowCloseOnTouchOutside, false)) {
    setCloseOnTouchOutsideIfNotSet(true);
}

LayoutParams 影响LP.flags |= FLAG_NOT_TOUCH_MODAL | FLAG_WATCH_OUTSIDE_TOUCH

完整实现链路

用户触摸 Dialog 外部区域
  → WMS 发送 ACTION_OUTSIDE 到该 Dialog
    → PhoneWindow 收到 ACTION_OUTSIDE
      → 回调 to Window.onTouchEvent()
        → 调用 dispatchTouchEvent()
          → 因为 closeOnTouchOutside=true
            → 判断触摸坐标在 DecorView 外部
              → 调用 Activity.finish() (如果 attached 到 Activity)

使用场景

  • Dialog 样式 ActivityTheme.AppCompat.Dialog 默认开启
  • 底部菜单 / ActionSheet
  • Dropdown 菜单
  • PopupWindow
android:windowAnimationStyle
<item name="android:windowAnimationStyle">@style/MyWindowAnimation</item>
mAnimationStyle = a.getResourceId(
    R.styleable.Window_windowAnimationStyle, 0);

LayoutParams 影响LP.windowAnimations = mAnimationStyle

这个资源 ID 传给 WMS,WMS 在窗口出现/消失时查找对应的 WindowAnimation 资源并播放。

自定义窗口动画

<!-- 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
<item name="android:windowBackgroundBlurRadius">25dp</item>

→ API 31+,配合 windowBlurBehind 使用,设置背景模糊半径。值越大越模糊,但 GPU 开销越大。

android:backgroundDimAmount
<item name="android:backgroundDimAmount">0.6</item>

→ 配合 FLAG_DIM_BEHIND 使用,设置下层窗口变暗的程度(0.0=不变暗,1.0=全黑)。Dialog 的默认值大约是 0.5。

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

→ 是否启用背景变暗(通常与 FLAG_DIM_BEHIND 联动)。

android:windowSplashScreenBackground (API 31+)
<item name="android:windowSplashScreenBackground">@color/splash_bg</item>

→ Android 12+ SplashScreen API 使用的启动窗口背景色。等价于在代码中:

SplashScreen splashScreen = SplashScreen.installSplashScreen(this);
// 背景色通过 Theme 属性设置,代码中不需要额外调用
android:windowSplashScreenAnimationDuration (API 31+)
<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.flagsFLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE
windowDisablePreview构造函数LP.flagsFLAG_DISABLE_PREVIEW
windowEnableSplitTouch构造函数LP.flagsFLAG_SPLIT_TOUCH
windowShowWallpaper构造函数LP.flagsFLAG_SHOW_WALLPAPER
windowBlurBehind构造函数LP.flagsFLAG_BLUR_BEHIND (API 31+)
windowSecure构造函数LP.flags + InputChannelFLAG_SECURE
windowNotTouchablegenerateLayoutLP.flagsFLAG_NOT_TOUCHABLE
windowFullscreengenerateLayoutLP.flagsFLAG_FULLSCREEN
windowTranslucentStatusgenerateLayoutLP.flagsFLAG_TRANSLUCENT_STATUS (+ 隐式 LAYOUT_IN_SCREEN)
windowTranslucentNavigationgenerateLayoutLP.flagsFLAG_TRANSLUCENT_NAVIGATION
windowDrawsSystemBarBackgroundsgenerateLayoutLP.flagsFLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
windowDimBehindgenerateLayoutLP.flagsFLAG_DIM_BEHIND
windowCloseOnTouchOutsidegenerateLayoutLP.flagsFLAG_NOT_TOUCH_MODAL | FLAG_WATCH_OUTSIDE_TOUCH
尺寸映射
windowFixedWidthgenerateLayoutLP.width固定像素值
windowFixedHeightgenerateLayoutLP.height固定像素值
windowFixedWidthMajorgenerateLayoutLP.width条件覆写(长边)
windowFixedWidthMinorgenerateLayoutLP.width条件覆写(短边)
windowMinWidthMajorgenerateLayoutLP.minWidth最小宽度(长边)
windowMinWidthMinorgenerateLayoutLP.minWidth最小宽度(短边)
windowElevationgenerateLayoutLP.elevationfloat (px)
windowIsFloating构造函数mIsFloatingLP.width/height/gravityWRAP_CONTENT + CENTER
系统栏映射
statusBarColorgenerateLayoutmStatusBarColor → InsetsControllerColor int
navigationBarColorgenerateLayoutmNavigationBarColor → InsetsControllerColor int
windowLightStatusBargenerateLayoutInsetsControllerAPPEARANCE_LIGHT_STATUS_BARS
windowLightNavigationBargenerateLayoutInsetsControllerAPPEARANCE_LIGHT_NAVIGATION_BARS
enforceNavigationBarContrastgenerateLayoutInsetsControllerBoolean
enforceStatusBarContrastgenerateLayoutInsetsControllerBoolean
软键盘映射
windowSoftInputModegenerateLayoutLP.softInputMode位掩码组合
像素与绘制映射
windowIsTranslucent构造函数 + generateLayoutLP.formatPixelFormat.TRANSLUCENT
windowClipToOutlinegenerateLayoutLP.flagsFLAG_LAYOUT_NO_LIMITS
转场动画映射
windowActivityTransitionsgenerateLayoutmActivityTransitionsAllowedboolean
windowEnterTransitiongenerateLayoutmEnterTransitionTransition ref
windowExitTransitiongenerateLayoutmExitTransitionTransition ref
windowReenterTransitiongenerateLayoutmReenterTransitionTransition ref
windowReturnTransitiongenerateLayoutmReturnTransitionTransition ref
windowSharedElementEnterTransitiongenerateLayoutmSharedElementEnterTransitionTransition ref
windowSharedElementExitTransitiongenerateLayoutmSharedElementExitTransitionTransition ref
windowAllowEnterTransitionOverlapgenerateLayoutmAllowEnterTransitionOverlapboolean
windowAllowReturnTransitionOverlapgenerateLayoutmAllowReturnTransitionOverlapboolean
windowTransitionBackgroundFadeDurationgenerateLayoutmBackgroundFadeDurationMillislong (ms)
背景映射
windowBackgroundgenerateLayoutmBackgroundDrawable → DecorViewDrawable/Color
windowBackgroundFallbackgenerateLayoutmBackgroundFallbackDrawable/Color
windowBackgroundBlurBehindgenerateLayoutmBackgroundBlurBehindboolean
windowBackgroundBlurRadiusgenerateLayoutmBackgroundBlurRadiusint
backgroundDimAmountgenerateLayoutLP.dimAmountfloat
Feature 映射
windowNoTitlegenerateLayoutrequestFeature(FEATURE_NO_TITLE)
windowActionBargenerateLayoutrequestFeature(FEATURE_ACTION_BAR)
windowActionModeOverlaygenerateLayoutrequestFeature(FEATURE_ACTION_MODE_OVERLAY)
动画映射
windowAnimationStyle构造函数LP.windowAnimationsstyle resource id
Cutout 映射
windowLayoutInDisplayCutoutModegenerateLayoutLP.layoutInDisplayCutoutModeint enum
Edge-to-Edge 映射
windowOptOutEdgeToEdgeEnforcement构造函数 (API 35+)退出强制 Edge-to-Edgeboolean
其他
windowContentOverlaygenerateLayoutmContentOverlay → DecorViewDrawable
windowTitleSizegenerateLayoutTitleView textSizepx
windowSwipeToDismissgenerateLayout注册滑动监听器boolean
windowSplashScreenBackground— (SplashScreen API)启动画面背景Color
windowSplashScreenAnimationDuration— (SplashScreen API)启动画面动画时长int (ms)
windowContentTransitionsgenerateLayoutmContentTransitionsAllowedboolean

7. 实战场景

以下每个场景都给出了完整的 Theme XML 配置等价的 Java 代码,以及关键属性的解释。

7.1 全屏 Activity

效果:隐藏状态栏和导航栏,Window 占据全部屏幕。

<!-- 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 代码

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
    setContentView(R.layout.activity_fullscreen);
}

现代推荐方式(Android 11+,允许用户下滑唤出系统栏):

// 使用 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 的背景完全透明,可以看到下层窗口内容。

<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 中使用

<activity
    android:name=".TransparentActivity"
    android:theme="@style/Theme.Transparent" />

使用范例

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(与上面类似,但加上了浮动窗口行为):

<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 颜色统一。

<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 等价代码

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 图片查看器(沉浸式)

效果:图片充满屏幕,状态栏和导航栏以半透明浮层方式覆盖,点击可切换。

<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>

点击切换显示/隐藏系统栏的代码

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 锁屏上显示窗口

效果:在锁屏上方显示一个窗口(如来电界面、闹钟)。

<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>

⚠️ 注意windowShowWhenLockedwindowTurnScreenOnwindowDismissKeyguard 等属性不能直接在 Theme XML 中设置,因为它们需要特殊权限或必须在代码中使用 setFlags() 动态设置。上述 XML 仅是示意对应关系。实际实现为:

// 在 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 禁止截屏的敏感页面

效果:阻止系统截屏和录屏。

<style name="Theme.Secure" parent="Theme.Material3.DayNight.NoActionBar">
    <item name="android:windowSecure">true</item>
</style>

Java 等价代码

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
    setContentView(R.layout.activity_secure);
}

7.7 软键盘适配表单页面

场景一:聊天页面(底部输入框 + 消息列表)

<style name="Theme.Chat" parent="Theme.Material3.DayNight.NoActionBar">
    <item name="android:windowSoftInputMode">stateHidden|adjustResize</item>
</style>
  • stateHidden:进入页面时不弹出键盘
  • adjustResize:键盘弹出时压缩消息列表,使最新消息可见

场景二:登录页面(顶部 Logo + 底部输入框)

<style name="Theme.Login" parent="Theme.Material3.DayNight.NoActionBar">
    <item name="android:windowSoftInputMode">stateHidden|adjustResize</item>
</style>
  • 同样使用 adjustResize,但需要对布局做处理——如使用 ConstraintLayout 让 Logo 随窗口压缩而缩放或上移

场景三:搜索页面

<style name="Theme.Search" parent="Theme.Material3.DayNight.NoActionBar">
    <item name="android:windowSoftInputMode">stateAlwaysHidden|adjustResize</item>
</style>
  • stateAlwaysHidden:即使用户上次在别的页面打开了键盘,进入搜索页也不自动弹出

8. 最佳实践与常见陷阱

8.1 statusBarColor 不生效的 4 种可能

<!-- 正确配置(缺一不可) -->
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@color/custom</item>

检查清单

  1. windowTranslucentStatus 是否为 true
  2. windowDrawsSystemBarBackgrounds 是否为 true
  3. statusBarColor 是否被父 Theme 覆盖?(使用 styles.xmlparent 链追查)
  4. ☐ 代码中是否调用了 getWindow().setStatusBarColor(Color.TRANSPARENT) 或其他 setter 覆盖了 Theme 值?

8.2 透明 Activity 背景显示为黑色

<!-- 错误 -->
<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

@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 设置的正确姿势

// 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):

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

新代码(适配 Edge-to-Edge):

// 方案 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 需要同时满足以下条件:

  1. windowClipToOutline = true
  2. windowBackground 设置了带有圆角/自定义形状的 Drawable
  3. windowElevation 已设置(需要 Z 轴高度来触发阴影/裁剪)

8.7 Shared Element Transition 闪烁

如果 Shared Element Transition 期间出现白色闪烁:

<!-- 增加背景淡出时长,平滑过渡 -->
<item name="android:windowTransitionBackgroundFadeDuration">300</item>
<!-- 确保背景不是纯白 -->
<item name="android:windowBackground">@color/window_background</item>

9. 总结

Android Theme 属性到 PhoneWindow 的映射链路核心归纳为四层:

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 分钟。建议收藏,作为随手查阅的速查手册。