HarmonyOS6 - 鸿蒙LED滚动字幕实战案例
开发环境为:
开发工具:DevEco Studio 6.0.1 Release
API版本是:API21
本文所有代码都已使用模拟器测试成功!
1. 效果
HarmonyOS6 - 鸿蒙LED滚动字幕实战案例
2. 需求
- 手机横屏展示一段文字,可以滚动展示,如用户在演唱会现场或好友过生日时,可通过手机屏幕展示滚动字幕,显示应援口号或祝福语。
- 可以自定义如下内容:内容,字体大小,字体演示,背景颜色,滚动速度
- 滚动页面点击屏幕后,显示返回按钮,即可退回到设置页面
3. 分析
针对以上需求,开发思路如下:
- 设备屏幕信息获取
- 初始化时获取屏幕的宽度、高度和像素密度,用于后续的布局和字体大小适配。
- UI与窗口管理
- 获取UI上下文和主窗口对象,用于控制窗口行为(如全屏、系统栏显示/隐藏、屏幕方向切换)。
- 状态管理
- 使用状态变量管理字幕内容、字体大小、颜色、背景色、滚动开关、速度、全屏状态等,实现数据驱动的UI更新。
- 字幕内容处理
- 根据是否滚动,动态计算字幕内容:若需要滚动,则在原始内容后添加空格和重复内容,以形成连续滚动效果。
- 使用文本测量工具获取文本宽度,用于判断是否需要滚动及计算空格数量。
- 界面布局构建
- 使用堆叠布局(Stack)管理全屏和普通两种界面状态:
- 非全屏模式:显示字幕配置组件(可调整内容、字体、颜色、滚动速度等)和"全屏显示"按钮。
- 全屏模式:显示横向滚动的Marquee组件,并提供一个可隐藏的返回按钮。
- 使用堆叠布局(Stack)管理全屏和普通两种界面状态:
- 全屏与横屏切换
- 点击"全屏显示"按钮时,切换到横屏、隐藏系统栏、启用全屏布局,并计算顶部安全区域以避免遮挡。
- 全屏状态下点击屏幕可切换返回按钮的显示/隐藏,点击返回按钮则恢复竖屏和非全屏状态。
- 滚动字幕实现
- 使用Marquee组件实现字幕滚动,支持控制滚动启停、速度、循环次数,并提供开始、回弹、结束等事件回调。
- 安全区域适配
- 通过扩展安全区域,确保内容不被系统UI(如状态栏、导航栏)遮挡,提升用户体验。
- 交互与反馈
- 通过按钮点击、屏幕点击等交互,实现全屏切换、返回、按钮显隐等功能,并添加日志记录关键操作事件。
核心思路:通过状态变量驱动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,即可测试效果了
最后
- 希望本文对你有所帮助!
- 本人如有任何错误或不当之处,请留言指出,谢谢!