第14章 - 主题定制
嗨,朋友!我是长安。
一个优秀的 App 需要有统一、美观的视觉风格。这一章,我们来学习如何在 Flutter 中进行主题定制,包括颜色、字体、组件样式,以及备受欢迎的深色模式。
🎨 ThemeData 基础
Flutter 使用 ThemeData 来定义应用的整体视觉风格。
基本设置
MaterialApp(
title: 'My App',
theme: ThemeData(
// 主色调
primarySwatch: Colors.blue,
// 或使用 colorScheme(推荐)
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
// 使用 Material 3
useMaterial3: true,
),
home: const HomePage(),
)
完整的 ThemeData 配置
ThemeData(
// 使用 Material 3
useMaterial3: true,
// 颜色方案
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
// 主色
primaryColor: Colors.blue,
// 脚手架背景色
scaffoldBackgroundColor: Colors.grey[100],
// AppBar 主题
appBarTheme: const AppBarTheme(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
),
// 卡片主题
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
// 按钮主题
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
// 输入框主题
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
// 文字主题
textTheme: const TextTheme(
headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
headlineMedium: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
titleLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.w600),
titleMedium: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
bodyLarge: TextStyle(fontSize: 16),
bodyMedium: TextStyle(fontSize: 14),
),
// 图标主题
iconTheme: const IconThemeData(
color: Colors.blue,
size: 24,
),
// 分割线主题
dividerTheme: const DividerThemeData(
thickness: 1,
space: 1,
color: Colors.grey,
),
)
🌓 深色模式
定义亮色和深色主题
class MyApp extends StatelessWidget {
const MyApp({super.key});
// 亮色主题
static final ThemeData lightTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
scaffoldBackgroundColor: Colors.grey[100],
appBarTheme: const AppBarTheme(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
);
// 深色主题
static final ThemeData darkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
scaffoldBackgroundColor: const Color(0xFF121212),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF1E1E1E),
foregroundColor: Colors.white,
),
);
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: ThemeMode.system, // 跟随系统
home: const HomePage(),
);
}
}
手动切换深色模式
使用 Provider 或简单的 ValueNotifier 来管理主题切换:
// 主题管理器
class ThemeManager extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.system;
ThemeMode get themeMode => _themeMode;
bool get isDarkMode {
if (_themeMode == ThemeMode.system) {
return WidgetsBinding.instance.platformDispatcher.platformBrightness ==
Brightness.dark;
}
return _themeMode == ThemeMode.dark;
}
void setThemeMode(ThemeMode mode) {
_themeMode = mode;
notifyListeners();
}
void toggleTheme() {
if (_themeMode == ThemeMode.light) {
_themeMode = ThemeMode.dark;
} else {
_themeMode = ThemeMode.light;
}
notifyListeners();
}
}
// 在 main.dart 中使用
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => ThemeManager(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
return MaterialApp(
title: 'My App',
theme: ThemeData.light(useMaterial3: true),
darkTheme: ThemeData.dark(useMaterial3: true),
themeMode: themeManager.themeMode,
home: const HomePage(),
);
}
}
// 在设置页面切换主题
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
Widget build(BuildContext context) {
final themeManager = context.watch<ThemeManager>();
return Scaffold(
appBar: AppBar(title: const Text('设置')),
body: ListView(
children: [
ListTile(
title: const Text('深色模式'),
trailing: Switch(
value: themeManager.isDarkMode,
onChanged: (_) => themeManager.toggleTheme(),
),
),
ListTile(
title: const Text('主题模式'),
subtitle: Text(_getThemeModeText(themeManager.themeMode)),
onTap: () => _showThemeModeDialog(context, themeManager),
),
],
),
);
}
String _getThemeModeText(ThemeMode mode) {
switch (mode) {
case ThemeMode.system:
return '跟随系统';
case ThemeMode.light:
return '浅色';
case ThemeMode.dark:
return '深色';
}
}
void _showThemeModeDialog(BuildContext context, ThemeManager themeManager) {
showDialog(
context: context,
builder: (context) => SimpleDialog(
title: const Text('选择主题模式'),
children: [
RadioListTile<ThemeMode>(
title: const Text('跟随系统'),
value: ThemeMode.system,
groupValue: themeManager.themeMode,
onChanged: (value) {
themeManager.setThemeMode(value!);
Navigator.pop(context);
},
),
RadioListTile<ThemeMode>(
title: const Text('浅色'),
value: ThemeMode.light,
groupValue: themeManager.themeMode,
onChanged: (value) {
themeManager.setThemeMode(value!);
Navigator.pop(context);
},
),
RadioListTile<ThemeMode>(
title: const Text('深色'),
value: ThemeMode.dark,
groupValue: themeManager.themeMode,
onChanged: (value) {
themeManager.setThemeMode(value!);
Navigator.pop(context);
},
),
],
),
);
}
}
🎯 使用主题颜色
获取当前主题
// 获取 ThemeData
final theme = Theme.of(context);
// 使用颜色
Container(
color: theme.colorScheme.primary,
child: Text(
'主色文本',
style: TextStyle(color: theme.colorScheme.onPrimary),
),
)
// 使用文字样式
Text(
'标题',
style: theme.textTheme.headlineMedium,
)
// 检查是否是深色模式
final isDark = theme.brightness == Brightness.dark;
ColorScheme 常用颜色
// 获取 colorScheme
final colors = Theme.of(context).colorScheme;
// 主要颜色
colors.primary // 主色
colors.onPrimary // 主色上的内容颜色
colors.primaryContainer // 主色容器
colors.onPrimaryContainer
// 次要颜色
colors.secondary // 次色
colors.onSecondary
colors.secondaryContainer
colors.onSecondaryContainer
// 表面颜色
colors.surface // 表面色(卡片、对话框背景)
colors.onSurface // 表面上的内容颜色
colors.surfaceVariant
// 背景颜色
colors.background // 背景色
colors.onBackground
// 错误颜色
colors.error // 错误色
colors.onError
colors.errorContainer
实际使用示例
class ThemedCard extends StatelessWidget {
final String title;
final String content;
const ThemedCard({
super.key,
required this.title,
required this.content,
});
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
return Card(
color: colors.surface,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleLarge?.copyWith(
color: colors.onSurface,
),
),
const SizedBox(height: 8),
Text(
content,
style: theme.textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant,
),
),
const SizedBox(height: 16),
Row(
children: [
TextButton(
onPressed: () {},
child: Text(
'取消',
style: TextStyle(color: colors.secondary),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
),
child: const Text('确定'),
),
],
),
],
),
),
);
}
}
🔤 自定义字体
在主题中设置字体
ThemeData(
fontFamily: 'Roboto', // 全局默认字体
textTheme: const TextTheme(
displayLarge: TextStyle(
fontFamily: 'CustomFont',
fontSize: 32,
fontWeight: FontWeight.bold,
),
headlineMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
letterSpacing: -0.5,
),
titleLarge: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
),
titleMedium: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
letterSpacing: 0.15,
),
bodyLarge: TextStyle(
fontSize: 16,
letterSpacing: 0.5,
),
bodyMedium: TextStyle(
fontSize: 14,
letterSpacing: 0.25,
),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 1.25,
),
),
)
🧩 自定义组件主题
按钮主题
ThemeData(
// ElevatedButton 主题
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
elevation: 2,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
// TextButton 主题
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
textStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
// OutlinedButton 主题
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
side: const BorderSide(color: Colors.blue, width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
// FloatingActionButton 主题
floatingActionButtonTheme: const FloatingActionButtonThemeData(
elevation: 4,
shape: CircleBorder(),
),
)
输入框主题
ThemeData(
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.grey[100],
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.blue, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.red),
),
labelStyle: const TextStyle(color: Colors.grey),
hintStyle: TextStyle(color: Colors.grey[400]),
),
)
卡片和对话框主题
ThemeData(
// 卡片主题
cardTheme: CardTheme(
elevation: 2,
shadowColor: Colors.black26,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(8),
),
// 对话框主题
dialogTheme: DialogTheme(
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
titleTextStyle: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
// BottomSheet 主题
bottomSheetTheme: const BottomSheetThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
elevation: 8,
),
// SnackBar 主题
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
)
🎨 创建主题配置文件
建议将主题配置独立成文件:
// lib/theme/app_theme.dart
import 'package:flutter/material.dart';
class AppTheme {
// 品牌色
static const Color primaryColor = Color(0xFF2196F3);
static const Color secondaryColor = Color(0xFF03A9F4);
static const Color accentColor = Color(0xFFFF5722);
// 亮色主题
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.light,
secondary: secondaryColor,
),
scaffoldBackgroundColor: const Color(0xFFF5F5F5),
appBarTheme: const AppBarTheme(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
),
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
elevation: 2,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.grey[100],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: primaryColor, width: 2),
),
),
textTheme: _textTheme,
);
}
// 深色主题
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.dark,
secondary: secondaryColor,
),
scaffoldBackgroundColor: const Color(0xFF121212),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF1E1E1E),
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
),
cardTheme: CardTheme(
elevation: 4,
color: const Color(0xFF1E1E1E),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
elevation: 2,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: const Color(0xFF2C2C2C),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: primaryColor, width: 2),
),
),
textTheme: _textTheme,
);
}
// 通用文字主题
static const TextTheme _textTheme = TextTheme(
headlineLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
headlineMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
titleLarge: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
),
titleMedium: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
bodyLarge: TextStyle(fontSize: 16),
bodyMedium: TextStyle(fontSize: 14),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
);
}
// 在 main.dart 中使用
import 'theme/app_theme.dart';
MaterialApp(
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
home: const HomePage(),
)
📝 小结
这一章我们学习了:
- ✅ ThemeData 的基本配置
- ✅ 深色模式的实现
- ✅ 使用 ColorScheme
- ✅ 自定义组件主题(按钮、输入框、卡片等)
- ✅ 创建独立的主题配置文件
主题设计最佳实践
- 统一使用主题颜色 - 不要硬编码颜色值
- 使用 ColorScheme - Material 3 推荐的方式
- 支持深色模式 - 现代 App 必备功能
- 独立配置文件 - 便于维护和修改
- 测试两种模式 - 确保在亮色和深色下都好看
💪 练习题
- 创建一个自定义主题,包含你喜欢的配色方案
- 实现一个主题切换功能(亮色/深色/跟随系统)
- 创建一个主题预览页面,展示各种组件的样式
🚀 下一步
学会了主题定制后,下一章我们来学习 状态管理进阶,掌握 Provider 和 GetX!
由 编程指南 提供
