【HarmonyOS开发】案例-签名板基础上开发移动白板

大前端腾宇

共 26660字,需浏览 54分钟

 · 2024-04-11

f8f7c86aa652859c6e71327277a21d26.webp

前段时间写了个签名板的应用,在手机上测试还是很流畅的,可以将签名信息保存到手机相册,在此基础上加上数据共享、橡皮檫、前进、后退等功能,就是一个移动白板。

【HarmonyOS开发】案例-签名板开发

功能点分析

1、画笔功能:可设置画笔粗细和颜色;

2、橡皮擦功能:可设置橡皮擦大小;

3、撤回和回撤功能;

4、清空画板功能;

5、分布式,多设备共享数据;

6、保存当前绘制内容;

技术点拆分

  • Canvas画布组件的使用;

  • fs操作文件,buffer数据格式转换;

  • @ohos.file.picker保存图片;

  • 权限配置与调用授权;

  • 设备管理(@ohos.distributedHardware.deviceManager),数据共享(@ohos.data.distributedKVStore)

画笔绘制功能

使用onTouch方法,监听触摸事件

  1. 手指按下:使用方法moveTo记录起点;

    手指移动:使用方法beginPath创建新的路径,lineTo记录移动的点,并绘制;

              
                 // 创建一个新的绘制路径
              
              
                 this.crc.beginPath()
              
              
                 // 设置起点坐标
              
              
                 this.crc.moveTo(x, y)
              
              
                 // 设置移动点
              
              
                 this.crc.lineTo(x, y)
              
              
                 // 进行路径绘制
              
              
                 this.crc.stroke()
              
            

    设置画笔样式

              
                
                  // 线的宽度,可以通过Slider控制线的宽度
                
              
              
                this.crc.lineWidth = 5 
              
              
                
                  
    // 线的颜色,通过颜色选择器修改画笔颜色 this.crc.strokeStyle = 'rgba(0,0,0,0)'

    颜色选择器,通过Canvas的CanvasRenderingContext2D实现

              
                
                  @Component
                
              
              
                export struct ColorPickerView {
              
              
                  // 渲染设置-抗锯齿
              
              
                  private settings: RenderingContextSettings = new RenderingContextSettings(true)
              
              
                  // 颜色面板-画布渲染上下文对象
              
              
                  private crcBoard: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
              
              
                  // 颜色条-画布渲染上下文对象
              
              
                  private crcBar: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
              
              
                  // 颜色面板宽度
              
              
                  private boardWidth = 0
              
              
                  // 颜色面板高度
              
              
                  private boardHeight = 0
              
              
                  // 颜色面板指示器大小
              
              
                  @State boardIndicatorSize: number = 0
              
              
                  // 颜色面板指示器的X偏移量
              
              
                  @State boardIndicatorOffsetX: number = 0
              
              
                  // 颜色面板指示器的Y偏移量
              
              
                  @State boardIndicatorOffsetY: number = 0
              
              
                  // 颜色条宽度
              
              
                  private barWidth = 0
              
              
                  // 颜色条高度
              
              
                  private barHeight = 0
              
              
                  // 颜色条指示器大小
              
              
                  @State barIndicatorSize: number = 0
              
              
                  // 颜色条指示器的偏移量
              
              
                  @State barIndicatorOffsetY: number = 1
              
              
                  // 颜色选择改变回调
              
              
                  private colorChange: (color: string) => void
              
              
                
                  

    /** * 颜色面板 */ @Builder ColorBoard() { Canvas(this.crcBoard) .onAreaChange((oldValue: Area, newValue: Area) => { // 获取组件的宽高 this.boardWidth = parseInt(newValue.width.toString()) this.boardHeight = parseInt(newValue.height.toString()) this.boardIndicatorSize = this.boardWidth / 20 this.boardIndicatorOffsetX = this.boardWidth - 1 // 绘制颜色面板 this.drawColorBoard() }) }
    /** * 颜色面板指示器 */ @Builder ColorBoardIndicator() { if (this.boardIndicatorSize != 0) { Stack() { Stack() { } .width(this.boardIndicatorSize - 1) .height(this.boardIndicatorSize - 1) .border({ color: Color.White, width: 1, radius: this.boardIndicatorSize / 2 })
    Stack() { } .width(this.boardIndicatorSize) .height(this.boardIndicatorSize) .border({ color: Color.Black, width: 1, radius: this.boardIndicatorSize / 2 }) }.offset({ x: this.boardIndicatorOffsetX, y: this.boardIndicatorOffsetY }) .markAnchor({ x: '50%', y: '50%' }) } }
    /** * 颜色条 */ @Builder ColorBar() { Canvas(this.crcBar) .onAreaChange((oldValue: Area, newValue: Area) => { // 获取组件的宽高 this.barWidth = parseInt(newValue.width.toString()) this.barHeight = parseInt(newValue.height.toString()) this.barIndicatorSize = this.barWidth / 3 // 创建渐变色的范围 const grad = this.crcBar.createLinearGradient(0, 0, 0, this.barHeight) // 设置渐变颜色和比例。 grad.addColorStop(0, 'rgb(255, 0, 0)') grad.addColorStop(1 * 1 / 6, 'rgb(255, 255, 0)') grad.addColorStop(2 * 1 / 6, 'rgb(0, 255, 0)') grad.addColorStop(3 * 1 / 6, 'rgb(0, 255, 255)') grad.addColorStop(4 * 1 / 6, 'rgb(0, 0, 255)') grad.addColorStop(5 * 1 / 6, 'rgb(255, 0, 255)') grad.addColorStop(1, 'rgb(255, 0, 0)') // 设置渐变色 this.crcBar.fillStyle = grad // 绘制矩形 this.crcBar.fillRect(0, 0, this.barWidth, this.barHeight) }).width('100%') .borderWidth(0.5) }
    /** * 颜色条指示器 */ @Builder ColorBarIndicator() { Row() { if (this.barIndicatorSize != 0) { Path() .width(this.barIndicatorSize) .height(this.barIndicatorSize) .commands(`M0 0 L${vp2px(this.barIndicatorSize)} ${vp2px(this.barIndicatorSize / 2)} L0 ${vp2px(this.barIndicatorSize)} Z`) .fill('#00000000') .stroke(Color.Black) .strokeWidth(0.8)
    Blank()
    Path() .width(this.barIndicatorSize) .height(this.barIndicatorSize) .commands(`M0 ${vp2px(this.barIndicatorSize / 2)} L${vp2px(this.barIndicatorSize)} 0 L${vp2px(this.barIndicatorSize)} ${vp2px(this.barIndicatorSize)} Z`) .fill('#00000000') .stroke(Color.Black) .strokeWidth(0.8) } }.width('100%') .padding(1) .offset({ y: this.barIndicatorOffsetY }) .markAnchor({ y: '50%' }) }
    build() { Row() { // 颜色面板-宽度占比85% Stack() { // 颜色面板 this.ColorBoard() // 颜色面板指示器 this.ColorBoardIndicator() } .width('85%') .height('100%') .alignContent(Alignment.TopStart) .onTouch((event) => this.onTouchEventBoard(event))
    // 颜色条区域-宽度占比15% Row() { Stack() { // 颜色条 this.ColorBar() // 颜色条指示器 if (this.barIndicatorSize != 0) { this.ColorBarIndicator() } } .width('60%') .height('100%') .alignContent(Alignment.Top) .onTouch((event) => this.onTouchEventBar(event))
    }.width('15%').height('100%') .justifyContent(FlexAlign.Center)
    }.width('100%').height('100%') }
    /** * 绘制颜色面板 * @param bgColor 背景颜色 默认颜色:红色 */ drawColorBoard(bgColor = 'rgb(255, 0, 0)') { // 清空画布 this.crcBoard.clearRect(0, 0, this.boardWidth, this.boardHeight)
    // 绘制背景色 this.crcBoard.fillStyle = bgColor this.crcBoard.fillRect(0, 0, this.boardWidth, this.boardHeight)
    // 绘制渐变色:白色->透明色 const gradWhite = this.crcBoard.createLinearGradient(0, 0, this.boardWidth, 0) gradWhite.addColorStop(0, 'rgb(255,255,255)') gradWhite.addColorStop(1, 'rgba(255,255,255,0)') this.crcBoard.fillStyle = gradWhite this.crcBoard.fillRect(0, 0, this.boardWidth, this.boardHeight)
    // 绘制渐变色:黑色->透明色 const gradBlack = this.crcBoard.createLinearGradient(0, this.boardHeight, 0, 0) gradBlack.addColorStop(0, 'rgb(0,0,0)') gradBlack.addColorStop(1, 'rgba(0,0,0,0)') this.crcBoard.fillStyle = gradBlack this.crcBoard.fillRect(0, 0, this.boardWidth, this.boardHeight) }
    /** * 颜色条触摸事件 */ onTouchEventBar(event: TouchEvent) { // x坐标 let x = event.touches[0].x // y坐标 let y = event.touches[0].y // 触摸区域限制 if (x < 0)x = 0 if (x > this.barWidth - 1.2) x = this.barWidth - 1.2 if (y < 1)y = 1 if (y > this.barHeight - 1.2) y = this.barHeight - 1.2 console.log(`颜色条-当前坐标:x = ${x}, y = ${y}`) // 触摸y坐标赋值给指示器偏移量 this.barIndicatorOffsetY = y // 获取颜色条坐标点一个像素的颜色值 let imageData = this.crcBar.getImageData(x, y, px2vp(5), px2vp(5)) console.log(`--------颜色条-当前颜色:` + JSON.stringify(imageData)) // 绘制颜色面板 this.drawColorBoard(`rgb(${imageData.data[0]},${imageData.data[1]},${imageData.data[2]})`) // 获取颜色面板选中的颜色 this.getBoardSelectColor() }
    /** * 颜色面板触摸事件 */ onTouchEventBoard(event: TouchEvent) { // x坐标 let x = event.touches[0].x // y坐标 let y = event.touches[0].y // 触摸区域限制 if (x < 0)x = 0 if (x > this.boardWidth - 1) x = this.boardWidth - 1 if (y < 0)y = 0 if (y > this.boardHeight - 1) y = this.boardHeight - 1 // 触摸xy坐标赋值给指示器偏移量 this.boardIndicatorOffsetX = x this.boardIndicatorOffsetY = y // 获取颜色面板选中的颜色 this.getBoardSelectColor() } /** * 获取颜色面板选中的颜色 */ getBoardSelectColor() { console.log(`颜色面板-当前坐标:x = ${this.boardIndicatorOffsetX}, y = ${this.boardIndicatorOffsetY}`) // 获取坐标点一个像素的颜色值 let imageData = this.crcBoard.getImageData(this.boardIndicatorOffsetX, this.boardIndicatorOffsetY, px2vp(5), px2vp(5)) console.log(`颜色面板-当前颜色:` + JSON.stringify(imageData)) this.colorChange(`rgb(${imageData.data[0]},${imageData.data[1]},${imageData.data[2]})`) } }

    API9实现

              
                import { ColorPickerView } from '../../../../../../ColorPicker/src/main/ets/components/ColorPicker/ColorPicker'
              
              
                
                  
    @Entry @Component struct Index { @State color: string = 'rgb(255, 0, 0)'
    build() { Column() { Text('颜色选择器') .fontSize(80) .fontWeight(FontWeight.Bold) .fontColor(this.color)
    Stack() { ColorPickerView({ colorChange: (color) => { this.color = color } }) } .zIndex(999) .width(300) .height(300) .margin({ top: 30 })
    }.width('100%') .height('100%') .justifyContent(FlexAlign.Center) } }
            
          
            使用颜色选择器
          
            

橡皮擦功能

通过globalCompositeOperation绘制属性实现橡皮擦功能

      
        
          // 新内容在之前内容的之上
        
      
      
        this.crc.globalCompositeOperation = 'source-over'
      
      
        
          
// 新内容与之前内容相交位置变透明 this.crc.globalCompositeOperation = 'destination-out'

撤回和回撤功能

最简单的方法就是使用一个数组保存绘制信息,包含以下几个信息:

    • 是否为画笔 /橡皮擦

    • 画笔/橡皮擦的颜色或宽度;

    • 坐标点(X、Y);

每次绘制的信息,保存在数组中,在点击撤回时,撤回数+1;回撤时,撤回数-1,并截取数组,清空画布,遍历数组绘制笔画信息。

        
          
            /**
          
        
        
          
             * 坐标点
          
        
        
          
             * @param x 坐标点x
          
        
        
          
             * @param y 坐标点y
          
        
        
          
             */
          
        
        
          export class Coord {
        
        
            // 坐标点x
        
        
            x: number;
        
        
            // 坐标点y
        
        
            y: number;
        
        
            constructor(x: number, y: number) {
        
        
              this.x = x;
        
        
              this.y = y;
        
        
            }
        
        
          }
        
      
        
          
            /**
          
        
        
          
             * 绘制信息
          
        
        
          
             * @param lineColor 线颜色
          
        
        
          
             * @param lineWidth 线宽度
          
        
        
          
             * @param listCoord 坐标点集合
          
        
        
          
             */
          
        
        
          export class BoardInfoModel {
        
        
            // 是否为画笔,是:画笔,否:橡皮擦
        
        
            // 根据此字段设置绘制属性:合成操作globalCompositeOperation
        
        
            isPen: boolean;
        
        
            // 线颜色
        
        
            lineColor: string;
        
        
            // 线宽度
        
        
            lineWidth: number;
        
        
            // 坐标点集合
        
        
            listCoord: Array<Coord>;
        
        
            constructor(isPen: boolean, lineColor: string, lineWidth: number, listCoord: Array<Coord>) {
        
        
              this.isPen = isPen;
        
        
              this.lineColor = lineColor;
        
        
              this.lineWidth = lineWidth;
        
        
              this.listCoord = listCoord;
        
        
            }
        
        
          }
        
      

移动白板清空功能

白板一旦清空,需要屏蔽撤回/回撤/清空功能,并清空存储的数组绘制信息,还原画笔/橡皮擦样式等。

通过clearRect方法清空移动白板,

      
        
          // 撤回不可用
        
      
      
                this.revocationEnabled = false
      
      
                // 回撤不可用
      
      
                this.unRevocationEnabled = false
      
      
                // 清空不可用
      
      
                this.clearEnabled = false
      
      
                // 记录的数组清空
      
      
                this.listTempXY = []
      
      
                this.listAllXY = []
      
      
                // 选中画笔
      
      
                this.isSelectPen = true
      
      
                // 清空画布
      
      
                this.crc.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
      
    

保存当前内容为图片功能

提供图片保存功能,将画板内容存储到手机相册中,用于后续查看等。

      
        import picker from '@ohos.file.picker';
      
      
        import fs from '@ohos.file.fs';
      
      
        import buffer from '@ohos.buffer';
      
      
        import common from '@ohos.app.ability.common';
      
      
        import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
      
      
        
          
handleSave() { let context = getContext(this) as common.UIAbilityContext; const image: string = this.context.toDataURL() this.imageUrl = image console.log('========data============', JSON.stringify(image)) let atManager = abilityAccessCtrl.createAtManager(); atManager.requestPermissionsFromUser(context, ["ohos.permission.WRITE_MEDIA", "ohos.permission.READ_MEDIA"], (error, result) => { if (result) { console.log("requestPermissionsFromUser-result: " + JSON.stringify(result)); if (result.authResults[0] === 0) { this.saveImage(image) } } else { console.log("requestPermissionsFromUser-error: " + JSON.stringify(error)); } }); }
saveImage(base64: string) { //文件保存路径 let uri = ''; console.error('saveImage======666===>'); try { let PhotoSaveOptions = new picker.PhotoSaveOptions(); //保存图片默认名称 PhotoSaveOptions.newFileNames = [`image_${new Date().getTime()}.png`]; let photoPicker = new picker.PhotoViewPicker(); console.error('saveImage======999===>'); //调起系统的图片保存功能 try { photoPicker.save(PhotoSaveOptions).then((PhotoSaveResult) => { console.error('saveImage======888===>', JSON.stringify(PhotoSaveResult)); uri = PhotoSaveResult[0]; //获取图片的base64字符串 let imageStr = base64.split(',')[1]; //打开文件 let file = fs.openSync(uri, fs.OpenMode.READ_WRITE); //base64字符串转成buffer const decodeBuffer = buffer.from(imageStr, 'base64').buffer; //写入文件 fs.writeSync(file.fd, decodeBuffer); //关闭文件 fs.closeSync(file); console.error('saveImage======000===>'); }).catch((err: Error) => { console.error('saveImage======111===>', err + ''); }) } catch (err) { console.error('saveImage======010101===>', JSON.stringify(err)); } } catch (e) { console.error('saveImage====222=====>', e); } }

分布式功能

  • 通过@ohos.distributedDeviceManager获取设备信息;

  • 通过@ohos.data.distributedKVStore管理数据的共享;

具体可以参考:【HarmonyOS开发】分布式应用的开发实践

f78f8aba467e6daf16ac5b67521d02f3.webp

浏览 4
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报