切换主题后能否在运行时动态更新 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 等窗口属性,用于创建临时窗口。执行顺序大致如下:
- 系统准备启动目标 Activity。
- 系统读取 Manifest 中声明的 Activity theme。
- 系统根据
android:windowBackground创建 starting window。 - 应用进程开始创建 Activity。
- 执行
Activity.onCreate()。 - 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 或系统资源配置确定目标主题。
备注:内容仅供参考。
