#HarmonyOS NEXT体验官# 基于鸿蒙Next的App(创意工坊)解析6:如何将SVG插入到画布中 原创

蒙娜丽宁
发布于 2024-7-31 15:10
浏览
0收藏

本文会详细介绍如何将矢量图(SVG)插入画布,矢量图需要被编码为base64,然后直接保存到js代码中。如显示动物列表的js代码如下图所示:
#HarmonyOS NEXT体验官# 基于鸿蒙Next的App(创意工坊)解析6:如何将SVG插入到画布中-鸿蒙开发者社区
每一个svg是一个数组元素。最终的SVG显示界面如下图所示。
#HarmonyOS NEXT体验官# 基于鸿蒙Next的App(创意工坊)解析6:如何将SVG插入到画布中-鸿蒙开发者社区
这个界面是使用InsertImages组件实现的。。

InsertImages类实现的详细解释与注释

代码解释

这段代码实现了一个用于选择图像的组件 InsertImages。该组件由一组按钮和一个Webview组成,按钮用于选择不同的图像类别,Webview用于显示图像列表。用户可以点击按钮从不同的图像类别中选择图像,并将选择的结果通过回调函数返回。

详细代码注释

// 从相对路径引入常量,这些常量定义了一些面板和按钮的背景颜色
import { PANEL_BACKGROUND_COLOR, PANEL_BUTTON_BACKGROUND_COLOR } from '../common/const'

// 引入ArkWeb中的webview模块
import { webview } from '@kit.ArkWeb';

// 引入常用的工具函数,如去除引号
import { trimQuotes } from '../common/common';

// 使用@Entry标记这个组件为入口组件
@Entry

// 声明一个组件InsertImages
@Component
export struct InsertImages {
  
  // 可选的回调函数,当选择图像时触发,传递图像列表的名称和索引
  imageSelected?: (imageListName: string, imageListIndex: number) => void;
  
  // 可选的回调函数,当图像列表关闭时触发
  imageListClosed?: () => void;
  
  // 声明一个WebviewController,用于控制Webview组件
  controller1: webview.WebviewController = new webview.WebviewController();
  
  // 声明一个状态变量formatName,用于存储格式名称
  @State formatName: string = '';

  // build方法定义了组件的UI结构
  build() {
    // 创建一个Column组件,用于垂直布局
    Column() {
      
      // 创建一个Row组件,用于水平布局
      Row() {
        
        // 再次创建一个Column组件,内嵌的Column,用于放置多个按钮
        Column() {
          
          // 创建一个Row组件,用于放置选择按钮和关闭图标
          Row() {
            
            // 创建一个按钮组件,用于触发选择图像的操作
            Button({ type: ButtonType.Normal }) {
              // 按钮内部的文本
              Text('选择')
                .fontSize(16) // 设置字体大小
                .fontColor('#FFFFFF') // 设置字体颜色
                .width('100%') // 设置宽度为100%
                .height('100%') // 设置高度为100%
                .textAlign(TextAlign.Center) // 设置文本居中对齐
            }
            .borderRadius(5) // 设置按钮的圆角半径
            .width(60) // 设置按钮的宽度
            .height(30) // 设置按钮的高度
            .backgroundColor('#CC0000') // 设置按钮的背景颜色
            .onClick(() => { // 设置按钮点击事件
              
              // 调用Webview中的JavaScript函数获取选中的图像数据
              this.controller1.runJavaScript(`getSelectedImageData()`)
                .then(data => {
                  
                  if (data === '') { // 如果没有选中任何图像
                    // 显示一个提示对话框,告知用户未选择图像
                    AlertDialog.show({
                      message: '还没有选择图像',
                      alignment: DialogAlignment.Center,
                      primaryButton: {
                        value: '关闭',
                        action: () => {
                          // 对话框关闭时不执行任何操作
                        }
                      }
                    })
                  } else {
                    
                    // 将返回的JSON字符串解析为数组
                    let parsedArray: [string, number];
                    try {
                      parsedArray = JSON.parse(data);
                      // 提取图像列表名称和索引
                      let imageListName: string = parsedArray[0];
                      let imageListIndex: number = parsedArray[1];
                      
                      // 如果imageSelected回调函数存在,则调用该回调函数
                      if (this.imageSelected) {
                        this.imageSelected(imageListName, imageListIndex);
                      }
                    } catch (e) {
                      console.error("Error parsing JSON string:", e); // 捕捉解析错误并输出日志
                    }
                  }
                });
            })

            // 创建一个关闭图像列表的图标,点击时触发关闭事件
            Image($r('app.media.close')).width(20).height(20)
              .margin({ left: 5, top: 0 }) // 设置图标的左边距
              .onClick(() => {
                if (this.imageListClosed) { // 如果imageListClosed回调函数存在,则调用该回调函数
                  this.imageListClosed();
                }
              })
          }
          .width('100%') // 设置行宽度为100%
          .height(30) // 设置行高度为30
          .margin({ bottom: 15, top: 10 }) // 设置行的上下边距
          .justifyContent(FlexAlign.Center) // 设置内容居中对齐

          // 在Row下创建一个Text组件,用于显示分隔线
          Text() {
          } 
          .width('100%') // 设置分隔线的宽度为100%
          .height(2) // 设置分隔线的高度
          .backgroundColor('#CCCCCC') // 设置分隔线的背景颜色
          .margin({ bottom: 15 }) // 设置分隔线的下边距
          
          // 以下是创建多个按钮的代码,每个按钮对应一个图像类别,点击时更新Webview中的图像列表
          
          // 创建一个“动物”按钮
          Button({ type: ButtonType.Normal }) {
            Text('动物')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("animals",5)`);
          })

          // 创建一个“人物”按钮
          Button({ type: ButtonType.Normal }) {
            Text('人物')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("persons",5)`);
          })

          // 创建一个“美食”按钮
          Button({ type: ButtonType.Normal }) {
            Text('美食')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("finefoods",5)`);
          })

          // 创建一个“水果”按钮
          Button({ type: ButtonType.Normal }) {
            Text('水果')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("fruits",5)`);
          })

          // 创建一个“建筑”按钮
          Button({ type: ButtonType.Normal }) {
            Text('建筑')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("buildings",5)`);
          })

          // 创建一个“交通工具”按钮
          Button({ type: ButtonType.Normal }) {
            Text('交通工具')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("vehicles",5)`);
          })

          // 创建一个“星辰大海”按钮
          Button({ type: ButtonType.Normal }) {
            Text('星辰大海')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("stars",5)`);
          })

          // 创建一个“植物”按钮
          Button({ type: ButtonType

.Normal }) {
            Text('植物')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("plants",5)`);
          })

          // 创建一个“基础图形”按钮
          Button({ type: ButtonType.Normal }) {
            Text('基础图形')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("basics",5)`);
          })
        }
        .backgroundColor(PANEL_BACKGROUND_COLOR) // 设置内嵌Column的背景颜色
        .border({ width: 0, color: '#000000' }) // 设置边框宽度和颜色
        .height('100%') // 设置高度为100%
        .padding({ left: 10, right: 10 }) // 设置左右内边距
        .width(100) // 设置宽度为100
        
        // 创建另一个Column组件用于放置Webview
        Column() {
          // 创建一个Webview组件,用于展示图像列表
          Web({ src: $rawfile('photo_studio/system_image_list.html'), controller: this.controller1 })
            .size({ width: '100%', height: '100%' }) // 设置Webview的大小为100%
        }
        .width('100%') // 设置宽度为100%
        .height('100%') // 设置高度为100%
      }
      .width('100%') // 设置外部Row的宽度为100%
      .height('100%') // 设置外部Row的高度为100%
    }
    .width('100%') // 设置最外层Column的宽度为100%
    .height('100%') // 设置最外层Column的高度为100%
  }
}

代码的原理和实现过程

这个组件的主要功能是提供一个界面,让用户可以从多个预定义的图像类别中选择图像,并且通过与Webview的交互来展示不同类别的图像列表。

  1. 组件的结构

    • 使用ArkTS的UI组件(如ColumnRowButton等)来构建界面,包含一系列按钮和一个Webview。
    • 每个按钮对应不同的图像类别,点击按钮时,通过WebviewController来执行JavaScript代码,以更新Webview中的图像列表。
  2. 事件处理

    • 使用onClick事件处理按钮点击,当用户点击某个类别按钮时,WebviewController会运行对应的JavaScript函数,更新Webview中的内容。
    • 通过imageSelectedimageListClosed这两个可选的回调函数,组件可以在外部处理图像选择和列表关闭的逻辑。
  3. Webview交互

    • WebviewControllerrunJavaScript方法用于在Webview中执行JavaScript代码,组件通过这种方式与Webview进行通信。
  4. UI设计

    • 界面布局通过多个ColumnRow组件实现,按钮的样式和布局使用了ArkTS的样式属性(如borderRadiusbackgroundColormargin等)。
    • Webview组件显示图像列表的HTML页面,页面路径通过src属性指定。

这段代码展示了如何在ArkTS中构建一个复杂的UI组件,并结合Webview进行灵活的内容展示与交互。
在选择svg时,会调用imageSelected事件函数,该函数的代码如下:

imageSelected(imageListName: string, imageListIndex: number)
{  
     this.controller1.runJavaScript(`addSVGImage("${imageListName}",${imageListIndex});`).then(data => {
     
    });
   
    this.toggleInsertImagesPanel();
  }
}

在上面的代码中,调用了用js实现的addSVGImage函数,这个函数是根据svg图像的索引将svg图像插入到画布中,代码如下:

function addSVGImage(imageList, index) {
    try {
        // 使用fabric.loadSVGFromString来加载SVG代码
        fabric.loadSVGFromString(window[imageList][index], function (objects, options) {
            var svg = fabric.util.groupSVGElements(objects, options);

            // 计算尺寸缩放比例以适应预定尺寸
            const scale = Math.min(
                150 / svg.width,
                150 / svg.height
            );
            svg.scale(scale).set({
                left: 20,
                top: 20
            });

            // 将SVG图像添加到canvas
            canvas.add(svg);
            canvas.renderAll();
        });

    } catch(e) {

    }
}

上面的代码使用了loadSVGFromString函数装载SVG图像,下面是对这个函数的详细解释。

fabric.loadSVGFromString 函数的原型和用法详解

fabric.loadSVGFromStringFabric.js 库中用于从 SVG 字符串加载 SVG 图像的函数。Fabric.js 是一个强大的 JavaScript 库,用于在 HTML5 Canvas 上轻松绘制复杂的矢量图形和图片。

原型

fabric.loadSVGFromString(
  svgString: string,
  callback: (objects: fabric.Object[], options: object) => void,
  reviver?: (element: SVGElement, object: fabric.Object) => void
): void

参数解释

  1. svgString: string

    • 描述: SVG 图像的字符串形式。这个字符串包含了完整的 SVG 定义,例如 <svg> 标签及其内容。
    • 类型: string
    • 示例:
      const svgString = `
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
          <circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
        </svg>`;
      
  2. callback: (objects: fabric.Object[], options: object) => void

    • 描述: 一个回调函数,在 SVG 解析完成后调用。这个回调函数接收两个参数:
      • objects: Fabric.js 对象数组,每个对象代表解析后的 SVG 元素(例如,fabric.Circlefabric.Rect 等)。
      • options: 包含一些额外的选项,如 SVG 的 widthheight
    • 类型: function
    • 示例:
      function callback(objects, options) {
        const svgGroup = new fabric.Group(objects);
        canvas.add(svgGroup);
        canvas.renderAll();
      }
      
  3. reviver: (element: SVGElement, object: fabric.Object) => void (可选)

    • 描述: 一个可选的“复活器”函数,它在每个 SVG 元素被解析成 fabric.Object 对象之前调用。通过这个函数,你可以自定义每个对象的属性或行为。
    • 类型: function
    • 示例:
      function reviver(element, object) {
        if (object.type === 'circle') {
          object.set({ fill: 'blue' });
        }
      }
      

返回值

  • 返回值类型: void
  • 描述: 该函数没有返回值,而是通过回调函数来处理解析后的对象和选项。

用法示例

以下是一个完整的例子,演示如何使用 fabric.loadSVGFromString 加载 SVG 字符串,并将其添加到 Fabric.js 的画布中。

// 创建一个 Fabric.js 画布
const canvas = new fabric.Canvas('canvas');

// SVG 字符串
const svgString = `
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
    <circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
  </svg>`;

// 加载 SVG 字符串
fabric.loadSVGFromString(svgString, function(objects, options) {
  // 创建一个组(Group),将解析出的对象添加到组中
  const svgGroup = new fabric.Group(objects);
  
  // 添加组到画布上
  canvas.add(svgGroup);
  
  // 渲染画布
  canvas.renderAll();
});

详细说明

  • SVG 解析: fabric.loadSVGFromString 函数内部使用浏览器的原生 SVG 解析能力,解析传入的 SVG 字符串,并将其转化为 Fabric.js 可以理解的对象模型。
  • 异步操作: 这个函数是异步的,它不会立即返回解析结果,而是在完成解析后调用提供的回调函数。这意味着在解析过程中,主线程不会被阻塞,确保应用程序的响应性。
  • 对象创建: 解析后的 SVG 元素被转化为 Fabric.js 的对象,如 fabric.Circlefabric.Rectfabric.Path 等。这些对象可以像其他 Fabric.js 对象一样进行操作和管理。
  • 组管理: 解析出的对象通常会被分组(使用 fabric.Group),这样可以作为一个整体来管理。通过对组进行操作,可以轻松移动、缩放、旋转整个 SVG 图像。

使用场景

  • 动态加载 SVG: 你可以从服务器或用户输入动态加载 SVG 内容,并将其显示在 Fabric.js 的画布上。
  • 编辑 SVG: 解析后的 Fabric.js 对象可以直接编辑,允许你在应用程序中提供 SVG 图像的编辑功能。
  • 导入外部图形: 将外部的矢量图形导入到你的 Fabric.js 画布中,以便进行进一步的操作或展示。

通过 fabric.loadSVGFromString,你可以轻松地将 SVG 数据集成到 Fabric.js 的绘图环境中,为创建复杂、可交互的矢量图形应用提供了便利。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
标签
已于2024-7-31 15:15:20修改
收藏
回复
举报
回复
    相关推荐