在这里插入图片描述

颜色选择器算是工具类 App 里“看起来简单,做起来细节一堆”的功能:输入格式五花八门、要即时预览、还要能一键复制。

这篇笔记按真实项目的写法,把颜色选择器拆成几块:

  • 入口(工具列表):点击后打开弹窗。
  • 弹窗(交互容器):输入、预览、信息展示、复制。
  • 颜色解析与格式化:把 #RRGGBB / #AARRGGBB 转为 Color,同时输出 HEX / RGB / HSL。

颜色选择器工具的应用

颜色选择器在UI设计和开发中非常重要。设计师可以使用颜色选择器来选择合适的颜色。开发者可以使用颜色选择器来获取颜色的十六进制值或RGB值。

在工具页面中,颜色选择器工具已经定义在工具列表中:

{'icon': Icons.palette, 'title': '颜色选择器'},

上面这一行只是“展示”,项目里我一般会再加一个稳定的 key(后续做埋点/跳转/排序都用得到)。如果你现在不需要,可以先不加,但建议养成习惯。

点击入口:打开颜色选择器

工具列表的 onTap 不要直接塞一整坨 UI 代码,后面维护会很痛。把入口抽成一个方法,结构更清楚:

void _onToolTap(BuildContext context, String title) {
  if (title == '颜色选择器') {
    _openColorPicker(context);
    return;
  }
}

这里的重点是:

  • 入口只做分发:根据标题/标识选择打开哪个工具。
  • 具体实现下沉:弹窗、状态、解析逻辑都放到 _openColorPicker 里。

颜色选择器对话框的实现

当用户点击颜色选择器工具时,会显示一个对话框。弹窗本身只负责“壳”,内容交给一个独立组件,避免方法越写越大:

Future<void> _openColorPicker(BuildContext context) async {
  await showDialog<void>(
    context: context,
    builder: (_) => const _ColorPickerDialog(),
  );
}

这样拆开有两个好处:

  • 弹窗打开逻辑更干净:以后加埋点、加权限检查都好加。
  • UI 可复用:同样的 _ColorPickerDialog 未来也能拿去做“编辑主题色”等场景。

接下来是弹窗主体。为了避免引入额外状态管理,这里用 StatefulWidget 就够用。

class _ColorPickerDialog extends StatefulWidget {
  const _ColorPickerDialog();

  
  State<_ColorPickerDialog> createState() => _ColorPickerDialogState();
}

这段代码的目的很单纯:让弹窗内部拥有自己的状态(输入内容、解析后的颜色、展示字符串)。

输入框:尽量“宽容”,但要可控

真实使用里,用户经常会粘贴 #fff0xFF112233、带空格的字符串。我的建议是:

  • 先做“清洗”再解析(去空格、统一大小写、移除前缀)。
  • 解析失败不要崩,只提示“格式不对”,预览区显示一个默认色即可。

下面是输入控制器和监听更新(放在 _ColorPickerDialogState 里):

final _inputCtrl = TextEditingController(text: "#FF3B30");
Color? _parsed;


void initState() {
  super.initState();
  _parsed = tryParseColor(_inputCtrl.text);
  _inputCtrl.addListener(() {
    setState(() {
      _parsed = tryParseColor(_inputCtrl.text);
    });
  });
}

这里我会把 _parsed 允许为 null

  • null 表示解析失败,UI 侧可以更明确地展示错误状态。
  • 如果你强行给个默认色,用户可能不知道自己输入有问题(看起来“好像也能用”)。

颜色格式的支持

颜色选择器如果只支持 #RRGGBB,实际会被吐槽。这个版本我至少会做到:

  • HEX#RRGGBB#AARRGGBB(顺带兼容 0xFFRRGGBB 这种粘贴格式)。
  • RGB:展示 rgb(r, g, b),方便前端/设计工具互通。
  • HSL:对调色更直观,尤其是想要“同色系深浅”的时候。

颜色解析的实现

在Dart中,我们可以使用Color类来处理颜色。首先需要将颜色代码解析为Color对象:

Color? tryParseColor(String raw) {
  final input = raw.trim().toUpperCase();
  var hex = input;
  if (hex.startsWith('0X')) hex = hex.substring(2);
  if (hex.startsWith('#')) hex = hex.substring(1);

  if (hex.length == 6) {
    return Color(int.parse('FF$hex', radix: 16));
  }
  if (hex.length == 8) {
    return Color(int.parse(hex, radix: 16));
  }
  return null;
}

我这里故意没做 try/catch,因为长度不对就直接 return null 了;如果你还想支持 #RGB / #ARGB 这种短格式,再补一个分支就行。

颜色信息的格式化:别只给 HEX

解析到 Color 之后,下一步是生成一段“可复制的文本”。我会把 HEX / RGB / HSL 一次性拼出来:

String buildColorInfo(Color color) {
  final hex = '#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}';
  final rgb = 'rgb(${color.red}, ${color.green}, ${color.blue})';

  final hsl = HSLColor.fromColor(color);
  final hslText =
      'hsl(${hsl.hue.toStringAsFixed(0)}, ${(hsl.saturation * 100).toStringAsFixed(0)}%, ${(hsl.lightness * 100).toStringAsFixed(0)}%)';

  return 'HEX: $hex\nRGB: $rgb\nHSL: $hslText';
}

这段输出我保留了换行:

  • 复制出去更顺手:粘到备注/工单/聊天里可读性更好。
  • 统一大写 HEX:团队里经常会约定格式,避免同一种颜色出现两种写法。

预览区:让用户第一眼就知道“有没有解析成功”

预览区我通常会做两件事:

  • 解析失败就给一个明显的占位色(比如灰色)。
  • 显示边框,避免浅色在白底上“消失”。
Widget buildPreview(Color? color) {
  final preview = color ?? const Color(0xFFE0E0E0);
  return Container(
    height: 96,
    decoration: BoxDecoration(
      color: preview,
      border: Border.all(color: const Color(0xFFDDDDDD)),
      borderRadius: BorderRadius.circular(8),
    ),
  );
}

这块看起来只是 UI,但能显著减少用户的“我是不是输入错了”的不确定感。

复制按钮:用系统剪贴板 + 一个轻提示

复制功能别只 Navigator.pop,用户会以为没生效。更常见的做法是:复制成功后给个 SnackBar

Future<void> copyInfo(BuildContext context, String text) async {
  await Clipboard.setData(ClipboardData(text: text));
  if (!context.mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('已复制到剪贴板')),
  );
}

我会专门加一行 context.mounted

  • 避免异步回调时页面已销毁 导致报错。
  • 这在弹窗场景里尤其常见(用户点复制后立刻关闭)。

把这些拼起来:弹窗内容的最小闭环

最后把输入、预览、信息展示和复制按钮串起来。注意这里我刻意控制代码长度,只保留核心结构:


Widget build(BuildContext context) {
  final info = _parsed == null ? '请输入正确的颜色值' : buildColorInfo(_parsed!);

  return AlertDialog(
    title: const Text('颜色选择器'),
    content: SingleChildScrollView(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
            controller: _inputCtrl,
            decoration: const InputDecoration(hintText: '输入 #RRGGBB 或 #AARRGGBB'),
          ),
          const SizedBox(height: 12),
          buildPreview(_parsed),
          const SizedBox(height: 12),
          SelectableText(info),
        ],
      ),
    ),
    actions: [
      TextButton(onPressed: () => Navigator.pop(context), child: const Text('关闭')),
      TextButton(
        onPressed: _parsed == null ? null : () => copyInfo(context, info),
        child: const Text('复制'),
      ),
    ],
  );
}

这里有几个“体验细节”是我故意加上的:

  • SelectableText:用户不想点复制时,也能手动选中一段文本。
  • 复制按钮禁用:解析失败就不给点,减少误操作。
  • 最小闭环:输入 -> 预览 -> 信息 -> 复制,一次对话完成。

总结

颜色选择器这个功能,核心其实就两件事:

  • 解析要稳:输入再脏也别让页面崩。
  • 输出要友好:用户要的不是一个 Color 对象,而是能拿去用的 HEX/RGB/HSL 文本。

如果你后面要继续增强,我建议优先做:

  • 支持 #RGB / #ARGB 短格式。
  • 历史记录(最近用过的颜色一排小色块)。
  • 取色器(从图片中取色),这在工具类 App 里很受欢迎。

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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐