Flutter + OpenHarmony 实战:构建独立可复用的皮肤选择界面

个人主页:ujainu

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

文章目录

前言

在现代移动应用与游戏中,个性化体验已成为用户留存的关键要素。一个灵活、响应迅速且易于扩展的皮肤系统,不仅能提升视觉吸引力,还能为未来商业化(如付费主题)打下坚实基础。

本文将聚焦于 构建一个独立、完整、可直接集成 的皮肤选择界面(Skin Selection UI),实现对"球体"和"轨道"两类元素的颜色动态切换。我们将深入探讨:

  • 颜色状态的集中管理与响应式更新
  • 使用 Color.fromARGB 实现高质量随机色生成
  • UI 设置页与预览组件的实时联动
  • 架构设计:为何选用 Provider 而非单例?
  • 为未来贴图(Texture)与着色器(Shader)效果预留扩展点

环境说明 :本文假设 Flutter + OpenHarmony 开发环境已配置完毕(含 providershared_preferences 等依赖),不重复环境搭建步骤。

目标产出:一个可直接复制运行、结构清晰、注释完整的皮肤选择界面。


一、为什么需要独立的皮肤系统?

许多开发者在初期采用"硬编码颜色"或"全局变量"方式管理 UI 主题,但随着功能迭代,这种方式会带来:

  • 状态不同步:修改颜色后,部分界面未刷新
  • 耦合严重:业务逻辑与样式混杂,难以维护
  • 无法持久化:重启应用后设置丢失
  • 扩展困难:若未来要支持贴图、动态渐变、粒子特效,需大规模重构

因此,我们需要一个 解耦、可观察、可持久化、可扩展 的皮肤框架。而本文的核心,就是实现其前端交互层------皮肤选择界面


二、架构设计:Provider vs 单例(GameTheme)

❌ 方案一:GameTheme 单例

dart 复制代码
class GameTheme {
  static final instance = GameTheme._();
  Color ballColor = Colors.red;
  void setBallColor(Color c) { ballColor = c; }
}

问题

  • 无通知机制,UI 无法自动刷新
  • 无法与 StatefulWidgetsetState 解耦
  • 测试困难,违反依赖注入原则

✅ 方案二: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 还是动态渐变,都可在不破坏现有代码的前提下平滑接入。

相关推荐
多打代码2 小时前
2026.02.05 (贪心)买卖股票2 & 跳跃游戏 1 & 2
游戏
Betelgeuse762 小时前
【Flutter For OpenHarmony】 阶段复盘:从单页Demo到模块化App
flutter·ui·华为·交互·harmonyos
一起养小猫2 小时前
Flutter for OpenHarmony 实战:记忆翻牌游戏完整开发指南
flutter·游戏·harmonyos
lbb 小魔仙2 小时前
【HarmonyOS】DAY13:Flutter电商实战:从零开发注册页面(含密码验证、确认密码完整实现)
flutter·华为·harmonyos
jaysee-sjc2 小时前
【项目二】用GUI编程实现石头迷阵游戏
java·开发语言·算法·游戏
晚霞的不甘2 小时前
Flutter for OpenHarmony 豪华抽奖应用:从粒子背景到彩带动画的全栈实现
前端·学习·flutter·microsoft·前端框架
qq_12498707532 小时前
基于Javaweb的《战舰世界》游戏百科信息系统(源码+论文+部署+安装)
java·vue.js·人工智能·spring boot·游戏·毕业设计·计算机毕业设计
铅笔侠_小龙虾2 小时前
浅谈 Vue & React & Flutter 框架
vue.js·flutter·react.js
日光倾3 小时前
【Vue.js 入门笔记】 状态管理器Vuex
vue.js·笔记·flutter