PAYATHON 2026

切换主题后能否在运行时动态更新 Preview Window?

排障赵师傅

结论

Android 中的 Preview Window 通常称为 starting window。系统会在目标 Activity 执行 onCreate() 之前创建它,因此需要注意以下几点:

  • setTheme()、getWindow().setBackgroundDrawable() 等代码无法修改已经创建的 starting window。
  • setTheme() 只影响 Activity 自己的窗口和 View,不会更新系统已经显示的 starting window。
  • 重建 Activity 或重启应用进程后,系统也不会自动读取保存在 SharedPreferences 中的主题设置。
  • android:windowDisablePreview="true" 并非唯一的处理方式。要保留 starting window,系统必须在启动 Activity 前确定主题。较稳妥的方案是为浅色和深色模式分别准备 Activity 组件,并在 Manifest 中声明对应的静态主题。
  • Splash Activity 面临同样的问题。它的 starting window 也会在 onCreate() 前创建,所以仍需提前声明主题。

starting window 为什么仍然使用旧颜色

启动 Activity 时,系统会先从 Manifest 中读取 Activity 的主题资源,再读取该主题的 android:windowBackground 等窗口属性,用于创建临时窗口。执行顺序大致如下:

  1. 系统准备启动目标 Activity。
  2. 系统读取 Manifest 中声明的 Activity theme。
  3. 系统根据 android:windowBackground 创建 starting window。
  4. 应用进程开始创建 Activity。
  5. 执行 Activity.onCreate()。
  6. Activity 的真实窗口完成首次绘制后,系统移除 starting window。

应用调用 setTheme() 时,流程已经进行到第 5 步附近。即使把它写在 super.onCreate() 前面,也晚于 starting window 的创建时间。

@Override
protected void onCreate(Bundle savedInstanceState) {
    setTheme(R.style.AppTheme_Dark);
    super.onCreate(savedInstanceState);
}

这段代码可以为真实 Activity 设置主题,但无法修改此前已经显示的 starting window。

下面的代码同样只会修改 Activity 自己的窗口:

getWindow().setBackgroundDrawableResource(R.drawable.background_dark);

这通常不是简单的主题缓存问题

不同 Android 版本可能会复用 starting window、窗口表面或任务快照,具体策略也会受到启动方式和设备实现的影响。在 API 17 环境中遇到旧背景时,应先检查 Manifest theme,而非直接归因于 setTheme() 缓存。

创建 starting window 时,系统能够提前读取的内容只有 Manifest、资源和系统配置。系统不会调用应用代码去读取 SharedPreferences 中保存的 isDarkMode 值。

所以下面的流程无法保证 starting window 使用新主题:

preferences.edit().putBoolean("dark_mode", true).apply();
recreate();

recreate() 会让新的 Activity 实例应用深色主题,starting window 使用的主题仍可能来自 Manifest。

完全结束应用进程也无法解决这一时序问题。进程重新启动后,系统依旧会先创建 starting window,然后才运行读取偏好设置的应用代码。

保留 starting window 的可靠做法

如果主题由应用内的运行时设置决定,可以为浅色和深色主题分别声明一个可启动的 Activity 组件。系统启动对应组件时,就能提前确定要使用的主题。

先定义两套主题:

<!-- res/values/styles.xml -->
<style name="AppTheme.Light" parent="@android:style/Theme.Holo.Light">
    <item name="android:windowBackground">@drawable/background_light</item>
</style>

<style name="AppTheme.Dark" parent="@android:style/Theme.Holo">
    <item name="android:windowBackground">@drawable/background_dark</item>
</style>

然后在 Manifest 中为两个组件指定不同的主题:

<activity
    android:name=".LightHomeActivity"
    android:theme="@style/AppTheme.Light" />

<activity
    android:name=".DarkHomeActivity"
    android:theme="@style/AppTheme.Dark" />

两个 Activity 可以继承同一个基类,共用界面和业务逻辑:

public abstract class BaseHomeActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setTheme(getActivityTheme());
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
    }

    protected abstract int getActivityTheme();
}
public class LightHomeActivity extends BaseHomeActivity {

    @Override
    protected int getActivityTheme() {
        return R.style.AppTheme_Light;
    }
}
public class DarkHomeActivity extends BaseHomeActivity {

    @Override
    protected int getActivityTheme() {
        return R.style.AppTheme_Dark;
    }
}

切换主题时,保存设置并启动对应的组件:

private void switchTheme(boolean darkMode) {
    getSharedPreferences("settings", MODE_PRIVATE)
            .edit()
            .putBoolean("dark_mode", darkMode)
            .apply();

    Class<?> target = darkMode
            ? DarkHomeActivity.class
            : LightHomeActivity.class;

    Intent intent = new Intent(this, target);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    startActivity(intent);
    finish();
}

这样可以让 Manifest theme 与 Activity 实际使用的 theme 保持一致,后续创建的 starting window 也能取得对应的 windowBackground。

如果应用包含很多 Activity,可以用公共基类减少重复代码。还可以研究 activity-alias,为同一个目标 Activity 声明使用不同主题的别名。旧系统和不同厂商的实现可能存在差异,面向 API 17 时需要在目标设备上验证。相比之下,使用两个明确的 Activity 组件通常更容易控制。

禁用 Preview Window 会发生什么

可以通过下面的属性禁止系统创建 starting window:

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

这样可以消除旧背景闪现。不过在 Activity 绘制首帧前,界面可能出现空白、黑屏,或者让上一个界面停留得更久。应用启动较慢时,这些现象可能比颜色不一致更显眼。

只有在无法为 starting window 设计合适的静态外观时,才适合禁用它。

Splash Activity 的限制

Splash Activity 可以读取设置,再跳转到浅色或深色 Activity,但它自己的 starting window 仍由 Manifest theme 决定。下面的写法无法动态改变已经显示的 starting window:

@Override
protected void onCreate(Bundle savedInstanceState) {
    setTheme(loadSavedTheme());
    super.onCreate(savedInstanceState);
}

使用 Splash Activity 时,可以为它设置一个适用于两套主题的中性背景,再根据设置跳转到已经声明静态主题的目标组件。Splash Activity 无法充当运行时修改 starting window 的接口。

API 17 下还要注意什么

values-night、drawable-night 等资源限定符可以根据系统的 night 配置选择资源。应用内部保存的 Light/Dark 开关不会自动改变系统配置,因此只有当系统创建 starting window 时也能识别相同的配置变化,这种资源方案才会生效。

系统也不保证每次普通的 Activity 跳转都会显示 starting window。它是否出现以及持续多久,取决于进程状态、目标 Activity 的首次绘制速度和具体的 Android 实现。API 17 模拟器上的表现也未必与所有设备一致。

setTheme() 可以动态设置 Activity 的主题,但无法更改系统已经创建的 starting window。要保留预览效果并确保颜色正确,需要让系统在启动组件前,通过 Manifest 或系统资源配置确定目标主题。

备注:内容仅供参考。