在这里插入图片描述

目录

  1. 概述
  2. 工具功能
  3. 核心实现
  4. 实战案例
  5. 编译过程详解
  6. 工具扩展
  7. 最佳实践
  8. 常见问题

概述

本文档介绍如何在 Kotlin Multiplatform (KMP) 鸿蒙跨端开发中实现一个完整的颜色转换和分析工具系统。这个案例展示了如何使用 Kotlin 的数学计算、字符串处理和颜色理论来创建一个功能丰富的颜色处理工具。通过 KMP,这个工具可以无缝编译到 JavaScript,在 OpenHarmony 应用中运行,并支持用户输入进行实时处理。

工具的特点

  • 多格式支持:支持 Hex、RGB、HSL、RGBA 等多种颜色格式
  • 颜色转换:在不同颜色空间之间进行转换
  • 颜色分析:提供颜色的详细分析信息
  • 调色板生成:自动生成相关颜色
  • 跨端兼容:一份 Kotlin 代码可同时服务多个平台

工具功能

1. 颜色格式转换

  • Hex 格式:十六进制颜色代码 (#RRGGBB)
  • RGB 格式:红绿蓝三色值 (0-255)
  • HSL 格式:色调、饱和度、亮度
  • RGBA 格式:带透明度的 RGB

2. 颜色分析

  • RGB 值:提取红、绿、蓝三色值
  • HSL 值:计算色调、饱和度、亮度
  • 亮度计算:计算颜色的亮度值
  • 颜色名称:识别颜色的基本名称

3. 颜色特性

  • 浅色/深色:判断颜色是浅色还是深色
  • 推荐文字颜色:建议使用黑色或白色文字
  • 互补色:计算颜色的互补色
  • 调色板:生成更浅和更深的变体

4. 透明度处理

  • Alpha 值:提取透明度值
  • 百分比显示:显示透明度百分比
  • RGBA 字符串:生成 RGBA 格式字符串

5. 颜色验证

  • 格式检查:验证十六进制格式
  • 值范围检查:确保 RGB 值在 0-255 范围内
  • 错误提示:提供清晰的错误信息

核心实现

1. 十六进制验证

val isValidHex = cleanColor.matches(Regex("^#[0-9A-F]{6}([0-9A-F]{2})?$"))

代码说明:

这段代码使用正则表达式验证输入的颜色值是否为有效的十六进制格式。正则表达式 ^#[0-9A-F]{6}([0-9A-F]{2})?$ 的含义是:^ 表示字符串开始,# 匹配井号,[0-9A-F]{6} 匹配 6 个十六进制数字(RGB 值),([0-9A-F]{2})? 是可选的 2 个十六进制数字(透明度),$ 表示字符串结束。这样可以验证颜色值是否为标准的 #RRGGBB 或 #RRGGBBAA 格式。

2. RGB 值提取

val hex = cleanColor.substring(1)
val r = hex.substring(0, 2).toInt(16)
val g = hex.substring(2, 4).toInt(16)
val b = hex.substring(4, 6).toInt(16)

这段代码展示了如何从十六进制颜色代码中提取 RGB 值。首先使用 substring(1) 去掉颜色代码前面的 # 符号。然后分别提取红、绿、蓝三个分量,每个分量占 2 个十六进制数字。使用 substring() 方法截取对应位置的子字符串,然后使用 toInt(16) 将十六进制字符串转换为十进制整数。这样可以得到 0-255 范围内的 RGB 值。

3. HSL 值计算

val rNorm = r / 255.0
val gNorm = g / 255.0
val bNorm = b / 255.0

val maxVal = maxOf(rNorm, gNorm, bNorm)
val minVal = minOf(rNorm, gNorm, bNorm)
val l = (maxVal + minVal) / 2

val h = when {
    maxVal == minVal -> 0.0
    maxVal == rNorm -> (60 * ((gNorm - bNorm) / (maxVal - minVal)) + 360) % 360
    maxVal == gNorm -> (60 * ((bNorm - rNorm) / (maxVal - minVal)) + 120) % 360
    else -> (60 * ((rNorm - gNorm) / (maxVal - minVal)) + 240) % 360
}

这段代码实现了 RGB 到 HSL 的转换。首先将 RGB 值归一化到 0-1 范围。计算最大值和最小值,用于后续的计算。计算亮度 L,为最大值和最小值的平均值。计算色调 H,使用 when 表达式根据最大值的位置进行不同的计算。如果最大值和最小值相等(灰色),则色调为 0。如果红色最大,则使用相应的公式计算色调。如果绿色最大或蓝色最大,则使用不同的公式。最后使用模运算确保色调在 0-360 范围内。这是标准的 RGB 到 HSL 转换算法。

4. 亮度计算

val brightness = (r * 299 + g * 587 + b * 114) / 1000
val isLight = brightness > 128

这段代码计算颜色的亮度值。使用加权平均公式计算亮度,权重分别为 299(红)、587(绿)、114(蓝)。这些权重基于人眼对不同颜色的感知敏感度,绿色最敏感,红色次之,蓝色最不敏感。计算结果在 0-255 范围内。通过与 128 比较判断颜色是浅色还是深色。亮度大于 128 为浅色,否则为深色。这个计算对于确定在颜色上显示的文字颜色(黑色或白色)很重要。

5. 互补色计算

val complementHex = "#" + 
    (255 - r).toString(16).uppercase().padStart(2, '0') +
    (255 - g).toString(16).uppercase().padStart(2, '0') +
    (255 - b).toString(16).uppercase().padStart(2, '0')

这段代码计算颜色的互补色。互补色是指在色轮上相对的颜色。计算方法是将每个 RGB 分量从 255 中减去。例如,红色 (255, 0, 0) 的互补色是青色 (0, 255, 255)。首先计算每个分量的互补值(255 减去原值)。然后使用 toString(16) 将十进制数转换为十六进制字符串。使用 uppercase() 将小写字母转换为大写。使用 padStart(2, '0') 确保每个分量都是 2 位数字,不足时前面补 0。最后拼接成完整的十六进制颜色代码。


实战案例

案例:完整的颜色转换和分析工具

Kotlin 源代码
@OptIn(ExperimentalJsExport::class)
@JsExport
fun colorConverterAnalyzer(inputColor: String = "#FF5733"): String {
    if (inputColor.isEmpty()) {
        return "❌ 错误: 颜色值不能为空\n请输入十六进制颜色代码 (如: #FF5733)"
    }
    
    val cleanColor = inputColor.trim().uppercase()
    val isValidHex = cleanColor.matches(Regex("^#[0-9A-F]{6}([0-9A-F]{2})?$"))
    
    if (!isValidHex) {
        return "❌ 错误: 无效的颜色格式\n请使用 #RRGGBB 或 #RRGGBBAA 格式"
    }
    
    val hex = cleanColor.substring(1)
    val r = hex.substring(0, 2).toInt(16)
    val g = hex.substring(2, 4).toInt(16)
    val b = hex.substring(4, 6).toInt(16)
    val a = if (hex.length == 8) hex.substring(6, 8).toInt(16) else 255

这是颜色转换和分析的核心实现函数的开始部分,展示了输入验证和基本的颜色解析。函数使用 @OptIn(ExperimentalJsExport::class)@JsExport 注解,使其可以被编译成 JavaScript 并在 ArkTS 中调用。首先检查输入是否为空,如果为空则返回错误信息。使用 trim() 去除前后空格,uppercase() 转换为大写。使用正则表达式验证颜色格式,支持 #RRGGBB 和 #RRGGBBAA 两种格式。如果格式无效则返回错误信息。从十六进制字符串中提取 RGB 值,使用 toInt(16) 进行十六进制到十进制的转换。如果有透明度值则提取,否则默认为 255(完全不透明)。

val rNorm = r / 255.0
val gNorm = g / 255.0
val bNorm = b / 255.0

val maxVal = maxOf(rNorm, gNorm, bNorm)
val minVal = minOf(rNorm, gNorm, bNorm)
val l = (maxVal + minVal) / 2

val h = when {
    maxVal == minVal -> 0.0
    maxVal == rNorm -> (60 * ((gNorm - bNorm) / (maxVal - minVal)) + 360) % 360
    maxVal == gNorm -> (60 * ((bNorm - rNorm) / (maxVal - minVal)) + 120) % 360
    else -> (60 * ((rNorm - gNorm) / (maxVal - minVal)) + 240) % 360
}

val s = when {
    maxVal == minVal -> 0.0
    l < 0.5 -> (maxVal - minVal) / (maxVal + minVal)
    else -> (maxVal - minVal) / (2 - maxVal - minVal)
}

val rPercent = (r * 100) / 255
val gPercent = (g * 100) / 255
val bPercent = (b * 100) / 255

val rgbString = "rgb($r, $g, $b)"
val rgbaString = "rgba($r, $g, $b, ${(a / 255.0).toInt()})"
val hslString = "hsl(${h.toInt()}, ${(s * 100).toInt()}%, ${(l * 100).toInt()}%)"

val complementHex = "#" + 
    (255 - r).toString(16).uppercase().padStart(2, '0') +
    (255 - g).toString(16).uppercase().padStart(2, '0') +
    (255 - b).toString(16).uppercase().padStart(2, '0')

val brightness = (r * 299 + g * 587 + b * 114) / 1000
val isLight = brightness > 128

val colorName = getColorName(r, g, b)

val lighter = "#" +
    minOf(r + 50, 255).toString(16).uppercase().padStart(2, '0') +
    minOf(g + 50, 255).toString(16).uppercase().padStart(2, '0') +
    minOf(b + 50, 255).toString(16).uppercase().padStart(2, '0')
val darker = "#" +
    maxOf(r - 50, 0).toString(16).uppercase().padStart(2, '0') +
    maxOf(g - 50, 0).toString(16).uppercase().padStart(2, '0') +
    maxOf(b - 50, 0).toString(16).uppercase().padStart(2, '0')

return "🎨 颜色转换和分析\n" +
       "━━━━━━━━━━━━━━━━━━━━━\n" +
       "输入颜色: $cleanColor\n" +
       "颜色名称: $colorName\n\n" +
       "1️⃣ RGB 值:\n" +
       "  十进制: R=$r, G=$g, B=$b\n" +
       "  百分比: R=$rPercent%, G=$gPercent%, B=$bPercent%\n" +
       "  CSS: $rgbString\n\n" +
       "2️⃣ HSL 值:\n" +
       "  H: ${h.toInt()}°\n" +
       "  S: ${(s * 100).toInt()}%\n" +
       "  L: ${(l * 100).toInt()}%\n" +
       "  CSS: $hslString\n\n" +
       "3️⃣ 透明度:\n" +
       "  Alpha: $a (${(a * 100) / 255}%)\n" +
       "  RGBA: $rgbaString\n\n" +
       "4️⃣ 颜色特性:\n" +
       "  亮度: $brightness\n" +
       "  类型: ${if (isLight) "浅色" else "深色"}\n" +
       "  推荐文字: ${if (isLight) "黑色" else "白色"}\n\n" +
       "5️⃣ 相关颜色:\n" +
       "  互补色: $complementHex\n" +
       "  更浅: $lighter\n" +
       "  更深: $darker\n\n" +
       "━━━━━━━━━━━━━━━━━━━━━\n" +
       "✅ 分析完成!"

}

fun getColorName(r: Int, g: Int, b: Int): String {
return when {
r > 200 && g > 200 && b > 200 -> “白色”
r < 50 && g < 50 && b < 50 -> “黑色”
r > g && r > b -> “红色”
g > r && g > b -> “绿色”
b > r && b > g -> “蓝色”
r > 150 && g > 150 && b < 100 -> “黄色”
r > 150 && g < 100 && b > 150 -> “紫色”
r < 100 && g > 150 && b > 150 -> “青色”
else -> “混合色”
}
}

#### ArkTS 调用代码(带输入框)

```typescript
import { colorConverterAnalyzer } from './hellokjs';

@Entry
@Component
struct Index {
  @State message: string = '加载中...';
  @State results: string[] = [];
  @State caseTitle: string = '颜色转换和分析';
  @State inputText: string = '#FF5733';

  aboutToAppear(): void {
    this.loadResults();
  }

  loadResults(): void {
    try {
      const results: string[] = [];
      const algorithmResult = colorConverterAnalyzer(this.inputText);
      results.push(algorithmResult);
      
      this.results = results;
      this.message = '✓ 分析完成';
    } catch (error) {
      this.message = `✗ 错误: ${error}`;
    }
  }

这段 ArkTS 代码是鸿蒙应用的用户界面实现,展示了如何在 ArkTS 中导入和调用编译后的 Kotlin 颜色分析函数。首先使用 import 语句从编译后的 JavaScript 模块中导入 colorConverterAnalyzer 函数。使用 @State 装饰器定义四个响应式状态变量:message 用于显示状态信息,results 存储分析结果,caseTitle 存储案例标题,inputText 存储用户输入的颜色代码。aboutToAppear() 生命周期函数在组件加载时自动调用 loadResults() 进行初始化。loadResults() 方法调用 Kotlin 编译的 JavaScript 函数 colorConverterAnalyzer(),将用户输入的颜色代码传入,获取颜色分析结果,然后更新 resultsmessage。使用 try-catch 块捕获异常,如果发生错误则显示错误信息。

build() {
Column() {
// 顶部标题栏
Row() {
Text(‘KMP 鸿蒙跨端’)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Spacer()
Text(‘Kotlin 案例’)
.fontSize(14)
.fontColor(Color.White)
}
.width(‘100%’)
.height(50)
.backgroundColor(‘#3b82f6’)
.padding({ left: 20, right: 20 })
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.SpaceBetween)

  // 案例标题
  Column() {
    Text(this.caseTitle)
      .fontSize(20)
      .fontWeight(FontWeight.Bold)
      .fontColor('#1f2937')
    Text(this.message)
      .fontSize(13)
      .fontColor('#6b7280')
      .margin({ top: 5 })
  }
  .width('100%')
  .padding({ left: 20, right: 20, top: 20, bottom: 15 })
  .alignItems(HorizontalAlign.Start)

  // 输入框区域
  Column() {
    Text('输入颜色代码:')
      .fontSize(14)
      .fontWeight(FontWeight.Bold)
      .fontColor('#1f2937')
      .margin({ bottom: 8 })
    
    TextInput({ placeholder: '输入十六进制颜色 (如: #FF5733)...', text: this.inputText })
      .width('100%')
      .height(60)
      .padding(12)
      .border({ width: 1, color: '#d1d5db' })
      .borderRadius(6)
      .onChange((value: string) => {
        this.inputText = value
      })
    
    Button('分析')
      .width('100%')
      .height(40)
      .margin({ top: 12 })
      .backgroundColor('#3b82f6')
      .fontColor(Color.White)
      .onClick(() => {
        this.loadResults()
      })
  }
  .width('100%')
  .padding({ left: 16, right: 16, bottom: 16 })

  // 结果显示区域
  Scroll() {
    Column() {
      ForEach(this.results, (result: string) => {
        Column() {
          Text(result)
            .fontSize(13)
            .fontFamily('monospace')
            .fontColor('#374151')
            .width('100%')
            .margin({ top: 10 })
        }
        .width('100%')
        .padding(16)
        .backgroundColor(Color.White)
        .border({ width: 1, color: '#e5e7eb' })
        .borderRadius(8)
        .margin({ bottom: 12 })
      })
    }
    .width('100%')
    .padding({ left: 16, right: 16 })
  }
  .layoutWeight(1)
  .width('100%')

  // 底部按钮区域
  Row() {
    Button('示例颜色')
      .width('48%')
      .height(44)
      .backgroundColor('#10b981')
      .fontColor(Color.White)
      .fontSize(14)
      .onClick(() => {
        this.inputText = '#FF5733'
        this.loadResults()
      })

    Button('清空')
      .width('48%')
      .height(44)
      .backgroundColor('#6b7280')
      .fontColor(Color.White)
      .fontSize(14)
      .onClick(() => {
        this.inputText = ''
        this.results = []
      })
  }
  .width('100%')
  .padding({ left: 16, right: 16, bottom: 20 })
}
.width('100%')
.height('100%')
.backgroundColor('#f9fafb')

}
}

---

## 编译过程详解

### Kotlin 到 JavaScript 的转换

| Kotlin 特性 | JavaScript 等价物 |
|-----------|-----------------|
| toInt(16) | parseInt(str, 16) |
| toString(16) | number.toString(16) |
| padStart() | 字符串填充 |
| maxOf/minOf | Math.max/min |
| when 表达式 | if-else 语句 |

### 关键转换点

1. **十六进制转换**:转换为 JavaScript 的 parseInt 和 toString
2. **数学计算**:保持功能一致
3. **字符串处理**:转换为 JavaScript 字符串方法
4. **条件判断**:转换为 if-else 语句

---

## 工具扩展

### 扩展 1:添加 CMYK 转换

```kotlin
fun rgbToCmyk(r: Int, g: Int, b: Int): Quadruple<Int, Int, Int, Int> {
    val rNorm = r / 255.0
    val gNorm = g / 255.0
    val bNorm = b / 255.0
    val k = 1 - maxOf(rNorm, gNorm, bNorm)
    val c = (1 - rNorm - k) / (1 - k)
    val m = (1 - gNorm - k) / (1 - k)
    val y = (1 - bNorm - k) / (1 - k)
    return Quadruple((c * 100).toInt(), (m * 100).toInt(), (y * 100).toInt(), (k * 100).toInt())
}

这段代码实现了 RGB 到 CMYK 的转换。CMYK 是印刷行业使用的颜色模型。首先将 RGB 值归一化到 0-1 范围。计算黑色分量 K,为 1 减去 RGB 中的最大值。然后计算青色、品红色和黄色分量,使用相应的公式进行转换。最后将百分比值转换为整数并返回。这个转换对于准备印刷文件很重要。

扩展 2:添加颜色梯度生成

fun generateColorGradient(startColor: String, endColor: String, steps: Int): List<String> {
    // 实现颜色梯度生成
    return emptyList()
}

这段代码定义了颜色梯度生成函数的框架。颜色梯度是指在两种颜色之间平滑过渡的颜色序列。函数接收起始颜色、结束颜色和步数作为参数。实现方式是在两种颜色的 RGB 值之间进行线性插值,根据步数生成中间的颜色。这个功能对于创建渐变背景、热力图等可视化效果很有用。

扩展 3:添加颜色和谐

fun generateHarmony(baseColor: String, type: String): List<String> {
    // 生成互补色、三色和、四色和等
    return emptyList()
}

这段代码定义了颜色和谐生成函数的框架。颜色和谐是指根据色彩理论生成搭配良好的颜色组合。函数接收基础颜色和和谐类型作为参数。不同的和谐类型包括互补色(相对 180 度)、三色和(相隔 120 度)、四色和(相隔 90 度)等。这个功能对于设计师选择配色方案很有帮助。

扩展 4:添加颜色对比度检查

fun checkContrast(color1: String, color2: String): Double {
    // 计算 WCAG 对比度
    return 0.0
}

这段代码定义了颜色对比度检查函数的框架。颜色对比度是指两种颜色的亮度差异程度,用于评估文本和背景的可读性。WCAG(Web 内容无障碍指南)定义了对比度的标准,用于确保网站对视力受损的用户也是可读的。函数接收两种颜色作为参数,返回对比度值。对比度值越高,两种颜色的区分度越大。这个功能对于确保应用的可访问性很重要。


最佳实践

1. 使用 padStart() 格式化

// ✅ 好:使用 padStart()
val hex = r.toString(16).uppercase().padStart(2, '0')

// ❌ 不好:手动填充
val hex = if (r < 16) "0" + r.toString(16) else r.toString(16)

这段代码对比了两种十六进制格式化的方式。第一种方法使用 padStart() 函数,将字符串填充到指定长度,不足时在前面补充指定的字符。这种方式简洁、高效、易读。第二种方法手动检查数值大小,然后决定是否添加前导零。这种方式代码冗长,容易出错。使用 padStart() 是更好的做法。

2. 使用 maxOf/minOf

// ✅ 好:使用 maxOf/minOf
val lighter = minOf(r + 50, 255)

// ❌ 不好:使用 if 语句
val lighter = if (r + 50 > 255) 255 else r + 50

这段代码对比了两种限制数值范围的方式。第一种方法使用 minOf() 函数,直接返回两个值中的较小值。这种方式简洁、高效、意图明确。第二种方法使用 if 语句进行条件判断。这种方式代码冗长,不如函数式方法清晰。使用 maxOf()minOf() 是更优雅的做法。

3. 使用 when 表达式

// ✅ 好:使用 when
val colorName = when {
    r > g && r > b -> "红色"
    else -> "其他"
}

// ❌ 不好:使用多个 if
var colorName = ""
if (r > g && r > b) colorName = "红色"
else colorName = "其他"

这段代码对比了两种多条件判断的方式。第一种方法使用 when 表达式,这是 Kotlin 的特色功能,可以同时处理多个条件。when 表达式返回值,可以直接赋给常量,代码简洁、高效。第二种方法使用多个 if 语句,需要先声明可变变量,然后逐个赋值。这种方式代码冗长,容易出错。使用 when 表达式是 Kotlin 的最佳实践。

4. 验证输入

// ✅ 好:验证输入
if (!isValidHex) return "❌ 错误"

// ❌ 不好:不验证
val r = hex.substring(0, 2).toInt(16)  // 可能出错

这段代码对比了两种处理用户输入的方式。第一种方法先验证输入的有效性,如果无效则立即返回错误信息。这种方式可以防止后续操作因为无效输入而出错。第二种方法直接使用输入进行操作,没有验证。如果输入格式不正确,可能导致异常或错误的结果。验证输入是编写健壮代码的必要步骤。


常见问题

Q1: 如何转换为 CMYK?

A: 使用 RGB 到 CMYK 的转换公式:

fun rgbToCmyk(r: Int, g: Int, b: Int): String {
    val rNorm = r / 255.0
    val gNorm = g / 255.0
    val bNorm = b / 255.0
    val k = 1 - maxOf(rNorm, gNorm, bNorm)
    val c = ((1 - rNorm - k) / (1 - k) * 100).toInt()
    val m = ((1 - gNorm - k) / (1 - k) * 100).toInt()
    val y = ((1 - bNorm - k) / (1 - k) * 100).toInt()
    return "cmyk($c%, $m%, $y%, ${(k * 100).toInt()}%)"
}

Q2: 如何计算颜色对比度?

A: 使用 WCAG 对比度公式:

fun calculateContrast(color1: String, color2: String): Double {
    val lum1 = calculateLuminance(color1)
    val lum2 = calculateLuminance(color2)
    val lighter = maxOf(lum1, lum2)
    val darker = minOf(lum1, lum2)
    return (lighter + 0.05) / (darker + 0.05)
}

Q3: 如何生成颜色梯度?

A: 在两种颜色之间插值:

fun generateGradient(start: String, end: String, steps: Int): List<String> {
    val result = mutableListOf<String>()
    for (i in 0..steps) {
        val ratio = i.toDouble() / steps
        val r = (startR + (endR - startR) * ratio).toInt()
        val g = (startG + (endG - startG) * ratio).toInt()
        val b = (startB + (endB - startB) * ratio).toInt()
        result.add("#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}")
    }
    return result
}

Q4: 如何处理无效的颜色格式?

A: 使用正则表达式验证:

val isValidHex = color.matches(Regex("^#[0-9A-F]{6}([0-9A-F]{2})?$"))
if (!isValidHex) return "❌ 无效的颜色格式"

Q5: 如何生成互补色?

A: 在 HSL 色轮上旋转 180 度:

val complementH = (h + 180) % 360

总结

关键要点

  • ✅ 使用正则表达式验证颜色格式
  • ✅ 实现 RGB 到 HSL 的转换
  • ✅ 计算颜色的亮度和对比度
  • ✅ 生成相关颜色和调色板
  • ✅ KMP 能无缝编译到 JavaScript

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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐