PaletteX 是我业余写的一个调色板应用,基于 Flutter 和 HarmonyOS 6.0 构建,最核心的功能模块是一个颜色分类选择器。字面上看,它不过是一个选色工具;但做完之后我才意识到,这个项目真正在解决的,是人和颜色之间那条长期被忽略的语义鸿沟。
日常工作里,我们遇到最多的情况不是"找不到颜色",而是"说不清楚要哪种颜色"。设计师在评审会上说"这个按钮要暖一点的红",产品经理补一句"但不要太艳",等你回到工位打开取色器,面对的是色环上几十万种色值——什么是"暖一点"?什么算"太艳"?这些描述在自然语言里非常具体,但在 RGB 数字里没有任何直接对应关系。PaletteX 的出发点很简单:把人对颜色的模糊描述,翻译成可复现的精准色值。
这篇文章不是一份完整 App 的源码逐行解析,而是把两个层面的经验写出来:一是颜色分类选择器的算法设计与交互逻辑,二是 Flutter 项目在 HarmonyOS 6.0 真机上的适配过程。无论你是被 UI 细节折磨的客户端开发,还是想做一个垂直工具的独立开发者,这篇文章应该都能给你一点参考。
1. 选色工具最痛的地方:语义和色值之间缺一座桥
1.1 "暖一点的红"到底是多少号色
先还原一个我在之前的电商项目里反复遇到的场景。支付按钮的颜色上线前要调,设计师的原始需求是"一个有信任感的红",开发随手填了#D32F2F。设计师看了一眼截图说"太冷了",开发换成#C62828,设计师说"还是偏暗"。两个人来回改了七八轮,最后设计师从 Figma 里截了一张色块发过来,这场拉锯才结束。
问题不在沟通态度,而在于我们用"语义词汇"讨论一个"数值空间"里的事物。"暖""冷""暗""艳"这些词,在每个人的大脑里对应的是不同的色值区间,中间没有任何公共参照系。选色工具在这个环节几乎没起到作用,因为传统取色器让你做的是一道终极选择题:请直接给出最终色值。可你心里只有一个模糊区间,根本还没想好具体终点。
所以我把 PaletteX 的核心交互从"取色"改成了"分类"。用户进来先选的是方向:暖色系还是冷色系,红色系还是大地色系,鲜亮还是柔和。每选一个分类,候选颜色范围就被大幅收窄,剩下的只是在同类里挑一个微调。这个过程更像在逛一个有导购的实体店,而不是面对一堵没有贴标签的色号墙。
1.2 PaletteX 的定位:分类优先,而不是取色优先
PaletteX 的中文名叫"色域",正好点出这个项目的两个重点:一是颜色空间的"域",二是探索色值边界的"域"。它不是一个试图替代所有专业调色软件的工具,它的细分定位是"快速从语义出发找到可用颜色,并把满意的颜色组织成配色方案"。
我给应用定了三个核心能力,按优先级排序:
- 按分类浏览和筛选颜色,比如暖色、冷色、红色系、莫兰迪灰调;
- 用色相环和三个颜色通道滑杆做微调,选中后立即看到 HEX、RGB、HSL 值;
- 把多次选中的颜色保存为配色方案,支持本地存储和云端同步。
可以看到,"颜色分类选择器"并不是一个单纯的 UI 组件,它是整个应用的信息入口。所有选色行为都从分类开始,而不是从一个随机色块开始。
1.3 为什么不能直接拿现成 picker 库改造
第一版我确实动过这个心思。Flutter 社区有flutter_colorpicker这类库,色盘、滑杆、十六进制输入基本都有,拿来改改似乎能省一半工作量。但深入后发现两件事让这条路走不通:第一,这类库的交互逻辑是"先给你一个颜色,你自己拖到满意",它没有分类学习的完整闭环,我想要的"选分类 -> 看推荐 -> 微调"流程需要从零写;第二,也是更现实的问题,这套库的底层对 Android/iOS 的通道依赖比较重,放到 HarmonyOS 6.0 的 Flutter 通道上,插件层的兼容性要一个个踩。
与其在别人的框架上打补丁,不如自己掌控核心逻辑。颜色分类的算法其实不复杂,复杂的是交互设计和平台适配,这两块自己写反而心里有底。
2. 颜色分类选择器的底层逻辑:HSL 映射与色差判断
2.1 RGB 不适合做分类的原因
任何分类算法都要选对坐标空间。最直觉的方案是用 RGB,毕竟每个颜色在代码里就是三个 0 到 255 的整数。但真正做了分类才发现,RGB 空间里数值距离和人类感知距离完全不重合。
举个例子,RGB(255, 0, 0)和RGB(200, 0, 0)在数值上差了 55,肉眼看起来确实有差异;但RGB(0, 80, 0)和RGB(0, 90, 0)同样差 10,肉眼几乎分不出来。这不是视觉错觉,而是 RGB 是面向显示设备的颜色空间,它把人眼的非线性感知硬塞进了线性数值里。如果在 RGB 空间直接做聚类或者划分子空间,结果会和直觉严重偏离。
这也是为什么我做分类时把所有颜色先做一次空间转换,转进 HSL 再判断。HSL 把颜色的三个感知维度拆开:色相(Hue)是 0 到 360 度的角度,饱和度(Saturation)是颜色鲜艳程度,亮度(Lightness)是明暗程度。这个坐标系本身就更接近人脑理解颜色的方式。
2.2 用 HSL 色相区间切分的分类规则
在 HSL 空间里做分类就直观多了。色相角 360 度绕一圈,基本锚点是这样:0 度附近是红色,60 度是黄色,120 度是绿色,180 度是青色,240 度是蓝色,300 度是洋红。我给 PaletteX 设计了八套色相分类加上一套中性色分类,切分规则如下:
| 分类 | 色相范围 | 备注 |
|---|---|---|
| 红色系 | 345-15 | 包含偏暖的品红边 |
| 橙色系 | 15-45 | 朱砂、橘色都在这里 |
| 黄色系 | 45-75 | 金色、土黄也靠它 |
| 绿色系 | 75-150 | 覆盖从草绿到墨绿 |
| 青色系 | 150-195 | 蓝绿、青碧 |
| 蓝色系 | 195-260 | 从天蓝到藏蓝 |
| 紫色系 | 260-300 | 含紫罗兰、深紫 |
| 洋红系 | 300-345 | 偏粉的玫红 |
| 中性色 | 任意 | 饱和度低于阈值时进入 |
中性色单独拎出来很重要。很多人找灰色、米白、燕麦色的时候,是不会去想"这是什么色相"的,它们视觉上是"没有彩色倾向"的颜色。算法上我取饱和度低于 0.15 作为分界线,低于这条线统一归为中性色,否则按色相区间走。这样分类的误判率会低很多。
核心函数用 Dart 写并不长:
enum ColorFamily { red, orange, yellow, green, cyan, blue, purple, magenta, neutral } ColorFamily classifyColor(Color color) { final hsl = HSLColor.fromColor(color); final h = hsl.hue; final s = hsl.saturation; if (s < 0.15) return ColorFamily.neutral; if (h < 15 || h >= 345) return ColorFamily.red; if (h < 45) return ColorFamily.orange; if (h < 75) return ColorFamily.yellow; if (h < 150) return ColorFamily.green; if (h < 195) return ColorFamily.cyan; if (h < 260) return ColorFamily.blue; if (h < 330) return ColorFamily.purple; return ColorFamily.magenta; }色相分类只能回答"这是什么色系",不能满足"我要柔和一点的红"这种需求。所以我又在分类内部加了两个维度:按饱和度分鲜亮和柔和,按亮度分明亮和暗沉。这两个维度其实也是来自日常语言的直觉,做出来之后选色效率高了很多。
提示:如果只是做个人工具,开头不要贪多,先跑通色相分类就够了。饱和度、亮度子分类等数据量上来之后再慢慢加,不然样本颜色和分类标签的维护量会很可观。
2.3 相似颜色的量化:色差公式怎么选
分类做出来后,紧接着遇到一个需求:用户选中一个颜色,希望看到"和它最像的另外几个颜色"。这个需求在配色场景里非常实用,但要实现它需要一个量化两个颜色视觉差异的函数。
最粗糙的算法是 RGB 空间欧氏距离,sqrt((r1-r2)^2 + (g1-g2)^2 + (b1-b2)^2)。前面已经解释过,RGB 数值差跟视觉差不成正比,算出来的"相似色"经常让人莫名其妙。正规做法是转 CIE Lab 颜色空间再算距离,更讲究一点用 CIEDE2000 公式,它考虑了人眼在不同色相、饱和度和亮度下的感知差异。
CIEDE2000 的公式包含好几个修正项,写出来非常长,调色板应用用它做了一个月之后,我的结论是:大部分场景用 Lab 空间的欧氏距离(也叫 CIE76),已经完全满足需求。只有做纺织品、印刷品这种对色差极度敏感的专业场景才需要上 CIEDE2000。
double colorDistance(Color a, Color b) { final labA = rgbToLab(a); final labB = rgbToLab(b); return sqrt(pow(labA.l - labB.l, 2) + pow(labA.a - labB.a, 2) + pow(labA.b - labB.b, 2)); }从 sRGB 转到 Lab 需要经过 XYZ 中间空间,公式是固定的,网上很容易找到。我现在把它封装在color_math.dart里,随手取用。
还有一个意外收获:色距函数不只是用来找相似色,它还能做"防重复"校验。用户在保存自定义颜色到方案里的时候,我会拿新颜色和方案里已有颜色跑一遍色距,如果某个已有颜色距离小于阈值,就提示"这个颜色和已保存的某某颜色几乎一样",避免一个配色方案里塞进五个肉眼难分的重复色。
3. Flutter × HarmonyOS 6.0 的工程准备与数据建模
3.1 SDK 安装与鸿蒙平台目录的初始化
标题里写的 HarmonyOS 6.0,指的是应用要真机跑在鸿蒙系统上。准备工作比纯 Android 开发多一步:除了 Flutter SDK,还要装 DevEco Studio 和对应的鸿蒙 SDK,工程结构上也需要为鸿蒙初始化平台目录。
我个人的流程是这样:先确保 Flutter 本身能跑,配置好本机 PATH 后用flutter doctor看一眼基础环境;然后打开 DevEco Studio 把 SDK 组件补齐;最后回到 Flutter 工程里执行flutter create . --platforms=android,ios,ohos,让工具生成鸿蒙平台目录。这里要特别注意,ohos平台目录并不是所有版本的 Flutter SDK 都默认支持,有些版本需要切换到社区维护的鸿蒙分支,或者通过额外的构建工具链支持。稳妥的做法是:建完工程后立即跑一次空白构建,确认脚手架真的能出包,再开始写业务代码。
网络上有很多一键安装脚本,实测下来部分在鸿蒙新版本上会失败,比如某些 brew 脚本至今还会有部署报错。我的建议是不要在一键脚本上耗时间,按官方文档走一遍手动安装,弄清楚每一步在做什么,后面遇到问题才知道去哪查。这个教训是我在鸿蒙真机调试第一天用三个小时换来的。
3.2 状态管理选型:Riverpod 处理高频颜色状态
颜色选择器的状态变化非常高频:拖一下色相环角度变了、滑一下亮度条亮度变了、选中一个分类要刷新列表、从图库里提取主色要异步更新预览区。这种高频状态如果全部塞进setState,一会儿组件树就要爆炸。
我选了 Riverpod,理由有两条:一是它和 Flutter 的解耦做得干净,状态逻辑可以单独写成 Provider 层,方便单元测试;二是它的ref.watch可以精确监听某一个字段,色值变化时只有真正依赖该字段的组件会重建,性能上比整棵子树刷新好很多。
PaletteX 里我分了三个核心 Provider:
final colorFamilyProvider = StateProvider<ColorFamily>((ref) => ColorFamily.red); final selectedColorProvider = StateProvider<Color>((ref) => const Color(0xFFD32F2F)); final paletteListProvider = StreamProvider<List<Palette>>((ref) { return ref.watch(appDatabaseProvider).watchAllPalettes(); });前两个管用户的即时选中状态,第三个直接从数据库监听配色方案的更新。拖色环的时候只触发selectedColorProvider刷新,列表数据完全不受影响,实测在低端鸿蒙真机上也能保持 60 帧左右的流畅度。
3.3 配色方案与颜色条目的模型设计
数据建模我一开始想简单了,以为一个调色板应用存几个字符串就够了。真正做到保存配色方案的时候才发现要设计的字段比想象多。配色方案是"一对多"的关系:一个方案包含多个颜色,每个颜色在方案内还要有顺序、备注名称。
用 Drift 定义的表结构大概是这样的:
class Palettes extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text()(); TextColumn get coverHex => text()(); DateTimeColumn get updatedAt => dateTime()(); } class PaletteColors extends Table { IntColumn get id => integer().autoIncrement()(); IntColumn get paletteId => integer().references(Palettes, #id)(); TextColumn get hex => text()(); TextColumn get label => text().nullable()(); IntColumn get orderIndex => integer()(); }coverHex存方案封面色,列表页渲染时用一张大卡片直接展示;orderIndex控制方案内颜色排序。所有颜色值统一存 HEX 字符串而不是整数色值,因为 HEX 可读性好,日志里一眼能看懂,转回Color也就一行代码。
字段设计不必第一次就完美,但关系要理清。我在第一版把颜色直接塞成一个 JSON 字符串数组存进 Palettes 表,后来做"给每个颜色起名字、调整顺序"的时候被迫重构了一次。所以模型上宁可多建一张表,也比留一个 JSON 字段灵活。
4. 颜色分类选择器的实现:从交互到代码
4.1 分类标签与色值的双向映射
分类选择器的核心其实是一张映射表:分类标签可以映射到一组推荐颜色,一个具体色值也能映射回它的分类标签。这是一个双向映射,两个方向都要用到。
正向方向用得最多。用户点了"橙色系-柔和"分类,界面就列出所有被标记为"柔和橙"的颜色。我维护了一份颜色样本库,每个样本包含 HEX、所属分类、人工标注名称。比如#E8A87C会被标注为"暖杏仁",分类是橙/柔和。这份样本库是应用最初体验的基石,我花了整整两个晚上从各类设计系统和中国传统色里整理并校准了三百多个常用颜色。
反向映射则依赖第 2 节写的classifyColor。用户手动拖一个色环,应用实时算出它的分类,分类标签跟着更新,页面顶部的色卡名称也随之切换。这样做的好处是用户不会被固定样本库限制住,自由调节的结果同样能获得文字标签。
两个方向的结合还能做出一个贴心功能:用户在一个分类里选中一个颜色后,点"查看类似色",应用会拿当前色值去样本库里跑色距筛选,按距离从近到远展示同分类的候选颜色。这相当于给每个颜色装了一个"找亲戚"的入口。
4.2 色相环 + 三通道滑杆的手势实现
色相环是颜色分类选择器里最有手感的部分。我实现的是一个圆环加上内圈饱和度/亮度选择区域。手指在圆环上滑动时,根据手指位置和圆心的夹角算出当前色相角度,再映射成色值。
角度计算逻辑不复杂,用atan2:
double _angleFromOffset(Offset offset, Offset center) { final dx = offset.dx - center.dx; final dy = offset.dy - center.dy; return (atan2(dy, dx) * 180 / pi + 360) % 360; }拿到角度后转成HSLColor.fromAHSL(1, angle, saturation, lightness),再设置到状态里。细节在体验上很关键:手指按下时就要立刻响应,不能等移动才开始变化;另外每帧都更新状态的话,颜色预览区和十六进制值文本要轻量更新,不能做重布局。
三通道滑杆我没有做成常见的 R/G/B 三根,而是按 H/S/L 来。因为 H 通道的变化对颜色感知影响最大,S 和 L 是微调。每根滑杆的渐变背景是用LinearGradient实时生成的,例如色相滑杆的渐变是从 0 度到 360 度均匀铺开的一排颜色。这个渐变我预计算了一次,避免拖动时重复生成大数组,毕竟低端机上的垃圾回收压力能省则省。
色相环加三滑杆可以精确命中任何颜色,但对普通用户来说它本质上还是个微调工具。所以界面的交互路径设计成:先点分类 chips 看推荐色集合,选中一个最近似的,再进色相环微调。这样既保留精度,又不强迫用户一开始就面对数学坐标系。
4.3 关键词搜索:把日常词汇映射到分类
做搜索的时候我发现一个有意思的事:用户会直接搜"莫兰迪""雾霾蓝""奶茶色"这类词,而不是搜"低饱和度蓝色"。这让我决定维护一份"语义词典",把常见颜色词汇映射到具体的筛选条件或色值。
每种词条在代码里就是一个映射项。比如"莫兰迪"会自动应用"饱和度 < 0.25、亮度在 0.3 到 0.7 之间"这样的约束,再配合用户输入的其他颜色词(比如"绿"),组合成筛选条件;"奶茶色"更直接,我把它指向#D2B48C附近的色距搜索,返回三十个距离最近的候选色。
class SemanticColorEntry { final String keyword; final ColorFamily? family; final double? maxSaturation; final Range? lightnessRange; final Color? targetColor; }这套词典的价值在于产品开始有"人味"。第一次做的时候我只有三十来个词条,后来根据用户提问逐步补。日常使用中,搜索入口和分类入口是互补的:分类适合浏览,搜索适合目标明确但表达模糊的场景。
提示:语义词典要设计成可扩展的数据结构,不要写死在 switch-case 里。做成本地 JSON 文件加载,后续加词条不用发版,也可以在运营后台维护。
5. 配色方案的内嵌存储与多端同步
5.1 选择 Drift 而不是手写 SQLite
调色板应用能不能用简单键值存储?能,Hive 甚至 SharedPreferences 都能跑,但做到"多个配色方案、方案内多个颜色、每个颜色还要排序和命名"这个复杂度,键值存储的代码会比 SQL 混乱得多。
我最后选了 Drift,原因是它在 Flutter 生态里算是关系型数据库的最优解,底层是 SQLite,但编译期生成强类型代码,查询返回Stream,天然支持 UI 响应式刷新。这意味着我在 3.2 节里写的StreamProvider可以直接监听数据库表,任何增删改查都会自动流到界面上,不需要手动刷新。
唯一要提前验证的是鸿蒙平台的支持情况。Drift 依赖 sqflite 这类原生数据库插件,如果插件没有适配 HarmonyOS,整个数据层就要换方案。我在项目一开始就先把数据库读写跑通在真机上,而不是等界面写完再接入,避免最后一刻推倒重来。
5.2 表结构设计与流式查询
除了前面提到的 Palettes 和 PaletteColors,我还加了一张预置色样本表SeedColors,里面存了我手动整理的几百个分类颜色字段。这张表的查询不走流式,因为它基本是静态的,编译期导入一次就够了。
方案列表页用流式查询的好处很明显,我在调试时给某方案追加一个颜色,返回上一页列表自动出现新封面,不需要手动setState或触发refresh。Drift 的 watch 查询底层通过 SQLite 的invalidate机制实现,数据量小的时候几乎无感。
如果用一句话总结我对流式查询的建议:凡是用户可操作变更的数据,都用流式查询;只有纯静态的词典数据,才用一次性查询。这样代码里"数据变更后手动刷 UI"的代码几乎可以全部删除。
5.3 本地优先的增量同步策略
同步这个话题很敏感,容易做重。我的方案是"本地优先":所有读写先落在本地数据库,界面上永远从本地读;后台同步只是一条"上次修改时间 + 待同步操作队列"的异步流程。
具体做法是给每张业务表加一个updatedAt时间戳,同时在本地维护一张SyncQueue表,记录未同步的增删改操作。用户离线时正常创建/编辑配色方案,操作写入 SyncQueue;网络恢复后按队列顺序推给后端,成功后删除对应队列记录。后端返回数据时带上服务端时间戳,如果本地更新的时间更晚,就以本地为准,避免覆盖用户最新操作。
这套东西代码量大概三百到四百行,价值巨大。我用一个简单的 REST API 做后端,第一次在小规模测试机群里用的时候就发现,用户最在意的不是"实时多端同步",而是"换手机之后我的配色方案还在"。本地优先的策略能保证离线体验和在线体验一致,同步只是后台默默的加分项。
注意同步的真正常见 bug 不是推送失败,而是对账逻辑没写。我的队列里存的不只是"操作类型",还存操作前后的完整数据快照,这样即使推送接口幂等实现得不好,也可以用快照手动恢复状态。
6. HarmonyOS 6.0 构建适配与真机踩坑记录
6.1 从 flutter build 到 hap 包的全流程
在鸿蒙 6.0 真机上跑 Flutter 应用,构建链路比 Android 多一环:先由 Flutter 构建出平台的产物,再用 DevEco Studio 打包成 hap 安装包。这一步如果没跑通,后面的业务代码再正确也白搭。
我的第一轮构建是很朴素的流程:flutter build hap --debug,看是否产出 hap 文件,然后在 DevEco Studio 里打开工程根目录,用自带的签名配置跑一次真机安装。这里面最容易忽略的是签名配置。DevEco Studio 新建工程时会自动生成调试签名,但如果你像我一样是"先有 Flutter 工程再补鸿蒙平台目录",签名文件可能压根没有,安装到真机上会直接报权限错误。去项目build-profile.json5里确认签名配置指向有效文件,比重新创建工程省事得多。
另外我还踩过一个小坑:Flutter 构建出的 hap 包默认可能包含的是 debug 签名,无法直接上架应用市场。上架前需要切换到正式签名重新打 release 包,这个流程要提前给发布负责人讲清楚,避免临近发布才手忙脚乱。
6.2 Gradle 插件命令式 apply 报错的修法
构建过程中最典型的报错,信息大概是 "You are applying Flutter's main Gradle plugin imperatively using the apply method..."。这个报错我刚看到时愣了一下,因为同样的工程在 Android 平台构建是正常的,说明问题出在鸿蒙构建链路的 Gradle 插件装载方式上。
报错的意思是:Flutter 主 Gradle 插件被用apply plugin: ...这种命令式方式加载,但新版构建工具要求使用 plugins DSL 方式声明。修法也很直接,在项目的settings.gradle或根build.gradle里把插件改到plugins {}块里声明:
plugins { id 'com.flutter.gradle' version 'x.y.z' apply false }然后在模块里引用:
plugins { id 'com.flutter.gradle' }而不是:
apply plugin: 'com.flutter.gradle'这个报错对构建工具版本号非常敏感,不同 SDK 版本对应插件版本也不同。我的经验是:不要试图手动改版本号强行绕过,去官方发布说明里找到当前 Flutter 鸿蒙分支对应的插件版本,一次性写对。
6.3 用 bridge 调起鸿蒙相册提取图片主色
PaletteX 里有"从图片提取主色"的需求,这就绕不开调用鸿蒙系统的相册。Flutter 社区常见的image_picker插件针对 Android/iOS 实现,在鸿蒙上虽然部分可用,但底层走的还是套壳逻辑,稳定性不好。
我最终选择自己写一层桥接:在 Dart 侧注册一个MethodChannel,在鸿蒙原生侧调用系统的相册选择器,把选中图片写入临时目录,把文件路径回传给 Dart,再在 Dart 侧用颜色量化算法提取主色。
static const _channel = MethodChannel('palettex/gallery'); final String? path = await _channel.invokeMethod('pickImage');原生侧的代码量不大,核心就是调用 PhotoPicker 并返回 URI。由于这里要处理异步回调和页面跳转,代码里最容易踩的坑是 Context 生命周期问题,选择器回调回来时如果 Activity 已经重建,所持有的 Context 会失效。解决方案是不要长期持有 Activity 引用,用 ApplicationContext 来获取系统服务。
提取图片主色的算法,我用了简单的颜色直方图聚类。先把图片缩小到 64x64,采样像素点,按第 2 节的颜色分类函数归入各个分类,统计每个分类的像素占比,输出占比最高的三到五个代表色。效果比专业取色器略糙,但作为"给配色方案找灵感"的入口完全够用。
6.4 真机性能表现与包体瘦身
最后说性能和包体。PaletteX 在鸿蒙 6.0 真机上跑下来,整体流畅度符合预期。颜色预览区的刷新频率很高,我做了两个优化:一是色相环画布用了CustomPaint并启用了isComplex提示,二是滑杆渐变色做预计算,避免拖动时计算渐变数组。
包体方面,release 包在鸿蒙上大概是 65MB 左右,对一个 Flutter 应用来说不算离谱但仍有优化空间。我做了两件事瘦身:一是启用--tree-shake-icons并严格区分MaterialIcons和自定义 SVG 图标,不用的字体图标不再编译进包里;二是把样本颜色库从 Dart 静态常量改成首次启动时加载压缩 JSON,这避免了大量硬编码数据撑起代码段。
提示:调色板应用本质上对图形渲染性能敏感,不要只看模拟器表现,务必借一台中低端鸿蒙真机验证。色相环拖动这种高频率手势操作,在高端机上的帧率参考意义有限。
做这个项目给我最大的体会是:工具类 App 的"工具"二字,不只体现在功能强弱上,更体现在它是否真的顺着人的思维路径去设计选择器。颜色分类选择器看起来只是一个小小的交互组件,但它背后牵扯到颜色科学、数据建模、平台适配三层问题。PaletteX 现在还谈不上完美,至少它让我自己从"和设计师来回拉扯"的状态里解放了出来。下一步我准备把语义词典扩充到覆盖更细的中文颜色词汇,再顺手把中国传统的四十八色做成一整套可浏览的分类色卡,这个方向应该会让调色板应用更有自己的气质。