OS

应用整体架构与技术栈

该绘图应用采用了鸿蒙系统推荐的ArkUI框架进行开发,基于TypeScript语言编写,充分利用了鸿蒙系统的图形渲染和文件操作能力。应用整体架构遵循MVVM(Model-View-ViewModel)模式,通过@State装饰器实现状态与视图的双向绑定,确保数据变化时UI能够自动更新。

技术栈主要包括:

  • ArkUI框架:提供声明式UI开发能力,支持响应式布局和组件化开发
  • Canvas绘图API:通过CanvasRenderingContext2D实现底层绘图逻辑
  • 文件操作API:使用fileIo和fs模块进行文件读写和管理
  • 系统交互API:通过window、promptAction等模块实现系统交互功能

核心功能模块解析

状态管理与数据模型

应用使用@State装饰器管理核心状态,这些状态直接影响UI展示和用户交互:

@State brushSize: number = 10; // 画笔大小
@State brushColor: string = '#000000'; // 画笔颜色
@State backgroundColor1: string = '#FFFFFF'; // 背景颜色
@State isEraser: boolean = false; // 是否使用橡皮擦
@State drawingPoints: Array<Array<number>> = []; // 绘制的点数据
@State isDrawing: boolean = false; // 是否正在绘制

其中,drawingPoints是一个二维数组,用于存储绘制轨迹的坐标点,每个元素形如[x, y],记录了用户绘制时的每一个关键点。这种数据结构使得应用能够高效地重绘整个画布,即使在界面旋转或尺寸变化时也能保持绘制内容的完整性。

绘图核心逻辑实现

绘图功能的核心在于drawLine方法,它负责在画布上绘制线条,并根据是否为橡皮擦模式应用不同的绘制样式:

drawLine(x1: number, y1: number, x2: number, y2: number) {this.context.beginPath();this.context.moveTo(x1, y1);this.context.lineTo(x2, y2);// 设置画笔样式if (this.isEraser) {// 橡皮擦效果this.context.strokeStyle = this.backgroundColor1;this.context.lineWidth = this.brushSize * 1.5;} else {// 画笔效果this.context.strokeStyle = this.brushColor;this.context.lineWidth = this.brushSize;this.context.lineCap = 'round';this.context.lineJoin = 'round';}this.context.stroke();
}

橡皮擦功能的实现采用了巧妙的设计:通过将笔触颜色设置为背景色,并适当增加线条宽度,实现了擦除已有绘制内容的效果。lineCaplineJoin属性设置为round,使得线条端点和连接处呈现圆角效果,提升了绘制线条的美观度。

画布管理与交互处理

Canvas组件的交互处理是绘图应用的关键,代码中通过onTouch事件监听实现了绘制轨迹的记录:

onTouch((event) => {const touch: TouchObject = event.touches[0];const touchX = touch.x;const touchY = touch.y;switch (event.type) {case TouchType.Down:this.isDrawing = true;this.drawingPoints.push([touchX, touchY]);break;case TouchType.Move:if (this.isDrawing) {this.drawingPoints.push([touchX, touchY]);this.drawLine(touchX, touchY, touchX, touchY);}break;case TouchType.Up:this.isDrawing = false;break;}
});

这段代码实现了典型的触摸事件三阶段处理:

  • 按下(Down):开始绘制,记录起始点
  • 移动(Move):持续记录移动轨迹,绘制线条
  • 抬起(Up):结束绘制

通过这种方式,应用能够准确捕捉用户的绘制意图,并将其转化为画布上的线条。

界面设计与用户体验优化

响应式布局设计

应用采用了ArkUI的响应式布局特性,确保在不同尺寸的屏幕上都能良好显示:

build() {Column() {// 顶部工具栏Row({ space: 15 }) { /* 工具栏组件 */ }// 颜色选择区Row({ space: 5 }) { /* 颜色选择组件 */ }// 绘画区域Stack() { /* Canvas组件 */ }// 底部操作区Column() { /* 说明文本和保存按钮 */ }}.width('100%').height('100%');
}

根布局使用Column垂直排列各功能区块,顶部工具栏、颜色选择区、绘画区域和底部操作区依次排列。各组件使用百分比宽度(如width('90%'))和相对单位,确保界面元素能够根据屏幕尺寸自动调整。

交互组件设计

应用提供了直观的用户交互组件,包括:

  1. 工具栏
    • 清除按钮:一键清空画布
    • 橡皮擦/画笔切换按钮:通过颜色变化直观显示当前模式
    • 画笔大小滑块:实时调整画笔粗细
  1. 颜色选择区
    • 预设七种常用颜色,选中时显示黑色边框
    • 点击颜色块即可切换当前画笔颜色
  1. 画布区域
    • 初始状态显示提示文本"点击开始绘画"
    • 支持手势绘制,实时显示绘制内容
  1. 保存功能
    • 底部醒目的保存按钮,点击后将画布内容保存为PNG图片

图片保存与文件操作

图片导出功能实现

图片保存功能是该应用的重要组成部分,通过exportCanvas方法实现:

exportCanvas() {try {// 获取画布数据URLconst dataUrl = this.context.toDataURL('image/png');if (!dataUrl) {promptAction.showToast({message: '获取画布数据失败',duration: 2000});return;}// 解析Base64数据const base64Data = dataUrl.split(';base64,').pop() || '';const bufferData = new Uint8Array(base64Data.length);for (let i = 0; i < base64Data.length; i++) {bufferData[i] = base64Data.charCodeAt(i);}// 生成保存路径const timestamp = Date.now();const fileName = `drawing_${timestamp}.png`;const fileDir = getContext().filesDir;const filePath = `${fileDir}/${fileName}`;// 写入文件fileIo.open(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY).then((file) => {// 写入文件内容并处理后续逻辑}).catch((err:Error) => {// 错误处理});} catch (error) {console.error('导出画布时发生错误:', error);promptAction.showToast({message: '保存图片失败',duration: 2000});}
}

该方法首先通过toDataURL获取画布的PNG格式数据URL,然后将Base64编码的数据转换为Uint8Array,最后使用fileIo模块将数据写入文件系统。这种实现方式确保了画布内容能够准确地保存为图片文件。

文件操作与错误处理

代码中采用了Promise链式调用处理文件操作的异步逻辑,并包含了完整的错误处理机制:

fileIo.open(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY).then((file) => {fileIo.write(file.fd, bufferData.buffer).then(() => {fileIo.close(file.fd).then(() => {promptAction.showToast({message: '保存图片成功',duration: 2000});}).catch((err: Error) => {console.error('关闭文件失败:', err);promptAction.showToast({message: '保存图片失败',duration: 2000});});}).catch((err:Error) => {console.error('写入文件失败:', err);fileIo.close(file.fd).then(() => {promptAction.showToast({message: '保存图片失败',duration: 2000});});});
}).catch((err:Error) => {console.error('打开文件失败:', err);promptAction.showToast({message: '保存图片失败',duration: 2000});
});

这种分层的错误处理方式确保了无论在文件打开、写入还是关闭阶段发生错误,都能给出适当的错误提示,并确保资源被正确释放。

技术要点

关键技术要点

  1. 状态管理:使用@State实现数据与UI的双向绑定,简化了状态更新逻辑
  2. Canvas绘图:掌握CanvasRenderingContext2D的基本操作,包括路径绘制、样式设置等
  3. 异步操作:通过Promise和async/await处理文件操作等异步任务
  4. 响应式布局:利用ArkUI的布局组件和百分比单位实现适配不同屏幕的界面

总结

本文介绍的鸿蒙绘图应用实现了基础的绘图功能,包括画笔绘制、橡皮擦、颜色选择和图片保存等核心功能。通过ArkUI框架和Canvas绘图API的结合,展示了鸿蒙系统在图形应用开发方面的强大能力。

对于开发者而言,该应用可以作为进一步开发复杂绘图应用的基础。通过添加更多绘图工具(如矩形、圆形、文本工具)、图像处理功能(如滤镜、调整亮度对比度)以及云同步功能,能够将其拓展为功能完善的绘图应用。

在鸿蒙生态不断发展的背景下,掌握这类图形应用的开发技术,将有助于开发者创造出更多优秀的用户体验,满足不同用户的需求。

附:代码

import { mediaquery, promptAction, window } from '@kit.ArkUI';
import { fileIo } from '@kit.CoreFileKit';
import preferences from '@ohos.data.preferences';@Entry
@Component
struct Index {@State brushSize: number = 10; // 画笔大小@State brushColor: string = '#000000'; // 画笔颜色@State backgroundColor1: string = '#FFFFFF'; // 背景颜色@State isEraser: boolean = false; // 是否使用橡皮擦@State drawingPoints: Array<Array<number>> = []; // 绘制的点数据@State isDrawing: boolean = false; // 是否正在绘制// 预设颜色private presetColors: Array<string> = ['#000000', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF'];// 画布参数private canvasWidth: number = 0;private canvasHeight: number = 0;private context: CanvasRenderingContext2D = new CanvasRenderingContext2D({ antialias: true});// 页面初始化aboutToAppear(): void {// 设置页面背景色window.getLastWindow(getContext()).then((windowClass) => {windowClass.setWindowBackgroundColor('#F5F5F5');});}// 清除画布clearCanvas() {this.drawingPoints = [];this.redrawCanvas();}// 重绘画布redrawCanvas() {this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);this.context.fillStyle = this.backgroundColor1;this.context.fillRect(0, 0, this.canvasWidth, this.canvasHeight);// 重绘所有绘制点for (let i = 0; i < this.drawingPoints.length; i++) {const point = this.drawingPoints[i];if (i > 0) {const prevPoint = this.drawingPoints[i - 1];this.drawLine(prevPoint[0], prevPoint[1], point[0], point[1]);}}}// 绘制线条drawLine(x1: number, y1: number, x2: number, y2: number) {this.context.beginPath();this.context.moveTo(x1, y1);this.context.lineTo(x2, y2);// 设置画笔样式if (this.isEraser) {// 橡皮擦效果this.context.strokeStyle = this.backgroundColor1;this.context.lineWidth = this.brushSize * 1.5;} else {// 画笔效果this.context.strokeStyle = this.brushColor;this.context.lineWidth = this.brushSize;this.context.lineCap = 'round';this.context.lineJoin = 'round';}this.context.stroke();}// 导出画布为图片exportCanvas() {try {// 获取画布数据URLconst dataUrl = this.context.toDataURL('image/png');if (!dataUrl) {promptAction.showToast({message: '获取画布数据失败',duration: 2000});return;}// 解析Base64数据const base64Data = dataUrl.split(';base64,').pop() || '';const bufferData = new Uint8Array(base64Data.length);for (let i = 0; i < base64Data.length; i++) {bufferData[i] = base64Data.charCodeAt(i);}// 生成保存路径const timestamp = Date.now();const fileName = `drawing_${timestamp}.png`;const fileDir = getContext().filesDir;const filePath = `${fileDir}/${fileName}`;// 写入文件fileIo.open(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY).then((file) => {fileIo.write(file.fd, bufferData.buffer).then(() => {fileIo.close(file.fd).then(() => {promptAction.showToast({message: '保存图片成功',duration: 2000});console.info(`图片已保存至: ${filePath}`);}).catch((err: Error) => {console.error('关闭文件失败:', err);promptAction.showToast({message: '保存图片失败',duration: 2000});});}).catch((err:Error) => {console.error('写入文件失败:', err);fileIo.close(file.fd).then(() => {promptAction.showToast({message: '保存图片失败',duration: 2000});});});}).catch((err:Error) => {console.error('打开文件失败:', err);promptAction.showToast({message: '保存图片失败',duration: 2000});});} catch (error) {console.error('导出画布时发生错误:', error);promptAction.showToast({message: '保存图片失败',duration: 2000});}}build() {Column() {// 顶部工具栏Row({ space: 15 }) {// 清除按钮Button('清除').width('20%').height('8%').fontSize(14).backgroundColor('#FFCCCC').onClick(() => {this.clearCanvas();});// 橡皮擦按钮Button(this.isEraser ? '橡皮擦':'画笔').width('18%').height('8%').fontSize(14).backgroundColor(this.isEraser ? '#FFCCCC' : '#CCFFCC').onClick(() => {this.isEraser = !this.isEraser;});// 画笔大小控制Column() {Text('画笔').fontSize(12).margin({ bottom: 2 });Slider({min: 1,max: 30,value: this.brushSize,// showTips: true}).width('60%').onChange((value: number) => {this.brushSize = value;});}.width('30%');}.width('100%').padding(10).backgroundColor('#E6E6E6');// 颜色选择区Row({ space: 5 }) {ForEach(this.presetColors, (color: string) => {Stack() {// 显示颜色块Column().width(30).height(30).borderRadius(5).backgroundColor(color).borderWidth(this.brushColor === color ? 2 : 0).borderColor('#000000') // 统一使用黑色边框表示选中状态,避免颜色冲突.onClick(() => {this.brushColor = color;this.isEraser = false; // 切换颜色时取消橡皮擦模式console.log(`Selected color: ${color}`)});}.width(30).height(30).onClick(() => {this.brushColor = color;this.isEraser = false; // 切换颜色时取消橡皮擦模式});});}.width('100%').padding(10).backgroundColor('#FFFFFF');// 绘画区域Stack() {Canvas(this.context).aspectRatio(3/4).width('90%').height('60%').backgroundColor(this.backgroundColor1).borderRadius(10).onReady(() => {this.context.fillStyle = this.backgroundColor1;this.context.fillRect(0, 0, this.canvasWidth, this.canvasHeight);}).onAreaChange((oldVal, newVal) => {this.canvasWidth = newVal.width as number;this.canvasHeight = newVal.height as number;this.context.fillStyle = this.backgroundColor1;this.context.fillRect(0, 0, this.canvasWidth, this.canvasHeight);}).onTouch((event) => {const touch: TouchObject = event.touches[0];const touchX = touch.x;const touchY = touch.y;switch (event.type) {case TouchType.Down:this.isDrawing = true;this.drawingPoints.push([touchX, touchY]);break;case TouchType.Move:if (this.isDrawing) {this.drawingPoints.push([touchX, touchY]);// 使用更平滑的绘制方式this.drawLine(touchX, touchY, touchX, touchY);}break;case TouchType.Up:this.isDrawing = false;break;}});// 提示文本if (this.drawingPoints.length === 0) {Text('点击开始绘画').fontSize(18).fontColor('#999').fontStyle(FontStyle.Italic);}}.width('100%').margin({ top: 20, bottom: 30 });// 底部说明Text('简单绘画板 - 拖动手指即可绘制').fontSize(14).fontColor('#666').margin({ bottom: 20 });Button('保存图片', { type: ButtonType.Normal, stateEffect: true }).width('90%').height(40).fontSize(16).fontColor('#333333').backgroundColor('#E0E0E0').borderRadius(8).onClick(() => {this.exportCanvas();});}.width('100%').height('100%');}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/86130.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/86130.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/86130.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据分析和可视化:Py爬虫-XPath解析章节要点总结

重要知识点 XPath 概述&#xff1a;XPath 是一门可以在 XML 文件中查找信息的语言&#xff0c;也可用于 HTML 文件。它功能强大&#xff0c;提供简洁明了的路径表达式和多个函数&#xff0c;用于字符串、数值、时间比较等。1999 年成为 W3C 标准&#xff0c;常用于爬虫中抓取网…

深入理解PHP中的生成器(Generators)

创建一个生成器非常简单。你只需要像定义普通函数一样定义它&#xff0c;但是使用yield关键字来产出值。例如&#xff0c;以下是一个简单的斐波那契数列生成器&#xff1a; function fibonacci() {$num1 0;$num2 1;while (true) {yield $num1;$temp $num1 $num2;$num1 $n…

ubuntu 系统 pgm图片和png相互转化

ubuntu 系统 pgm图片和png相互转化。 安装转化工具&#xff1a; sudo apt-get install imagemagick pgm转为png指令如下: convert input.pgm output.png png转为pgm指令如下: convert input.png output.pgm

leetcode:98. 验证二叉搜索树

学习要点 加深纯递归算法的理解 题目链接 98. 验证二叉搜索树 - 力扣&#xff08;LeetCode&#xff09; 题目描述 解法&#xff1a;纯递归 vector<int> v;void dfs(TreeNode* root){if(root nullptr){return;}dfs(root->left);v.push_back(root->val);dfs(root…

如何确定IP的缺省子网掩码是多少?

IP地址 201.100.200.1 的缺省子网掩码由其 IP地址类别 决定。以下是判断步骤&#xff1a; 1. 确定IP地址类别 IPv4地址分为 A、B、C、D、E 五类&#xff0c;根据第一个字节&#xff08;前8位&#xff09;的范围划分&#xff1a; A类&#xff1a;1.0.0.0 ~ 126.255.255.255&am…

Vue.js 粒子连线动画组件 - FlyingLines

Vue.js 粒子连线动画组件 - FlyingLines 使用指南 &#x1f31f; 简介 FlyingLines 是一个基于 Vue.js 的炫酷粒子连线动画组件&#xff0c;可以为您的网站添加动态的背景效果。该组件具有以下特点&#xff1a; ✨ 流畅动画&#xff1a;基于 Canvas 的高性能渲染&#x1f5b…

无人机交互控制技术要点

一、技术要点 1. 物理交互设计 仿生柔性形态学&#xff1a;采用梯度刚度复合材料&#xff08;如硅胶-碳纤维&#xff09;设计柔性抓取器&#xff0c;模仿鸟类爪部结构&#xff0c;实现被动碰撞抑制与动态力生成&#xff0c;支持高速交互&#xff08;>3 m/s&#xff09;和…

qt集成openssl

第一&#xff1a;下载项目中对应版本的openssl的库 https://openssl-library.org/source/old/1.0.2/ 老版本的openssl的下载地址&#xff0c;这个下载的好像是源码&#xff0c;还要编译。 https://indy.fulgan.com/SSL/ 在这里下载不需要编译&#xff0c;下载下来直接用dll文件…

【鸿蒙HarmonyOS Next App实战开发】​​ArkUI时钟界面实现解析:动态双模式时钟与沉浸式体验​

在鸿蒙next系统上&#xff0c;通过ArkTS写了个时钟显示页面&#xff0c;集成在【图影工具箱】应用中&#xff0c;应用商店可以下载使用。 这个页面实现起来比较简单&#xff0c;就是左边一个模拟时钟&#xff0c;右边一个数字时钟&#xff08;包含时间和日期的文字&#xff09…

ios签名错误的解决办法

另一种最常见的解决方案。在终端中运行以下命令。您应该添加自己的钥匙串名称和密码。security lock-keychain temp.keychainsecurity unlock-keychain -pp ssw0rd temp.keychain在这种情况下&#xff0c;使用钥匙串名称为“temp”&#xff0c;其密码为“p ssw0rd”。此外&am…

C#读取OPCUA节点数据

本人第一次接触OPCUA&#xff0c;如有不对的地方望指正&#xff0c;获取的是公司的OPCUA服务器的数据 方式一&#xff1a; 测试环境: window11 vs2022 OPCFoundation.NetStandard.Opc.Ua .net framework 4.8 (2025-06-23 经过测试&#xff0c;.net8也可以使用这套.net …

OpenCV计算机视觉实战(11)——边缘检测详解

OpenCV计算机视觉实战&#xff08;11&#xff09;——边缘检测详解 0. 前言1. Sobel 算子与方向梯度1.1 Sobel 算子简介1.2 实现过程 2. Laplacian 边缘检测2.1 Laplacian 算子简介2.2 实现过程 3. Canny 算法3.1 Canny 算法简介3.2 实现过程 小结系列链接 0. 前言 边缘检测能…

哈尔滨idc服务器租用-青蛙云

在数字化浪潮汹涌的当下&#xff0c;企业对于服务器的需求愈发强烈。哈尔滨作为东北地区重要的经济文化中心&#xff0c;其 IDC 服务器租用市场也呈现出蓬勃发展的态势。众多企业在寻求 IDC 服务器租用时&#xff0c;青蛙云凭借自身显著优势脱颖而出&#xff0c;成为众多用户的…

Zephyr 系统深入解析:SoC 支持包结构与中断调度器调优实践

本文将全面深入讲解 Zephyr RTOS 的 SoC 支持包设计架构&#xff08;SoC Series / SoC Variant&#xff09;、中断系统实现、调度器原理、时间片与优先级调优技巧&#xff0c;以及如何在实际项目中构建自定义 SoC 支持包、实现高效的调度器策略和系统性能优化。全文超过 5000 字…

FPGA基础 -- Verilog 结构建模之模块参数值

Verilog 中模块参数值&#xff08;parameter&#xff09;的使用&#xff0c;这是结构建模和模块可配置设计的核心机制&#xff0c;广泛应用于 总线宽度配置、流水线级数、功能开关、模块复用 等场景。 一、什么是模块参数值&#xff08;parameter&#xff09; parameter 是 Ver…

Skrill是什么?中国用户能用吗?安全吗?完整指南

什么是Skrill&#xff1f; Skrill 前身为 Moneybookers&#xff0c;成立于 2001 年&#xff0c;总部位于英国伦敦&#xff0c;目前隶属于 Paysafe 集团。作为一个多功能电子支付平台&#xff0c;Skrill 支持全球 100 多个国家和地区、40 多种货币&#xff0c;被广泛用于&#…

java+vue+SpringBoo校园部门资料管理系统(程序+数据库+报告+部署教程+答辩指导)

源代码数据库LW文档&#xff08;1万字以上&#xff09;开题报告答辩稿ppt部署教程代码讲解代码时间修改工具 技术实现 开发语言&#xff1a;后端&#xff1a;Java 前端&#xff1a;vue框架&#xff1a;springboot数据库&#xff1a;mysql 开发工具 JDK版本&#xff1a;JDK1.…

Java中的Map实现类详解

Java中的Map实现类详解 Java集合框架提供了多种Map接口的实现&#xff0c;每种实现都有其特定的使用场景和特点。以下是主要的Map实现类及其特性分析&#xff1a; 1. 通用Map实现 HashMap 特点&#xff1a;基于哈希表的实现&#xff0c;允许null键和null值线程安全&#xf…

Pytorch Lightning 进阶 1 - 梯度检查点(Gradient Checkpointing)

梯度检查点&#xff08;Gradient Checkpointing&#xff09;是一种在深度学习训练中优化显存使用的技术&#xff0c;尤其适用于处理大型模型&#xff08;如Transformer架构&#xff09;时显存不足的情况。下面用简单的例子解释其工作原理和优缺点&#xff1a; 核心原理 深度学…

SpreadJS 迷你图:数据趋势可视化的利器

引言 在数据处理和分析领域&#xff0c;直观地展示数据趋势对于理解数据和做出决策至关重要。迷你图作为一种简洁而有效的数据可视化方式&#xff0c;在显示数据趋势方面发挥着重要作用&#xff0c;尤其在与他人共享数据时&#xff0c;能够快速传达关键信息。SpreadJS 作为一款…