鸿蒙原生开发——从零构建配色方案生成器
一、引言
配色是设计中最核心也最具挑战性的环节之一。无论你是开发 App 的独立开发者、做 PPT 的职场人士,还是设计海报的运营人员,选择一组和谐的颜色都是绕不开的课题。专业的配色工具(如 Adobe Color、Coolors)背后是色彩科学的支撑——色调在色环上的位置关系决定了配色的和谐度。
从技术角度看,配色方案生成是一个HSL 色彩空间操作问题。前 7 个游戏类 Demo 都在"逻辑规则"领域(博弈、匹配、展开、推理),而配色生成器的核心是在"连续数学空间"中进行计算——色调(Hue)是一个 0°-360° 的圆环,饱和度(Saturation)和亮度(Lightness)在 0%-100% 之间变化。三种运算共同决定了最终的颜色值。
本文用 ArkUI 从零构建一个配色方案生成器,包含基于黄金角分布的 5 色方案生成、锁定/解锁微调、饱和度滑块调节、色值点击复制和预览卡片。HSL 到 Hex 的转换使用完整的色彩空间数学——色相分段映射 + RGB 归一化 + Hex 编码。
阅读完本文,你将能够:
- 使用 HSL 色彩空间表示和计算颜色
- 用黄金角(137°)分布生成视觉和谐的配色方案
- 实现 HSL → RGB → Hex 的完整色彩空间转换链
- 用锁定/解锁机制实现定向调色(冻结满意色,重新生成其余色)
二、配色理论基础
2.1 HSL 色彩空间
在计算机图形学中,颜色有多种表示方式。前端开发者最熟悉的是 Hex(如 #667eea)和 RGB(如 rgb(102, 126, 234)),但从"生成和谐配色"的角度看,HSL 是最合适的色彩空间:
- H(色相 Hue):0°-360°,在色环上的位置。0°=红色,120°=绿色,240°=蓝色
- S(饱和度 Saturation):0%-100%,颜色的纯度。0%=灰色,100%=最鲜艳
- L(亮度 Lightness):0%-100%,颜色的明暗。0%=黑色,50%=纯色,100%=白色
为什么不用 RGB?因为"和谐配色"本质上是在色环上找角度关系——互补色(180°对立)、三角色(120°均分)、类似色(30°相邻)。这些关系在 HSL 的色环上是一目了然的数学关系,但在 RGB 的三维空间中却是难以直观表达的曲面。
2.2 黄金角分布
本 Demo 使用黄金角(Golden Angle)来分布 5 个颜色的色调。黄金角约为 137.508°,来自黄金比例 φ:
黄金角 = 360° / φ² ≈ 137.5°
其中 φ = (1 + √5) / 2 ≈ 1.618
黄金角的特性在于:它是色环上"最无理"的角度。用黄金角连续旋转,永远不会回到完全相同的位置——这与向日葵种子、松果鳞片的排列原理相同。在配色上,这意味着用黄金角生成的 5 个色调在视觉上天然不重复且分布均匀,但又不像等分角(72°=360°/5)那样机械。
代码实现:
generatePalette(): void {
const arr: PaletteColor[] = [];
for (let i = 0; i < 5; i++) {
if (i < this.colors.length && this.colors[i].locked) {
arr.push(this.colors[i]); // 保留锁定色
} else {
const h = (this.baseHue + i * 137) % 360;
arr.push({ hex: this.hslToHex(h, this.saturation, 55), locked: false });
}
}
this.colors = arr;
}
5 个颜色的色调分别为:baseHue, baseHue+137°, baseHue+274°, baseHue+51°, baseHue+188°(取模 360° 后)。饱和度由全局滑块控制(默认 70%),亮度固定为 55%(偏暗但不过暗,适合作为 UI 主色)。
2.3 锁定/解锁机制
配色生成器的核心交互模式是"锁定-重新生成"循环:
- 随机生成一组 5 色方案
- 你对其中 2 个颜色满意,点击 🔒 锁定
- 再次随机生成——锁定的 2 个颜色保持不变,另外 3 个重新生成
- 重复 2-3 直到整个方案满意
这种交互模式来自 Coolors 等专业配色工具,解决了一个核心矛盾:完全随机生成难以控制,而完全手动调整需要专业色彩知识。"锁定 + 重新生成"是两者之间的最优平衡。
toggleLock(idx: number): void {
const arr: PaletteColor[] = [];
for (let i = 0; i < this.colors.length; i++) {
if (i === idx) {
arr.push({ hex: this.colors[i].hex, locked: !this.colors[i].locked });
} else {
arr.push(this.colors[i]);
}
}
this.colors = arr;
}
2.4 交互流程
- 初始生成:随机
baseHue+ 黄金角 × 4 = 5 色方案 - 锁定颜色:点击色块下的 🔒 → 该色被冻结
- 重新生成:点击"随机生成" → 只有未锁定的颜色重新计算
- 偏移色相:点击"偏移色相" → 所有未锁定颜色的色相整体旋转 30°
- 调整饱和度:拖动滑块 → 所有未锁定颜色的饱和度同步更新
- 复制色值:点击色块 → Hex 值被记录,"已复制"反馈 1.5 秒
- 预览效果:底部预览卡片实时展示当前配色在 UI 中的表现

三、HSL 到 Hex 的转换
3.1 算法原理
HSL → Hex 的转换路径是 HSL → RGB → Hex,其中 HSL → RGB 是最核心的一步。
HSL 到 RGB 的转换基于色相分段映射。给定 HSL(h, s, l),首先计算两个中间量:
c = (1 - |2l - 1|) × s ← 色度(chroma)
x = c × (1 - |(h/60) % 2 - 1|) ← 中间值
m = l - c/2 ← 亮度调整量
然后根据色相 h 落入的 60° 区间,计算临时 RGB(r1, g1, b1):
| 色相区间 | r1 | g1 | b1 | 颜色过渡 |
|---|---|---|---|---|
| 0°-60° | c | x | 0 | 红→黄 |
| 60°-120° | x | c | 0 | 黄→绿 |
| 120°-180° | 0 | c | x | 绿→青 |
| 180°-240° | 0 | x | c | 青→蓝 |
| 240°-300° | x | 0 | c | 蓝→品红 |
| 300°-360° | c | 0 | x | 品红→红 |
最后将临时值加上 m 并放大到 0-255:
R = round((r1 + m) × 255)
G = round((g1 + m) × 255)
B = round((b1 + m) × 255)
3.2 ArkTS 实现
hslToHex(h: number, s: number, l: number): string {
const sNorm = s / 100;
const lNorm = l / 100;
const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = lNorm - c / 2;
let r1 = 0; let g1 = 0; let b1 = 0;
if (h < 60) { r1 = c; g1 = x; b1 = 0; }
else if (h < 120) { r1 = x; g1 = c; b1 = 0; }
else if (h < 180) { r1 = 0; g1 = c; b1 = x; }
else if (h < 240) { r1 = 0; g1 = x; b1 = c; }
else if (h < 300) { r1 = x; g1 = 0; b1 = c; }
else { r1 = c; g1 = 0; b1 = x; }
const r = Math.round((r1 + m) * 255);
const g = Math.round((g1 + m) * 255);
const b = Math.round((b1 + m) * 255);
return '#' + this.toHex2(r) + this.toHex2(g) + this.toHex2(b);
}
六个 if-else 分支对应色环的六个 60° 扇区。每个扇区内,三种颜色分量中有一项始终为 0,一项等于色度 c,一项随色相在 [0, c] 之间线性变化。这种"二大一小"的模式保证了色环上相邻色相的平滑过渡。
3.3 RGB → Hex 编码
三个颜色分量(0-255)分别转换为两位十六进制:
toHex2(v: number): string {
const s = '0123456789ABCDEF';
return s.charAt(Math.floor(v / 16)) + s.charAt(v % 16);
}
除以 16 取商和余数,在十六进制字符表中查表——比 toString(16) 更可靠(ArkTS 中数字的 toString 方法参数支持不确定)。
注意:ArkTS 不支持字符串索引访问(s[i]),必须使用 .charAt(i) 替代。这是 ArkTS 严格的类型检查带来的约束——string 类型的属性访问不被允许,只能通过 .charAt() 方法获取指定位置的字符。
四、饱和度调节
饱和度滑块使用 ArkUI 的 Slider 组件:
Slider({
style: SliderStyle.InSet,
value: this.saturation,
min: 10,
max: 100,
step: 5
})
.blockColor('#667eea')
.trackColor('#E0E0E8')
.selectedColor('#667eea')
.trackThickness(6)
.width(200)
.onChange((v: number) => { this.updateSaturation(v); })
SliderStyle.InSet 样式(无刻度标记的连续滑块),步长 5%,最小值 10%(完全不饱和的灰色在配色中通常无用)。onChange 回调触发后:
updateSaturation(s: number): void {
this.saturation = s;
const arr: PaletteColor[] = [];
for (let i = 0; i < this.colors.length; i++) {
if (this.colors[i].locked) {
arr.push(this.colors[i]);
} else {
const h = (this.baseHue + i * 137) % 360;
arr.push({ hex: this.hslToHex(h, this.saturation, 55), locked: false });
}
}
this.colors = arr;
}
同样遵循锁定/解锁规则:锁定的颜色不受饱和度变化影响,未锁定的颜色以新饱和度重新计算。这使得用户可以"锁定中意的几个颜色,调整整体的鲜艳度直到满意"。
五、预览卡片
配色方案生成器的独特价值在于:它不只是给你 5 个颜色代码,而是让你看到这些颜色在实际界面中的效果。
Column() {
// 预览 header(使用颜色 0 作为背景)
Row() {
Text('标题文字')
.fontColor(this.textColorFor(this.colors[0].hex))
}
.backgroundColor(this.colors[0].hex)
// 预览 body
Column() {
Text('这是一段正文内容,展示配色方案在实际界面中的效果。')
Row() {
Button('主要按钮') // 使用颜色 1
.backgroundColor(this.colors[1].hex)
Button('次要按钮') // 使用颜色 2
.backgroundColor(this.colors[2].hex + '22') // 半透明
}
Row() {
Text('●') // 使用颜色 3
.fontColor(this.colors[3].hex)
Text('状态标识')
}
}
}
5 个颜色在预览中各司其职:
- 颜色 0:标题栏背景 → 主色
- 颜色 1:主要按钮 → 强调色
- 颜色 2:次要按钮文字/边框 → 辅助色
- 颜色 3:状态标识 → 点缀色
- 颜色 4:预留(可用于图标、链接等)
5.1 智能文字颜色
当颜色用作背景时,需要根据背景的亮度自动选择白色或深色文字:
textColorFor(bgHex: string): string {
const r = this.hexVal(bgHex, 1, 2);
const g = this.hexVal(bgHex, 3, 4);
const b = this.hexVal(bgHex, 5, 6);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 150 ? '#1a1a2e' : '#FFFFFF';
}
亮度计算公式使用 BT.601 标准(人眼对绿色最敏感、红色次之、蓝色最弱)。阈值 150 将亮度区间粗略分为两半——低亮度背景用白色文字(对比度足够),高亮度背景用深色文字(避免白色淹没在浅色中)。
六、UI 设计
6.1 页面结构
┌──────────────────────────────────────────┐
│ 🎨 配色方案(深色标题栏) │
├──────────────────────────────────────────┤
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │#6678│ │#1677│ │#52C4│ │#FF98│ │#FF4D│ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ 🔒 │ │ 🔓 │ │ 🔓 │ │ 🔓 │ │ 🔓 │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │
│ 点击色块复制色值 | 点击 🔒 锁定颜色 │
├──────────────────────────────────────────┤
│ 饱和度 [════════○══════════] 70% │
├──────────────────────────────────────────┤
│ 🎲 随机生成 ↔ 偏移色相 │
├──────────────────────────────────────────┤
│ 预览效果 │
│ ┌──────────────────────────────────┐ │
│ │ 标题文字(颜色 0 背景) │ │
│ ├──────────────────────────────────┤ │
│ │ 这是一段正文内容,展示配色方案 │ │
│ │ 在实际界面中的效果。 │ │
│ │ [主要按钮](颜色 1) [次要](颜色 2)│ │
│ │ ● 状态标识(颜色 3) │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────┘
6.2 色板布局
5 个色板水平排列,使用 FlexAlign.SpaceEvenly 均匀分布:
Row() {
ForEach(this.colors, (pc: PaletteColor, idx: number) => {
Column() {
Column() {
if (this.copiedIdx === idx) {
Text('已复制')
} else {
Text(pc.hex)
}
}
.width(60).height(70)
.backgroundColor(pc.hex)
.onClick(() => { this.copyHex(pc.hex, idx); })
Text(pc.locked ? '🔒' : '🔓')
.onClick(() => { this.toggleLock(idx); })
}
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
每个色板 60vp × 70vp,比例接近 6:7——不是正方形,略竖长,在视觉上形成"色卡"的感觉。Hex 值直接显示在色块内部,使用智能文字颜色确保在任何背景色上都可读。
点击色块时,copyHex() 通过 setInterval 在 1.5 秒后重置 copiedIdx,色块上短暂显示"已复制"文字确认操作成功。
七、完整代码结构
PalettePage (~230 行)
├── 接口定义
│ └── PaletteColor { hex, locked }
├── 状态变量
│ ├── @State colors[5] — 当前 5 色方案
│ ├── @State saturation — 饱和度(10-100)
│ ├── @State baseHue — 基础色相(0-360)
│ └── @State copiedIdx — 复制反馈索引
├── 色彩数学
│ ├── hslToHex() — HSL→RGB→Hex 完整转换链
│ ├── toHex2() — 单字节→两位十六进制
│ ├── hexVal() — Hex子串→十进制数值
│ └── textColorFor() — 背景亮度→文字颜色
├── 配色逻辑
│ ├── generatePalette() — 黄金角 × 5 色生成
│ ├── shiftHues() — 色相整体偏移 30°
│ ├── toggleLock() — 锁定/解锁切换
│ └── updateSaturation() — 饱和度调节
├── 交互
│ └── copyHex() — 色值复制 + 视觉反馈
├── 视图
│ ├── 标题栏 — 🎨 配色方案
│ ├── 5 色色板 — Row + ForEach
│ ├── 饱和度滑块 — Slider 组件
│ ├── 操作按钮 — 随机生成 + 偏移色相
│ └── 预览卡片 — 模拟 UI 组件展示
└── 生命周期
└── aboutToAppear() — 初始随机生成
八、总结
本文从零构建了一个配色方案生成器。与前面 7 个游戏类 Demo 的"离散规则逻辑"不同,配色生成器的核心是连续色彩空间中的数学运算——HSL 色环上的角度分布、RGB 归一化和十六进制编码。从技术角度看,它是 HSL 色彩空间、黄金角分布、锁定式迭代优化和智能文字对比度的完整示例。
核心要点回顾:
-
HSL 色彩空间:H(色相 0°-360°)确定颜色在色环上的位置,S(饱和度 0%-100%)控制鲜艳度,L(亮度 0%-100%)控制明暗。在 HSL 空间中进行配色计算比在 RGB 中更自然——和谐配色(互补、三角、黄金角)本质上是色环上的角度关系。
-
黄金角分布:
hue[i] = baseHue + i × 137°将 5 个色调以黄金角(360°/φ²)间距分布在色环上。黄金角是"最无理"的角度,保证色调天然不重复且视觉均匀,比等分角(72°)更自然。 -
HSL → RGB → Hex 转换链:色相落入 6 个 60° 扇区之一 → 计算 (r1, g1, b1) 临时值 → 加上亮度调整量 m → 放大到 [0, 255] → 每个分量编码为两位十六进制。完整的转换链共约 40 行代码,无任何依赖。
-
锁定/解锁迭代优化:
toggleLock()翻转单个颜色的locked标志,generatePalette()和shiftHues()只更新未锁定颜色。这种"锁定满意色、重试不满意色"的模式是专业配色工具的标配交互。 -
智能文字对比度:
textColorFor()使用 BT.601 亮度公式(0.299R + 0.587G + 0.114B)判断背景明暗,阈值 150 决定使用深色还是白色文字。确保 Hex 值在任何背景色上都清晰可读。 -
Slider 饱和度调节:
Slider({ style: SliderStyle.InSet })实现饱和度从 10% 到 100% 的连续调节(步长 5%)。updateSaturation()对未锁定颜色以新饱和度重新执行 HSL→Hex 转换,锁定色保持不变。
配色方案生成器是一款面向设计师和开发者的实用工具。这个 230 行的 ArkUI 实现抓住了色彩空间计算的核心乐趣:随机生成时的惊喜感、锁定满意颜色的策略感、饱和度调至最佳时的满足感、以及在预览卡片中看到完整配色效果时的成就感。它是本系列首篇设计工具类文章,也是 HSL 色彩空间计算和迭代式锁定优化的完整示例。
更多推荐




所有评论(0)