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();
}
橡皮擦功能的實現采用了巧妙的設計:通過將筆觸顏色設置為背景色,并適當增加線條寬度,實現了擦除已有繪制內容的效果。lineCap
和lineJoin
屬性設置為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%')
)和相對單位,確保界面元素能夠根據屏幕尺寸自動調整。
交互組件設計
應用提供了直觀的用戶交互組件,包括:
- 工具欄:
-
- 清除按鈕:一鍵清空畫布
- 橡皮擦/畫筆切換按鈕:通過顏色變化直觀顯示當前模式
- 畫筆大小滑塊:實時調整畫筆粗細
- 顏色選擇區:
-
- 預設七種常用顏色,選中時顯示黑色邊框
- 點擊顏色塊即可切換當前畫筆顏色
- 畫布區域:
-
- 初始狀態顯示提示文本"點擊開始繪畫"
- 支持手勢繪制,實時顯示繪制內容
- 保存功能:
-
- 底部醒目的保存按鈕,點擊后將畫布內容保存為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});
});
這種分層的錯誤處理方式確保了無論在文件打開、寫入還是關閉階段發生錯誤,都能給出適當的錯誤提示,并確保資源被正確釋放。
技術要點
關鍵技術要點
- 狀態管理:使用@State實現數據與UI的雙向綁定,簡化了狀態更新邏輯
- Canvas繪圖:掌握CanvasRenderingContext2D的基本操作,包括路徑繪制、樣式設置等
- 異步操作:通過Promise和async/await處理文件操作等異步任務
- 響應式布局:利用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%');}
}