Flutter+HarmonyOS跨端实战—第03篇:UI 设计规范与主题系统
·
打造统一的深色主题视觉体验
前言
做过几个项目后,我发现 UI 不统一是最让人头疼的问题。颜色到处硬编码,组件样式不一致,后期改起来要改几十个文件。
这次做 CleanMark AI,我花了一天时间建立设计规范和主题系统。虽然前期多花了时间,但后面开发效率提升了至少 30%。
这篇文章我会分享如何建立一套可维护的 UI 系统。
一、设计规范制定
1.1 颜色系统
CleanMark AI 采用深色主题,主色调是紫色。我参考了需求文档,整理出完整的颜色规范:
主色调:
- 主紫色:
#8B5CF6- 按钮、高亮、选中状态 - 深紫色:
#7C3AED- 按钮按下状态 - 浅紫色:
#A78BFA- 辅助元素
功能色:
- 成功色:
#22C55E- 成功提示、已完成状态 - 警告色:
#F59E0B- 积分、警告提示 - 危险色:
#EF4444- 错误提示、删除操作 - 信息色:
#60A5FA- 信息提示
背景色:
- 主背景:
#0F1115- 页面背景 - 次背景:
#0F172A- 备用背景 - 卡片背景:
#1A1D24- 卡片、弹窗背景 - 卡片背景2:
#1C1C1E- 备用卡片背景
文字色:
- 主文字:
#FFFFFF- 标题、重要文字 - 次文字:
#BAC2D2- 描述文字 - 辅助文字:
#9CA3AF- 提示文字 - 禁用文字:
#6B7280- 禁用状态
1.2 颜色常量定义
// lib/app/app_colors.dart
import 'package:flutter/material.dart';
/// 应用全局颜色常量
class AppColors {
AppColors._(); // 私有构造函数,防止实例化
// 背景色
static const Color bgDark = Color(0xFF0F1115);
static const Color bgDark2 = Color(0xFF0F172A);
static const Color cardDark = Color(0xFF1A1D24);
static const Color cardDark2 = Color(0xFF1C1C1E);
// 主色调
static const Color purple = Color(0xFF8B5CF6);
static const Color purpleLight = Color(0xFFA78BFA);
static const Color purpleDark = Color(0xFF7C3AED);
// 功能色
static const Color gold = Color(0xFFF59E0B);
static const Color orange = Color(0xFFEC5B13);
static const Color green = Color(0xFF22C55E);
static const Color red = Color(0xFFEF4444);
static const Color blue = Color(0xFF60A5FA);
// 灰度色
static const Color gray200 = Color(0xFFBAC2D2);
static const Color gray400 = Color(0xFF9CA3AF);
static const Color gray500 = Color(0xFF6B7280);
static const Color gray600 = Color(0xFF4B5563);
static const Color gray700 = Color(0xFF374151);
static const Color gray800 = Color(0xFF1F2937);
// 基础色
static const Color white = Colors.white;
static const Color black = Colors.black;
}
为什么要定义颜色常量?
- 统一管理:所有颜色在一个文件中,修改方便
- 语义化:
AppColors.purple比Color(0xFF8B5CF6)更易读 - 避免硬编码:不会出现同一个颜色写了多个不同的值
- IDE 支持:可以自动补全,减少拼写错误
1.3 间距与圆角规范
间距系统(8px 基准):
// lib/core/constants/spacing.dart
class Spacing {
Spacing._();
static const double xs = 4.0; // 超小间距
static const double sm = 8.0; // 小间距
static const double md = 16.0; // 中等间距(默认)
static const double lg = 24.0; // 大间距
static const double xl = 32.0; // 超大间距
static const double xxl = 48.0; // 特大间距
}
圆角规范:
// lib/core/constants/radius.dart
class AppRadius {
AppRadius._();
static const double sm = 8.0; // 小圆角
static const double md = 12.0; // 中等圆角
static const double lg = 16.0; // 大圆角(按钮)
static const double xl = 20.0; // 超大圆角(卡片)
static const double full = 999.0; // 完全圆角(头像)
}
使用示例:
Container(
padding: const EdgeInsets.all(Spacing.md),
margin: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.sm,
),
decoration: BoxDecoration(
color: AppColors.cardDark,
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Text('内容'),
)
二、主题系统实现
2.1 ThemeData 配置
Flutter 提供了 ThemeData 来统一管理主题,我们基于此构建自己的主题系统:
// lib/app/theme.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'app_colors.dart';
/// 全局主题配置
class AppTheme {
AppTheme._();
static ThemeData get dark => ThemeData(
// 基础配置
brightness: Brightness.dark,
scaffoldBackgroundColor: AppColors.bgDark,
useMaterial3: true,
// 颜色方案
colorScheme: const ColorScheme.dark(
primary: AppColors.purple, // 主色
secondary: AppColors.gold, // 次要色
surface: AppColors.cardDark, // 表面色(卡片)
error: AppColors.red, // 错误色
),
// AppBar 主题
appBarTheme: const AppBarTheme(
backgroundColor: AppColors.bgDark,
elevation: 0, // 无阴影
centerTitle: true,
titleTextStyle: TextStyle(
color: AppColors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
iconTheme: IconThemeData(color: AppColors.white),
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
),
),
// 文字主题
textTheme: const TextTheme(
displayLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppColors.white,
),
displayMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppColors.white,
),
headlineMedium: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: AppColors.white,
),
titleLarge: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.white,
),
titleMedium: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.white,
),
bodyLarge: TextStyle(
fontSize: 16,
color: AppColors.gray200,
),
bodyMedium: TextStyle(
fontSize: 14,
color: AppColors.gray400,
),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.white,
),
),
// 输入框主题
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.cardDark,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
// 按钮主题
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.purple,
foregroundColor: AppColors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 0,
),
),
// 卡片主题
cardTheme: CardTheme(
color: AppColors.cardDark,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
);
}
2.2 应用主题
// lib/app/app.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'theme.dart';
class App extends StatelessWidget {
final GoRouter router;
const App({required this.router, super.key});
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'CleanMark AI',
theme: AppTheme.dark, // 应用主题
routerConfig: router,
debugShowCheckedModeBanner: false,
);
}
}
三、渐变效果实现
3.1 常用渐变定义
CleanMark AI 大量使用渐变效果,我把常用的渐变封装成常量:
// lib/core/constants/gradients.dart
import 'package:flutter/material.dart';
import '../../app/app_colors.dart';
class AppGradients {
AppGradients._();
/// 紫色渐变(主按钮)
static const LinearGradient purple = LinearGradient(
colors: [AppColors.purple, AppColors.purpleDark],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 橙色渐变(次要按钮)
static const LinearGradient orange = LinearGradient(
colors: [AppColors.orange, AppColors.orangeAlt],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// 深色渐变(背景)
static const LinearGradient darkBg = LinearGradient(
colors: [Color(0xFF1e0b4a), AppColors.black],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
);
/// 卡片渐变
static const LinearGradient card = LinearGradient(
colors: [AppColors.cardDark, AppColors.cardDark2],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
}
3.2 渐变按钮组件
// lib/shared/widgets/gradient_button.dart
import 'package:flutter/material.dart';
class GradientButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final Gradient gradient;
final double height;
final double borderRadius;
const GradientButton({
required this.text,
required this.onPressed,
this.gradient = AppGradients.purple,
this.height = 48,
this.borderRadius = 16,
super.key,
});
Widget build(BuildContext context) {
return Container(
height: height,
decoration: BoxDecoration(
gradient: onPressed != null ? gradient : null,
color: onPressed == null ? AppColors.gray700 : null,
borderRadius: BorderRadius.circular(borderRadius),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(borderRadius),
child: Center(
child: Text(
text,
style: const TextStyle(
color: AppColors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
);
}
}
使用示例:
GradientButton(
text: '立即开始',
gradient: AppGradients.purple,
onPressed: () {
context.push('/image-upload');
},
)
本篇小结(第一部分)
这部分我们完成了:
- ✅ 颜色系统设计与常量定义
- ✅ 间距与圆角规范
- ✅ ThemeData 主题配置
- ✅ 渐变效果封装
下一部分我会继续讲解常用组件的封装和响应式布局。
四、常用组件封装
4.1 状态徽章组件
历史记录页面需要显示任务状态(已完成、处理中、已取消),我封装了一个通用的状态徽章组件:
// lib/shared/widgets/status_badge.dart
import 'package:flutter/material.dart';
import '../../app/app_colors.dart';
class StatusBadge extends StatelessWidget {
final String text;
final Color color;
const StatusBadge({
required this.text,
required this.color,
super.key,
});
// 工厂构造函数:根据状态自动选择颜色
factory StatusBadge.fromStatus(String status) {
Color color;
String text;
switch (status) {
case 'completed':
case 'DONE':
color = AppColors.green;
text = '已完成';
break;
case 'processing':
case 'PROCESSING':
color = AppColors.gold;
text = '处理中';
break;
case 'cancelled':
case 'CANCELLED':
color = AppColors.gray400;
text = '已取消';
break;
default:
color = AppColors.gray400;
text = '未知';
}
return StatusBadge(text: text, color: color);
}
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Text(
text,
style: TextStyle(
color: color,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
);
}
}
使用示例:
// 方式1:直接指定颜色和文字
StatusBadge(text: '已完成', color: AppColors.green)
// 方式2:根据状态自动选择(推荐)
StatusBadge.fromStatus(task.status)
4.2 记录卡片组件
历史记录列表中的卡片组件,展示任务信息:
// lib/features/history/widgets/record_card.dart
class RecordCard extends StatelessWidget {
final TaskModel task;
final String time;
final String statusText;
final VoidCallback? onTap;
const RecordCard({
required this.task,
required this.time,
required this.statusText,
this.onTap,
super.key,
});
Color get _statusColor {
switch (task.status) {
case 'completed':
case 'DONE':
return AppColors.green;
case 'processing':
case 'PROCESSING':
return AppColors.gold;
default:
return AppColors.gray400;
}
}
String get _fileName {
if (task.inputFile != null) {
return task.inputFile!.split('/').last;
}
return '未知文件';
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.slate800.withOpacity(0.4),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.slate700.withOpacity(0.3),
),
),
child: Column(
children: [
// 顶部:图标 + 标题 + 状态
Row(
children: [
// 图标
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: AppColors.purple.withOpacity(0.1),
),
child: const Icon(
Icons.image,
color: AppColors.purple,
size: 20,
),
),
const SizedBox(width: 12),
// 标题和时间
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'图片去水印',
style: TextStyle(
color: Color(0xFFF1F5F9),
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
time,
style: const TextStyle(
color: AppColors.gray500,
fontSize: 11,
),
),
],
),
),
// 状态和积分
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
StatusBadge.fromStatus(task.status),
const SizedBox(height: 4),
const Text(
'-1 积分',
style: TextStyle(
color: AppColors.red,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
],
),
const SizedBox(height: 12),
// 底部:文件名
Container(
padding: const EdgeInsets.only(top: 12),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: AppColors.slate700.withOpacity(0.5),
),
),
),
child: Row(
children: [
Expanded(
child: Text(
_fileName,
style: const TextStyle(
color: AppColors.gray400,
fontSize: 12,
fontStyle: FontStyle.italic,
),
overflow: TextOverflow.ellipsis,
),
),
if (onTap != null)
const Icon(
Icons.chevron_right,
color: AppColors.gray500,
size: 16,
),
],
),
),
],
),
),
);
}
}
设计要点:
- 视觉层次:通过颜色、字号、字重区分主次信息
- 状态反馈:点击时有视觉反馈(GestureDetector)
- 信息密度:在有限空间内展示关键信息
- 可扩展性:通过参数控制显示内容
五、响应式布局
5.1 屏幕尺寸适配
不同设备的屏幕尺寸差异很大,需要做响应式适配:
// lib/core/utils/screen_utils.dart
import 'package:flutter/material.dart';
class ScreenUtils {
ScreenUtils._();
/// 获取屏幕宽度
static double width(BuildContext context) {
return MediaQuery.of(context).size.width;
}
/// 获取屏幕高度
static double height(BuildContext context) {
return MediaQuery.of(context).size.height;
}
/// 是否为小屏设备(宽度 < 360)
static bool isSmallScreen(BuildContext context) {
return width(context) < 360;
}
/// 是否为平板设备(宽度 >= 600)
static bool isTablet(BuildContext context) {
return width(context) >= 600;
}
/// 根据屏幕宽度返回不同的值
static T responsive<T>(
BuildContext context, {
required T mobile,
T? tablet,
}) {
if (isTablet(context) && tablet != null) {
return tablet;
}
return mobile;
}
}
使用示例:
// 根据屏幕尺寸调整布局
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(
ScreenUtils.responsive(
context,
mobile: 16.0,
tablet: 24.0,
),
),
child: Column(
children: [
// 小屏设备显示单列,平板显示双列
if (ScreenUtils.isTablet(context))
Row(
children: [
Expanded(child: _buildCard1()),
const SizedBox(width: 16),
Expanded(child: _buildCard2()),
],
)
else
Column(
children: [
_buildCard1(),
const SizedBox(height: 16),
_buildCard2(),
],
),
],
),
);
}
5.2 安全区域适配
iOS 设备有刘海屏、底部横条,需要使用 SafeArea 避免内容被遮挡:
Scaffold(
body: SafeArea(
child: Column(
children: [
// 内容
],
),
),
)
注意事项:
- 全屏页面(如启动页)不需要 SafeArea
- 有 AppBar 的页面,AppBar 会自动处理顶部安全区域
- 底部导航栏会自动处理底部安全区域
六、实战案例:首页卡片设计
6.1 Hero Banner 卡片
首页顶部的大卡片,使用渐变背景和阴影效果:
// lib/features/home/widgets/hero_banner.dart
class HeroBanner extends StatelessWidget {
final VoidCallback onTap;
const HeroBanner({required this.onTap, super.key});
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFF8B5CF6),
Color(0xFF7C3AED),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppColors.purple.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 图标
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.auto_fix_high,
color: Colors.white,
size: 24,
),
),
const SizedBox(height: 16),
// 标题
const Text(
'AI 智能去水印',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
// 描述
Text(
'一键移除图片和视频中的水印',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
const SizedBox(height: 16),
// 按钮
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'立即体验 →',
style: TextStyle(
color: AppColors.purple,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
);
}
}
设计亮点:
- 渐变背景:紫色渐变营造科技感
- 阴影效果:增加层次感和立体感
- 半透明元素:图标背景使用半透明白色
- 视觉引导:箭头引导用户点击
本篇完整小结
这篇文章我们完成了:
- ✅ 颜色系统设计(主色、功能色、背景色)
- ✅ 间距与圆角规范(8px 基准)
- ✅ ThemeData 主题配置
- ✅ 渐变效果封装
- ✅ 常用组件封装(状态徽章、记录卡片)
- ✅ 响应式布局适配
- ✅ 实战案例:Hero Banner 设计
关键要点:
- 统一管理颜色和间距,避免硬编码
- 封装常用组件,提高复用性
- 使用 ThemeData 统一主题
- 做好响应式适配,支持不同屏幕尺寸
思考题
- 为什么要定义颜色常量而不是直接写 Color(0xFF…)?
- 什么时候应该封装成组件,什么时候直接写在页面里?
- 如何平衡设计美观和开发效率?
下一篇预告:第04篇 - HarmonyOS 平台差异与适配策略
更多推荐


所有评论(0)