
个人主页:ujainu
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
文章目录
- 前言
-
- 一、为什么需要独立的皮肤系统?
- [二、架构设计:Provider vs 单例(GameTheme)](#二、架构设计:Provider vs 单例(GameTheme))
-
- [❌ 方案一:`GameTheme` 单例](#❌ 方案一:
GameTheme单例) - [✅ 方案二:`Provider` + `ChangeNotifier`](#✅ 方案二:
Provider+ChangeNotifier)
- [❌ 方案一:`GameTheme` 单例](#❌ 方案一:
- 三、核心数据模型:SkinData
- 四、状态管理器:SkinProvider
- 五、皮肤选择界面实现
-
- [1. 主界面布局](#1. 主界面布局)
- [2. 颜色选择器对话框](#2. 颜色选择器对话框)
- [六、主程序入口与 Provider 注册](#六、主程序入口与 Provider 注册)
- [七、未来扩展:贴图与 Shader 预留](#七、未来扩展:贴图与 Shader 预留)
- 八、完整可运行代码(独立皮肤选择界面)
- 结语
前言
在现代移动应用与游戏中,个性化体验已成为用户留存的关键要素。一个灵活、响应迅速且易于扩展的皮肤系统,不仅能提升视觉吸引力,还能为未来商业化(如付费主题)打下坚实基础。
本文将聚焦于 构建一个独立、完整、可直接集成 的皮肤选择界面(Skin Selection UI),实现对"球体"和"轨道"两类元素的颜色动态切换。我们将深入探讨:
- 颜色状态的集中管理与响应式更新
- 使用
Color.fromARGB实现高质量随机色生成 - UI 设置页与预览组件的实时联动
- 架构设计:为何选用
Provider而非单例? - 为未来贴图(Texture)与着色器(Shader)效果预留扩展点
✅ 环境说明 :本文假设 Flutter + OpenHarmony 开发环境已配置完毕(含
provider、shared_preferences等依赖),不重复环境搭建步骤。✅ 目标产出:一个可直接复制运行、结构清晰、注释完整的皮肤选择界面。
一、为什么需要独立的皮肤系统?
许多开发者在初期采用"硬编码颜色"或"全局变量"方式管理 UI 主题,但随着功能迭代,这种方式会带来:
- 状态不同步:修改颜色后,部分界面未刷新
- 耦合严重:业务逻辑与样式混杂,难以维护
- 无法持久化:重启应用后设置丢失
- 扩展困难:若未来要支持贴图、动态渐变、粒子特效,需大规模重构
因此,我们需要一个 解耦、可观察、可持久化、可扩展 的皮肤框架。而本文的核心,就是实现其前端交互层------皮肤选择界面。
二、架构设计:Provider vs 单例(GameTheme)
❌ 方案一:GameTheme 单例
dart
class GameTheme {
static final instance = GameTheme._();
Color ballColor = Colors.red;
void setBallColor(Color c) { ballColor = c; }
}
问题:
- 无通知机制,UI 无法自动刷新
- 无法与
StatefulWidget的setState解耦 - 测试困难,违反依赖注入原则
✅ 方案二:Provider + ChangeNotifier
Flutter 官方推荐的状态管理方案,天然支持:
- 响应式更新 :调用
notifyListeners()自动刷新所有监听者 - 作用域控制 :通过
ProviderScope限定状态范围 - 测试友好:可轻松 mock 或替换实现
结论 :选用
Provider,兼顾简洁性、可维护性与性能。
三、核心数据模型:SkinData
我们首先定义皮肤的数据结构。当前仅包含颜色,但结构已为未来扩展预留字段。
dart
import 'dart:math';
class SkinData {
final Color ballColor;
final Color trackColor;
const SkinData({
required this.ballColor,
required this.trackColor,
});
// 默认皮肤
factory SkinData.defaultSkin() => const SkinData(
ballColor: Color(0xFFFF6B6B), // 活力红
trackColor: Color(0xFF4E54C8), // 深邃蓝
);
// 随机生成皮肤(避免过暗/过亮)
factory SkinData.randomSkin(Random random) {
Color _generateVibrantColor() {
// R/G/B 均在 [55, 255] 区间,确保亮度适中
return Color.fromARGB(
255,
random.nextInt(200) + 55,
random.nextInt(200) + 55,
random.nextInt(200) + 55,
);
}
return SkinData(
ballColor: _generateVibrantColor(),
trackColor: _generateVibrantColor(),
);
}
// 序列化:用于 SharedPreferences 存储
Map<String, dynamic> toJson() => {
'ball': ballColor.value,
'track': trackColor.value,
};
// 反序列化
factory SkinData.fromJson(Map<String, dynamic> json) {
return SkinData(
ballColor: Color(json['ball'] as int),
trackColor: Color(json['track'] as int),
);
}
}
🔍 关键优化:
Color.fromARGB(255, r, g, b)是生成颜色的标准方式- 限制 RGB 最小值为 55,避免生成接近黑色的"无效色"
value属性返回int(ARGB 值),可直接存入本地存储
四、状态管理器:SkinProvider
这是整个皮肤系统的中枢,负责状态持有、变更通知与持久化。
dart
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SkinProvider with ChangeNotifier {
SkinData _skin = SkinData.defaultSkin();
SkinData get currentSkin => _skin;
// 从本地加载皮肤(首次启动时调用)
Future<void> loadFromPrefs() async {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString('user_skin');
if (jsonString != null) {
try {
final json = jsonDecode(jsonString) as Map<String, dynamic>;
_skin = SkinData.fromJson(json);
notifyListeners(); // 触发 UI 刷新
} catch (e) {
// 解析失败则保留默认皮肤
}
}
}
// 保存到本地
Future<void> _saveToPrefs() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('user_skin', jsonEncode(_skin.toJson()));
}
// 应用随机皮肤
void applyRandomSkin() {
_skin = SkinData.randomSkin(Random());
_saveToPrefs();
notifyListeners();
}
// 单独设置球体颜色
void setBallColor(Color color) {
_skin = SkinData(ballColor: color, trackColor: _skin.trackColor);
_saveToPrefs();
notifyListeners();
}
// 单独设置轨道颜色
void setTrackColor(Color color) {
_skin = SkinData(ballColor: _skin.ballColor, trackColor: color);
_saveToPrefs();
notifyListeners();
}
}
✅ 设计亮点:
- 所有 public 方法均触发
notifyListeners(),确保 UI 同步- 每次变更自动持久化,无需手动"保存"按钮
- 方法命名语义清晰,职责单一
五、皮肤选择界面实现
现在构建核心 UI ------ SkinSelectionPage。它包含:
- 球体/轨道颜色预览
- 点击弹出颜色选择器
- "随机皮肤"快捷按钮
1. 主界面布局
dart
class SkinSelectionPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0F0F1A),
appBar: AppBar(
title: const Text('皮肤设置', style: TextStyle(color: Colors.white)),
backgroundColor: const Color(0xFF0F0F1A),
foregroundColor: Colors.white,
elevation: 0,
),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Consumer<SkinProvider>(
builder: (context, skinProvider, _) {
final skin = skinProvider.currentSkin;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'自定义你的视觉风格',
style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 32),
// 球体颜色设置项
_buildColorItem(
label: '球体颜色',
color: skin.ballColor,
onTap: () => _showColorPicker(context, isBall: true),
),
const SizedBox(height: 24),
// 轨道颜色设置项
_buildColorItem(
label: '轨道颜色',
color: skin.trackColor,
onTap: () => _showColorPicker(context, isBall: false),
),
const SizedBox(height: 40),
// 随机皮肤按钮
Center(
child: ElevatedButton.icon(
onPressed: () => skinProvider.applyRandomSkin(),
icon: const Icon(Icons.shuffle, size: 20),
label: const Text('随机生成皮肤'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
),
),
),
],
);
},
),
),
);
}
// 构建单个颜色设置项
Widget _buildColorItem({
required String label,
required Color color,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Row(
children: [
// 颜色预览圆
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(color: Colors.white30, width: 2),
),
),
const SizedBox(width: 16),
Text(label, style: const TextStyle(color: Colors.white, fontSize: 18)),
],
),
);
}
// 弹出颜色选择器
void _showColorPicker(BuildContext context, {required bool isBall}) {
showColorPickerDialog(context, isBall: isBall);
}
}
2. 颜色选择器对话框
为兼容 OpenHarmony(可能不支持第三方库),我们使用原生 AlertDialog + GridView 实现:
dart
void showColorPickerDialog(BuildContext context, {required bool isBall}) {
final provider = Provider.of<SkinProvider>(context, listen: false);
// 预设颜色池(可根据需求扩展)
final List<Color> presetColors = [
Colors.red, Colors.pink, Colors.purple, Colors.deepPurple,
Colors.indigo, Colors.blue, Colors.cyan, Colors.teal,
Colors.green, Colors.lightGreen, Colors.lime, Colors.yellow,
Colors.amber, Colors.orange, Colors.deepOrange, Colors.brown,
];
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(isBall ? '选择球体颜色' : '选择轨道颜色'),
content: SizedBox(
height: 220,
child: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: presetColors.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
final selectedColor = presetColors[index];
if (isBall) {
provider.setBallColor(selectedColor);
} else {
provider.setTrackColor(selectedColor);
}
Navigator.of(ctx).pop(); // 关闭对话框
},
child: Container(
decoration: BoxDecoration(
color: presetColors[index],
shape: BoxShape.circle,
border: Border.all(color: Colors.white30, width: 2),
),
),
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消'),
),
],
),
);
}
✅ 用户体验:点击颜色块立即应用并关闭,操作流畅。
六、主程序入口与 Provider 注册
在 main.dart 中初始化 SkinProvider 并加载本地皮肤:
dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化皮肤提供者
final skinProvider = SkinProvider();
await skinProvider.loadFromPrefs(); // 从 SharedPreferences 加载
runApp(
ChangeNotifierProvider(
create: (_) => skinProvider,
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '皮肤系统示例',
debugShowCheckedModeBanner: false,
theme: ThemeData(useMaterial3: true),
home: SkinSelectionPage(),
);
}
}
⚠️ 注意:
loadFromPrefs()必须在runApp前完成,确保首次渲染即显示正确皮肤。
七、未来扩展:贴图与 Shader 预留
虽然当前只处理颜色,但 SkinData 结构已为未来升级做好准备:
dart
class SkinData {
final Color ballColor;
final Color trackColor;
// 预留字段(当前未使用)
final String? ballTexturePath; // e.g., 'assets/textures/gold_ball.png'
final String? trackShaderPath; // e.g., 'shaders/rainbow_track.frag'
// ...
}
在渲染层(如 CustomPainter)中,可这样扩展:
dart
if (skin.ballTexturePath != null) {
// 使用 ImageShader 绘制贴图
} else {
// 使用纯色
}
如此,未来只需新增字段和绘制逻辑,无需改动现有架构。
八、完整可运行代码(独立皮肤选择界面)
以下代码可直接复制到 main.dart 中运行,无需依赖其他游戏逻辑:
dart
// main.dart - 独立皮肤选择界面(Flutter + OpenHarmony)
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
// ==================== 皮肤数据模型 ====================
class SkinData {
final Color ballColor;
final Color trackColor;
const SkinData({required this.ballColor, required this.trackColor});
factory SkinData.defaultSkin() => const SkinData(
ballColor: Color(0xFFFF6B6B),
trackColor: Color(0xFF4E54C8),
);
factory SkinData.randomSkin(Random random) {
Color _generateVibrantColor() {
return Color.fromARGB(
255,
random.nextInt(200) + 55,
random.nextInt(200) + 55,
random.nextInt(200) + 55,
);
}
return SkinData(
ballColor: _generateVibrantColor(),
trackColor: _generateVibrantColor(),
);
}
Map<String, dynamic> toJson() => {'ball': ballColor.value, 'track': trackColor.value};
factory SkinData.fromJson(Map<String, dynamic> json) {
return SkinData(
ballColor: Color(json['ball'] as int),
trackColor: Color(json['track'] as int),
);
}
}
// ==================== 皮肤状态管理器 ====================
class SkinProvider with ChangeNotifier {
SkinData _skin = SkinData.defaultSkin();
SkinData get currentSkin => _skin;
Future<void> loadFromPrefs() async {
final prefs = await SharedPreferences.getInstance();
final data = prefs.getString('user_skin');
if (data != null) {
try {
final json = jsonDecode(data) as Map<String, dynamic>;
_skin = SkinData.fromJson(json);
notifyListeners();
} catch (e) {}
}
}
Future<void> _saveToPrefs() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('user_skin', jsonEncode(_skin.toJson()));
}
void applyRandomSkin() {
_skin = SkinData.randomSkin(Random());
_saveToPrefs();
notifyListeners();
}
void setBallColor(Color color) {
_skin = SkinData(ballColor: color, trackColor: _skin.trackColor);
_saveToPrefs();
notifyListeners();
}
void setTrackColor(Color color) {
_skin = SkinData(ballColor: _skin.ballColor, trackColor: color);
_saveToPrefs();
notifyListeners();
}
}
// ==================== 颜色选择器对话框 ====================
void showColorPickerDialog(BuildContext context, {required bool isBall}) {
final provider = Provider.of<SkinProvider>(context, listen: false);
final List<Color> colors = [
Colors.red, Colors.pink, Colors.purple, Colors.deepPurple,
Colors.indigo, Colors.blue, Colors.cyan, Colors.teal,
Colors.green, Colors.lightGreen, Colors.lime, Colors.yellow,
Colors.amber, Colors.orange, Colors.deepOrange, Colors.brown,
];
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(isBall ? '选择球体颜色' : '选择轨道颜色'),
content: SizedBox(
height: 220,
child: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: colors.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
if (isBall) provider.setBallColor(colors[index]);
else provider.setTrackColor(colors[index]);
Navigator.of(ctx).pop();
},
child: Container(
decoration: BoxDecoration(
color: colors[index],
shape: BoxShape.circle,
border: Border.all(color: Colors.white30, width: 2),
),
),
);
},
),
),
actions: [
TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('取消')),
],
),
);
}
// ==================== 皮肤选择主界面 ====================
class SkinSelectionPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0F0F1A),
appBar: AppBar(
title: const Text('皮肤设置', style: TextStyle(color: Colors.white, fontSize: 20)),
backgroundColor: const Color(0xFF0F0F1A),
foregroundColor: Colors.white,
elevation: 0,
),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Consumer<SkinProvider>(
builder: (context, skinProvider, _) {
final skin = skinProvider.currentSkin;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('自定义你的视觉风格',
style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold)),
const SizedBox(height: 32),
GestureDetector(
onTap: () => showColorPickerDialog(context, isBall: true),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: skin.ballColor,
shape: BoxShape.circle,
border: Border.all(color: Colors.white30, width: 2),
),
),
const SizedBox(width: 16),
const Text('球体颜色', style: TextStyle(color: Colors.white, fontSize: 18)),
],
),
),
const SizedBox(height: 24),
GestureDetector(
onTap: () => showColorPickerDialog(context, isBall: false),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: skin.trackColor,
shape: BoxShape.circle,
border: Border.all(color: Colors.white30, width: 2),
),
),
const SizedBox(width: 16),
const Text('轨道颜色', style: TextStyle(color: Colors.white, fontSize: 18)),
],
),
),
const SizedBox(height: 40),
Center(
child: ElevatedButton.icon(
onPressed: () => skinProvider.applyRandomSkin(),
icon: const Icon(Icons.shuffle),
label: const Text('随机生成皮肤'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
),
),
),
],
);
},
),
),
);
}
}
// ==================== 主程序入口 ====================
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final skinProvider = SkinProvider();
await skinProvider.loadFromPrefs();
runApp(
ChangeNotifierProvider(
create: (_) => skinProvider,
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '皮肤系统',
debugShowCheckedModeBanner: false,
home: SkinSelectionPage(),
);
}
}
运行界面:

点击随机生成皮肤

结语
本文成功构建了一个 独立、完整、可直接复用 的皮肤选择界面。通过 Provider 实现响应式状态管理,结合 Color.fromARGB 生成高质量随机色,并通过 Consumer 实现 UI 与状态的无缝联动。
更重要的是,架构设计充分考虑了未来扩展性------无论是贴图、Shader 还是动态渐变,都可在不破坏现有代码的前提下平滑接入。