主题系统是现代 App 的标配。用户可以根据自己的喜好选择深色或浅色主题,甚至自定义主题颜色。这篇文章来实现一个完整的主题系统,包括多主题定义、动态切换、样式管理和持久化存储。

为什么需要主题系统
在开发 Steam 资讯 App 的过程中,我们发现用户对界面外观的需求差异很大。有些用户喜欢在夜间使用深色主题来保护眼睛,有些用户则习惯在白天使用浅色主题。如果 App 只支持一种主题,就会让一部分用户感到不适。
一个好的主题系统应该具备以下特点:
- 集中管理 - 所有颜色配置集中在一个地方,便于维护和修改
- 易于扩展 - 添加新主题时只需要添加新的颜色配置
- 实时切换 - 用户切换主题时,整个 App 立即响应
- 自动适配 - 支持跟随系统主题自动切换
主题配置的设计
首先需要定义 App 支持的所有主题。每个主题包含一套完整的颜色配置,用于控制整个 App 的外观。
先定义主题颜色的接口:
tsx
interface ThemeColors {
background: string; // 页面背景色
surface: string; // 卡片和容器背景
surfaceLight: string; // 较浅的表面色
text: string; // 主要文字颜色
textSecondary: string; // 次要文字颜色
primary: string; // 主色调
accent: string; // 辅助色
border: string; // 边框颜色
success: string; // 成功状态
warning: string; // 警告状态
error: string; // 错误状态
}
这个接口定义了主题所需的所有颜色属性。每个属性都有明确的用途:
background- 页面最底层的背景色,通常是最深的颜色surface- 卡片、对话框等容器的背景色surfaceLight- 用于分割线、输入框等较浅的表面text- 主要内容文字,需要有足够的对比度textSecondary- 辅助信息、描述文字等primary- 主色调,用于按钮、链接等强调元素accent- 辅助色,用于补充主色调border- 边框和分割线的颜色success/warning/error- 状态指示颜色
接口设计 - 通过 TypeScript 接口来定义主题结构,可以在编译时就发现错误。如果某个主题缺少某个颜色属性,TypeScript 会立即报错。这样可以避免运行时的颜色缺失问题。
然后创建具体的主题配置:
tsx
const THEMES: Record<string, ThemeColors> = {
dark: {
background: '#171a21',
surface: '#1b2838',
surfaceLight: '#2a475e',
text: '#ffffff',
textSecondary: '#8f98a0',
primary: '#66c0f4',
accent: '#acdbf5',
border: '#2a475e',
success: '#a4d007',
warning: '#f57c00',
error: '#d32f2f',
},
light: {
background: '#f5f5f5',
surface: '#ffffff',
surfaceLight: '#eeeeee',
text: '#212121',
textSecondary: '#757575',
primary: '#1976d2',
accent: '#42a5f5',
border: '#e0e0e0',
success: '#388e3c',
warning: '#f57c00',
error: '#d32f2f',
},
};
这里定义了两个主题:深色和浅色。深色主题采用 Steam 的官方配色,浅色主题则采用更明亮的颜色。
配置管理 - 通过集中管理所有颜色,可以确保整个 App 的配色一致。如果要修改主题,只需要改这个配置对象就行。后续如果要添加新主题,比如高对比度主题,只需要在这里添加新的配置就可以。这种设计让主题系统非常灵活。
创建主题 Hook
为了方便在组件中使用主题颜色,创建一个自定义 Hook 来获取当前主题的颜色配置。这样做的好处是,所有组件都能通过统一的方式访问主题颜色,而不需要关心主题的具体实现细节。
tsx
export const useTheme = () => {
const {theme} = useApp();
const getTheme = () => {
if (theme === 'auto') {
const isDark = Appearance.getColorScheme() === 'dark';
return THEMES[isDark ? 'dark' : 'light'];
}
return THEMES[theme] || THEMES.dark;
};
return getTheme();
};
这个 Hook 的核心逻辑包括三个部分:
- 获取全局主题状态 - 从 AppContext 中获取用户选择的主题
- 自动模式处理 - 如果用户选择了"自动",则根据系统设置来决定使用深色还是浅色
- 兜底处理 - 如果主题不存在或无效,默认使用深色主题
Hook 的优势 - 通过自定义 Hook,可以把主题逻辑封装起来。组件只需要调用
useTheme()就能获得当前主题的颜色,不需要关心主题的具体实现。这样即使后续要修改主题逻辑,也只需要修改这个 Hook,不需要改动所有使用主题的组件。
使用这个 Hook 非常简单直观:
tsx
const colors = useTheme();
<View style={{backgroundColor: colors.background}}>
<Text style={{color: colors.text}}>Hello World</Text>
</View>
这样就能获得当前主题的颜色,并应用到组件上。当用户切换主题时,这些颜色会自动更新。
主题选择界面的实现
主题设置页面需要显示所有可用的主题,让用户选择。这个页面应该提供清晰的视觉反馈,让用户知道当前选择了哪个主题。
先定义主题选项的数据结构:
tsx
const themeOptions = [
{id: 'dark', label: '深色主题', icon: '🌙', description: '适合夜间使用'},
{id: 'light', label: '浅色主题', icon: '☀️', description: '适合白天使用'},
{id: 'auto', label: '自动', icon: '🔄', description: '根据系统设置自动切换'},
];
这个数组定义了所有可用的主题选项。每个选项包含:
id- 主题的唯一标识符,用于在状态中查找label- 主题的显示名称icon- 主题的图标,用 emoji 表示description- 主题的描述,帮助用户理解
数据驱动 - 通过数组来定义主题选项,如果要添加新的主题,只需要在数组中添加一项就行。这样的设计让代码更灵活,也更容易维护。
然后实现主题选择的 UI:
tsx
export const ThemeSettingsScreen = () => {
const {theme, setTheme} = useApp();
const colors = useTheme();
return (
<View style={[styles.container, {backgroundColor: colors.background}]}>
<Header title="主题设置" showBack />
<ScrollView style={styles.content}>
<View style={styles.themeList}>
{themeOptions.map(option => (
<TouchableOpacity
key={option.id}
style={[
styles.themeItem,
{backgroundColor: colors.surface, borderColor: colors.border},
theme === option.id && {borderColor: colors.primary, borderWidth: 2},
]}
onPress={() => setTheme(option.id)}
>
<Text style={styles.themeIcon}>{option.icon}</Text>
<View style={styles.themeInfo}>
<Text style={[styles.themeName, {color: colors.text}]}>
{option.label}
</Text>
<Text style={[styles.themeDesc, {color: colors.textSecondary}]}>
{option.description}
</Text>
</View>
{theme === option.id && (
<Text style={[styles.checkmark, {color: colors.primary}]}>✓</Text>
)}
</TouchableOpacity>
))}
</View>
</ScrollView>
<TabBar />
</View>
);
};
这个界面的关键实现细节:
- 动态背景色 - 页面背景色从
useTheme()获取,确保页面本身也遵循当前主题 - 条件样式 - 当前选中的主题会显示不同的边框颜色和宽度
- 视觉反馈 - 选中的主题项右侧显示对勾符号,让用户清楚地知道当前选择
用户体验 - 通过对勾符号和边框颜色的组合,用户可以一眼看出当前选择了哪个主题。这种视觉反馈很重要,能让用户更有信心地做出选择。
主题预览功能
为了让用户在选择主题前能看到效果,应该在设置页面中添加一个预览区域。这样用户可以直观地看到不同主题的外观。
tsx
<View style={styles.previewSection}>
<Text style={[styles.previewTitle, {color: colors.text}]}>主题预览</Text>
<View style={[styles.previewBox, {backgroundColor: colors.surface, borderColor: colors.border}]}>
<Text style={[styles.previewText, {color: colors.text}]}>主要文字</Text>
<Text style={[styles.previewText, {color: colors.textSecondary}]}>次要文字</Text>
<TouchableOpacity style={[styles.previewBtn, {backgroundColor: colors.primary}]}>
<Text style={{color: colors.background}}>按钮</Text>
</TouchableOpacity>
<View style={[styles.previewBorder, {borderTopColor: colors.border}]} />
</View>
</View>
预览区域展示了主题中最常用的几个颜色:
- 主要文字 - 使用
colors.text显示,这是最常见的文字颜色 - 次要文字 - 使用
colors.textSecondary显示,用于描述和辅助信息 - 按钮 - 使用
colors.primary作为背景色,这是最常见的交互元素 - 分割线 - 使用
colors.border作为边框颜色,用于视觉分隔
实时预览 - 预览区域会实时显示当前选中主题的效果。当用户切换主题时,预览区域会立即更新,让用户能够立即看到效果。这大大提高了用户体验。
主题切换的实时生效
当用户切换主题时,整个 App 的颜色应该立即改变。这需要在 AppContext 中实现响应式的主题切换。
在 AppContext 中,setTheme 函数的实现很简单:
tsx
const setTheme = (theme: string) =>
setState(prev => ({...prev, theme}));
但关键是,所有使用 useTheme() Hook 的组件都会自动重新渲染。这是因为 Hook 内部依赖了全局状态中的 theme 属性。当 theme 改变时,Hook 会返回新的颜色配置,组件会自动重新渲染。
响应式更新 - React 的响应式系统确保了当状态改变时,所有依赖这个状态的组件都会自动更新。这样用户切换主题后,整个 App 的颜色会立即改变,无需手动刷新。
主题设置的持久化存储
用户的主题选择应该被保存,这样下次打开 App 时还能使用之前选择的主题。可以使用 AsyncStorage 来实现持久化存储。
tsx
import AsyncStorage from '@react-native-async-storage/async-storage';
const saveTheme = async (theme: string) => {
try {
await AsyncStorage.setItem('app_theme', theme);
} catch (error) {
console.error('Error saving theme:', error);
}
};
const loadTheme = async () => {
try {
const saved = await AsyncStorage.getItem('app_theme');
return saved || 'dark';
} catch (error) {
console.error('Error loading theme:', error);
return 'dark';
}
};
这两个函数分别用于保存和加载主题设置:
saveTheme- 将主题设置保存到本地存储,使用setItem方法loadTheme- 从本地存储加载主题设置,如果没有保存过则返回默认值'dark'
错误处理 - 使用 try-catch 来捕获可能的错误。如果保存或加载失败,应该有合理的兜底方案,比如使用默认主题。这样即使本地存储出现问题,App 也能正常运行。
然后在 AppProvider 中集成这些函数:
tsx
export const AppProvider = ({children}: {children: ReactNode}) => {
const [state, setState] = useState<AppState>({
theme: 'dark',
// ... 其他初始状态
});
useEffect(() => {
// 应用启动时加载保存的主题设置
loadTheme().then(theme => {
setState(prev => ({...prev, theme}));
});
}, []);
useEffect(() => {
// 当主题改变时保存到本地
saveTheme(state.theme);
}, [state.theme]);
// ... 其他代码
};
这个实现的流程是:
- 应用启动 - 在 AppProvider 挂载时,调用
loadTheme()加载保存的主题设置 - 主题改变 - 当用户改变主题时,自动调用
saveTheme()保存新的主题设置 - 下次启动 - 下次打开 App 时,会自动加载之前保存的主题设置
持久化策略 - 通过
useEffect监听主题的变化,当主题改变时自动保存。这样用户的设置能在应用关闭后保留,提供了无缝的用户体验。
系统主题检测
为了提供更好的用户体验,可以在"自动"模式下根据系统设置来自动切换主题。这样当用户改变系统主题时,App 会自动跟随。
tsx
import {Appearance, useColorScheme} from 'react-native';
export const useTheme = () => {
const {theme} = useApp();
const systemTheme = useColorScheme();
const getTheme = () => {
if (theme === 'auto') {
return THEMES[systemTheme === 'dark' ? 'dark' : 'light'];
}
return THEMES[theme] || THEMES.dark;
};
return getTheme();
};
这个实现的关键点:
- 系统主题检测 - 使用
useColorScheme()Hook 获取系统当前的主题设置 - 自动切换 - 如果用户选择了"自动"模式,则使用系统主题
- 手动模式 - 如果用户手动选择了主题,则忽略系统设置
系统集成 - 通过
useColorScheme()Hook,可以获取系统当前的主题设置。这样 App 可以和系统主题保持一致,提供更好的整体体验。
在组件中应用主题
现在在任何组件中都可以使用 useTheme() Hook 来获取主题颜色。比如在 HomeScreen 中:
tsx
export const HomeScreen = () => {
const colors = useTheme();
return (
<View style={[styles.container, {backgroundColor: colors.background}]}>
<Header title="Steam 资讯" />
<ScrollView style={{backgroundColor: colors.background}}>
<View style={[styles.section, {backgroundColor: colors.surface}]}>
<Text style={[styles.title, {color: colors.text}]}>特惠推荐</Text>
<Text style={[styles.subtitle, {color: colors.textSecondary}]}>
今日特惠游戏
</Text>
</View>
</ScrollView>
</View>
);
};
这样做的好处是:
- 统一管理 - 所有的颜色都通过
useTheme()来获取 - 自动更新 - 当用户切换主题时,这些颜色会自动更新
- 代码简洁 - 不需要在每个组件中硬编码颜色值
代码组织 - 通过统一使用
useTheme()来获取颜色,代码变得更清晰。如果要修改主题颜色,只需要在THEMES配置中修改就行,不需要在代码中搜索和替换。
主题样式的完整定义
为了让主题系统完整运作,需要定义所有相关的样式。这些样式应该尽可能地使用主题颜色,而不是硬编码的颜色值。
tsx
const styles = StyleSheet.create({
container: {flex: 1},
content: {flex: 1},
themeList: {paddingVertical: 16, paddingHorizontal: 16},
themeItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 8,
marginBottom: 12,
borderWidth: 1,
},
themeIcon: {fontSize: 32, marginRight: 16},
themeInfo: {flex: 1},
themeName: {fontSize: 16, fontWeight: '600', marginBottom: 2},
themeDesc: {fontSize: 12},
checkmark: {fontSize: 20, fontWeight: 'bold'},
previewSection: {paddingHorizontal: 16, paddingVertical: 24},
previewTitle: {fontSize: 16, fontWeight: 'bold', marginBottom: 12},
previewBox: {
borderRadius: 8,
padding: 16,
borderWidth: 1,
},
previewText: {fontSize: 14, marginBottom: 8},
previewBtn: {paddingVertical: 8, paddingHorizontal: 16, borderRadius: 6, marginTop: 8},
previewBorder: {borderTopWidth: 1, marginTop: 12},
});
这些样式定义了主题设置页面的布局。关键点包括:
- 灵活的布局 - 使用 flexbox 实现响应式布局
- 可复用的样式 - 定义通用的样式类,可以在多个地方使用
- 视觉层级 - 通过不同的字体大小和颜色来区分不同的信息层级
样式管理 - 虽然这些样式中的颜色值是硬编码的,但在实际使用时,这些颜色会被
useTheme()返回的动态颜色覆盖。这样既保证了样式的清晰,又实现了主题的动态切换。
主题系统的扩展
当需要添加新的主题时,只需要在 THEMES 对象中添加新的配置就行。比如添加一个高对比度主题:
tsx
const THEMES: Record<string, ThemeColors> = {
// ... 现有主题
highContrast: {
background: '#000000',
surface: '#1a1a1a',
surfaceLight: '#333333',
text: '#ffffff',
textSecondary: '#cccccc',
primary: '#ffff00',
accent: '#00ff00',
border: '#666666',
success: '#00ff00',
warning: '#ffff00',
error: '#ff0000',
},
};
然后在主题选项中添加新的选项:
tsx
const themeOptions = [
// ... 现有选项
{id: 'highContrast', label: '高对比度', icon: '⚡', description: '高对比度主题'},
];
这样就完成了新主题的添加,无需修改任何其他代码。
可扩展性 - 这种设计让添加新主题变得非常简单。只需要定义新的颜色配置和选项,就能立即支持新主题。这正是好的架构设计的体现。
小结
主题系统展示了如何实现一个完整的主题管理方案:
- 集中配置 - 通过配置对象集中管理所有主题颜色
- 自定义 Hook - 创建
useTheme()Hook 简化组件中的主题使用 - 全局状态 - 主题设置存储在全局状态中,确保整个 App 都能访问
- 实时切换 - 用户切换主题时,整个 App 的颜色立即改变
- 持久化存储 - 用户的主题选择被保存,下次打开 App 时自动恢复
- 系统集成 - 支持自动模式,根据系统设置自动切换主题
- 易于扩展 - 添加新的主题只需要在配置对象中添加就行
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net