当前位置: 首页 > article >正文

鸿蒙UI开发——颜色选择器

1、概 述

取色对话框在实际项目开发中非常常见(效果类似如下图),有朋友留言希望讨论一下,那我们来实现一个基本的颜色选择器。

图片

申明:

核心代码参考自项目:https://gitee.com/yranfantasy/harmony-color-picker。感谢这位朋友

图片

2、颜色转换工具函数

在开始实现前,我们一般会用到几个转换工具函数,例如将 #ff000格式的颜色转换为[r,g,b]数组,同样,我们需要将[r,g,b]数组转换为对应的颜色表达字符串。相关的函数已经非常成熟,本文贴一个,代码如下(color-utils.ets文件):

// color-utils.etsclass ColorUtils {  hexToRgb(hex: string): [number, number, number] {    const r = parseInt(hex.slice(1, 3), 16);    const g = parseInt(hex.slice(3, 5), 16);    const b = parseInt(hex.slice(5, 7), 16);    return [r, g, b];  }  hexToHsv(hex: string): [number, number, number] {    hex = hex.replace('#', '');    const r = parseInt(hex.slice(0, 2), 16);    const g = parseInt(hex.slice(2, 4), 16);    const b = parseInt(hex.slice(4, 6), 16);    const max = Math.max(r, g, b);    const min = Math.min(r, g, b);    const value = max / 255;    let saturation = (max !== 0) ? ((max - min) / max) : 0;    let hue = 0;    if (saturation === 0) {      hue = 0;    } else {      switch (max) {        case r:          hue = ((g - b) / (max - min)) + (g < b ? 6 : 0);          break;        case g:          hue = ((b - r) / (max - min)) + 2;          break;        case b:          hue = ((r - g) / (max - min)) + 4;          break;      }      hue /= 6;      hue = hue >= 0 ? hue : hue + 1;    }    return [hue * 360, saturation, value];  }  hsvToHex(h: number, s: number, v: number): string {    let r: number = 0, g: number = 0, b: number = 0;    let i = Math.floor(h / 60);    let f = h / 60 - i;    let p = v * (1 - s);    let q = v * (1 - f * s);    let t = v * (1 - (1 - f) * s);    switch (i % 6) {      case 0:        r = v;        g = t;        b = p;        break;      case 1:        r = q;        g = v;        b = p;        break;      case 2:        r = p;        g = v;        b = t;        break;      case 3:        r = p;        g = q;        b = v;        break;      case 4:        r = t;        g = p;        b = v;        break;      case 5:        r = v;        g = p;        b = q;        break;    }    r = Math.round(r * 255);    g = Math.round(g * 255);    b = Math.round(b * 255);    return `#${this.toHex(r)}${this.toHex(g)}${this.toHex(b)}`;  }  toHex(n: number) {    let hex = n.toString(16);    return hex.length === 1 ? '0' + hex : hex;  }}export default new ColorUtils()

3、实现取色面板

我们的取色面板合适是使用两个Canvas去实现。

Canvas的使用我们已经讨论过很多个案例了,这里不再展开介绍基本用法,参考文章:

  • 鸿蒙UI系统组件15——画布(Canvas)

  • 鸿蒙开发实战——使用Canvas绘制环形文字;

  • 鸿蒙开发实战——抽奖转盘

我们想实现的取色面板效果如下:

图片

代码如下(注意第2行,我们导入了上一步的工具函数):​​​​​​​

import { Size } from '@kit.ArkUI'import ColorUtils from './color-utils'class Point {  x: number = 0  y: number = 0  constructor(x: number, y: number)  constructor();  constructor(x: number = 0, y: number = 0) {    this.x = x;    this.y = y;  }}@CustomDialogexport struct ColorPickDialog {  controller: CustomDialogController  private settings: RenderingContextSettings = new RenderingContextSettings(true)  private hueContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)  private satValContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)  /**   * hue panel point   */  private hueTrackerPointSize: Size = { width: 20, height: 20 }  @State private hueTrackerPoint: Point =    new Point(0 - this.hueTrackerPointSize.height * 0.5, 0 - this.hueTrackerPointSize.width * 0.5)  /**   * satVal panel point   */  private satValTrackerPointSize: Size = { width: 24, height: 24 }  @State private satValTrackerPoint: Point =    new Point(0 - this.satValTrackerPointSize.width * 0.5, 0 - this.satValTrackerPointSize.height * 0.5)  /**   * 默认颜色   */  @Link color: string  @State private hue: number = 360;  @State private sat: number = 0;  @State private val: number = 0;  aboutToAppear(): void {    // hex to hsv    const hsv = ColorUtils.hexToHsv(this.color)    this.hue = hsv[0]    this.sat = hsv[1]    this.val = hsv[2]  }  build() {    Column() {      this.TitleBar()      this.SatValPanel()      Row() {        this.HuePanel()      }.margin(15)      Stack() {        TextInput({ text: this.color })          .fontColor('#1d2129')          .fontSize(16)          .backgroundColor(Color.Transparent)          .onEditChange((isEditing) => {            if (!isEditing) {              const hsv = ColorUtils.hexToHsv(this.color)              this.hue = hsv[0]              this.sat = hsv[1]              this.val = hsv[2]              // invalidate canvas              this.invalidateHuePanel()              this.invalidateSatValPanel()            }          })          .onChange((value) => {            this.color = value          })      }      .width(120)      .height(35)      .backgroundColor('#e5e6eb')      .margin({ bottom: 10 })    }  }  @Builder  private TitleBar() {    RelativeContainer() {      Row() {        Text('鸿蒙取色器')          .fontWeight(FontWeight.Bold)          .fontSize(16)          .layoutWeight(1)          .textAlign(TextAlign.Center)      }      .justifyContent(FlexAlign.Center)      .alignRules( {        'center': {'anchor': '__container__', 'align': VerticalAlign.Center },        'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start }      })      Row() {        SymbolGlyph($r('sys.symbol.xmark'))          .fontSize(20)          .fontWeight(FontWeight.Bold)          .fontColor([$r('sys.color.font_primary')])          .onClick(() => {            this.controller.close();          })      }      .justifyContent(FlexAlign.Center)      .alignRules(        {          'right': { 'anchor': '__container__', 'align': HorizontalAlign.End },          'center': {'anchor': '__container__', 'align': VerticalAlign.Center }        }      )      .margin({right: 10})    }    .width('100%')    .height(40)  }  @Builder  private SatValPanel() {    Stack() {      Canvas(this.satValContext)        .width('90%')        .height(200)        .margin({ top: 20 })        .onReady(() => {          this.drawSatValPanel(true)        })        .onTouch((event) => {          let x = event.touches[0].x          let y = event.touches[0].y          if (x >= this.satValContext.width) {            x = this.satValContext.width          }          if (x < 0) {            x = 0          }          if (y >= this.satValContext.height) {            y = this.satValContext.height          }          if (y < 0) {            y = 0          }          this.satValTrackerPoint =            new Point(x - this.satValTrackerPointSize.width * 0.5, y - this.satValTrackerPointSize.height * 0.5)          this.color = this.getColor()          const p = this.pointToSatVal(x, y)          this.sat = p[0]          this.val = p[1]        })      Shape() {        Circle()          .size(this.satValTrackerPointSize)          .fill(Color.Transparent)          .borderRadius(this.satValTrackerPointSize.width / 2)          .border({ color: Color.White, width: 4 })          .fill(Color.Transparent);      }      .margin({ top: 20 })      .enabled(false)      .focusOnTouch(false)      .position({ x: this.satValTrackerPoint.x, y: this.satValTrackerPoint.y })    }  }  private drawSatValPanel(isUpdateTrackerPoint: boolean = false) {    this.satValContext.clearRect(0, 0, this.satValContext.width, this.satValContext.height)    this.satValContext.fillStyle = ColorUtils.hsvToHex(this.hue, 1, 1);    this.satValContext.fillRect(0, 0, this.satValContext.width, this.satValContext.height);    const whiteGradient = this.satValContext.createLinearGradient(0, 0, this.satValContext.width, 0);    whiteGradient.addColorStop(0, "#fff");    whiteGradient.addColorStop(1, "transparent");    this.satValContext.fillStyle = whiteGradient;    this.satValContext.fillRect(0, 0, this.satValContext.width, this.satValContext.height);    const blackGradient = this.satValContext.createLinearGradient(0, 0, 0, this.satValContext.height);    blackGradient.addColorStop(0, "transparent");    blackGradient.addColorStop(1, "#000");    this.satValContext.fillStyle = blackGradient;    this.satValContext.fillRect(0, 0, this.satValContext.width, this.satValContext.height);    if (isUpdateTrackerPoint) {      const p = this.setValToPoint(this.sat, this.val)      this.satValTrackerPoint =        new Point(p.x - this.satValTrackerPointSize.width / 2, p.y - this.satValTrackerPointSize.height / 2)    }  }  private invalidateSatValPanel() {    this.satValContext.clearRect(0, 0, this.satValContext.width, this.satValContext.height)    this.drawSatValPanel(true)  }  private setValToPoint(sat: number, val: number): Point {    const width = this.satValContext.width    const height = this.satValContext.height    const p = new Point()    p.x = sat * width + 0    p.y = (1 - val) * height + 0    return p  }  private pointToSatVal(x: number, y: number): [number, number] {    const width = this.satValContext.width    const height = this.satValContext.height    if (x < 0) {      x = 0    } else if (x > width) {      x = width    } else {      x = x - 0    }    if (y < 0) {      y = 0    } else if (y > height) {      y = height    } else {      y = y - 0    }    return [1 / width * x, 1 - (1 / height * y)]  }  @Builder  private HuePanel() {    Stack() {      Canvas(this.hueContext)        .width('100%')        .height(20)        .onReady(() => {          this.drawHuePanel()          this.drawSatValPanel()        })        .onTouch((event) => {          let x = event.touches[0].x          let y = event.touches[0].y          let xMaxBoundary = this.hueContext.width          let xMinBoundary = 0          if (x > xMaxBoundary) {            x = xMaxBoundary          }          if (x < xMinBoundary) {            x = xMinBoundary          }          this.hueTrackerPoint = new Point(x - this.hueTrackerPointSize.width * 0.5, y)          this.hue = this.pointToHue(x)          this.invalidateHuePanel()          this.color = this.getColor()        })      Shape() {        Circle()          .size(this.hueTrackerPointSize)          .fill(Color.Transparent)          .borderRadius(this.hueTrackerPointSize.width / 2)          .border({ color: Color.White, width: 4 })          .fill(Color.Transparent);      }      .enabled(false)      .focusOnTouch(false)      .position({ x: this.hueTrackerPoint.x, y: 0 })    }  }  private drawHuePanel() {    const grad = this.hueContext.createLinearGradient(0, 0, this.hueContext.width, this.hueContext.height);    let hue = new Array<string>(361)    let count = 0    for (let i = hue.length - 1; i >= 0; i--, count++) {      hue[count] = ColorUtils.hsvToHex(i, 1, 1)      grad.addColorStop(1 - i / 360, hue[count])    }    this.hueContext.fillStyle = grad    this.hueContext.fillRect(0, 0, this.hueContext.width, this.hueContext.height)    const p = this.hueToPoint(this.hue)    this.hueTrackerPoint = new Point(p.x - this.hueTrackerPointSize.width / 2, 0)  }  private invalidateHuePanel() {    this.hueContext.clearRect(0, 0, this.hueContext.width, this.hueContext.height)    this.drawHuePanel()    this.drawSatValPanel()  }  /**   * Get coordinate points based on hue   * @param hue   * @returns   */  private hueToPoint(hue: number): Point {    const width = this.hueContext.width    const p = new Point()    p.x = (width - (hue * width / 360) + 0)    p.y = 0    return p  }  /**   * Calculate hue value based on x coordinate   * @param x axis   * @returns   */  private pointToHue(x: number) {    if (x < 0) {      x = 0    } else if (x > this.hueContext.width) {      x = this.hueContext.width    } else {      x = x - 0    }    let hue = 360 - (x * 360 / this.hueContext.width)    if (hue < 0) {      hue = 0    } else if (hue > 360) {      hue = 360    }    return hue  }  private getColor(): string {    return ColorUtils.hsvToHex(this.hue, this.sat, this.val)  }}

4、在其他界面中使用

如果我们需要在其他page中使用,代码示例如下:​​​​​​​

import { ColorPickDialog } from '../components/colorPicker/index';@Entry@Componentstruct Index {  @State color: string = '#ff0000';  colorPickDialogController: CustomDialogController | null = new CustomDialogController({    builder: ColorPickDialog({color: this.color}),    alignment: DialogAlignment.Center,    width: '80%',    cornerRadius: 15,    backgroundColor: $r('sys.color.background_primary')  })  build() {    Column({space: 10}) {      Text(this.color)      Shape() {        Rect()          .width(100)          .height(100)          .fill(this.color)      }      Button('选择颜色')        .onClick(() => {          this.colorPickDialogController?.open()        })    }    .height('100%')    .width('100%')  }  aboutToDisappear(): void {    this.colorPickDialogController = null  }}

实现效果如下:

图片

5、one more thing

最后,文章中的源代码地址如下:

https://gitee.com/lantingshuxu/harmony-class-room-demos/tree/feat/colorPicker

http://www.kler.cn/a/502049.html

相关文章:

  • Colossal-AI:深度学习大规模分布式训练框架
  • 如何将 sqlserver 数据迁移到 mysql
  • 后端技术选型 sa-token校验学习 中 文档学习
  • JAVA之单例模式
  • Kafka 主题管理
  • C#版 软件开发6大原则与23种设计模式
  • 【Ubuntu与Linux操作系统:七、系统高级管理】
  • 【论文速读】| 利用大语言模型在灰盒模糊测试中生成初始种子
  • Django Admin 中为自定义操作添加权限控制
  • Folder Icons v2.0.2 文件/文件夹图标美化 支持M、Intel芯片
  • 【南京工业大学主办 | JPCS独立出版 | 高届数、会议历史好 | 投稿领域广泛】第八届智能制造与自动化国际学术会议(IMA 2025)
  • Rank-Analysis——LOL 排位战绩查询分析器
  • 【LeetCode: 763. 划分字母区间 + 贪心】
  • Bash语言的语法糖
  • 对React中类组件和函数组件的理解?有什么区别?
  • ansible 检查目录大小
  • 【C++】size_t究竟是什么?全面解析与深入拓展
  • CSS3的aria-hidden学习
  • 每日一题(三):压缩字符串(行程长度编码)
  • vue城市道路交通流量预测可视化系统
  • 《变形金刚-游戏》V1.0官方学习版
  • Redis 为什么要引入 Pipeline机制?
  • C++中锁和互斥量的原理、区别和使用建议
  • 提升Docker运维效率:实用技巧与最佳实践
  • 【opencv】第8章 图像轮廓与图像分割修复
  • [Python学习日记-76] 网络编程中的 socket 开发 —— 介绍、工作流程、socket 模块用法和函数介绍