一、应用概述
调色板(Color Picker)是一款实用的颜色选择工具,广泛应用于设计、绘画、主题定制等场景。本项目基于 HarmonyOS ArkTS 框架实现了一个功能完善的调色板应用,包含 20 种预设颜色、RGB 三通道滑块、HEX 色值实时显示以及颜色预览区域。本文将从色彩模型原理、滑块联动算法、声明式 UI 设计、HarmonyOS 特性等角度进行全面深度分析。
二、架构设计
2.1 整体架构
┌──────────────────────────────────────────┐ │ ColorPickerView.ets │ │ (主视图:组合所有子组件) │ ├──────────────────────────────────────────┤ │ ┌─────────────┐ ┌─────────────────┐ │ │ │ ColorPreview │ │ PresetGrid │ │ │ │ (颜色预览) │ │ (20 预设色块) │ │ │ └─────────────┘ └─────────────────┘ │ │ ┌──────────────────────────────────┐ │ │ │ RGBSliderGroup │ │ │ │ (R 滑块 ────●─── 255) │ │ │ │ (G 滑块 ────●─── 255) │ │ │ │ (B 滑块 ────●─── 255) │ │ │ └──────────────────────────────────┘ │ │ ┌──────────────────────────────────┐ │ │ │ HexDisplay │ │ │ │ (#FF5733) │ │ │ └──────────────────────────────────┘ │ └──────────────────────────────────────────┘2.2 数据流
预设点击 / 滑块拖动 → 更新 RGB 数值 → 更新 HEX 色值 → 更新预览区域背景色 → UI 响应式重渲染三、核心技术实现
3.1 颜色数据模型
// ColorModel.ets export class RGBColor { red: number; // 0-255 green: number; // 0-255 blue: number; // 0-255 constructor(red: number = 0, green: number = 0, blue: number = 0) { this.red = this.clamp(red); this.green = this.clamp(green); this.blue = this.clamp(blue); } // 值域限定 private clamp(value: number): number { return Math.max(0, Math.min(255, Math.round(value))); } // RGB → HEX 转换 toHex(): string { const r = this.red.toString(16).padStart(2, '0'); const g = this.green.toString(16).padStart(2, '0'); const b = this.blue.toString(16).padStart(2, '0'); return `#${r}${g}${b}`.toUpperCase(); } // HEX → RGB 静态工厂 static fromHex(hex: string): RGBColor { const clean = hex.replace('#', ''); if (clean.length !== 6) return new RGBColor(0, 0, 0); const r = parseInt(clean.substring(0, 2), 16); const g = parseInt(clean.substring(2, 4), 16); const b = parseInt(clean.substring(4, 6), 16); return new RGBColor(r, g, b); } // 获取 CSS 颜色字符串 toCssString(): string { return `rgb(${this.red}, ${this.green}, ${this.blue})`; } }3.2 预设颜色数据
// PresetColors.ets export const PRESET_COLORS: RGBColor[] = [ // 红色系 new RGBColor(255, 51, 51), // 亮红 new RGBColor(204, 0, 0), // 深红 new RGBColor(255, 102, 102), // 粉红 // 橙色系 new RGBColor(255, 153, 51), // 橙 new RGBColor(255, 204, 102), // 杏黄 // 黄色系 new RGBColor(255, 255, 51), // 亮黄 new RGBColor(255, 255, 204), // 乳黄 // 绿色系 new RGBColor(51, 204, 51), // 亮绿 new RGBColor(0, 153, 0), // 深绿 new RGBColor(102, 204, 102), // 草绿 // 蓝色系 new RGBColor(51, 102, 255), // 亮蓝 new RGBColor(0, 0, 204), // 深蓝 new RGBColor(102, 178, 255), // 天蓝 // 紫色系 new RGBColor(153, 51, 255), // 紫 new RGBColor(204, 153, 255), // 淡紫 // 中性色 new RGBColor(255, 255, 255), // 白 new RGBColor(204, 204, 204), // 浅灰 new RGBColor(128, 128, 128), // 中灰 new RGBColor(64, 64, 64), // 深灰 new RGBColor(0, 0, 0), // 黑 ];3.3 RGB 转 HEX 算法详解
RGB 转 HEX 的本质是将十进制颜色值转换为十六进制表示:
RGB(255, 87, 51) → HEX(#FF5733) 转换过程: R = 255 → 0xFF → "FF" G = 87 → 0x57 → "57" B = 51 → 0x33 → "33" 拼接 → "#FF5733"十进制与十六进制的对应关系:
| 十进制 | 十六进制 | 十进制 | 十六进制 |
|---|---|---|---|
| 0 | 00 | 128 | 80 |
| 16 | 10 | 192 | C0 |
| 32 | 20 | 240 | F0 |
| 64 | 40 | 255 | FF |
| 100 | 64 |
转换公式:
十六进制字符串 = decimal.toString(16).padStart(2, '0').toString(16):将数字转换为十六进制字符串.padStart(2, '0'):确保输出至少两位,不足前面补零
3.4 完整的调色板 UI
// Index.ets import { RGBColor } from './ColorModel'; import { PRESET_COLORS } from './PresetColors'; @Entry @Component struct Index { @State currentColor: RGBColor = new RGBColor(255, 87, 51); @State hexDisplay: string = '#FF5733'; build() { Column() { // 标题 Text('调色板 🎨') .fontSize(28) .fontWeight(FontWeight.Bold) .margin({ top: 20, bottom: 5 }) Text('选择颜色或拖动滑块调整') .fontSize(14) .fontColor(Color.Gray) .margin({ bottom: 15 }) // 1. 颜色预览区域 Column() { Text('') .width('100%') .height(120) .backgroundColor(this.currentColor.toCssString()) .borderRadius(16) .shadow({ radius: 8, color: '#40000000', offsetY: 4 }) } .width('90%') .padding(10) // 2. HEX 色值显示 Row() { Text('HEX') .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor(Color.Gray) Text(this.hexDisplay) .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(this.currentColor.toCssString()) .margin({ left: 15 }) .fontFamily('monospace') } .width('90%') .justifyContent(FlexAlign.Start) .padding({ left: 10, top: 10, bottom: 10 }) // 3. RGB 三通道滑块 Column() { // R 通道 Row() { Text('R') .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor('#FF3333') .width(30) Slider({ value: this.currentColor.red, min: 0, max: 255, step: 1 }) .width('70%') .height(40) .blockColor('#FF3333') .trackThickness(6) .onChange((value: number) => { this.currentColor.red = value; this.updateDisplay(); }) Text(`${this.currentColor.red}`) .fontSize(16) .fontWeight(FontWeight.Medium) .width(40) .textAlign(TextAlign.End) } .width('90%') .padding(5) // G 通道 Row() { Text('G') .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor('#33CC33') .width(30) Slider({ value: this.currentColor.green, min: 0, max: 255, step: 1 }) .width('70%') .height(40) .blockColor('#33CC33') .trackThickness(6) .onChange((value: number) => { this.currentColor.green = value; this.updateDisplay(); }) Text(`${this.currentColor.green}`) .fontSize(16) .fontWeight(FontWeight.Medium) .width(40) .textAlign(TextAlign.End) } .width('90%') .padding(5) // B 通道 Row() { Text('B') .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor('#3366FF') .width(30) Slider({ value: this.currentColor.blue, min: 0, max: 255, step: 1 }) .width('70%') .height(40) .blockColor('#3366FF') .trackThickness(6) .onChange((value: number) => { this.currentColor.blue = value; this.updateDisplay(); }) Text(`${this.currentColor.blue}`) .fontSize(16) .fontWeight(FontWeight.Medium) .width(40) .textAlign(TextAlign.End) } .width('90%') .padding(5) } .width('100%') // 分隔线 Divider() .width('90%') .margin({ top: 10, bottom: 10 }) // 4. 预设颜色网格 Text('预设颜色') .fontSize(16) .fontWeight(FontWeight.Bold) .width('90%') .margin({ bottom: 8 }) Grid() { ForEach(PRESET_COLORS, (color: RGBColor, index: number) => { // 预设色块 Column() { Button() .width(44) .height(44) .backgroundColor(color.toCssString()) .borderRadius(8) .border({ width: color === this.currentColor ? 3 : 1, color: color === this.currentColor ? '#2B6CB0' : '#E2E8F0' }) .onClick(() => { this.currentColor = color; this.updateDisplay(); }) } .width(44) .height(44) }, (color: RGBColor) => color.toHex()) } .columnsTemplate('1fr 1fr 1fr 1fr 1fr') .rowsTemplate('1fr 1fr 1fr 1fr') .width('90%') .padding(5) } .width('100%') .height('100%') .backgroundColor('#F7FAFC') .overflow(Overflow.Scroll) } // 更新显示 updateDisplay(): void { this.hexDisplay = this.currentColor.toHex(); } }四、RGB 颜色模型深入
4.1 色彩空间原理
RGB(红绿蓝)是加色混色模型,广泛应用于电子显示设备。其核心原理:
- R(Red,红色):波长 ~700nm,0-255 表示从无到饱和
- G(Green,绿色):波长 ~546nm,人眼最敏感的通道
- B(Blue,蓝色):波长 ~435nm,能量最高
三个通道各 8 位(0-255),总计 24 位色,可表示 16,777,216 种颜色。
4.2 滑块联动算法
当滑块拖动时,需要实时更新三个数值并同步到 UI:
// 滑块值变化 → 更新颜色 → 刷新所有相关 UI onChange((value: number) => { this.currentColor.red = value; // 更新模型 this.updateDisplay(); // 触发 UI 刷新 })关键点在于@State的响应式机制:
currentColor被@State装饰- 修改
currentColor.red后,由于对象引用未变,需通过方法调用触发变更检测 updateDisplay()更新hexDisplay字符串,该字符串是@State变量,触发重渲染
注意:ArkTS 的@State是浅比较。修改对象的属性(如currentColor.red = value)时,如果对象引用不变,框架可能检测不到变化。解决方案:
// 方案一:重新赋值整个对象 this.currentColor = new RGBColor(value, this.currentColor.green, this.currentColor.blue); // 方案二:使用 @Observed 装饰器使对象深度响应 @Observed export class RGBColor { ... }4.3 颜色空间转换扩展
除了 RGB ↔ HEX,实际开发中可能还需要 HSV/HSL 转换:
// RGB → HSV 转换 function rgbToHsv(r: number, g: number, b: number): { h: number, s: number, v: number } { r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const delta = max - min; let h = 0; if (delta !== 0) { if (max === r) h = ((g - b) / delta) % 6; else if (max === g) h = (b - r) / delta + 2; else h = (r - g) / delta + 4; h = Math.round(h * 60); if (h < 0) h += 360; } const s = max === 0 ? 0 : (delta / max) * 100; const v = max * 100; return { h, s: Math.round(s), v: Math.round(v) }; }五、HarmonyOS 特性分析
5.1 Slider 组件
HarmonyOS 的Slider组件提供了丰富的自定义选项:
Slider({ value: this.currentColor.red, min: 0, max: 255, step: 1 }) .width('70%') .height(40) .blockColor('#FF3333') // 滑块按钮颜色 .trackThickness(6) // 轨道厚度 .onChange((value: number) => { // 值变化回调 })Slider 关键属性:
min/max:取值范围step:步长(颜色调整通常为 1)blockColor:滑块按钮颜色(按通道区分)trackThickness:轨道粗细
5.2 Grid 网格预设布局
20 个预设颜色使用Grid组件排列为 5 列 4 行:
Grid() { ForEach(PRESET_COLORS, (color: RGBColor, index: number) => { // 色块 }, (color: RGBColor) => color.toHex()) } .columnsTemplate('1fr 1fr 1fr 1fr 1fr') // 5 列 .rowsTemplate('1fr 1fr 1fr 1fr') // 4 行5.3 响应式状态管理
@State currentColor:当前选中的颜色,驱动预览和 HEX 显示@State hexDisplay:HEX 字符串,单独存储以便字体颜色同步
5.4 阴影与圆角
.shadow({ radius: 8, color: '#40000000', offsetY: 4 }) .borderRadius(16)HarmonyOS 的shadowAPI 支持自定义阴影半径、颜色和偏移,为 UI 添加层次感。
六、UI/UX 设计
6.1 交互反馈
- 预设点击:点击色块立即更新预览和滑块
- 滑块拖动:实时联动,拖动滑块时颜色同步变化
- 选中高亮:当前选中预设加蓝色边框
- 数值显示:滑块右侧实时显示当前通道值
6.2 视觉设计
布局层次: 标题 → 子标题 颜色预览区(大块) HEX 色值(等宽字体) RGB 滑块(按通道颜色标识) —— 分隔线 —— 预设颜色(5×4 网格)- 预览区域使用大色块,直观感受颜色
- HEX 值使用等宽字体(monospace),便于阅读
- 滑块按通道着色(红/绿/蓝),视觉区分度高
- 预设颜色覆盖主要色系
6.3 色彩辅助功能
// 颜色复制到剪贴板 function copyToClipboard(text: string): void { // HarmonyOS 剪贴板 API // pasteboard.createData(PasteType.PLAIN_TEXT, text) console.info(`已复制颜色: ${text}`); }七、最佳实践总结
7.1 代码组织
- 颜色模型与 UI 分离
- 枚举预设颜色为独立模块
- 工具函数(RGB↔HEX 转换)集中管理
7.2 状态管理
- 使用 @Observed 装饰器实现深度响应式
- 避免在组件内部修改 props
- 使用单一数据源(Single Source of Truth)
7.3 性能优化
- 滑块 onChange 回调中避免复杂计算
- 使用离散 step(step=1)而非连续模式
- ForEach 使用稳定的 key(颜色 HEX 值)
7.4 可扩展性
- 支持 HSL/HSV 模式切换
- 支持透明度(Alpha)通道
- 支持颜色收藏和近期使用
- 支持从图片取色
八、扩展功能:HSL 色相环选择器
除了 RGB 滑块,可以添加更直观的色相环选择器:
@Component struct HueSlider { @Prop hue: number; onChange?: (hue: number) => void; build() { // 渐变轨道:从红到紫跨越整个色相环 Slider({ value: this.hue, min: 0, max: 360, step: 1 }) .width('80%') .trackThickness(20) .blockColor('#FFFFFF') .onChange((value: number) => { if (this.onChange) this.onChange(value); }) } }九、完整代码清单
// ColorModel.ets @Observed export class RGBColor { red: number; green: number; blue: number; constructor(red: number = 0, green: number = 0, blue: number = 0) { this.red = Math.max(0, Math.min(255, Math.round(red))); this.green = Math.max(0, Math.min(255, Math.round(green))); this.blue = Math.max(0, Math.min(255, Math.round(blue))); } toHex(): string { const r = this.red.toString(16).padStart(2, '0'); const g = this.green.toString(16).padStart(2, '0'); const b = this.blue.toString(16).padStart(2, '0'); return `#${r}${g}${b}`.toUpperCase(); } toCssString(): string { return `rgb(${this.red}, ${this.green}, ${this.blue})`; } }// PresetColors.ets import { RGBColor } from './ColorModel'; export const PRESET_COLORS: RGBColor[] = [ new RGBColor(255, 51, 51), new RGBColor(204, 0, 0), new RGBColor(255, 102, 102), new RGBColor(255, 153, 51), new RGBColor(255, 204, 102), new RGBColor(255, 255, 51), new RGBColor(255, 255, 204), new RGBColor(51, 204, 51), new RGBColor(0, 153, 0), new RGBColor(102, 204, 102), new RGBColor(51, 102, 255), new RGBColor(0, 0, 204), new RGBColor(102, 178, 255), new RGBColor(153, 51, 255), new RGBColor(204, 153, 255), new RGBColor(255, 255, 255), new RGBColor(204, 204, 204), new RGBColor(128, 128, 128), new RGBColor(64, 64, 64), new RGBColor(0, 0, 0), ];// ColorUtils.ets import { RGBColor } from './ColorModel'; export function hexToRgb(hex: string): RGBColor { const clean = hex.replace('#', ''); if (clean.length !== 6) return new RGBColor(0, 0, 0); const r = parseInt(clean.substring(0, 2), 16); const g = parseInt(clean.substring(2, 4), 16); const b = parseInt(clean.substring(4, 6), 16); return new RGBColor(r, g, b); } export function isLightColor(color: RGBColor): boolean { // 相对亮度公式 const luminance = 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue; return luminance > 186; } export function getContrastTextColor(bgColor: RGBColor): string { return isLightColor(bgColor) ? '#000000' : '#FFFFFF'; }十、总结
通过本篇文章,我们深入解析了 HarmonyOS 调色板应用的完整实现:
- RGB 颜色模型—— 加色混色原理、24 位色空间
- RGB ↔ HEX 转换算法—— 十进制与十六进制互转
- 滑块联动—— Slider 组件使用、三通道实时同步
- 预设颜色选择—— 20 色覆盖主要色系、Grid 网格布局
- 声明式 UI—— @State/@Observed 响应式编程、条件渲染
调色板应用虽然功能看似简单,但涵盖了颜色科学、UI 交互、响应式编程等多个重要领域。从颜色理论到具体实现,每一步都体现了工程设计与用户体验的平衡。希望本文能为 HarmonyOS 开发者提供一个完整的技术参考。