Appearance
AppCompatDelegate 深度解析
字数统计:约 10000 字
难度等级:⭐⭐⭐⭐⭐
面试重要度:⭐⭐⭐⭐⭐
目录
1. AppCompatDelegate 概述
1.1 什么是 AppCompatDelegate?
AppCompatDelegate 是 AndroidX AppCompat 库中的核心类,它是 AppCompatActivity 的功能实现者,负责处理以下关键任务:
kotlin
// AppCompatDelegate 的类结构
public abstract class AppCompatDelegate {
// 核心职责:
// 1. 主题和应用样式的管理
// 2. ActionBar 的创建和生命周期管理
// 3. 深色/浅色模式的检测和控制
// 4. 夜间模式的自动检测和手动设置
// 继承关系
public abstract class AppCompatDelegateImpl extends AppCompatDelegate
public class VectorEnabledTintResourcesWrapper
// 静态工具方法
public static AppCompatDelegate create(...)
public static void installViewFactory()
public static void installMenuFactory()
}1.2 AppCompatDelegate 的设计模式
AppCompatDelegate 使用了多种设计模式,理解这些模式对于深入理解其工作原理至关重要:
kotlin
// 1. 单例模式 - 全局配置
public abstract class AppCompatDelegate {
private static final AppCompatDelegateImpl DEFAULTDelegate;
static {
DEFAULTDelegate = create(null, null);
}
public static AppCompatDelegate getDefaultDelegate() {
return DEFAULTDelegate;
}
}
// 2. 工厂模式 - 创建不同实现
public static AppCompatDelegate create(
Context context,
WindowCallback callback,
Bundle icicle) {
if (context == null) {
return new AppCompatDelegateImplV14();
} else if (Build.VERSION.SDK_INT >= 21) {
return new AppCompatDelegateImplV21();
} else {
return new AppCompatDelegateImplV14();
}
}
// 3. 策略模式 - 夜间模式策略
public interface NightModeManager {
int getDefaultNightMode();
void setDefaultNightMode(int mode);
}
// 4. 委托模式 - Activity 委托给 Delegate
public class AppCompatActivity {
private AppCompatDelegate mDelegate;
@Override
protected void onCreate(Bundle savedInstanceState) {
mDelegate.installViewFactory();
mDelegate.installMenuFactory();
super.onCreate(savedInstanceState);
mDelegate.onCreate(savedInstanceState);
}
}1.3 AppCompatDelegate 在生命周期中的调用
理解 AppCompatDelegate 在 Activity 生命周期中的调用时序至关重要:
kotlin
// Activity 生命周期中 AppCompatDelegate 的调用
class AppCompatActivity : ComponentActivity() {
private lateinit var mDelegate: AppCompatDelegateImpl
override fun onCreate(savedInstanceState: Bundle?) {
// 1. 初始化 Delegate
mDelegate = AppCompatDelegateImpl.createActivityDelegate(this)
// 2. 安装 View 工厂(在 super.onCreate() 之前)
mDelegate.installViewFactory()
// 3. 安装 Menu 工厂(在 super.onCreate() 之前)
mDelegate.installMenuFactory()
// 4. 调用父类 onCreate
super.onCreate(savedInstanceState)
// 5. Delegate 处理 onCreate
mDelegate.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
mDelegate.onResume()
}
override fun onStart() {
super.onStart()
mDelegate.onStart()
}
override fun onStop() {
super.onStop()
mDelegate.onStop()
}
override fun onDestroy() {
super.onDestroy()
mDelegate.onDestroy()
}
override fun onBackPressed() {
mDelegate.onBackPressed()
}
}1.4 AppCompatDelegate 版本演进
不同 API 级别使用不同的 Delegate 实现:
kotlin
// API 7-13: AppCompatDelegateImplV7
// 提供最基础的 ActionBar 支持
class AppCompatDelegateImplV7 : AppCompatDelegateImpl() {
override fun installViewFactory() {
// 简单的 View 工厂安装
ActivityViewGroupCompat.setActivity(this)
}
}
// API 14-20: AppCompatDelegateImplV14
// 添加更多兼容性支持
class AppCompatDelegateImplV14 : AppCompatDelegateImplV7() {
override fun onCreate(savedInstanceState: Bundle?) {
// 添加夜间模式支持
super.onCreate(savedInstanceState)
initializeNightMode()
}
}
// API 21+: AppCompatDelegateImplV21
// 使用原生 Material Design 特性
class AppCompatDelegateImplV21 : AppCompatDelegateImplV14() {
override fun applyDayNight() {
// 使用原生 API 设置主题
if (Build.VERSION.SDK_INT >= 26) {
setWindowFlag(Window.FEATURE_OVERRIDES, true)
}
}
}2. 主题管理系统
2.1 主题的底层机制
Android 主题是一个包含多个属性(Attribute)的样式(Style),理解属性解析过程对主题管理至关重要:
xml
<!-- 主题定义 -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- 颜色属性 -->
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
<!-- 组件样式属性 -->
<item name="buttonStyle">@style/AppButton</item>
<item name="toolbarStyle">@style/AppToolbar</item>
<!-- 窗口属性 -->
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<!-- 属性引用示例 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary" <!-- 引用主题属性 -->
android:textColor="?android:attr/textColorPrimary" /> <!-- 引用框架属性 -->2.2 主题属性查找顺序
当控件引用主题属性时,系统会按照以下顺序查找:
kotlin
// 主题属性查找流程图
private TypedValue resolveAttribute(int resid, Resources.Theme theme) {
// 1. 当前控件的样式(style 属性)
style ->
// 2. 当前 Activity 的主题(android:theme)
activityTheme ->
// 3. 应用的默认主题(android:theme)
applicationTheme ->
// 4. 系统默认主题
systemTheme
}
// 实际查找示例
@NonNull
@Override
public TypedValue resolveAttribute(@AttrRes int resId, TypedValue outValue, boolean resolveRefs) {
// 1. 首先查找当前样式
TypedValue value = mTheme.resolveAttribute(resId, outValue, resolveRefs);
// 2. 如果未找到,递归查找父主题
if (value == null) {
value = resolveFromParentTheme(resId, outValue, resolveRefs);
}
return value;
}2.3 属性作用域
理解不同属性的作用域对于主题管理至关重要:
xml
<!-- 框架属性 (android:attr) -->
<!-- 这些是 Android 系统提供的标准属性 -->
<TextView
android:textColor="@android:color/primary_text_dark"
android:textSize="@android:dimen/app_icon_size" />
<!-- 主题属性 (attr) -->
<!-- 这些是 AppCompat 和 Material 提供的属性 -->
<TextView
android:textColor="?attr/colorPrimary"
android:background="?attr/selectableItemBackground" />
<!-- 颜色资源 -->
<!-- 这些是静态颜色定义 -->
<TextView
android:textColor="@color/primary"
android:background="@drawable/button_bg" />2.4 主题覆盖与继承
xml
<!-- 基础主题 -->
<style name="Base.AppTheme" parent="Theme.MaterialComponents.DayNight">
<item name="colorPrimary">@color/primary</item>
<item name="colorSecondary">@color/secondary</item>
</style>
<!-- 带 ActionBar 的主题 -->
<style name="AppTheme.DarkActionBar" parent="Base.AppTheme">
<item name="windowActionBar">true</item>
<item name="windowNoTitle">false</item>
</style>
<!-- 不带 ActionBar 的主题 -->
<style name="AppTheme.NoActionBar" parent="Base.AppTheme">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowContentTransitions">true</item>
</style>
<!-- 透明主题 -->
<style name="AppTheme.Transparent" parent="Base.AppTheme">
<item name="android:windowIsTranslucent">true</item>
<item name="android:background">@android:color/transparent</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
</style>2.5 动态主题应用
kotlin
// 在运行时动态应用主题
class ThemeManager {
companion object {
const val PREF_THEME = "theme_preference"
const val THEME_LIGHT = "light"
const val THEME_DARK = "dark"
const val THEME_SYSTEM = "system"
}
fun applyTheme(context: Context, themeName: String) {
val themeRes = when (themeName) {
THEME_LIGHT -> R.style.AppTheme_Light
THEME_DARK -> R.style.AppTheme_Dark
THEME_SYSTEM -> R.style.AppTheme_System
else -> R.style.AppTheme_Light
}
// 保存主题选择
saveThemePreference(context, themeName)
// 重启应用以应用新主题
restartApplication(context)
}
fun getThemeForContext(context: Context): String {
val prefs = context.getSharedPreferences(PREF_THEME, Context.MODE_PRIVATE)
return prefs.getString(PREF_THEME, THEME_SYSTEM) ?: THEME_SYSTEM
}
private fun saveThemePreference(context: Context, themeName: String) {
context.getSharedPreferences(PREF_THEME, Context.MODE_PRIVATE)
.edit()
.putString(PREF_THEME, themeName)
.apply()
}
private fun restartApplication(context: Context) {
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
(context.applicationContext as? Application)?.exitApplication()
}
}2.6 使用 ThemeOverlay 进行局部主题覆盖
xml
<!-- 定义 ThemeOverlay -->
<style name="ThemeOverlay.App.Button" parent="">
<item name="colorPrimary">@color/white</item>
<item name="colorOnPrimary">@color/primary</item>
<item name="backgroundTint">@color/secondary</item>
</style>
<!-- 应用到特定视图 -->
<Button
style="@style/Widget.MaterialComponents.Button"
android:text="Themed Button"
app:theme="@style/ThemeOverlay.App.Button" />
<!-- 或者在代码中应用 -->
<TextView
android:id="@+id/specialText"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val textView = findViewById<TextView>(R.id.specialText)
textView.apply {
// 应用 ThemeOverlay
context.theme.applyStyle(R.style.ThemeOverlay.App.Button, true)
}
}
}2.7 主题属性动态查询
kotlin
// 动态获取主题属性值
class ThemeUtils {
companion object {
fun getColor(context: Context, attrRes: Int): Int {
val typedValue = TypedValue()
context.theme.resolveAttribute(attrRes, typedValue, true)
return typedValue.data
}
fun getDimension(context: Context, attrRes: Int): Float {
val typedValue = TypedValue()
context.theme.resolveAttribute(attrRes, typedValue, true)
return TypedValue.complexToDimension(
typedValue.data,
context.resources.displayMetrics
)
}
fun getDrawable(context: Context, attrRes: Int): Drawable? {
val typedValue = TypedValue()
context.theme.resolveAttribute(attrRes, typedValue, true)
return ContextCompat.getDrawable(context, typedValue.resourceId)
}
}
}
// 使用示例
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 获取主题颜色
val primaryColor = ThemeUtils.getColor(this, R.attr.colorPrimary)
val accentColor = ThemeUtils.getColor(this, R.attr.colorAccent)
// 获取主题维度
val actionBarHeight = ThemeUtils.getDimension(this, R.attr.actionBarSize)
// 获取主题 drawable
val rippleDrawable = ThemeUtils.getDrawable(this, R.attr/selectableItemBackground)
}
}3. 日间/夜间模式原理
3.1 夜间模式的检测机制
AppCompatDelegate 通过多种方式检测是否应该启用夜间模式:
kotlin
// AppCompatDelegate 中的夜间模式检测
class AppCompatDelegateImplV19 extends AppCompatDelegateImplV14 {
private int getNightMode(): int {
// 1. 检查是否设置了默认夜间模式
int defaultNightMode = mNightModeManager.getDefaultNightMode();
if (defaultNightMode == MODE_NIGHT_NO) {
return MODE_NIGHT_NO;
} else if (defaultNightMode == MODE_NIGHT_YES) {
return MODE_NIGHT_YES;
}
// 2. 跟随系统设置
if (defaultNightMode == MODE_NIGHT_FOLLOW_SYSTEM) {
return getNightModeFromSystem();
}
// 3. 自动检测(基于时间)
if (defaultNightMode == MODE_NIGHT_AUTO_BATTERY) {
return getNightModeFromBatteryOpt();
}
// 4. 默认跟随系统
return getNightModeFromSystem();
}
private int getNightModeFromSystem(): int {
Configuration config = getResources().getConfiguration();
int uiMode = config.uiMode;
if ((uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES) {
return MODE_NIGHT_YES;
} else {
return MODE_NIGHT_NO;
}
}
private int getNightModeFromBatteryOpt(): int {
// 根据电池优化模式决定
boolean isBatterySaverOn = isBatterySaverOn();
return isBatterySaverOn ? MODE_NIGHT_YES : getNightModeFromSystem();
}
}3.2 夜间模式的常量定义
kotlin
// AppCompatDelegate 中的夜间模式常量
public class AppCompatDelegate {
public static final int MODE_NIGHT_NO = 0; // 始终禁用夜间模式
public static final int MODE_NIGHT_YES = 1; // 始终启用夜间模式
public static final int MODE_NIGHT_AUTO_BATTERY = 2; // 根据电池优化模式自动切换
public static final int MODE_NIGHT_FOLLOW_SYSTEM = 3; // 跟随系统设置
public static final int MODE_NIGHT_UNSPECIFIED = -1; // 未指定(默认跟随系统)
// 使用示例
companion object {
fun setNightMode() {
// 始终禁用夜间模式
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
// 始终启用夜间模式
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
// 根据电池优化自动切换
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
// 跟随系统设置(推荐)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
}
}3.3 配置变更处理
当夜间模式发生变化时,系统会触发配置变更,需要正确处理:
xml
<!-- AndroidManifest.xml -->
<activity
android:name=".MyActivity"
android:configChanges="uiMode|orientation|screenSize">
</activity>kotlin
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 应用保存的夜间模式设置
applySavedNightMode()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// 检查夜间模式是否变化
val nightModeChanged = (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) !=
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK)
if (nightModeChanged) {
// 处理夜间模式变化
updateUIForNightMode()
}
}
private fun applySavedNightMode() {
val prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE)
val nightMode = prefs.getString(PREF_NIGHT_MODE, "system")
val mode = when (nightMode) {
"light" -> AppCompatDelegate.MODE_NIGHT_NO
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
AppCompatDelegate.setDefaultNightMode(mode)
}
private fun updateUIForNightMode() {
// 更新 UI 组件
// 重新加载图片资源
// 更新颜色等
}
}3.4 夜间模式资源限定符
使用资源限定符为夜间模式提供专用资源:
res/
├── values/
│ ├── colors.xml # 浅色模式颜色
│ ├── themes.xml # 浅色模式主题
│ └── strings.xml
├── values-night/
│ ├── colors.xml # 深色模式颜色
│ ├── themes.xml # 深色模式主题
│ └── strings.xml
├── drawable/
│ ├── bg_card.xml # 浅色背景
│ └── ic_launcher.xml
├── drawable-night/
│ ├── bg_card.xml # 深色背景
│ └── ic_launcher.xml
├── layout/
│ └── activity_main.xml
└── layout-night/
└── activity_main.xml # 可选:深色模式专属布局xml
<!-- res/values/colors.xml (浅色模式) -->
<resources>
<color name="background">#FFFFFF</color>
<color name="surface">#FFFFFF</color>
<color name="on_background">#000000</color>
<color name="on_surface">#000000</color>
<color name="primary">#1976D2</color>
<color name="on_primary">#FFFFFF</color>
</resources>
<!-- res/values-night/colors.xml (深色模式) -->
<resources>
<color name="background">#121212</color>
<color name="surface">#1E1E1E</color>
<color name="on_background">#FFFFFF</color>
<color name="on_surface">#FFFFFF</color>
<color name="primary">#90CAF9</color>
<color name="on_primary">#000000</color>
</resources>3.5 使用 Night Mode 的 Preview
在 Android Studio 中预览夜间模式:
kotlin
@Preview(name = "Light Mode", group = "Day Night")
@Preview(name = "Dark Mode", group = "Day Night", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun GreetingPreview() {
MyAppTheme {
Greeting("Android")
}
}
@Preview(
name = "Light Mode",
uiMode = Configuration.UI_MODE_NIGHT_NO,
showBackground = true,
showSystemUi = true
)
@Preview(
name = "Dark Mode",
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
showSystemUi = true
)
@Composable
fun MyLayoutPreview() {
MyLayout()
}3.6 夜间模式的自动切换监听
kotlin
class NightModeObserver(private val context: Context) {
private var nightModeCallback: ((Boolean) -> Unit)? = null
fun setOnNightModeChangedListener(callback: (Boolean) -> Unit) {
nightModeCallback = callback
}
fun startObserving() {
// 使用 RegisterReceiver 监听配置变化
val filter = IntentFilter(Configuration.CONFIGURATION_CHANGED)
context.registerReceiver(broadcastReceiver, filter)
}
fun stopObserving() {
try {
context.unregisterReceiver(broadcastReceiver)
} catch (e: IllegalArgumentException) {
// Receiver not registered
}
}
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Configuration.CONFIGURATION_CHANGED) {
val currentNightMode = isNightModeEnabled()
nightModeCallback?.invoke(currentNightMode)
}
}
}
private fun isNightModeEnabled(): Boolean {
val uiMode = context.resources.configuration.uiMode
return (uiMode and Configuration.UI_MODE_NIGHT_YES) == Configuration.UI_MODE_NIGHT_YES
}
}
// 使用示例
class MyActivity : AppCompatActivity() {
private lateinit var nightModeObserver: NightModeObserver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
nightModeObserver = NightModeObserver(this)
nightModeObserver.setOnNightModeChangedListener { isDarkMode ->
// 更新 UI
updateTheme(isDarkMode)
}
nightModeObserver.startObserving()
}
override fun onDestroy() {
super.onDestroy()
nightModeObserver.stopObserving()
}
}4. 手动切换主题
4.1 基础的手动切换实现
kotlin
class ThemeSwitcher(private val context: Context) {
companion object {
private const val PREF_NAME = "theme_prefs"
private const val KEY_THEME = "theme_mode"
}
enum class ThemeMode {
LIGHT, DARK, SYSTEM
}
private val prefs: SharedPreferences =
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
fun getCurrentThemeMode(): ThemeMode {
val modeString = prefs.getString(KEY_THEME, ThemeMode.SYSTEM.name)
return ThemeMode.valueOf(modeString ?: ThemeMode.SYSTEM.name)
}
fun setThemeMode(mode: ThemeMode) {
prefs.edit().putString(KEY_THEME, mode.name).apply()
applyTheme()
}
fun toggleTheme() {
val currentMode = getCurrentThemeMode()
val newMode = when (currentMode) {
ThemeMode.LIGHT -> ThemeMode.DARK
ThemeMode.DARK -> ThemeMode.LIGHT
ThemeMode.SYSTEM -> ThemeMode.LIGHT
}
setThemeMode(newMode)
}
private fun applyTheme() {
// 重新创建活动以应用新主题
recreateActivity()
}
private fun recreateActivity() {
val activity = context as? Activity
activity?.recreate()
}
}4.2 在 Settings 界面中切换主题
kotlin
class SettingsFragment : Fragment() {
private lateinit var themeSwitcher: ThemeSwitcher
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
themeSwitcher = ThemeSwitcher(requireContext())
setupThemeSelector()
}
private fun setupThemeSelector() {
val currentMode = themeSwitcher.getCurrentThemeMode()
// 设置单选按钮组
binding.themeRadioGroup.checkedRadioButtonId = when (currentMode) {
ThemeSwitcher.ThemeMode.LIGHT -> R.id.radioLight
ThemeSwitcher.ThemeMode.DARK -> R.id.radioDark
ThemeSwitcher.ThemeMode.SYSTEM -> R.id.radioSystem
}
binding.themeRadioGroup.setOnCheckedChangeListener { _, checkedId ->
val newMode = when (checkedId) {
R.id.radioLight -> ThemeSwitcher.ThemeMode.LIGHT
R.id.radioDark -> ThemeSwitcher.ThemeMode.DARK
R.id.radioSystem -> ThemeSwitcher.ThemeMode.SYSTEM
else -> ThemeSwitcher.ThemeMode.SYSTEM
}
themeSwitcher.setThemeMode(newMode)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}4.3 应用级别的统一主题管理
kotlin
class AppThemeManager {
companion object {
@Volatile
private var instance: AppThemeManager? = null
fun getInstance(context: Context): AppThemeManager {
return instance ?: synchronized(this) {
instance ?: AppThemeManager(context.applicationContext).also {
instance = it
}
}
}
}
private val context: Context
private val prefs: SharedPreferences
enum class Theme {
LIGHT, DARK, FOLLOW_SYSTEM
}
init {
this.context = context
this.prefs = context.getSharedPreferences(
context.getString(R.string.app_name),
Context.MODE_PRIVATE
)
}
fun getTheme(): Theme {
val themeString = prefs.getString(KEY_THEME, Theme.FOLLOW_SYSTEM.name)
return Theme.valueOf(themeString ?: Theme.FOLLOW_SYSTEM.name)
}
fun setTheme(theme: Theme) {
prefs.edit().putString(KEY_THEME, theme.name).apply()
updateNightMode(theme)
}
fun toggleTheme() {
val currentTheme = getTheme()
val newTheme = when (currentTheme) {
Theme.LIGHT -> Theme.DARK
Theme.DARK -> Theme.LIGHT
Theme.FOLLOW_SYSTEM -> Theme.LIGHT
}
setTheme(newTheme)
}
private fun updateNightMode(theme: Theme) {
val nightMode = when (theme) {
Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES
Theme.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
AppCompatDelegate.setDefaultNightMode(nightMode)
}
private companion object {
const val KEY_THEME = "theme_mode"
}
}
// 在 Application 中初始化
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 初始化主题管理器
val themeManager = AppThemeManager.getInstance(this)
val currentTheme = themeManager.getTheme()
updateNightModeForApp(currentTheme)
}
private fun updateNightModeForApp(theme: AppThemeManager.Theme) {
val nightMode = when (theme) {
AppThemeManager.Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
AppThemeManager.Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES
AppThemeManager.Theme.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
AppCompatDelegate.setDefaultNightMode(nightMode)
}
}4.4 在 BaseActivity 中统一处理主题
kotlin
abstract class BaseActivity : AppCompatActivity() {
protected val themeManager: AppThemeManager by lazy {
AppThemeManager.getInstance(applicationContext)
}
override fun attachBaseContext(newBase: Context) {
// 在 attachBaseContext 中应用主题(必须在 super.attachBaseContext 之前)
val theme = themeManager.getTheme()
val context = applyTheme(newBase, theme)
super.attachBaseContext(context)
}
private fun applyTheme(context: Context, theme: AppThemeManager.Theme): Context {
val themeResId = when (theme) {
AppThemeManager.Theme.LIGHT -> R.style.AppTheme_Light
AppThemeManager.Theme.DARK -> R.style.AppTheme_Dark
AppThemeManager.Theme.FOLLOW_SYSTEM -> R.style.AppTheme_System
}
val themeObj = createTheme()
themeObj.applyStyle(themeResId, true)
return ContextThemeWrapper(context, themeResId)
}
// 或者使用 AppCompatDelegate 的方式
override fun onCreate(savedInstanceState: Bundle?) {
// 在 super.onCreate() 之前设置夜间模式
val theme = themeManager.getTheme()
val nightMode = when (theme) {
AppThemeManager.Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
AppThemeManager.Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES
AppThemeManager.Theme.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
AppCompatDelegate.setDefaultNightMode(nightMode)
super.onCreate(savedInstanceState)
}
}4.5 平滑主题切换动画
kotlin
class SmoothThemeSwitcher(private val activity: Activity) {
fun switchTheme(newTheme: AppThemeManager.Theme) {
// 1. 保存当前状态
val currentState = saveState()
// 2. 播放过渡动画
activity.window.setStatusBarColor(Color.TRANSPARENT)
activity.window.navigationBarColor = Color.TRANSPARENT
// 3. 淡出动画
val fadeOut = AlphaAnimation(1f, 0f).apply {
duration = 200
fillAfter = true
}
activity.window.decorView.startAnimation(fadeOut)
// 4. 切换主题
themeManager.setTheme(newTheme)
// 5. 淡入动画
activity.window.decorView.postDelayed({
val fadeIn = AlphaAnimation(0f, 1f).apply {
duration = 200
fillAfter = true
}
activity.window.decorView.startAnimation(fadeIn)
// 6. 恢复状态栏颜色
fadeIn.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation?) {}
override fun onAnimationEnd(animation: Animation?) {
activity.window.setStatusBarColor(getStatusBarColor())
activity.window.navigationBarColor = getNavigationBarColor()
}
override fun onAnimationRepeat(animation: Animation?) {}
})
// 7. 重建 Activity
activity.recreate()
}, 200)
}
private fun saveState(): Bundle {
return Bundle().apply {
// 保存需要保留的状态
putString("currentTab", getCurrentTab())
putInt("scrollPosition", getScrollPosition())
}
}
private fun restoreState(bundle: Bundle) {
// 恢复状态
}
}4.6 主题切换的最佳实践
kotlin
// 最佳实践 1:在 Application 中统一设置
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 在最早的生命周期设置主题
val theme = ThemePrefs.getTheme(this)
AppCompatDelegate.setDefaultNightMode(getNightModeFromTheme(theme))
}
}
// 最佳实践 2:使用 DayNight 主题父类
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- 自动根据夜间模式切换 -->
</style>
// 最佳实践 3:提供主题预览
class ThemePreviewFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 显示当前主题预览
showThemePreview()
// 允许用户预览不同主题
binding.themeLightPreview.setOnClickListener {
previewTheme(Theme.LIGHT)
}
binding.themeDarkPreview.setOnClickListener {
previewTheme(Theme.DARK)
}
}
private fun previewTheme(theme: Theme) {
// 临时应用主题进行预览
val dialog = ThemePreviewDialog(this, theme)
dialog.show()
}
}
// 最佳实践 4:记住用户的选择
class ThemePrefs {
companion object {
private const val PREF_NAME = "app_prefs"
private const val KEY_THEME = "theme_mode"
}
fun saveTheme(context: Context, theme: AppThemeManager.Theme) {
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.edit()
.putString(KEY_THEME, theme.name)
.apply()
}
fun getTheme(context: Context): AppThemeManager.Theme {
val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
val themeString = prefs.getString(KEY_THEME, AppThemeManager.Theme.FOLLOW_SYSTEM.name)
return AppThemeManager.Theme.valueOf(themeString)
}
}5. followSystem 模式详解
5.1 followSystem 模式的工作原理
kotlin
// followSystem 模式的实现原理
class AppCompatDelegateImplV19 : AppCompatDelegateImplV14() {
override fun applyDayNight(): Boolean {
val nightMode = getDefaultNightMode()
return when (nightMode) {
MODE_NIGHT_NO -> false
MODE_NIGHT_YES -> true
MODE_NIGHT_FOLLOW_SYSTEM -> {
// 跟随系统设置
getNightModeFromSystem()
}
MODE_NIGHT_AUTO_BATTERY -> {
// 根据电池优化模式决定
getNightModeFromBatteryOpt()
}
else -> false
}
}
private fun getNightModeFromSystem(): Boolean {
// 检查系统 UI 模式
val config = resources.configuration
val uiMode = config.uiMode
// UI_MODE_NIGHT_MASK 提取夜间模式位
return (uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES
}
}5.2 系统夜间模式的检测
kotlin
// 多种方式检测系统夜间模式
class SystemNightModeDetector {
fun isNightMode(context: Context): Boolean {
// 方法 1: 通过 Configuration 检测
return isNightModeViaConfiguration(context)
}
private fun isNightModeViaConfiguration(context: Context): Boolean {
val config = context.resources.configuration
val uiMode = config.uiMode
return (uiMode and Configuration.UI_MODE_NIGHT_YES) == Configuration.UI_MODE_NIGHT_YES
}
// API 29+ 可以使用更精确的方法
private fun isNightModeViaUiModeManager(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val uiModeManager = context.getSystemService(UI_MODE_SERVICE) as UiModeManager
return uiModeManager.nightMode == UiModeManager.MODE_NIGHT_YES
}
return isNightModeViaConfiguration(context)
}
// 监听系统夜间模式变化
fun observeNightModeChanges(context: Context, callback: (Boolean) -> Unit) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Configuration.CONFIGURATION_CHANGED) {
val isNight = isNightMode(context)
callback(isNight)
}
}
}
val filter = IntentFilter(Configuration.CONFIGURATION_CHANGED)
context.registerReceiver(receiver, filter)
}
}5.3 处理 followSystem 模式的边界情况
kotlin
class FollowSystemThemeManager {
// 边界情况 1: 系统快速切换主题
private var isUpdatingTheme = false
fun handleThemeChange(context: Context) {
if (isUpdatingTheme) return // 防止重复更新
isUpdatingTheme = true
try {
val isSystemNightMode = SystemNightModeDetector().isNightMode(context)
applyTheme(context, isSystemNightMode)
} finally {
isUpdatingTheme = false
}
}
// 边界情况 2: 24 小时制和 12 小时制的自动切换
fun handleAutomaticTimeBasedSwitch() {
// 有些系统会根据时间自动切换夜间模式
// 我们需要确保我们的应用正确响应这种变化
}
// 边界情况 3: 多显示器设置
fun handleMultipleDisplaySettings() {
// 某些设备可能有多个显示器,每个显示器可能有不同的主题设置
}
// 边界情况 4: 省电模式影响
fun handleBatterySaverMode(context: Context) {
val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager
if (powerManager.isPowerSaveMode) {
// 省电模式下,系统可能强制使用深色模式
// 我们需要决定是跟随系统还是保持用户设置
}
}
}5.4 followSystem 与用户设置的冲突处理
kotlin
class ThemeConflictResolver {
enum class ThemePriority {
USER_OVERRIDE, // 用户明确设置(最高优先级)
FOLLOW_SYSTEM, // 跟随系统
DEFAULT_LIGHT // 默认浅色
}
private var userThemeOverride: AppThemeManager.Theme? = null
fun resolveTheme(context: Context): AppThemeManager.Theme {
// 1. 检查用户是否有明确的主题设置
val savedTheme = ThemePrefs.getTheme(context)
if (savedTheme != AppThemeManager.Theme.FOLLOW_SYSTEM) {
return savedTheme // 用户明确设置,优先使用
}
// 2. 跟随系统设置
val isSystemNight = SystemNightModeDetector().isNightMode(context)
return if (isSystemNight) {
AppThemeManager.Theme.DARK
} else {
AppThemeManager.Theme.LIGHT
}
}
fun setUserThemeOverride(theme: AppThemeManager.Theme) {
userThemeOverride = theme
ThemePrefs.saveTheme(context, theme)
}
fun clearUserThemeOverride() {
userThemeOverride = null
ThemePrefs.saveTheme(context, AppThemeManager.Theme.FOLLOW_SYSTEM)
}
}5.5 在 followSystem 模式下提供用户覆盖
kotlin
class ThemeWithOverrideManager {
companion object {
const val PREF_KEY = "theme_override"
}
private val prefs: SharedPreferences
init {
this.prefs = context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE)
}
fun getCurrentTheme(): AppThemeManager.Theme {
// 检查是否有用户覆盖
val override = prefs.getString(OVERRIDE_KEY, null)
if (override != null) {
return AppThemeManager.Theme.valueOf(override)
}
// 没有覆盖,跟随系统
return AppThemeManager.Theme.FOLLOW_SYSTEM
}
fun setOverride(theme: AppThemeManager.Theme) {
prefs.edit().putString(OVERRIDE_KEY, theme.name).apply()
applyTheme(theme)
}
fun clearOverride() {
prefs.edit().remove(OVERRIDE_KEY).apply()
applyTheme(AppThemeManager.Theme.FOLLOW_SYSTEM)
}
private fun applyTheme(theme: AppThemeManager.Theme) {
val nightMode = when (theme) {
AppThemeManager.Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
AppThemeManager.Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
AppCompatDelegate.setDefaultNightMode(nightMode)
}
}
// UI 实现
class ThemeSettingsDialogFragment : DialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ThemeSettingsBinding.inflate(inflater, container, false).root.also { root ->
val binding = ThemeSettingsBinding.bind(root)
setupThemeOptions(binding)
setupOverrideOptions(binding)
}
}
private fun setupThemeOptions(binding: ThemeSettingsBinding) {
binding.themeRadioSystem.setOnCheckedChangeListener { _, checked ->
if (checked) {
themeManager.clearOverride()
}
}
binding.themeRadioLight.setOnCheckedChangeListener { _, checked ->
if (checked) {
themeManager.setOverride(AppThemeManager.Theme.LIGHT)
}
}
binding.themeRadioDark.setOnCheckedChangeListener { _, checked ->
if (checked) {
themeManager.setOverride(AppThemeManager.Theme.DARK)
}
}
}
private fun setupOverrideOptions(binding: ThemeSettingsBinding) {
// 显示当前是跟随系统还是用户覆盖
val currentTheme = themeManager.getCurrentTheme()
when (currentTheme) {
AppThemeManager.Theme.LIGHT -> {
binding.themeRadioLight.isChecked = true
binding.overrideHint.text = "已强制使用浅色主题"
}
AppThemeManager.Theme.DARK -> {
binding.themeRadioDark.isChecked = true
binding.overrideHint.text = "已强制使用深色主题"
}
AppThemeManager.Theme.FOLLOW_SYSTEM -> {
binding.themeRadioSystem.isChecked = true
binding.overrideHint.text = "跟随系统主题设置"
}
}
}
}5.6 测试 followSystem 模式
kotlin
// Unit Test
class FollowSystemModeTest {
@Test
fun testFollowSystemLightMode() {
// 模拟系统浅色模式
val mockConfig = Configuration().apply {
uiMode = Configuration.UI_MODE_NIGHT_NO
}
val detector = SystemNightModeDetector(mockConfig)
assertFalse(detector.isNightMode())
}
@Test
fun testFollowSystemDarkMode() {
// 模拟系统深色模式
val mockConfig = Configuration().apply {
uiMode = Configuration.UI_MODE_NIGHT_YES
}
val detector = SystemNightModeDetector(mockConfig)
assertTrue(detector.isNightMode())
}
@Test
fun testUserOverrideTakesPrecedence() {
// 测试用户覆盖优先级
val resolver = ThemeConflictResolver()
resolver.setUserThemeOverride(AppThemeManager.Theme.DARK)
val theme = resolver.resolveTheme(mockContext)
assertEquals(AppThemeManager.Theme.DARK, theme)
}
}
// UI Test
@get:Rule
val activityTestRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testThemeChangeOnSystemNightModeToggle() {
// 1. 初始状态 - 浅色模式
activityTestRule.scenario.onActivity { activity ->
val rootView = activity.findViewById<View>(android.R.id.content)
val backgroundColor = ThemeUtils.getColor(activity, R.attr.colorSurface)
assertEquals(Color.WHITE, backgroundColor)
}
// 2. 模拟系统切换到深色模式
activityTestRule.scenario.onActivity { activity ->
val config = activity.resources.configuration.apply {
uiMode = uiMode or Configuration.UI_MODE_NIGHT_YES
}
activity.resources.updateConfiguration(config, activity.resources.displayMetrics)
}
// 3. 验证主题已更新
activityTestRule.scenario.onActivity { activity ->
val rootView = activity.findViewById<View>(android.R.id.content)
val backgroundColor = ThemeUtils.getColor(activity, R.attr.colorSurface)
assertEquals(Color.parseColor("#121212"), backgroundColor)
}
}6. 源码深度分析
6.1 AppCompatDelegate 创建流程
kotlin
// AppCompatDelegate 的创建流程
public static AppCompatDelegate create(
Context context,
WindowCallback callback,
Bundle icicle) {
// 1. 根据上下文类型创建不同的实现
if (context == null) {
// 没有 Context,使用基础实现
return new AppCompatDelegateImplV14();
}
// 2. 检测 Activity 类型
if (context instanceof Activity) {
return createActivityDelegate((Activity) context);
}
// 3. 检测 Application 类型
if (context instanceof Application) {
return new AppCompatDelegateImplV14();
}
// 4. 默认实现
return new AppCompatDelegateImplV14();
}
// Activity Delegate 创建
private static AppCompatDelegate createActivityDelegate(Activity activity) {
// 根据 API 级别创建不同的实现
if (Build.VERSION.SDK_INT >= 29) {
return new AppCompatDelegateImplV29(activity);
} else if (Build.VERSION.SDK_INT >= 21) {
return new AppCompatDelegateImplV21(activity);
} else {
return new AppCompatDelegateImplV14(activity);
}
}6.2 主题应用流程
kotlin
// 主题应用的完整流程
class AppCompatDelegateImplV14 extends AppCompatDelegateImplV7 {
@Override
public void installViewFactory() {
// 1. 安装 AppCompat 的 View 工厂
ViewCompat.setFactory(activity, new AppCompatViewInflater());
}
@Override
public void installMenuFactory() {
// 2. 安装 AppCompat 的 Menu 工厂
MenuBuilder.setFactory(new AppCompatMenuInflater());
}
@Override
public void onCreate(Bundle icicle) {
// 3. 应用主题
applyDayNight();
// 4. 创建 ActionBar
installActionBar();
// 5. 初始化夜间模式
initializeNightMode();
}
private void applyDayNight() {
// 获取当前的夜间模式设置
int nightMode = getDefaultNightMode();
// 应用相应的主题
if (shouldUseNightTheme(nightMode)) {
applyNightTheme();
} else {
applyDayTheme();
}
}
private void applyNightTheme() {
// 应用夜间主题
Context themeContext = createNightContext(context);
setContext(themeContext);
}
}6.3 夜间模式 Manager 源码分析
kotlin
// NightModeManager 的完整实现
class NightModeManager {
private static final String PREF_NAME = "androidx.appcompat.theme.night_mode";
private static final String PREF_KEY = "night_mode";
private SharedPreferences prefs;
private int nightMode = MODE_NIGHT_FOLLOW_SYSTEM;
init {
prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
loadNightMode();
}
fun getDefaultNightMode(): Int {
return nightMode;
}
fun setDefaultNightMode(int mode) {
// 1. 更新内部状态
nightMode = mode;
// 2. 保存到 SharedPreferences
saveNightMode(mode);
// 3. 通知所有 Activity 重建
notifyModeChanged();
}
private fun loadNightMode() {
nightMode = prefs.getInt(PREF_KEY, MODE_NIGHT_FOLLOW_SYSTEM);
}
private fun saveNightMode(int mode) {
prefs.edit().putInt(PREF_KEY, mode).apply();
}
private fun notifyModeChanged() {
// 通知所有注册的监听器
for (callback in modeChangeCallbacks) {
callback.onNightModeChanged(nightMode);
}
}
interface NightModeChangedListener {
fun onNightModeChanged(mode: Int)
}
}6.4 ContextThemeWrapper 源码分析
kotlin
// ContextThemeWrapper 是如何工作的
public class ContextThemeWrapper extends ContextWrapper {
private static final int THEME_ATTR_PRIVATE = 0x01010000;
private final Resources.Theme mTheme;
private final int mThemeResId;
public ContextThemeWrapper(Context base, int themeResId) {
super(base);
mThemeResId = themeResId;
// 创建新的 Theme 对象
mTheme = base.getTheme();
mTheme.applyStyle(themeResId, true);
}
@Override
public Resources.Theme getTheme() {
// 返回包装后的 Theme
return mTheme;
}
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
// 重新应用主题
mTheme.applyStyle(mThemeResId, true);
}
}6.5 View 工厂机制源码
kotlin
// AppCompatViewInflater 源码分析
class AppCompatViewInflater implements LayoutInflater.Factory2 {
@Override
public View onCreateView(
String name,
Context context,
AttributeSet attrs) {
// 1. 检查是否是 AppCompat 组件
if (isAppCompatView(name)) {
// 返回 AppCompat 版本的 View
return createAppCompatView(name, context, attrs);
}
// 2. 检查是否是支持库组件
if (isSupportLibraryView(name)) {
return createSupportLibraryView(name, context, attrs);
}
// 3. 返回默认 View
return null;
}
@Override
public View onCreateView(
String name,
Context context,
AttributeSet attrs,
boolean isInheritContextTheme) {
// 处理继承主题的情况
if (isInheritContextTheme) {
context = createThemedContext(context, attrs);
}
return onCreateView(name, context, attrs);
}
private Context createThemedContext(Context context, AttributeSet attrs) {
// 从属性中提取主题
TypedValue themeValue = new TypedValue();
attrs.getValue(
androidx.appcompat.R.attr.theme,
themeValue,
true);
if (themeValue.resourceId != 0) {
return new ContextThemeWrapper(context, themeValue.resourceId);
}
return context;
}
private View createAppCompatView(
String name,
Context context,
AttributeSet attrs) {
// 根据 View 名称创建对应的 AppCompat 组件
return switch(name) {
case "Button" -> new AppCompatButton(context, attrs);
case "EditText" -> new AppCompatEditText(context, attrs);
case "TextView" -> new AppCompatTextView(context, attrs);
case "ImageView" -> new AppCompatImageView(context, attrs);
case "SeekBar" -> new AppCompatSeekBar(context, attrs);
case "Spinner" -> new AppCompatSpinner(context, attrs);
case "Toolbar" -> new Toolbar(context, attrs);
default -> null;
};
}
}6.6 夜间模式切换源码
kotlin
// 夜间模式切换的完整流程
class AppCompatDelegateImplV19 extends AppCompatDelegateImplV14 {
@Override
public void applyDayNight() {
// 1. 获取当前的夜间模式设置
int nightMode = getDefaultNightMode();
// 2. 决定是否使用夜间主题
boolean useNightTheme = shouldUseNightTheme(nightMode);
// 3. 应用主题
if (useNightTheme) {
applyNightTheme();
} else {
applyDayTheme();
}
// 4. 通知 UI 更新
notifyThemeChanged();
}
private boolean shouldUseNightTheme(int nightMode) {
return switch(nightMode) {
case MODE_NIGHT_NO -> false;
case MODE_NIGHT_YES -> true;
case MODE_NIGHT_AUTO_BATTERY -> isBatterySaverOn();
case MODE_NIGHT_FOLLOW_SYSTEM -> getNightModeFromSystem();
default -> false;
};
}
private boolean getNightModeFromSystem() {
Configuration config = getResources().getConfiguration();
return (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES;
}
private void applyNightTheme() {
// 应用夜间主题
Context themeContext = createNightContext(context);
setContext(themeContext);
// 重建所有 View
recreateViews();
}
private Context createNightContext(Context context) {
// 创建带夜间主题的 Context
return new ContextThemeWrapper(context, R.style.Theme_Night);
}
}7. 最佳实践与陷阱
7.1 最佳实践
实践 1: 在正确的生命周期设置主题
kotlin
// ✅ 正确:在 attachBaseContext 中设置
abstract class ThemedActivity : AppCompatActivity() {
override fun attachBaseContext(newBase: Context) {
val theme = getThemeForActivity()
val themedContext = ContextThemeWrapper(newBase, theme)
super.attachBaseContext(themedContext)
}
private fun getThemeForActivity(): Int {
return when {
isDarkModeEnabled() -> R.style.AppTheme_Dark
else -> R.style.AppTheme_Light
}
}
}
// ✅ 正确:在 super.onCreate() 之前设置
abstract class ThemedActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val nightMode = getNightModeForActivity()
AppCompatDelegate.setDefaultNightMode(nightMode)
super.onCreate(savedInstanceState)
}
private fun getNightModeForActivity(): Int {
return when (themeManager.getTheme()) {
Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES
Theme.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
}
}
// ❌ 错误:在 setContentView 之后设置
class WrongActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 太晚了!View 已经创建了
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
}实践 2: 使用 DayNight 主题
xml
<!-- ✅ 推荐:使用 DayNight 主题 -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorSurface">@color/surface</item>
</style>
<!-- ❌ 不推荐:使用固定主题 -->
<style name="AppTheme" parent="Theme.MaterialComponents.DarkActionBar">
<!-- 不会自动切换夜间模式 -->
</style>实践 3: 提供主题切换入口
kotlin
// 在 Settings 中提供主题切换
class SettingsActivity : AppCompatActivity() {
private lateinit var themeSwitcher: ThemeSwitcher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
themeSwitcher = ThemeSwitcher(this)
setupThemeSelector()
setupThemePreview()
}
private fun setupThemeSelector() {
// 提供三个选项:浅色、深色、跟随系统
binding.radioLight.setOnClickListener {
themeSwitcher.setTheme(Theme.LIGHT)
}
binding.radioDark.setOnClickListener {
themeSwitcher.setTheme(Theme.DARK)
}
binding.radioSystem.setOnClickListener {
themeSwitcher.setTheme(Theme.SYSTEM)
}
}
private fun setupThemePreview() {
// 提供主题预览功能
binding.previewButton.setOnClickListener {
showThemePreviewDialog()
}
}
}实践 4: 处理配置变更
kotlin
// 处理夜间模式配置变更
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 保存夜间模式状态
isNightMode = (resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_YES) ==
Configuration.UI_MODE_NIGHT_YES
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// 检测夜间模式是否变化
val newNightMode = (newConfig.uiMode and
Configuration.UI_MODE_NIGHT_YES) ==
Configuration.UI_MODE_NIGHT_YES
if (newNightMode != isNightMode) {
isNightMode = newNightMode
updateUIForNightMode()
}
}
private fun updateUIForNightMode() {
// 更新 UI 元素
// 重新加载图片
// 更新颜色等
}
}实践 5: 测试夜间模式
kotlin
// 使用 Android Studio 的 Preview 功能
@Preview(name = "Light", group = "DayNight")
@Preview(name = "Dark", group = "DayNight", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun MyComposablePreview() {
MyAppTheme {
MyComposable()
}
}
// 编写 Unit Test
@Test
fun testNightModeResources() {
val lightColor = ContextCompat.getColor(context, R.color.background)
val darkColor = ContextCompat.getColor(context, R.color.background)
// 验证浅色和深色模式颜色不同
assertNotEquals(lightColor, darkColor)
}
// 编写 UI Test
@Test
fun testThemeSwitch() {
activityTestRule.scenario.onActivity { activity ->
// 验证浅色模式
assertEquals(Color.WHITE, activity.window.decorView.background)
// 切换到深色模式
themeSwitcher.setTheme(Theme.DARK)
// 验证深色模式
assertEquals(Color.BLACK, activity.window.decorView.background)
}
}7.2 常见陷阱
陷阱 1: 主题设置时机错误
kotlin
// ❌ 陷阱:在错误的时机设置主题
class BadActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 太晚了!View 已经使用默认主题创建了
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
}
// ✅ 解决:在正确的时机设置主题
class GoodActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// 在 super.onCreate() 之前设置
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}陷阱 2: 忘记保存用户设置
kotlin
// ❌ 陷阱:不保存用户主题设置
class BadThemeSwitcher {
fun switchTheme(theme: Theme) {
AppCompatDelegate.setDefaultNightMode(getNightMode(theme))
// 没有保存设置,重启后会丢失
}
}
// ✅ 解决:保存用户设置
class GoodThemeSwitcher {
fun switchTheme(theme: Theme) {
// 保存设置
prefs.edit().putString(KEY_THEME, theme.name).apply()
// 应用主题
AppCompatDelegate.setDefaultNightMode(getNightMode(theme))
}
}陷阱 3: 忽略配置变更
kotlin
// ❌ 陷阱:不处理配置变更
class BadActivity : AppCompatActivity() {
// 没有声明 configChanges
// 系统会重启 Activity
}
// ✅ 解决:处理配置变更
class GoodActivity : AppCompatActivity() {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// 处理夜间模式变化
}
}陷阱 4: 硬编码颜色值
kotlin
// ❌ 陷阱:硬编码颜色值
class BadUI {
private fun setBackgroundColor(view: View) {
view.setBackgroundColor(Color.WHITE) // 硬编码
}
}
// ✅ 解决:使用主题属性
class GoodUI {
private fun setBackgroundColor(view: View) {
val color = ThemeUtils.getColor(context, R.attr.colorSurface)
view.setBackgroundColor(color)
}
}陷阱 5: 忘记更新图片资源
kotlin
// ❌ 陷阱:图片在深色模式下不协调
// 只有浅色模式的图片
res/drawable/ic_logo.xml
// ✅ 解决:提供深色模式专用图片
res/drawable/ic_logo.xml // 浅色模式
res/drawable-night/ic_logo.xml // 深色模式8. 面试考点大全
基础考点(初级工程师)
Q1: 什么是 AppCompatDelegate?它的主要作用是什么?
答案要点:
1. AppCompatDelegate 是 AndroidX AppCompat 库的核心类
2. 主要作用:
- 管理主题和样式
- 处理 ActionBar 的创建和生命周期
- 控制深色/浅色模式
- 提供兼容性 API
3. 使用场景:
- AppCompatActivity 通过它实现兼容功能
- 手动管理主题时可以独立使用Q2: 如何设置深色模式?有哪些模式可选?
答案要点:
设置方式:
AppCompatDelegate.setDefaultNightMode(mode)
可选模式:
1. MODE_NIGHT_NO - 始终浅色
2. MODE_NIGHT_YES - 始终深色
3. MODE_NIGHT_FOLLOW_SYSTEM - 跟随系统
4. MODE_NIGHT_AUTO_BATTERY - 根据电池优化
5. MODE_NIGHT_UNSPECIFIED - 未指定Q3: 如何实现手动主题切换?
答案要点:
步骤:
1. 保存用户选择(SharedPreferences)
2. 设置夜间模式
3. 重建 Activity(recreate())
代码示例:
AppCompatDelegate.setDefaultNightMode(mode)
prefs.edit().putString(KEY, mode).apply()
recreate()进阶考点(中级工程师)
Q4: followSystem 模式是如何工作的?
答案要点:
工作原理:
1. 检测系统 UI 模式(Configuration.UI_MODE_NIGHT_MASK)
2. 根据系统设置决定是否使用深色主题
3. 监听配置变更,自动响应系统主题变化
关键点:
- 通过 Configuration.uiMode 检测
- 系统主题变化会触发 CONFIGURATION_CHANGED
- 可以结合用户覆盖实现灵活的主题管理Q5: 主题属性查找的顺序是什么?
答案要点:
查找顺序:
1. 控件的 style 属性
2. Activity 的 theme 属性
3. Application 的 theme 属性
4. 系统默认主题
使用 ?attr.xxx 可以引用主题属性
使用 ?android:attr.xxx 可以引用框架属性Q6: 如何正确处理配置变更?
答案要点:
处理配置变更的方式:
1. 在 Manifest 中声明 configChanges
2. 重写 onConfigurationChanged 方法
3. 保存和恢复状态
示例:
android:configChanges="uiMode|orientation"
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// 处理夜间模式变化
}高级考点(高级工程师/架构师)
Q7: AppCompatDelegate 的源码实现原理是什么?
答案要点:
核心实现:
1. 工厂模式创建不同 API 版本的实现
2. 委托模式将功能委托给 Delegate
3. ContextThemeWrapper 包装 Context 实现主题切换
4. View 工厂机制提供 AppCompat View
源码流程:
create() -> AppCompatDelegateImplVxx
onCreate() -> installViewFactory() -> installMenuFactory() -> applyDayNight()Q8: 如何实现主题的动态切换而不重启 Activity?
答案要点:
方案 1: 使用 ThemeOverlay
- 针对特定视图应用覆盖主题
- 不影响整个 Activity
方案 2: 使用 ContextThemeWrapper
- 为特定 View 创建新的 Context
- 只更新需要变化的 View
方案 3: 使用动画过渡
- 在 recreate() 前后添加过渡动画
- 提供平滑的切换体验Q9: Material Design 3 的动态取色如何实现?
答案要点:
Android 12+ 支持动态取色:
1. 使用 DynamicColors 类获取动态颜色
2. 应用主题属性
3. 提供降级方案
代码示例:
val dynamicColors = DynamicColors.getDynamicColors(context)
dynamicColors.getPrimary()?.let {
theme.applyColor(R.attr.colorPrimary, it)
}Q10: 如何设计一个灵活的主题管理系统?
答案要点:
设计要点:
1. 主题枚举类型(LIGHT, DARK, SYSTEM)
2. 使用单例模式管理主题状态
3. 提供主题监听器接口
4. 支持主题预览功能
5. 提供用户覆盖机制
6. 正确处理配置变更
7. 提供回退方案
架构设计:
- ThemeManager:主题管理的单例
- ThemePrefs:主题设置的存储
- NightModeObserver:监听系统主题变化
- ThemeSwitcher:提供主题切换 API
- ThemePreview:提供主题预览功能9. 参考资料
官方文档
源码地址
推荐文章
本文完,感谢阅读!