【鸿蒙HarmonyOS Next App实战开发】ArkUI纯色图生成器
本文基于鸿蒙ArkUI框架实现了一个支持自定义颜色、尺寸、透明度及圆角的纯色图生成工具,并集成了图片保存与分享功能。以下从架构设计、核心功能与性能优化三个维度展开解析。应用商店搜索【图影工具箱】,点击“纯色图”即可查看实际效果。
一、技术架构与设计理念
-
分层架构设计
- UI层:通过声明式UI构建预览区(Canvas)和参数控制区(Slider/Button)。
- 逻辑层:状态管理(
@State
)驱动参数实时更新,如颜色值、透明度等。 - 服务层:沙箱文件操作(
CustomImageUtil
)、设备能力调用(AppUtil
)。
-
核心依赖模块
import { CanvasRenderingContext2D } from '@arkui.canvas'; // Canvas绘图核心 import { PreferencesUtil } from './Utils/PreferencesUtil'; // 本地持久化存储 import { CustomImageUtil } from './Utils/customUtils/CustomImageUtil'; // 图片处理工具
- 状态管理:
@State
变量(如selectedColor
、colorOpacity
)绑定UI动态更新。 - 生命周期控制:
aboutToAppear
恢复用户设置,aboutToDisappear
保存当前配置。
- 状态管理:
二、核心功能实现解析
1. 动态参数控制模块
-
颜色选择:
通过ColorPickDialog
组件实现取色器交互,点击预览区弹出对话框:.onClick(() => this.colorPickDialogController?.open())
选色结果同步至Canvas渲染(
drawColor()
方法)。 -
尺寸调节:
双滑动条控制宽高(40px~2000px),实时显示像素值:Slider({ value: this.imageWidth, min: 40, max: 2000 }).onChange((value) => this.imageWidth = value)
配合
+/-
按钮微调尺寸,确保操作精度。 -
透明度与圆角:
- 透明度范围(0~1,步长0.01):
Slider
绑定colorOpacity
变量。 - 圆角范围(0~100px):通过
drawRoundedRect
方法生成圆角路径。
- 透明度范围(0~1,步长0.01):
2. Canvas绘图与优化
- 自适应预览区:
根据屏幕尺寸动态计算预览图比例,保持宽高比:const maxWidth = vp2px(this.screenWidth) * 0.9; // 限制最大宽度为屏幕90% const previewHeight = (previewWidth * this.imageHeight) / this.imageWidth; // 等比缩放
- 离屏渲染技术:
保存图片时使用OffscreenCanvasRenderingContext2D
生成高清原图:
避免主线程渲染阻塞,提升性能。const tempCanvas = new OffscreenCanvasRenderingContext2D(vpWidth, vpHeight); this.drawRoundedRect(tempCanvas, 0, 0, vpWidth, vpHeight, actualBorderRadius);
3. 图片保存与分享
- 沙箱存储:
CustomImageUtil.saveCanvasToSandbox
将图片写入应用沙箱目录,文件名用UUID防冲突。 - 系统分享能力:
调用CustomImageUtil.shareImage
唤起系统分享菜单,支持社交平台分发。
三、性能优化与体验设计
1. 渲染性能提升
- 按需重绘:
参数变化时仅更新Canvas(updateCanvasSize()
),而非全局重渲染。 - 圆角路径算法优化:
使用quadraticCurveTo
绘制平滑圆角,替代arc
减少计算量:canvas.quadraticCurveTo(0, 0, cornerRadius, 0); // 贝塞尔曲线优化
2. 用户体验增强
- 实时预览反馈:
所有参数调整(如透明度滑块)即时触发Canvas重绘,实现“所见即所得”。 - 持久化存储:
PreferencesUtil
保存用户最后一次配置(颜色/尺寸等),提升使用连贯性。 - 防误触设计:
尺寸按钮设置边界限制(>40px且<2000px
),避免无效操作。
3. 视觉层次设计
- 卡片化布局:
@Extend
装饰器定义settingsCard
样式,统一阴影与圆角:.shadow({ radius: 8, color: '#1A000000' }) // 柔和阴影提升层次感 .backgroundColor($r('sys.color.ohos_id_color_sub_background')) // 动态主题适配
- 交互反馈:
颜色预览按钮添加borderColor
高亮边框,增强可识别性。
结语
本文实现的纯色图生成器,通过状态驱动UI、Canvas离屏渲染与本地化存储,在保障性能的同时提供了极致交互体验。其模块化设计(如分离CustomImageUtil
工具类)可复用于其他图像处理场景(如证件照生成、海报设计)。
设计原则总结:
原则 | 实现方式 |
---|---|
实时性 | 状态绑定+按需重绘机制 |
轻量化 | 纯前端实现,无额外依赖 |
用户友好 | 持久化存储+沙箱文件管理 |
扩展性 | 模块化工具类设计 |
具体代码如下:
import { ColorPickDialog } from './ColorPicker/components/ColorPickerDialog';
import { AppUtil } from './Utils/AppUtil';
import { PreferencesUtil } from './Utils/PreferencesUtil';
import { promptAction } from '@kit.ArkUI';
import CustomImageUtil from './Utils/customUtils/CustomImageUtil';
import { TitleBar } from '../components/TitleBar';
import { BusinessError } from '@kit.BasicServicesKit';
import { CustomSaveButton } from '../components/SaveButton';@Extend(Text)
function sectionTitle() {.fontSize(18).fontWeight(FontWeight.Medium).fontColor($r('sys.color.ohos_id_color_text_primary')).margin({ bottom: 12 })
}@Extend(Column)
function settingsCard() {.width('100%').padding(20).borderRadius(16).backgroundColor($r('sys.color.ohos_id_color_sub_background')).shadow({radius: 8,color: '#1A000000',offsetX: 2,offsetY: 4})
}@Entry
@Component
struct SolidColorPage {@State @Watch('updateCanvasSize') selectedColor: string = '#E3F2FD';@State imageWidth: number = 500;@State imageHeight: number = 500;@State previewWidth: number = 0;@State previewHeight: number = 0;@State colorOpacity: number = 1.0;@State colorBorderRadius: number = 0;canvas: CanvasRenderingContext2D = new CanvasRenderingContext2D();colorPickDialogController: CustomDialogController | null = new CustomDialogController({builder: ColorPickDialog({ color: this.selectedColor }),alignment: DialogAlignment.Center,width: '80%',cornerRadius: 15,backgroundColor: $r('sys.color.background_primary')});screenWidth: number = 0;screenHeight: number = 0;async aboutToAppear(): Promise<void> {AppUtil.setWindowKeepScreenOn(true);// 恢复上次的设置this.selectedColor = await PreferencesUtil.getString('solidColor') || '#E3F2FD';this.imageWidth = await PreferencesUtil.getNumber('solidColorWidth') || 500;this.imageHeight = await PreferencesUtil.getNumber('solidColorHeight') || 500;this.colorOpacity = await PreferencesUtil.getNumber('solidColorOpacity') || 1.0;this.colorBorderRadius = await PreferencesUtil.getNumber('solidColorBorderRadius') || 0;this.updateCanvasSize();}aboutToDisappear(): void {// 保存设置PreferencesUtil.put('solidColor', this.selectedColor);PreferencesUtil.put('solidColorWidth', this.imageWidth);PreferencesUtil.put('solidColorHeight', this.imageHeight);PreferencesUtil.put('solidColorOpacity', this.colorOpacity);PreferencesUtil.put('solidColorBorderRadius', this.colorBorderRadius);this.colorPickDialogController?.close();}updateCanvasSize(): void {// 计算预览尺寸,保持宽高比const maxWidth: number = vp2px(this.screenWidth) * 0.9;const maxHeight: number = vp2px(this.screenHeight) * 0.9;// 计算保持宽高比的预览尺寸let previewWidth: number = maxWidth;let previewHeight: number = (previewWidth * this.imageHeight) / this.imageWidth;// 如果高度超出限制,则按高度缩放if (previewHeight > maxHeight) {previewHeight = maxHeight;previewWidth = (previewHeight * this.imageWidth) / this.imageHeight;}this.previewWidth = previewWidth;this.previewHeight = previewHeight;this.drawColor();}drawColor(): void {this.canvas.reset();this.canvas.fillStyle = this.selectedColor;this.canvas.globalAlpha = this.colorOpacity;this.drawRoundedRect(this.canvas, 0, 0, px2vp(this.previewWidth), px2vp(this.previewHeight),this.colorBorderRadius);this.canvas.fill();}private drawRoundedRect(canvas: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, x: number, y: number,vpWidth: number, vpHeight: number, cornerRadius: number): void {// 创建圆角路径canvas.beginPath();canvas.moveTo(cornerRadius, 0);canvas.lineTo(vpWidth - cornerRadius, 0);canvas.quadraticCurveTo(vpWidth, 0, vpWidth, cornerRadius);canvas.lineTo(vpWidth, vpHeight - cornerRadius);canvas.quadraticCurveTo(vpWidth, vpHeight, vpWidth - cornerRadius, vpHeight);canvas.lineTo(cornerRadius, vpHeight);canvas.quadraticCurveTo(0, vpHeight, 0, vpHeight - cornerRadius);canvas.lineTo(0, cornerRadius);canvas.quadraticCurveTo(0, 0, cornerRadius, 0);canvas.closePath();canvas.clip();}async saveImage(): Promise<void> {let vpWidth = px2vp(this.imageWidth);let vpHeight = px2vp(this.imageHeight);const tempCanvas: OffscreenCanvasRenderingContext2D = new OffscreenCanvasRenderingContext2D(vpWidth, vpHeight);tempCanvas.fillStyle = this.selectedColor;tempCanvas.globalAlpha = this.colorOpacity;// 计算实际图片的圆角大小const scaleRatio = this.imageWidth / this.previewWidth;const actualBorderRadius = this.colorBorderRadius * scaleRatio;this.drawRoundedRect(tempCanvas, 0, 0, vpWidth, vpHeight, actualBorderRadius);tempCanvas.fill();try {// 保存到沙箱目录const filePath = await CustomImageUtil.saveCanvasToSandbox(tempCanvas, vpWidth, vpHeight);// 分享图片await CustomImageUtil.shareImage(filePath);} catch (error) {promptAction.showToast({ message: '保存或分享失败:' + (error as BusinessError).message });}}build() {Column() {// 顶部栏TitleBar({title: '纯色图生成'})// 预览区域Column() {Canvas(this.canvas).width(px2vp(this.previewWidth)).height(px2vp(this.previewHeight)).onClick(() => {this.colorPickDialogController?.open();})}.width('100%').height('40%').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {this.screenWidth = Number(newValue.width);this.screenHeight = Number(newValue.height);this.updateCanvasSize();})// 设置区域Scroll() {Column({ space: 20 }) {// 颜色选择Column() {Text('选择颜色').sectionTitle()Row({ space: 15 }) {Button().width(60).height(60).backgroundColor(this.selectedColor).borderRadius(30).borderWidth(3).borderColor($r('app.color.border_color')).shadow({radius: 8,color: '#1A000000',offsetX: 0,offsetY: 2})Text(this.selectedColor).fontSize(16).fontWeight(FontWeight.Medium).fontColor($r('sys.color.ohos_id_color_text_primary'))}}.settingsCard().onClick(() => {this.colorPickDialogController?.open();})// 尺寸设置Column() {Text('图片尺寸').sectionTitle()Row({ space: 8 }) {Column() {Text('宽度:').fontSize(14).fontColor($r('sys.color.ohos_id_color_text_secondary'))Text(`${this.imageWidth}px`).fontSize(16).fontWeight(FontWeight.Medium).fontColor($r('sys.color.ohos_id_color_text_primary'))}.width(60)Button() {SymbolGlyph($r('sys.symbol.minus')).fontSize(20).fontColor(['#FF6B6B'])}.backgroundColor('#FFF0F0').borderColor('#FF6B6B').borderWidth(1).padding(5).onClick(() => {if (this.imageWidth > 40) {this.imageWidth -= 1;this.updateCanvasSize();}})Slider({value: this.imageWidth,min: 40,max: 2000,step: 1,}).showTips(true, this.imageWidth + '').layoutWeight(1).onChange((value: number) => {this.imageWidth = value;this.updateCanvasSize();})Button() {SymbolGlyph($r('sys.symbol.plus')).fontSize(20).fontColor(['#4CAF50'])}.backgroundColor('#F0FFF0').borderColor('#4CAF50').borderWidth(1).padding(5).onClick(() => {if (this.imageWidth < 2000) {this.imageWidth += 1;this.updateCanvasSize();}})}.justifyContent(FlexAlign.Center).width('100%').margin({ bottom: 15 })Row({ space: 8 }) {Column() {Text('高度:').fontSize(14).fontColor($r('sys.color.ohos_id_color_text_secondary'))Text(`${this.imageHeight}px`).fontSize(16).fontWeight(FontWeight.Medium).fontColor($r('sys.color.ohos_id_color_text_primary'))}.width(60)Button() {SymbolGlyph($r('sys.symbol.minus')).fontSize(20).fontColor(['#FF6B6B'])}.backgroundColor('#FFF0F0').borderColor('#FF6B6B').borderWidth(1).padding(5).onClick(() => {if (this.imageHeight > 40) {this.imageHeight -= 1;this.updateCanvasSize();}})Slider({value: this.imageHeight,min: 40,max: 2000,step: 1}).showTips(true, this.imageHeight + '').layoutWeight(1).onChange((value: number) => {this.imageHeight = value;this.updateCanvasSize();})Button() {SymbolGlyph($r('sys.symbol.plus')).fontSize(20).fontColor(['#4CAF50'])}.backgroundColor('#F0FFF0').borderColor('#4CAF50').borderWidth(1).padding(5).onClick(() => {if (this.imageHeight < 2000) {this.imageHeight += 1;this.updateCanvasSize();}})}.justifyContent(FlexAlign.Center).width('100%')}.settingsCard()// 透明度设置Column() {Text('透明度').sectionTitle()Row({ space: 12 }) {Text('0').fontSize(14).fontColor($r('sys.color.ohos_id_color_text_secondary'))Slider({value: this.colorOpacity,min: 0,max: 1,step: 0.01}).showTips(true, this.colorOpacity.toFixed(2)).width(200).onChange((value: number) => {this.colorOpacity = value;this.updateCanvasSize();})Text('1').fontSize(14).fontColor($r('sys.color.ohos_id_color_text_secondary'))}.justifyContent(FlexAlign.Center).width('100%').margin({ bottom: 10 })}.settingsCard()// 圆角设置Column() {Text('圆角大小').sectionTitle()Row({ space: 12 }) {Text('0').fontSize(14).fontColor($r('sys.color.ohos_id_color_text_secondary'))Slider({value: this.colorBorderRadius,min: 0,max: 100,step: 1}).showTips(true, this.colorBorderRadius + '').width(200).onChange((value: number) => {this.colorBorderRadius = value;this.updateCanvasSize();})Text('100').fontSize(14).fontColor($r('sys.color.ohos_id_color_text_secondary'))}.justifyContent(FlexAlign.Center).width('100%').margin({ bottom: 10 })}.settingsCard()CustomSaveButton().onClick(()=>{this.saveImage();})}.width('100%').padding(20)}.edgeEffect(EdgeEffect.Spring).layoutWeight(1).height('50%')}.width('100%').height('100%').backgroundColor($r('app.color.index_tab_bar'))}
}