Flutter 实战:random_color 随机颜色生成器的动画过渡、历史色板与鸿蒙适配解析
Flutter 实战:random_color 随机颜色生成器的动画过渡、历史色板与鸿蒙适配解析
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
随机颜色生成器是一个非常适合拆解 Flutter 动画、颜色和交互状态的小项目。它看起来只是“点一下换个颜色”,但源码里实际覆盖了 随机 RGB 生成、背景色动画过渡、HEX/RGB 文本展示、对比色计算、历史色板、手势触发 和 浮动按钮刷新 等多个知识点。
random_color 的实现集中在 lib/main.dart 中:页面启动后生成初始随机色,用户点击页面、上滑页面或点击刷新按钮时都会生成新颜色;背景通过 AnimatedBuilder 和 Color.lerp 做 500ms 过渡;最近生成的 10 个颜色会进入历史列表,用户可以点击历史色块回到某个颜色。
颜色类工具的体验重点不只在生成随机值,还在于过渡是否自然、文字是否可读、色值是否清晰、历史状态是否方便回看。

图示说明:上图展示 Flutter 页面在移动端的布局组织方式。random_color 的实际界面由全屏颜色背景、色值卡片、历史色板和刷新按钮组成。
一、项目定位与功能边界
1.1 应用定位
random_color 是一个随机颜色生成与展示工具,适合用于 UI 配色灵感、Flutter 动画学习、颜色格式展示和手势交互练习。它不依赖网络接口,也没有复杂三方插件,核心能力全部由 Dart 和 Flutter Widget 实现。
项目当前支持:
- 随机生成 RGB 颜色。
- 背景色在新旧颜色之间平滑过渡。
- 展示 HEX 色值和 RGB 色值。
- 根据背景亮度自动切换黑白前景色。
- 点击任意位置生成新颜色。
- 上滑生成新颜色。
- 点击刷新按钮生成新颜色。
- 保留最近 10 个历史颜色。
- 点击历史色块恢复对应颜色。
1.2 功能模块
| 功能模块 | 页面表现 | 源码实现 |
|---|---|---|
| 随机颜色 | 每次生成新的背景色 | math.Random() + Color.fromARGB |
| 动画过渡 | 背景色平滑变化 | AnimationController + Color.lerp |
| 色值展示 | HEX 与 RGB 文本 | _colorToHex()、_colorToRgb() |
| 对比色 | 深色背景白字,浅色背景黑字 | computeLuminance() |
| 手势触发 | 点击或上滑生成颜色 | GestureDetector |
| 历史色板 | 横向最近颜色列表 | ListView.builder |
| 刷新按钮 | 右下角生成颜色按钮 | FloatingActionButton |
1.3 技术栈
| 技术点 | 使用位置 | 价值 |
|---|---|---|
| Flutter | 页面、卡片、手势、动画 | 快速构建交互界面 |
| Dart | 随机数、字符串转换、扩展方法 | 处理颜色逻辑 |
| Material 3 | 应用主题和基础控件 | useMaterial3: true |
| StatefulWidget | 当前色、历史色和动画状态 | 管理交互变化 |
| AnimationController | 控制颜色过渡进度 | 让刷新更自然 |
二、工程结构与运行环境
2.1 工程结构
random_color 是标准 Flutter 工程,核心逻辑集中在 lib/main.dart。
| 文件或目录 | 作用 |
|---|---|
lib/main.dart |
应用入口、随机颜色、动画、历史色板和 UI |
pubspec.yaml |
Flutter SDK 与测试依赖声明 |
test/widget_test.dart |
Widget 测试入口 |
ohos/ |
鸿蒙平台工程目录 |
analysis_options.yaml |
Dart 静态分析规则 |
2.2 运行命令
flutter doctor
flutter pub get
flutter run
项目依赖很轻,主要使用 Flutter SDK 和 Dart 标准库能力。dart:math 用于生成随机数,Flutter 动画系统用于颜色过渡。
2.3 依赖声明
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
这种依赖结构对跨端验证很友好:核心逻辑在 Dart 层,鸿蒙侧主要关注动画、手势、颜色渲染、字体和透明度表现。
三、应用入口与主题配置
3.1 main 函数
Flutter 应用从 main() 进入:
import 'package:flutter/material.dart';
import 'dart:math' as math;
void main() {
runApp(const RandomColorApp());
}
dart:math 被命名为 math,后续通过 math.Random() 生成 RGB 随机值。
3.2 根组件
class RandomColorApp extends StatelessWidget {
const RandomColorApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Random Color',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink),
useMaterial3: true,
),
home: const RandomColorHomePage(title: 'Random Color'),
);
}
}
根组件负责配置应用标题、主题和首页。由于随机色、历史色和动画进度都在页面内部变化,所以根组件保持无状态即可。
3.3 主题色
colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink)
应用主题使用粉色作为种子色。页面启动后实际 AppBar 和按钮颜色会跟随当前随机色变化,因此主题色更多用于初始风格和 Material 组件默认状态。
四、StatefulWidget 与动画混入
4.1 首页组件
class RandomColorHomePage extends StatefulWidget {
const RandomColorHomePage({super.key, required this.title});
final String title;
State<RandomColorHomePage> createState() => _RandomColorHomePageState();
}
首页需要响应用户手势、颜色变化和动画播放,因此使用 StatefulWidget。
4.2 SingleTickerProviderStateMixin
class _RandomColorHomePageState extends State<RandomColorHomePage>
with SingleTickerProviderStateMixin {
// ...
}
SingleTickerProviderStateMixin 为 AnimationController 提供 vsync。这个页面只有一个动画控制器,用单 ticker 就足够。
4.3 核心状态字段
Color _currentColor = Colors.pink;
Color _previousColor = Colors.white;
final List<Color> _history = [];
late AnimationController _controller;
late Animation<double> _animation;
| 字段 | 类型 | 作用 |
|---|---|---|
_currentColor |
Color |
当前展示颜色 |
_previousColor |
Color |
动画起始颜色 |
_history |
List<Color> |
最近生成的颜色列表 |
_controller |
AnimationController |
控制动画时间 |
_animation |
Animation<double> |
输出 0 到 1 的插值进度 |
五、动画初始化与生命周期
5.1 initState
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
_generateColor();
}
页面创建后先初始化动画控制器,再创建曲线动画,最后生成初始随机色。
5.2 动画时长
duration: const Duration(milliseconds: 500)
500ms 的过渡不算突兀,也不会拖慢操作反馈,适合颜色变化这种轻量视觉动画。
5.3 dispose
void dispose() {
_controller.dispose();
super.dispose();
}
AnimationController 持有 ticker 资源,页面销毁时必须释放。这个生命周期处理对动画类页面非常关键。
六、随机颜色生成逻辑
6.1 生成 RGB 值
final random = math.Random();
_currentColor = Color.fromARGB(
255,
random.nextInt(256),
random.nextInt(256),
random.nextInt(256),
);
nextInt(256) 会生成 0 到 255 之间的整数,刚好对应 RGB 每个通道的取值范围。Alpha 固定为 255,表示完全不透明。
6.2 保存前一个颜色
_previousColor = _currentColor;
在生成新颜色前,先把当前颜色保存为 _previousColor。后续动画会从旧颜色插值到新颜色。
6.3 完整生成方法
void _generateColor() {
final random = math.Random();
setState(() {
_previousColor = _currentColor;
_currentColor = Color.fromARGB(
255,
random.nextInt(256),
random.nextInt(256),
random.nextInt(256),
);
_history.insert(0, _currentColor);
if (_history.length > 10) {
_history.removeLast();
}
});
_controller.reset();
_controller.forward();
}
这个方法同时完成颜色生成、历史写入、长度限制和动画播放。
七、历史色板设计
7.1 插入最新颜色
_history.insert(0, _currentColor);
新颜色插入到列表最前面,用户看到的第一个色块就是最新颜色。
7.2 限制历史数量
if (_history.length > 10) {
_history.removeLast();
}
历史列表最多保留 10 个颜色,避免页面横向列表无限增长。
7.3 历史色块点击
onTap: () {
setState(() {
_previousColor = _currentColor;
_currentColor = _history[index];
});
}
点击历史色块会把当前颜色切换到对应历史颜色。源码中这一步没有重新播放动画控制器,因此恢复历史颜色时更接近即时切换。
八、颜色格式化:HEX 与 RGB
8.1 RGB 文本
String _colorToRgb(Color color) {
return 'RGB(${color.r.toInt()}, ${color.g.toInt()}, ${color.b.toInt()})';
}
RGB 文本直接读取 Color 的 r、g、b 通道,并转换为整数展示。
8.2 Color 扩展方法
extension ColorExtension on Color {
String toHexString() {
return '${r.toInt().toRadixString(16).padLeft(2, '0')}'
'${g.toInt().toRadixString(16).padLeft(2, '0')}'
'${b.toInt().toRadixString(16).padLeft(2, '0')}';
}
}
扩展方法把 RGB 三个通道转成 16 进制字符串,每个通道补齐两位。
8.3 HEX 文本细节
String _colorToHex(Color color) {
return '#${color.toHexString().substring(2).toUpperCase()}';
}
这里有一个源码细节:toHexString() 已经返回 6 位 RGB 字符串,再执行 substring(2) 会截掉前两位。也就是说当前展示会更接近 #GGBB 形式,而不是完整 #RRGGBB。如果希望展示完整 HEX,可以直接使用 color.toHexString().toUpperCase()。
String formatFullHex(Color color) {
return '#${color.toHexString().toUpperCase()}';
}
文章按当前源码真实表现说明这个点,方便读者理解显示结果和颜色通道之间的关系。
九、对比色算法
9.1 computeLuminance
Color _getContrastColor(Color color) {
final luminance = color.computeLuminance();
return luminance > 0.5 ? Colors.black : Colors.white;
}
computeLuminance() 会计算颜色亮度。亮度高时使用黑色文字,亮度低时使用白色文字。
9.2 应用位置
对比色用于 AppBar、图标和提示文字。
appBar: AppBar(
title: Text(widget.title),
backgroundColor: _currentColor,
foregroundColor: _getContrastColor(_currentColor),
)
这样无论当前背景是浅色还是深色,标题和按钮都能保持可读。
9.3 对比色表
| 背景亮度 | 前景色 | 目的 |
|---|---|---|
| 大于 0.5 | 黑色 | 浅色背景提高对比 |
| 小于等于 0.5 | 白色 | 深色背景提高对比 |
随机颜色不可控,所以文字颜色不能写死。对比色算法是颜色生成器的可用性基础。
十、动画过渡实现
10.1 AnimatedBuilder
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
color: Color.lerp(
_previousColor,
_currentColor,
_animation.value,
),
),
child: SafeArea(child: Column()),
);
},
)
AnimatedBuilder 会在动画值变化时重新构建背景容器。
10.2 Color.lerp
Color.lerp(_previousColor, _currentColor, _animation.value)
Color.lerp 根据动画进度在旧颜色和新颜色之间插值。_animation.value 从 0 变化到 1,背景色也从旧颜色过渡到新颜色。
10.3 动画播放流程
生成新颜色
保存 previousColor
写入 currentColor
reset 动画控制器
forward 播放动画
AnimatedBuilder 重建背景
Color.lerp 输出过渡颜色
这个流程是 Flutter 中处理颜色过渡的经典写法。
十一、手势交互设计
11.1 点击任意位置
GestureDetector(
onTap: _generateColor,
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container();
},
),
)
整个页面都可以点击生成颜色,交互成本很低。
11.2 上滑生成颜色
onVerticalDragEnd: (details) {
if (details.primaryVelocity! < 0) {
_generateColor();
}
}
primaryVelocity < 0 表示向上滑动结束。这个手势让页面除了点击外,还有一种更接近浏览色卡的交互方式。
11.3 浮动按钮刷新
floatingActionButton: FloatingActionButton(
onPressed: _generateColor,
backgroundColor: _currentColor,
foregroundColor: _getContrastColor(_currentColor),
child: const Icon(Icons.refresh),
)
刷新按钮提供明确的操作入口,也和全屏点击形成互补。
十二、页面布局结构
12.1 Scaffold 骨架
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
backgroundColor: _currentColor,
foregroundColor: _getContrastColor(_currentColor),
),
body: GestureDetector(
onTap: _generateColor,
child: AnimatedBuilder(...),
),
floatingActionButton: FloatingActionButton(...),
);
页面由 AppBar、全屏可点击背景和右下角按钮组成。
12.2 SafeArea 与 Column
SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.colorize),
Text('Tap anywhere'),
Card(...),
Text('Swipe up for new color'),
SizedBox(child: ListView.builder(...)),
],
),
)
SafeArea 避免内容被状态栏、刘海屏或系统手势区域遮挡。
12.3 色值卡片
Card(
margin: const EdgeInsets.symmetric(horizontal: 32),
color: Colors.white.withValues(alpha: 0.9),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Text(_colorToHex(_currentColor)),
Text(_colorToRgb(_currentColor)),
],
),
),
)
色值卡片使用接近白色的半透明背景,让文字在大多数随机背景上都比较清楚。
十三、历史色板 UI
13.1 横向列表
SizedBox(
height: 50,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _history.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
setState(() {
_previousColor = _currentColor;
_currentColor = _history[index];
});
},
child: Container(),
);
},
),
)
历史色板使用横向列表,适合展示一组小色块。
13.2 色块样式
Container(
width: 50,
height: 50,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: _history[index],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.white, width: 2),
),
)
固定尺寸色块可以避免列表跳动,白色边框让色块在深色背景上更容易辨认。
13.3 历史列表价值
历史色板让随机生成不再是一次性结果。用户可以快速回看刚才出现过的颜色,也可以对比多个颜色之间的视觉差异。
十四、边界场景与真实限制
14.1 随机性
源码每次调用 _generateColor() 都创建新的 math.Random()。这适合轻量工具使用,但不能保证颜色不重复。随机颜色出现重复是正常现象。
14.2 历史数量
历史只保留 10 个颜色,超过后删除最后一个。这让 UI 保持紧凑,但也意味着更早的颜色不会长期保留。
14.3 HEX 展示
当前 _colorToHex() 会截取 toHexString() 的后四位。RGB 文本仍能完整展示三个通道,HEX 文本则需要按源码真实表现理解。
14.4 透明度 API
源码使用 withValues(alpha: 0.7) 和 withValues(alpha: 0.9) 控制透明度。跨端验证时需要观察目标 Flutter 版本和平台渲染效果。
十五、Widget 测试设计
15.1 基础渲染测试
import 'package:flutter_test/flutter_test.dart';
import '../lib/main.dart';
void main() {
testWidgets('random color renders home page', (tester) async {
await tester.pumpWidget(const RandomColorApp());
expect(find.text('Random Color'), findsWidgets);
expect(find.text('Tap anywhere'), findsOneWidget);
expect(find.text('Swipe up for new color'), findsOneWidget);
});
}
这个测试验证根组件、提示文本和首页基础结构。
15.2 点击生成测试
testWidgets('tap page keeps hex and rgb card visible', (tester) async {
await tester.pumpWidget(const RandomColorApp());
await tester.tap(find.text('Tap anywhere'));
await tester.pump();
expect(find.textContaining('RGB('), findsOneWidget);
});
由于颜色是随机的,测试不应断言具体色值,而应验证关键 UI 仍然存在。
15.3 刷新按钮测试
testWidgets('refresh button triggers animation flow', (tester) async {
await tester.pumpWidget(const RandomColorApp());
await tester.tap(find.byIcon(Icons.refresh));
await tester.pump(const Duration(milliseconds: 500));
expect(find.byIcon(Icons.colorize), findsOneWidget);
});
这个测试覆盖浮动按钮和动画推进。
15.4 测试命令
flutter test
保持测试里的根组件名称与源码一致,可以避免默认模板测试遗留造成编译失败。
十六、鸿蒙适配观察
16.1 适配优势
random_color 的核心逻辑全部在 Dart 和 Flutter Widget 层完成,没有复杂原生插件依赖,因此鸿蒙侧重点是渲染、动画和手势。
| 维度 | 当前项目情况 | 鸿蒙侧关注点 |
|---|---|---|
| 随机颜色 | Dart math.Random |
多端逻辑一致 |
| 动画过渡 | AnimationController |
帧率和颜色插值 |
| 手势 | GestureDetector |
点击和上滑识别 |
| 透明色 | withValues |
透明度渲染效果 |
| 历史色板 | 横向 ListView |
滚动和色块边界 |
16.2 构建命令参考
flutter clean
flutter pub get
flutter build hap
具体命令取决于所使用的鸿蒙 Flutter 适配环境。对这个项目而言,主要验证页面启动、颜色动画、手势触发和色值展示。
16.3 运行验证要点
- 应用可以正常启动到随机颜色页面。
- 点击页面能生成新颜色。
- 上滑手势能生成新颜色。
- 颜色过渡动画没有明显卡顿。
- AppBar 前景色在深浅背景下保持可读。
- 历史色板能横向展示并点击恢复颜色。
鸿蒙适配中,随机色逻辑通常不是风险点,颜色渲染、透明度、动画流畅度、手势识别和文字可读性更值得仔细验证。
十七、性能与可维护性
17.1 性能特征
项目计算量很小,性能主要取决于动画和重绘。
| 维度 | 当前表现 |
|---|---|
| 随机生成 | 常量级 |
| 历史列表 | 最多 10 个 |
| 动画时长 | 500ms |
| 页面结构 | 单屏展示 |
| 重绘方式 | 动画驱动背景重建 |
17.2 当前结构优点
- 状态字段少,含义明确。
- 颜色生成逻辑集中在
_generateColor()。 - 对比色逻辑单独封装,便于复用。
- 历史数量受控,页面不会无限增长。
- 动画控制器生命周期处理完整。
17.3 可演进方向
如果项目继续扩展,可以把颜色格式化能力独立出来。
String rgbToHex(Color color) {
final r = color.r.toInt().toRadixString(16).padLeft(2, '0');
final g = color.g.toInt().toRadixString(16).padLeft(2, '0');
final b = color.b.toInt().toRadixString(16).padLeft(2, '0');
return '#${(r + g + b).toUpperCase()}';
}
抽离后可以单独测试 HEX 输出、RGB 输出和对比色逻辑。
十八、常见问题与优化建议
18.1 为什么使用 SingleTickerProviderStateMixin
页面只有一个动画控制器,需要一个 ticker 驱动动画帧。SingleTickerProviderStateMixin 正好满足这个需求。
18.2 为什么要保存 _previousColor
动画需要知道起点和终点。_previousColor 是过渡起点,_currentColor 是过渡终点,两者配合 Color.lerp 才能生成平滑变化。
18.3 为什么要计算对比色
随机背景可能非常亮,也可能非常暗。固定使用白字或黑字都会遇到不可读情况,因此需要根据亮度自动选择前景色。
18.4 为什么历史色板只保留 10 个
历史数量太多会占用横向空间,也会让用户难以浏览。10 个颜色足够回看最近结果,同时保持 UI 简洁。
18.5 为什么点击历史色块没有播放同样的动画
源码点击历史色块只更新 _currentColor,没有调用动画控制器的 reset() 和 forward()。因此历史恢复更接近即时切换,这是当前实现的真实表现。
18.6 为什么适合做鸿蒙适配示例
它同时覆盖颜色渲染、动画、手势、透明度、历史列表和对比色,这些都属于 Flutter 视觉类应用在鸿蒙侧需要重点观察的能力。
总结
random_color 用一个单页 Flutter 应用完成了随机颜色生成器的核心闭环:通过 math.Random() 生成 RGB 颜色,通过 AnimationController 和 Color.lerp 做颜色过渡,通过 computeLuminance() 保证文字可读,通过历史色板保留最近颜色。
从工程角度看,这个项目的亮点是交互轻、结构清楚、动画逻辑完整。它不仅能展示颜色,还能让用户通过点击、上滑和刷新按钮快速生成新结果,并通过历史色块回看最近颜色。
从鸿蒙适配角度看,项目没有复杂原生依赖,重点在颜色渲染、动画流畅度、手势识别、透明度表现和字体可读性。把这些细节验证好,就能获得比较稳定的跨端视觉体验。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
更多推荐




所有评论(0)