HarmonyOS NEXT 实战:开发一个精美的随机颜色生成器

HarmonyOS NEXT 实战:开发一个精美的随机颜色生成器

本文详细记录了使用 HarmonyOS NEXT 开发随机颜色生成器的完整过程,涵盖颜色算法设计、Grid 网格布局、状态管理、交互设计等核心技术点,适合初学者入门实战参考。

一、项目背景

作为设计师或开发者,我们经常需要寻找合适的配色方案。有时候灵感枯竭,需要一个工具来随机生成颜色,激发创意灵感。虽然网上有很多在线配色工具,但一个简洁的手机应用可以随时随地使用,更加方便。

本文将带领大家使用 HarmonyOS NEXT 开发一个功能完善的随机颜色生成器应用,最终实现:

  • ✅ 一键生成随机颜色
  • ✅ 显示 HEX 和 RGB 两种颜色值
  • ✅ 颜色历史记录(最多保存 12 个)
  • ✅ 点击历史色块快速应用
  • ✅ 清空历史记录
  • ✅ 精美的 UI 设计(Material 风格)

二、开发环境

项目 版本
DevEco Studio 5.0.3.403
HarmonyOS SDK API 23(6.1.0)
设备类型 Phone
项目模型 Stage 模型

三、项目结构

复制代码
MyApplication/
├── AppScope/
│   └── app.json5                    # 应用全局配置
├── entry/
│   ├── src/main/
│   │   ├── ets/
│   │   │   ├── entryability/
│   │   │   │   └── EntryAbility.ets # 应用入口
│   │   │   └── pages/
│   │   │       └── Index.ets        # 主页面(核心代码)
│   │   ├── resources/
│   │   │   └── base/
│   │   │       ├── element/
│   │   │       │   └── string.json  # 字符串资源
│   │   │       └── media/           # 图片资源
│   │   └── module.json5             # 模块配置
│   ├── build-profile.json5          # 构建配置
│   └── oh-package.json5             # 依赖配置
├── build-profile.json5              # 项目构建配置
└── hvigorfile.ts                    # 构建脚本

四、核心功能设计

4.1 需求分析

随机颜色生成器需要实现以下功能:

功能 说明
生成随机色 点击按钮或预览区生成随机颜色
颜色值显示 同时显示 HEX 和 RGB 格式
历史记录 保存最近生成的颜色(最多 12 个)
快速应用 点击历史色块可应用该颜色
清空历史 一键清空所有历史记录

4.2 状态变量设计

typescript 复制代码
@Entry
@Component
struct Index {
  @State currentColor: string = '#FF6200EE';   // 当前显示颜色(带透明度)
  @State hexValue: string = '#6200EE';          // HEX 颜色值
  @State rValue: number = 98;                   // R 分量
  @State gValue: number = 0;                    // G 分量
  @State bValue: number = 238;                  // B 分量
  @State colorHistory: string[] = [];           // 颜色历史数组
  @State copyFeedback: string = '';             // 操作反馈提示

  private maxHistory: number = 12;              // 最大历史记录数
}

五、核心算法实现

5.1 随机颜色生成算法

typescript 复制代码
generateColor(): void {
  // 生成鲜艳的随机颜色(RGB 值范围 30-230)
  let r = Math.floor(Math.random() * 200) + 30;
  let g = Math.floor(Math.random() * 200) + 30;
  let b = Math.floor(Math.random() * 200) + 30;

  this.rValue = r;
  this.gValue = g;
  this.bValue = b;

  // 格式化为 HEX
  let hexR = r.toString(16).padStart(2, '0').toUpperCase();
  let hexG = g.toString(16).padStart(2, '0').toUpperCase();
  let hexB = b.toString(16).padStart(2, '0').toUpperCase();

  this.hexValue = `#${hexR}${hexG}${hexB}`;
  this.currentColor = `#FF${hexR}${hexG}${hexB}`;

  // 自动加入历史
  this.addToHistory();
}

算法说明

为什么用 Math.random() * 200 + 30

如果直接使用 Math.random() * 255,生成的颜色可能会太亮(接近白色)或太暗(接近黑色),视觉效果不好。

typescript 复制代码
// ❌ 不推荐:可能生成太亮或太暗的颜色
let r = Math.floor(Math.random() * 256);

// ✅ 推荐:生成鲜艳的颜色
let r = Math.floor(Math.random() * 200) + 30;  // 范围: 30-230

这样生成的颜色亮度适中,更加鲜艳美观。

HEX 格式转换
typescript 复制代码
let hexR = r.toString(16).padStart(2, '0').toUpperCase();
  • toString(16):将数字转为 16 进制字符串
  • padStart(2, '0'):不足两位前面补 0(如 F0F
  • toUpperCase():转大写(如 ffFF

示例

  • R = 98 → 62
  • G = 0 → 00
  • B = 238 → EE
  • 结果:#6200EE

5.2 颜色历史管理

typescript 复制代码
addToHistory(): void {
  // 去重:如果已存在相同颜色,先移除
  let index = this.colorHistory.indexOf(this.hexValue);
  if (index >= 0) {
    this.colorHistory.splice(index, 1);
  }

  // 插入到最前(最新的在最前面)
  this.colorHistory.unshift(this.hexValue);

  // 限制历史数量(最多保存 12 个)
  if (this.colorHistory.length > this.maxHistory) {
    this.colorHistory.pop();
  }

  // 强制刷新数组(触发 UI 更新)
  this.colorHistory = [...this.colorHistory];
}

设计要点

  1. 去重:相同的颜色只保留一个,新的放在最前面
  2. 限制数量:最多保存 12 个,超过时移除最旧的
  3. 强制刷新:使用展开运算符创建新数组,确保 UI 更新

5.3 应用历史颜色

typescript 复制代码
applyColor(color: string): void {
  this.hexValue = color;
  this.currentColor = '#FF' + color.substring(1);
  
  // 解析 RGB 值
  this.rValue = parseInt(color.substring(1, 3), 16);
  this.gValue = parseInt(color.substring(3, 5), 16);
  this.bValue = parseInt(color.substring(5, 7), 16);
  
  this.showFeedback('✅ 已应用颜色');
}

HEX 转 RGB

typescript 复制代码
// HEX: #6200EE
// substring(1, 3) = "62" → R
// substring(3, 5) = "00" → G
// substring(5, 7) = "EE" → B

this.rValue = parseInt("62", 16);  // 98
this.gValue = parseInt("00", 16);  // 0
this.bValue = parseInt("EE", 16);  // 238

六、UI 界面设计

6.1 整体布局结构

typescript 复制代码
build() {
  Column() {
    // 1. 颜色预览区(占据 4/7 高度)
    Column() { ... }
      .layoutWeight(4)

    // 2. 颜色历史区域(占据 3/7 高度)
    Column() { ... }
      .layoutWeight(3)

    // 3. 底部工具栏
    Row() { ... }

    // 4. 操作反馈提示
    if (this.copyFeedback) { ... }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#FFF2F2F7')
}

6.2 颜色预览区

typescript 复制代码
Column() {
  // 颜色值显示
  Column() {
    Text(this.hexValue)
      .fontSize(36)
      .fontWeight(FontWeight.Bold)
      .fontColor(Color.White)

    Text(`RGB(${this.rValue}, ${this.gValue}, ${this.bValue})`)
      .fontSize(16)
      .fontColor('#DDFFFFFF')
      .margin({ top: 6 })
      .fontWeight(FontWeight.Medium)
  }
  .alignItems(HorizontalAlign.Center)
  .justifyContent(FlexAlign.Center)
  .width('100%')
  .layoutWeight(1)

  // 提示文字
  Text('点击任意位置生成新颜色')
    .fontSize(14)
    .fontColor('#99FFFFFF')
    .margin({ bottom: 30 })
}
.width('100%')
.layoutWeight(4)
.backgroundColor(this.currentColor)
.onClick(() => {
  this.generateColor();
})

设计亮点

  • 颜色预览区背景色动态变化
  • 点击预览区任意位置即可生成新颜色
  • 同时显示 HEX 和 RGB 两种格式

6.3 颜色历史区域

标题栏
typescript 复制代码
Row() {
  Text('颜色历史')
    .fontSize(16)
    .fontWeight(FontWeight.Medium)
    .fontColor('#CC000000')

  Blank()

  Text(`${this.colorHistory.length}`)
    .fontSize(14)
    .fontColor('#66000000')

  if (this.colorHistory.length > 0) {
    Text('清空')
      .fontSize(14)
      .fontColor('#FF007AFF')
      .margin({ left: 12 })
      .onClick(() => {
        this.colorHistory = [];
      })
  }
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 8 })
历史色块网格
typescript 复制代码
if (this.colorHistory.length > 0) {
  Grid() {
    ForEach(this.colorHistory, (color: string, index: number) => {
      GridItem() {
        Column() {
          // 色块
          Row()
            .width('100%')
            .layoutWeight(1)
            .backgroundColor(color)
            .borderRadius(8)
            .onClick(() => {
              this.applyColor(color);
            })

          // 颜色标签
          Text(color.length > 7 ? color.substring(0, 7) : color)
            .fontSize(10)
            .fontColor('#66000000')
            .margin({ top: 2 })
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
        }
        .width('100%')
        .height(70)
      }
    })
  }
  .columnsTemplate('1fr 1fr 1fr 1fr')  // 4 列
  .rowsTemplate('1fr 1fr 1fr')          // 3 行
  .columnsGap(6)
  .rowsGap(6)
  .padding({ left: 16, right: 16, bottom: 8 })
  .width('100%')
  .layoutWeight(1)
}

Grid 布局说明

  • columnsTemplate('1fr 1fr 1fr 1fr'):4 列等宽
  • rowsTemplate('1fr 1fr 1fr'):3 行等高
  • 最多显示 12 个颜色(4 × 3)
空状态提示
typescript 复制代码
else {
  Column() {
    Text('点击上方生成颜色\n历史记录将显示在这里')
      .fontSize(14)
      .fontColor('#66000000')
      .textAlign(TextAlign.Center)
      .lineHeight(22)
  }
  .width('100%')
  .layoutWeight(1)
  .justifyContent(FlexAlign.Center)
}

6.4 底部工具栏

typescript 复制代码
Row() {
  // 生成随机色按钮
  Button({ type: ButtonType.Capsule, stateEffect: true }) {
    Row() {
      Text('🎲')
        .fontSize(18)
      Text(' 生成随机色')
        .fontSize(16)
        .fontColor(Color.White)
        .fontWeight(FontWeight.Medium)
    }
    .alignItems(VerticalAlign.Center)
  }
  .width(180)
  .height(48)
  .backgroundColor('#FF6200EE')
  .onClick(() => {
    this.generateColor();
  })

  // 保存到历史按钮
  Button({ type: ButtonType.Circle, stateEffect: true }) {
    Text('+')
      .fontSize(24)
      .fontColor('#FF6200EE')
      .fontWeight(FontWeight.Bold)
  }
  .width(48)
  .height(48)
  .backgroundColor('#FFE8DEF8')
  .margin({ left: 16 })
  .onClick(() => {
    this.addToHistory();
  })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding({ top: 8, bottom: 16 })
.backgroundColor(Color.White)

两个按钮功能

按钮 类型 功能
🎲 生成随机色 胶囊按钮 生成新的随机颜色
+ 圆形按钮 手动添加当前颜色到历史

注意:生成随机色时会自动添加到历史,所以圆形按钮主要用于手动保存喜欢的颜色。

6.5 圆角卡片效果

历史区域使用圆角卡片设计:

typescript 复制代码
Column() {
  // 历史内容...
}
.width('100%')
.layoutWeight(3)
.backgroundColor(Color.White)
.borderRadius({ topLeft: 20, topRight: 20 })
.margin({ top: -20 })
.padding({ top: 16 })

设计要点

  • 只设置顶部圆角(topLefttopRight
  • margin({ top: -20 }):向上偏移,与预览区重叠
  • 形成悬浮卡片效果

七、完整代码

typescript 复制代码
@Entry
@Component
struct Index {
  @State currentColor: string = '#FF6200EE';
  @State hexValue: string = '#6200EE';
  @State rValue: number = 98;
  @State gValue: number = 0;
  @State bValue: number = 238;
  @State colorHistory: string[] = [];
  @State copyFeedback: string = '';

  private maxHistory: number = 12;

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

  build() {
    Column() {
      // 颜色预览区
      Column() {
        Column() {
          Text(this.hexValue)
            .fontSize(36)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)

          Text(`RGB(${this.rValue}, ${this.gValue}, ${this.bValue})`)
            .fontSize(16)
            .fontColor('#DDFFFFFF')
            .margin({ top: 6 })
            .fontWeight(FontWeight.Medium)
        }
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
        .width('100%')
        .layoutWeight(1)

        Text('点击任意位置生成新颜色')
          .fontSize(14)
          .fontColor('#99FFFFFF')
          .margin({ bottom: 30 })
      }
      .width('100%')
      .layoutWeight(4)
      .backgroundColor(this.currentColor)
      .onClick(() => {
        this.generateColor();
      })

      // 颜色历史区域
      Column() {
        Row() {
          Text('颜色历史')
            .fontSize(16)
            .fontWeight(FontWeight.Medium)
            .fontColor('#CC000000')

          Blank()

          Text(`${this.colorHistory.length}`)
            .fontSize(14)
            .fontColor('#66000000')

          if (this.colorHistory.length > 0) {
            Text('清空')
              .fontSize(14)
              .fontColor('#FF007AFF')
              .margin({ left: 12 })
              .onClick(() => {
                this.colorHistory = [];
              })
          }
        }
        .width('100%')
        .padding({ left: 16, right: 16, bottom: 8 })

        if (this.colorHistory.length > 0) {
          Grid() {
            ForEach(this.colorHistory, (color: string, index: number) => {
              GridItem() {
                Column() {
                  Row()
                    .width('100%')
                    .layoutWeight(1)
                    .backgroundColor(color)
                    .borderRadius(8)
                    .onClick(() => {
                      this.applyColor(color);
                    })

                  Text(color.length > 7 ? color.substring(0, 7) : color)
                    .fontSize(10)
                    .fontColor('#66000000')
                    .margin({ top: 2 })
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                }
                .width('100%')
                .height(70)
              }
            })
          }
          .columnsTemplate('1fr 1fr 1fr 1fr')
          .rowsTemplate('1fr 1fr 1fr')
          .columnsGap(6)
          .rowsGap(6)
          .padding({ left: 16, right: 16, bottom: 8 })
          .width('100%')
          .layoutWeight(1)
        } else {
          Column() {
            Text('点击上方生成颜色\n历史记录将显示在这里')
              .fontSize(14)
              .fontColor('#66000000')
              .textAlign(TextAlign.Center)
              .lineHeight(22)
          }
          .width('100%')
          .layoutWeight(1)
          .justifyContent(FlexAlign.Center)
        }
      }
      .width('100%')
      .layoutWeight(3)
      .backgroundColor(Color.White)
      .borderRadius({ topLeft: 20, topRight: 20 })
      .margin({ top: -20 })
      .padding({ top: 16 })

      // 底部工具栏
      Row() {
        Button({ type: ButtonType.Capsule, stateEffect: true }) {
          Row() {
            Text('🎲')
              .fontSize(18)
            Text(' 生成随机色')
              .fontSize(16)
              .fontColor(Color.White)
              .fontWeight(FontWeight.Medium)
          }
          .alignItems(VerticalAlign.Center)
        }
        .width(180)
        .height(48)
        .backgroundColor('#FF6200EE')
        .onClick(() => {
          this.generateColor();
        })

        Button({ type: ButtonType.Circle, stateEffect: true }) {
          Text('+')
            .fontSize(24)
            .fontColor('#FF6200EE')
            .fontWeight(FontWeight.Bold)
        }
        .width(48)
        .height(48)
        .backgroundColor('#FFE8DEF8')
        .margin({ left: 16 })
        .onClick(() => {
          this.addToHistory();
        })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .padding({ top: 8, bottom: 16 })
      .backgroundColor(Color.White)

      // 操作反馈
      if (this.copyFeedback) {
        Text(this.copyFeedback)
          .fontSize(13)
          .fontColor(Color.White)
          .backgroundColor('#CC000000')
          .borderRadius(16)
          .padding({ left: 16, right: 16, top: 6, bottom: 6 })
          .margin({ bottom: 8 })
          .transition({ type: TransitionType.Insert, opacity: 0 })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFF2F2F7')
  }

  generateColor(): void {
    let r = Math.floor(Math.random() * 200) + 30;
    let g = Math.floor(Math.random() * 200) + 30;
    let b = Math.floor(Math.random() * 200) + 30;

    this.rValue = r;
    this.gValue = g;
    this.bValue = b;

    let hexR = r.toString(16).padStart(2, '0').toUpperCase();
    let hexG = g.toString(16).padStart(2, '0').toUpperCase();
    let hexB = b.toString(16).padStart(2, '0').toUpperCase();

    this.hexValue = `#${hexR}${hexG}${hexB}`;
    this.currentColor = `#FF${hexR}${hexG}${hexB}`;

    this.addToHistory();
  }

  addToHistory(): void {
    let index = this.colorHistory.indexOf(this.hexValue);
    if (index >= 0) {
      this.colorHistory.splice(index, 1);
    }

    this.colorHistory.unshift(this.hexValue);

    if (this.colorHistory.length > this.maxHistory) {
      this.colorHistory.pop();
    }

    this.colorHistory = [...this.colorHistory];
  }

  applyColor(color: string): void {
    this.hexValue = color;
    this.currentColor = '#FF' + color.substring(1);
    this.rValue = parseInt(color.substring(1, 3), 16);
    this.gValue = parseInt(color.substring(3, 5), 16);
    this.bValue = parseInt(color.substring(5, 7), 16);
    this.showFeedback('✅ 已应用颜色');
  }

  showFeedback(msg: string): void {
    this.copyFeedback = msg;
    setTimeout(() => {
      this.copyFeedback = '';
    }, 2000);
  }
}

八、运行效果

九、踩坑记录

9.1 颜色格式问题

问题:设置背景色时,HEX 格式需要带透明度。

原因 :ArkUI 的 backgroundColor 要求 #AARRGGBB 格式(AA 为透明度)。

解决

typescript 复制代码
// ❌ 错误:缺少透明度
this.currentColor = '#6200EE';

// ✅ 正确:添加 FF 透明度(完全不透明)
this.currentColor = '#FF6200EE';

9.2 数组更新不触发 UI 刷新

问题:修改数组后,Grid 列表没有更新。

原因:直接修改数组元素不会触发响应式更新。

解决:创建新数组替换

typescript 复制代码
// ❌ 错误:直接修改不会触发更新
this.colorHistory.push(color);

// ✅ 正确:创建新数组
this.colorHistory = [...this.colorHistory];

9.3 Grid 布局间距问题

问题:Grid 子项之间没有间距,挤在一起。

解决 :使用 columnsGaprowsGap 属性

typescript 复制代码
Grid() {
  // ...
}
.columnsGap(6)
.rowsGap(6)

9.4 圆角卡片遮盖问题

问题:历史区域的圆角卡片遮住了预览区底部。

解决:使用负边距让卡片向上偏移

typescript 复制代码
.borderRadius({ topLeft: 20, topRight: 20 })
.margin({ top: -20 })  // 向上偏移

十、总结与扩展

10.1 项目亮点

  1. 智能颜色生成:避免生成太亮或太暗的颜色
  2. 历史管理:去重、限制数量、最新的在最前
  3. Grid 网格布局:整齐展示 12 个颜色
  4. 圆角卡片设计:现代感 UI
  5. 多种交互方式:点击预览区、点击按钮、点击历史色块

10.2 可扩展功能

  • 复制颜色值到剪贴板
  • 颜色收藏功能
  • 导出配色方案
  • 深色模式适配
  • 颜色名称显示(如 "薰衣草紫")
  • 支持更多颜色格式(HSL、CMYK)

10.3 学习收获

知识点 在项目中的应用
@State 装饰器 响应式状态管理
Math.random() 随机数生成
toString(16) 进制转换
Grid 组件 网格布局
ForEach 列表渲染
Button 类型 胶囊/圆形按钮

十一、参考资料


如果这篇文章对你有帮助,欢迎点赞、收藏、评论!有任何问题也可以留言讨论~ 🎨

相关推荐
G_dou_1 小时前
Flutter三方库适配OpenHarmony【color_picker】HSL 调色器项目完整实战
flutter·harmonyos
G_dou_1 小时前
Flutter三方库适配OpenHarmony【random_number】随机数生成器项目完整实战
flutter·harmonyos
FrameNotWork2 小时前
HarmonyOS 6.1 云应用客户端适配实战(三):触摸输入与坐标映射
华为·harmonyos
●VON2 小时前
鸿蒙Flutter实战:日期选择器与截止日期高亮提醒
android·flutter·华为·harmonyos·鸿蒙
●VON2 小时前
鸿蒙Flutter实战:Material 3种子色亮暗双主题系统
android·flutter·harmonyos
慧海灵舟2 小时前
鸿蒙南向开发教程 Day 3:OpenHarmony 线程管理
华为·harmonyos·写文章,赢小鸿ai
想你依然心痛2 小时前
HarmonyOS 6(API 23)实战:打造“光味智厨“——AI烹饪新体验
人工智能·华为·ar·harmonyos·智能体
颜淡慕潇3 小时前
低成本搭建鸿蒙PC运行环境:基于 Docker 的 x86_64 服务器
服务器·docker·harmonyos
慧海灵舟4 小时前
鸿蒙南向开发教程 Day 6:事件标志组(Event Flags)
华为·harmonyos