HarmonyOS6 - 鸿蒙LED滚动字幕实战案例

HarmonyOS6 - 鸿蒙LED滚动字幕实战案例

开发环境为:

开发工具:DevEco Studio 6.0.1 Release

API版本是:API21

本文所有代码都已使用模拟器测试成功!

1. 效果

HarmonyOS6 - 鸿蒙LED滚动字幕实战案例

2. 需求

  1. 手机横屏展示一段文字,可以滚动展示,如用户在演唱会现场或好友过生日时,可通过手机屏幕展示滚动字幕,显示应援口号或祝福语。
  2. 可以自定义如下内容:内容,字体大小,字体演示,背景颜色,滚动速度
  3. 滚动页面点击屏幕后,显示返回按钮,即可退回到设置页面

3. 分析

针对以上需求,开发思路如下:

  1. 设备屏幕信息获取
    • 初始化时获取屏幕的宽度、高度和像素密度,用于后续的布局和字体大小适配。
  2. UI与窗口管理
    • 获取UI上下文和主窗口对象,用于控制窗口行为(如全屏、系统栏显示/隐藏、屏幕方向切换)。
  3. 状态管理
    • 使用状态变量管理字幕内容、字体大小、颜色、背景色、滚动开关、速度、全屏状态等,实现数据驱动的UI更新。
  4. 字幕内容处理
    • 根据是否滚动,动态计算字幕内容:若需要滚动,则在原始内容后添加空格和重复内容,以形成连续滚动效果。
    • 使用文本测量工具获取文本宽度,用于判断是否需要滚动及计算空格数量。
  5. 界面布局构建
    • 使用堆叠布局(Stack)管理全屏和普通两种界面状态:
      • 非全屏模式:显示字幕配置组件(可调整内容、字体、颜色、滚动速度等)和"全屏显示"按钮。
      • 全屏模式:显示横向滚动的Marquee组件,并提供一个可隐藏的返回按钮。
  6. 全屏与横屏切换
    • 点击"全屏显示"按钮时,切换到横屏、隐藏系统栏、启用全屏布局,并计算顶部安全区域以避免遮挡。
    • 全屏状态下点击屏幕可切换返回按钮的显示/隐藏,点击返回按钮则恢复竖屏和非全屏状态。
  7. 滚动字幕实现
    • 使用Marquee组件实现字幕滚动,支持控制滚动启停、速度、循环次数,并提供开始、回弹、结束等事件回调。
  8. 安全区域适配
    • 通过扩展安全区域,确保内容不被系统UI(如状态栏、导航栏)遮挡,提升用户体验。
  9. 交互与反馈
    • 通过按钮点击、屏幕点击等交互,实现全屏切换、返回、按钮显隐等功能,并添加日志记录关键操作事件。

核心思路:通过状态变量驱动UI变化,利用Marquee组件实现滚动效果,结合窗口管理实现全屏/横屏切换,最终完成一个可配置、可交互的LED滚动字幕应用。

4. 开发

根据以上开发思路,新建主页面,代码如下:

js 复制代码
import display from '@ohos.display'
import window from '@ohos.window';
import { common } from '@kit.AbilityKit';
import { MeasureUtils } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { Constants } from '../common/Constants';
import { ContentConfig } from '../component/ContentConfig';

/**
 * Desc: 案例:LED滚动字幕
 * Author: 波波老师(weixin: javabobo0513)
 */
@Entry
@Component
struct Page15 {
  screenWidth: number = display.getDefaultDisplaySync().width;
  scaledDensity: number = display.getDefaultDisplaySync().scaledDensity;
  screenHeight: number = display.getDefaultDisplaySync().height;
  context: common.UIAbilityContext = this.getUIContext()?.getHostContext() as common.UIAbilityContext;
  windowClass: window.Window = (this.context as common.UIAbilityContext).windowStage.getMainWindowSync();
  @State contentStrNormalize: ResourceStr = '';
  @State contentFontSize: number = Math.floor(this.screenWidth / this.scaledDensity);
  @State contentFontColor: string = Constants.DEFAULT_FONT_COLOR;
  @State contentBackGroundColor: string = Constants.DEFAULT_BACKGROUND_COLOR;
  @State whetherScroll: boolean = true;
  @State scrollSpeed: number = Constants.DEFAULT_SCROLL_SPEED;
  @State isFullScreen: boolean = false;
  @State isButtonVisible: boolean = false;
  @State contentStr: ResourceStr = '华仔,我爱你';
  @State uiContextMeasure: MeasureUtils = this.getUIContext().getMeasureUtils();
  @State contentStrWidth: number = 0;
  @State spaceWidth: number = 0;
  @State topMargin: number = 0;

  getMarqueeSrc(): ResourceStr {
    this.contentStrWidth = this.uiContextMeasure.measureText({
      textContent: this.contentStr,
      fontSize: this.contentFontSize
    });
    this.spaceWidth = this.uiContextMeasure.measureText({
      textContent: ' ',
      fontSize: this.contentFontSize
    });
    if (this.contentStrWidth > this.screenHeight) {
      return this.contentStr
    }

    if (this.whetherScroll) {
      return this.contentStr + ' '.repeat(this.screenHeight / this.spaceWidth) + this.contentStr;
    } else {
      return this.contentStr;
    }
  }

  build() {
    Stack({ alignContent: Alignment.Top }) {
      if (this.isFullScreen) {
        if (this.isButtonVisible) {
          Button('返回')
            .backgroundColor(Color.White)
            .fontColor(Color.Black)
            .zIndex(Constants.Z_INDEX)
            .margin({ top: this.topMargin })
            .onClick(() => {
              this.isFullScreen = false;
              this.isButtonVisible = false;
              this.windowClass.setWindowLayoutFullScreen(false);
              this.windowClass.setWindowSystemBarEnable(['status', 'navigation']);
              this.windowClass.setPreferredOrientation(window.Orientation.PORTRAIT);
            }
            )
        }
        // 横屏全屏滚动文字
        Marquee({
          start: this.whetherScroll,
          step: this.scrollSpeed,
          loop: Constants.INFINITE_LOOP,
          src: this.contentStrNormalize.toString(),
        })
          .width(Constants.FULL_PERCENT)
          .height(Constants.FULL_PERCENT)
          .fontSize(this.contentFontSize)
          .fontColor(this.contentFontColor)
          .align(Alignment.Center)
          .onStart(() => {
            hilog.info(0x0000, 'testTag', 'Succeeded in completing the onStart callback of marquee animation');
          })
          .onBounce(() => {
            hilog.info(0x0000, 'testTag', 'Succeeded in completing the onBounce callback of marquee animation');
          })
          .onFinish(() => {
            hilog.info(0x0000, 'testTag', 'Succeeded in completing the onFinish callback of marquee animation');
          })
          .onClick(() => {
            this.isButtonVisible = !this.isButtonVisible;
          })
      } else {
        ContentConfig({
          contentStr: this.contentStr,
          contentFontSize: this.contentFontSize,
          contentFontColor: this.contentFontColor,
          contentBackGroundColor: this.contentBackGroundColor,
          whetherScroll: this.whetherScroll,
          scrollSpeed: this.scrollSpeed
        });
        Button('全屏显示')
          .width(Constants.NINETY_PERCENT)
          .fontSize('16fp')
          .backgroundColor(Constants.DEFAULT_BACKGROUND_COLOR)
          .fontColor(Color.White)
          .alignSelf(ItemAlign.Center)
          .position({ left: Constants.FIVE_PERCENT, bottom: Constants.FIVE_PERCENT })
          .onClick(() => {
            this.contentStrNormalize = this.getMarqueeSrc();
            this.isFullScreen = true;
            this.windowClass.setWindowLayoutFullScreen(true);
            this.windowClass.setPreferredOrientation(window.Orientation.LANDSCAPE);
            this.topMargin = this.getUIContext()
              .px2vp(this.windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height)
            this.windowClass.setWindowSystemBarEnable([]);
          })
      }
    }
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    .backgroundColor(this.contentBackGroundColor)
    .height(Constants.FULL_PERCENT)
    .width(Constants.FULL_PERCENT)
  }
}

常量类Constants代码如下:

js 复制代码
export class Constants {
  // 默认背景颜色
  static readonly DEFAULT_BACKGROUND_COLOR: string = '#0a59f7';
  // 配置界面背景颜色
  static readonly CONFIG_BACKGROUND_COLOR = '#fff7f7f7';
  // 浅黑色
  static readonly LIGHT_DARK_COLOR = '#33000000';
  // 默认显示字体颜色
  static readonly DEFAULT_FONT_COLOR = '#ffffff';
  // 输入框背景颜色
  static readonly TEXTINPUT_BACKGROUND_COLOR = '#0d000000';
  // 默认滚动速度
  static readonly DEFAULT_SCROLL_SPEED: number = 10;
  // 100%
  static readonly FULL_PERCENT: string = '100%';
  // 90%
  static readonly NINETY_PERCENT = '90%';
  // 50%
  static readonly FIFTY_PERCENT: string = '50%';
  // 25%
  static readonly TWENTY_FIVE_PERCENT: string = '25%';
  // 5%
  static readonly FIVE_PERCENT = '5%';
  // 默认圆角半径
  static readonly BORDER_RADIUS: number = 16;
  // 颜色输入框圆角半径
  static readonly TEXTINPUT_BORDER_RADIUS: number = 8;
  // zIndex:1
  static readonly Z_INDEX: number = 1;
  // FontWeight: 600
  static readonly FONT_WEIGHT_600: number = 600;
  // 画布颜色选择器追踪点大小
  static readonly PANEL_TRACK_POINT_RADIUS: number = 10;
  // 颜色条选择器追踪点大小
  static readonly BAR_TRACK_POINT_RADIUS: number = 6;
  // 画布颜色选择器追踪点边界大小
  static readonly PANEL_TRACK_POINT_BORDER: number = 4;
  // 颜色条选择器追踪点边界大小
  static readonly BAR_TRACK_POINT_BORDER: number = 2;
  // 样例颜色图形大小
  static readonly COLOR_DEMO_RADIUS: number = 18;
  // 颜色条高度
  static readonly COLOR_BAR_HEIGHT: number = 8;
  // 颜色画布
  static readonly COLOR_PANEL_HEIGHT: number = 200;
  // HSV颜色模型色相范围
  static readonly HEU_SCALE: number = 360;
  // 无限循环
  static readonly INFINITE_LOOP: number = -1;
  // LayoutWeight list
  static readonly LAYOUT_WEIGHT: number[] = [1, 10]
}

ContentConfig文件代码如下:

js 复制代码
import { Constants } from '../common/Constants';
import { ColorSelector } from './ColorSelector';

@Component
export struct ContentConfig {
  @Link contentStr: ResourceStr;
  @Link contentFontSize: number;
  @Link contentFontColor: string;
  @Link contentBackGroundColor: string;
  @Link whetherScroll: boolean;
  @Link scrollSpeed: number;
  fontColorPickDialogController: CustomDialogController | null = new CustomDialogController({
    builder: ColorSelector({ color: this.contentFontColor }),
    alignment: DialogAlignment.Bottom,
    offset: { dx: 0, dy: '-90vp' },
    width: Constants.NINETY_PERCENT,
    cornerRadius: Constants.BORDER_RADIUS,
    backgroundColor: $r('sys.color.background_primary')
  })
  backgroundColorPickDialogController: CustomDialogController | null = new CustomDialogController({
    builder: ColorSelector({ color: this.contentBackGroundColor }),
    alignment: DialogAlignment.Bottom,
    offset: { dx: 0, dy: '-90vp' },
    width: Constants.NINETY_PERCENT,
    cornerRadius: Constants.BORDER_RADIUS,
    backgroundColor: $r('sys.color.background_primary')
  })

  build() {
    Column() {
      Text('手机LED滚动屏')
        .fontSize('26fp')
        .fontWeight(FontWeight.Bold)
        .margin({ left: '16vp', top: '10vp' })

      Text('显示内容')
        .fontSize('18fp')
        .fontWeight(Constants.FONT_WEIGHT_600)
        .margin({ left: '16vp', top: '22vp' })

      TextInput({ text: this.contentStr })
        .height('51vp')
        .borderRadius(Constants.BORDER_RADIUS)
        .fontSize('16fp')
        .backgroundColor(Color.White)
        .margin({ left: '16vp', right: '16vp', top: '11vp' })
        .onChange((value: string) => {
          this.contentStr = value;
        })

      Text('显示设置')
        .fontSize('18fp')
        .fontWeight(Constants.FONT_WEIGHT_600)
        .margin({ left: '16vp', top: '20vp' })

      Column() {
        Row() {
          Text('字体大小')
            .fontSize('16fp')
            .fontWeight(FontWeight.Medium)
            .margin({ left: '12vp' })

          TextInput({ text: this.contentFontSize.toString() })
            .textAlign(TextAlign.End)
            .fontColor('#99000000')
            .type(InputType.Number)
            .backgroundColor(Color.White)
            .width(Constants.FIFTY_PERCENT)
            .onChange((value: string) => {
              this.contentFontSize = Number(value);
            })
        }
        .width(Constants.FULL_PERCENT)
        .height('48vp')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Divider()
          .margin({ left: '12vp', right: '12vp' })
          .backgroundColor(Constants.LIGHT_DARK_COLOR)

        Row() {
          Text('字体颜色')
            .fontSize('16fp')
            .fontWeight(FontWeight.Medium)
            .margin({ left: '12vp' })

          Row() {
            Text(this.contentFontColor)
              .fontSize('14fp')
              .fontColor('#99000000')
              .margin({ right: '5vp' })

            Circle()
              .width('18vp')
              .height('18vp')
              .fill(this.contentFontColor)
              .stroke(Constants.LIGHT_DARK_COLOR)
              .margin({ right: '12vp' })
          }
        }
        .width(Constants.FULL_PERCENT)
        .height('48vp')
        .alignItems(VerticalAlign.Center)
        .justifyContent(FlexAlign.SpaceBetween)
        .onClick(() => {
          this.fontColorPickDialogController?.open()
        })

        Divider()
          .margin({ left: '12vp', right: '12vp' })
          .backgroundColor(Constants.LIGHT_DARK_COLOR)

        Row() {
          Text('背景颜色')
            .fontSize('16fp')
            .fontWeight(FontWeight.Medium)
            .margin({ left: '12vp' })

          Row() {
            Text(this.contentBackGroundColor)
              .fontSize('14fp')
              .fontColor('#99000000')
              .margin({ right: '5vp' })
            Circle()
              .width('18vp')
              .height('18vp')
              .fill(this.contentBackGroundColor)
              .stroke(Constants.LIGHT_DARK_COLOR)
              .margin({ right: '12vp' })
          }
        }
        .width(Constants.FULL_PERCENT)
        .height('48vp')
        .alignItems(VerticalAlign.Center)
        .justifyContent(FlexAlign.SpaceBetween)
        .onClick(() => {
          this.backgroundColorPickDialogController?.open()
        })

        Divider()
          .margin({ left: '12vp', right: '12vp' })
          .backgroundColor(Constants.LIGHT_DARK_COLOR)

        Row() {
          Text('是否滚动')
            .fontSize('16fp')
            .fontWeight(FontWeight.Medium)
            .margin({ left: '12vp' })

          Image(this.whetherScroll ? $r('app.media.enable') : $r('app.media.disable'))
            .width('36vp')
            .height('20vp')
            .margin({ right: '12vp' })
            .onClick(() => {
              this.whetherScroll = !this.whetherScroll;
            })
        }
        .width(Constants.FULL_PERCENT)
        .height('48vp')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Divider()
          .margin({ left: '12vp', right: '12vp' })
          .backgroundColor(Constants.LIGHT_DARK_COLOR)

        if (this.whetherScroll) {
          Row() {
            Text('滚动速度')
              .fontSize('16fp')
              .fontWeight(FontWeight.Medium)
              .margin({ left: '12vp' })

            TextInput({ text: this.scrollSpeed.toString() })
              .textAlign(TextAlign.End)
              .fontColor('#99000000')
              .type(InputType.Number)
              .backgroundColor(Color.White)
              .width(Constants.FIFTY_PERCENT)
              .onChange((value: string) => {
                this.scrollSpeed = Number(value);
              })
          }
          .width(Constants.FULL_PERCENT)
          .height('48vp')
          .justifyContent(FlexAlign.SpaceBetween)
          .alignItems(VerticalAlign.Center)
        }

      }
      .backgroundColor(Color.White)
      .margin({ left: '16vp', right: '16vp', top: '14vp' })
      .borderRadius(Constants.BORDER_RADIUS)
    }
    .alignItems(HorizontalAlign.Start)
    .backgroundColor(Constants.CONFIG_BACKGROUND_COLOR)
    .height(Constants.FULL_PERCENT)
    .width(Constants.FULL_PERCENT)
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  }

  aboutToDisappear(): void {
    this.fontColorPickDialogController = null
    this.backgroundColorPickDialogController = null;
  }
}

ColorSelector文件代码如下:

js 复制代码
import { Constants } from "../common/Constants";
import ColorUtils from "../common/utils/ColorUtils";

@CustomDialog
export struct ColorSelector {
  controller: CustomDialogController;
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private colorBarContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  private colorPanelContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  @State private colorBarTrackPoint: Point =
    new Point(0 - Constants.BAR_TRACK_POINT_RADIUS, 0 - Constants.BAR_TRACK_POINT_RADIUS);
  @State private colorPanelTrackPoint: Point =
    new Point(0 - Constants.PANEL_TRACK_POINT_RADIUS, 0 - Constants.PANEL_TRACK_POINT_RADIUS);
  @Link color: string;
  @State private hue: number = Constants.HEU_SCALE;
  @State private sat: number = 0;
  @State private val: number = 0;

  build() {
    Column() {
      Row() {
        Blank()
          .layoutWeight(Constants.LAYOUT_WEIGHT[0])
        Text('颜色选择器')
          .fontWeight(FontWeight.Bold)
          .fontSize('20fp')
          .textAlign(TextAlign.Center)
          .layoutWeight(Constants.LAYOUT_WEIGHT[1])

        SymbolGlyph($r('sys.symbol.xmark'))
          .fontSize('16fp')
          .fontWeight(FontWeight.Bold)
          .fontColor([$r('sys.color.font_primary')])
          .layoutWeight(Constants.LAYOUT_WEIGHT[0])
          .onClick(() => {
            this.controller.close();
          })
      }
      .margin({ bottom: '14vp', top: '14vp' })
      .width(Constants.FULL_PERCENT)
      .justifyContent(FlexAlign.SpaceBetween)

      this.setColorPanel();
      this.setColorBar();
      Row() {
        Circle()
          .height(2 * Constants.COLOR_DEMO_RADIUS)
          .width(2 * Constants.COLOR_DEMO_RADIUS)
          .fill(this.getColor())
          .borderRadius(Constants.COLOR_DEMO_RADIUS)
          .stroke(Constants.LIGHT_DARK_COLOR)

        Row() {
          Text('Hex: ')
            .fontSize('14fp')
          TextInput({ text: this.color })
            .fontSize('12fp')
            .borderRadius(Constants.TEXTINPUT_BORDER_RADIUS)
            .padding('4vp')
            .width(Constants.TWENTY_FIVE_PERCENT)
            .backgroundColor(Constants.TEXTINPUT_BACKGROUND_COLOR)
            .textAlign(TextAlign.Center)
            .onEditChange((isEditing) => {
              if (!isEditing) {
                const hsv = ColorUtils.hexToHsv(this.color)
                this.hue = hsv[0]
                this.sat = hsv[1]
                this.val = hsv[2]

                this.invalidateHuePanel()
                this.invalidateSatValPanel()
              }
            })
            .onChange((value) => {
              this.color = value
            })
        }
      }
      .width(Constants.NINETY_PERCENT)
      .margin({ top: '12vp', bottom: '12vp' })
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .borderRadius(Constants.BORDER_RADIUS)
  }

  invalidateSatValPanel() {
    this.colorPanelContext.clearRect(0, 0, this.colorPanelContext.width, this.colorPanelContext.height)
    this.drawColorPanel(true)
  }

  @Builder
  setColorBar() {
    Stack() {
      Canvas(this.colorBarContext)
        .width(Constants.NINETY_PERCENT)
        .height(Constants.COLOR_BAR_HEIGHT)
        .onReady(() => {
          this.drawColorBar()
          this.drawColorPanel()
        })
        .onTouch((event) => {
          let x = event.touches[0].x
          let y = event.touches[0].y
          let xMaxBoundary = this.colorBarContext.width
          let xMinBoundary = 0
          if (x > xMaxBoundary) {
            x = xMaxBoundary
          }
          if (x < xMinBoundary) {
            x = xMinBoundary
          }
          this.colorBarTrackPoint = new Point(x - Constants.BAR_TRACK_POINT_RADIUS, y)
          this.hue = this.pointToHue(x)
          this.invalidateHuePanel()
          this.color = this.getColor()
        })
      Shape() {
        Circle()
          .width(2 * Constants.BAR_TRACK_POINT_RADIUS)
          .height(2 * Constants.BAR_TRACK_POINT_RADIUS)
          .fill(Color.Transparent)
          .borderRadius(Constants.BAR_TRACK_POINT_RADIUS)
          .border({ color: Color.White, width: Constants.BAR_TRACK_POINT_BORDER })
      }
      .enabled(false)
      .focusOnTouch(false)
      .position({ x: this.colorBarTrackPoint.x, y: 0 })
    }
    .margin({ top: '12vp' })
  }

  invalidateHuePanel() {
    this.colorBarContext.clearRect(0, 0, this.colorBarContext.width, this.colorBarContext.height)
    this.drawColorBar()
    this.drawColorPanel()
  }

  pointToHue(x: number): number {
    if (x < 0) {
      x = 0
    } else if (x > this.colorBarContext.width) {
      x = this.colorBarContext.width
    } else {
      x = x - 0
    }
    let hue = Constants.HEU_SCALE - (x * Constants.HEU_SCALE / this.colorBarContext.width)
    if (hue < 0) {
      hue = 0
    } else if (hue > Constants.HEU_SCALE) {
      hue = Constants.HEU_SCALE
    }
    return hue
  }

  drawColorBar() {
    const grad = this.colorBarContext.createLinearGradient(0, 0, this.colorBarContext.width, 0);
    let count = 0
    for (let i = Constants.HEU_SCALE; i >= 0; i--, count++) {
      grad.addColorStop(1 - i / Constants.HEU_SCALE, ColorUtils.hsvToHex(i, 1, 1))
    }
    this.colorBarContext.fillStyle = grad
    this.colorBarContext.fillRect(0, 0, this.colorBarContext.width, this.colorBarContext.height)
    const p = this.hueToPoint(this.hue)
    this.colorBarTrackPoint = new Point(p.x - Constants.BAR_TRACK_POINT_RADIUS, 0)
  }

  hueToPoint(hue: number) {
    const width = this.colorBarContext.width
    const p = new Point()
    p.x = (width - (hue * width / Constants.HEU_SCALE))
    p.y = 0
    return p
  }

  @Builder
  private setColorPanel() {
    Stack() {
      Canvas(this.colorPanelContext)
        .width(Constants.NINETY_PERCENT)
        .height(Constants.COLOR_PANEL_HEIGHT)
        .onReady(() => {
          this.drawColorPanel(true)
        })
        .onTouch((event) => {
          let x = event.touches[0].x
          let y = event.touches[0].y
          if (x >= this.colorPanelContext.width) {
            x = this.colorPanelContext.width
          }
          if (x < 0) {
            x = 0
          }
          if (y >= this.colorPanelContext.height) {
            y = this.colorPanelContext.height
          }
          if (y < 0) {
            y = 0
          }
          this.colorPanelTrackPoint =
            new Point(x - Constants.PANEL_TRACK_POINT_RADIUS, y - Constants.PANEL_TRACK_POINT_RADIUS)
          this.color = this.getColor()
          const p = this.pointToSatVal(x, y)
          this.sat = p[0]
          this.val = p[1]
        })
      Shape() {
        Circle()
          .size({ width: 2 * Constants.PANEL_TRACK_POINT_RADIUS, height: 2 * Constants.PANEL_TRACK_POINT_RADIUS })
          .borderRadius(Constants.PANEL_TRACK_POINT_RADIUS)
          .border({ color: Color.White, width: Constants.PANEL_TRACK_POINT_BORDER })
          .fill(Color.Transparent)
      }
      .enabled(false)
      .focusOnTouch(false)
      .position({ x: this.colorPanelTrackPoint.x, y: this.colorPanelTrackPoint.y })
    }
  }

  drawColorPanel(isUpdateTrackerPoint: boolean = false) {
    this.colorPanelContext.clearRect(0, 0, this.colorPanelContext.width, this.colorPanelContext.height)
    this.colorPanelContext.fillStyle = ColorUtils.hsvToHex(this.hue, 1, 1);
    this.colorPanelContext.fillRect(0, 0, this.colorPanelContext.width, this.colorPanelContext.height);
    const whiteGradient = this.colorPanelContext.createLinearGradient(0, 0, this.colorPanelContext.width, 0);
    whiteGradient.addColorStop(0, '#fff');
    whiteGradient.addColorStop(1, 'transparent');
    this.colorPanelContext.fillStyle = whiteGradient;
    this.colorPanelContext.fillRect(0, 0, this.colorPanelContext.width, this.colorPanelContext.height);
    const blackGradient = this.colorPanelContext.createLinearGradient(0, 0, 0, this.colorPanelContext.height);
    blackGradient.addColorStop(0, 'transparent');
    blackGradient.addColorStop(1, '#000');
    this.colorPanelContext.fillStyle = blackGradient;
    this.colorPanelContext.fillRect(0, 0, this.colorPanelContext.width, this.colorPanelContext.height);
    if (isUpdateTrackerPoint) {
      const p = this.setValToPoint(this.sat, this.val)
      this.colorPanelTrackPoint =
        new Point(p.x - Constants.PANEL_TRACK_POINT_RADIUS, p.y - Constants.PANEL_TRACK_POINT_RADIUS)
    }
  }

  private setValToPoint(sat: number, val: number): Point {
    const width = this.colorPanelContext.width
    const height = this.colorPanelContext.height
    const p = new Point()
    p.x = sat * width
    p.y = (1 - val) * height
    return p
  }

  getColor(): string {
    return ColorUtils.hsvToHex(this.hue, this.sat, this.val);
  }

  private pointToSatVal(x: number, y: number): [number, number] {
    const width = this.colorPanelContext.width
    const height = this.colorPanelContext.height
    if (x < 0) {
      x = 0
    } else if (x > width) {
      x = width
    } else {
      x = x
    }
    if (y < 0) {
      y = 0
    } else if (y > height) {
      y = height
    } else {
      y = y
    }
    return [1 / width * x, 1 - (1 / height * y)]
  }

  aboutToAppear(): void {
    // 默认颜色转换成hsv
    const hsv = ColorUtils.hexToHsv(this.color)
    this.hue = hsv[0]
    this.sat = hsv[1]
    this.val = hsv[2]
  }
}

class Point {
  x: number = 0
  y: number = 0

  constructor(x: number = 0, y: number = 0) {
    this.x = x;
    this.y = y;
  }
}

ColorUtils文件代码如下:

js 复制代码
class ColorUtils {
  hexToHsv(hex: string): [number, number, number] {
    const r = parseInt(hex.slice(1, 3), 16) / 255;
    const g = parseInt(hex.slice(3, 5), 16) / 255;
    const b = parseInt(hex.slice(5, 7), 16) / 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 *= 60;
      if (h < 0) {
        h += 360;
      }
    }

    const s = max === 0 ? 0 : delta / max;
    const v = max;
    return [h, s, v];
  }

  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 interface ColorRgb {
  r: number;
  g: number;
  b: number;
}

function hsv2rgb(h: number, s: number, v: number): ColorRgb {
  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 { r: r, g: g, b: b };
}

export default new ColorUtils()

使用真机运行主页面Page15,即可测试效果了

最后

  • 希望本文对你有所帮助!
  • 本人如有任何错误或不当之处,请留言指出,谢谢!
相关推荐
阿钱真强道5 小时前
05 thingsboard-4.3-ubuntu20-rk3588-部署
linux·运维·服务器·鸿蒙
Ophelia(秃头版6 小时前
组件、页面、UIAbility、组件挂卸载的生命周期
harmonyos·arkts
晚霞的不甘6 小时前
Flutter for OpenHarmony 布局探秘:从理论到实战构建交互式组件讲解应用
开发语言·前端·flutter·正则表达式·前端框架·firefox·鸿蒙
zilikew18 小时前
Flutter框架跨平台鸿蒙开发——今日吃啥APP的开发流程
flutter·华为·harmonyos·鸿蒙
IT陈图图20 小时前
Flutter × OpenHarmony 跨端实践:从零构建一个轻量级视频播放器
flutter·音视频·鸿蒙·openharmony
Miguo94well20 小时前
Flutter框架跨平台鸿蒙开发——戒拖延APP的开发流程
flutter·华为·harmonyos·鸿蒙
zilikew1 天前
Flutter框架跨平台鸿蒙开发——个人名片管理APP的开发流程
flutter·华为·harmonyos·鸿蒙
ITUnicorn1 天前
【HarmomyOS6】ArkTS入门(三)
华为·harmonyos·arkts·鸿蒙·harmonyos6
A懿轩A1 天前
【2026 最新】Kuikly 编译开发 OpenHarmony 项目逐步详细教程带图操作Android Studio编译(Windows)
windows·harmonyos·鸿蒙·openharmony·kuikly