【HarmonyOS NEXT】鸿蒙使用ScanKit实现自定义扫码 (一)之业务流程和扫码

GeorgeGcs
• 阅读 2

##鸿蒙开发能力 ##HarmonyOS SDK应用服务##鸿蒙金融类应用 (金融理财# 一、前言 鸿蒙官方提供了ScanKit来实现自定义扫码的功能诉求。但是对于扫码业务的讲解缺失,所以这篇文章主要是通过扫码业务路程,串连官方Kit的接口。让大家能更深刻的理解自定义扫码业务。 官方Scan Kit接口说明 (1)鸿蒙提供的ScanKit具备以下五种能力:

  1. 扫码直达
  2. 自定义扫码,图像识码 (自定义扫码需要这两种能力组合在一起,所以我分类在一起)
  3. 码图生成
  4. 系统提供的默认界面扫码

(2)业内市面上的自定义扫码界面,主要由以下几个部分功能点构成:

  1. 扫码(单,多)【鸿蒙最多支持四个二维码的识别】
  2. 解析图片二维码
  3. 扫码动画
  4. 扫码振动和音效
  5. 无网络监测与提示
  6. 多码暂停选中点的绘制
  7. 扫码结果根据类型分开处理(应用内部处理,外部H5处理)【这个不做展开】
  8. 焦距控制(放大缩小) 二、功能设计思路:

首先我们需要绘制整体UI界面布局,常规分为相机流容器view,动画表现view,按钮控制区view。 1.创建相机视频流容器 在ScanKit中相机流通过XComponent组件作为相机流的容器。 @Builder ScanKitView(){ XComponent({ id: 'componentId', type: XComponentType.SURFACE, controller: this.mXComponentController }) .onLoad(async () => { // 视频流开始加载回调 }) .width(this.cameraWidth) // cameraWidth cameraHeight 参见步骤二 .height(this.cameraHeight)

} 2.需要测算XComponent呈现的相机流宽高 // 竖屏时获取屏幕尺寸,设置预览流全屏示例 setDisplay() { // 折叠屏无 or 折叠 if(display.getFoldStatus() == display.FoldStatus.FOLD_STATUS_UNKNOWN || display.getFoldStatus() == display.FoldStatus.FOLD_STATUS_FOLDED){ // 默认竖屏 let displayClass = display.getDefaultDisplaySync(); this.displayHeight = px2vp(displayClass.height); this.displayWidth = px2vp(displayClass.width);

}else{
  // 折叠屏展开 or 半展开
  let displayClass = display.getDefaultDisplaySync();
  let tempHeight = px2vp(displayClass.height);
  let tempWidth = px2vp(displayClass.width);
  console.info("debugDisplay", 'tempHeight: ' + tempHeight + " tempWidth: " + tempWidth);
  this.displayHeight = tempHeight + px2vp(8);
  this.displayWidth = ( tempWidth - px2vp(64) ) / 2;

}
console.info("debugDisplay", 'final displayHeight: ' + this.displayHeight + " displayWidth: " + this.displayWidth);

let maxLen: number = Math.max(this.displayWidth, this.displayHeight);
let minLen: number = Math.min(this.displayWidth, this.displayHeight);
const RATIO: number = 16 / 9;
this.cameraHeight = maxLen;
this.cameraWidth = maxLen / RATIO;
this.cameraOffsetX = (minLen - this.cameraWidth) / 2;

} 3.使用相机,需要用户同意申请的Camera权限 module.json5配置 "requestPermissions": [ { "name" : "ohos.permission.CAMERA", "reason": "$string:app_name", "usedScene": { "abilities": [ "EntryAbility" ], "when":"inuse" } } ] 需要注意时序,每次显示自定义扫码界面,都需要检查权限。所有建议放在onPageshow系统周期内。 async onPageShow() { await this.requestCameraPermission();

}

/**

  • 用户申请相机权限
  • / async requestCameraPermission() { let grantStatus = await this.reqPermissionsFromUser(); for (let i = 0; i < grantStatus.length; i++) { if (grantStatus[i] === 0) {
    // 用户授权,可以继续访问目标操作
    console.log(this.TAG, "Succeeded in getting permissions.");
    this.userGrant = true;
    } } } /**
  • 用户申请权限
  • @returns
  • / async reqPermissionsFromUser(): Promise<number[]> { let context = getContext() as common.UIAbilityContext; let atManager = abilityAccessCtrl.createAtManager(); let grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']); return grantStatus.authResults; }
  1. 配置初始化扫码相机 import { customScan } from '@kit.ScanKit'

    private setScanConfig(){ // 多码扫码识别,enableMultiMode: true 单码扫码识别enableMultiMode: false let options: scanBarcode.ScanOptions = { scanTypes: [scanCore.ScanType.ALL], enableMultiMode: true, enableAlbum: true } } // 初始化接口 customScan.init(options);

5.开启相机 此处需要注意时序,开启相机需要在权限检查后,配置初始化了相机,并且在XComponent相机视频流容器加载回调后进行。(如果需要配置闪光灯的处理,可在此处一同处理)【完整代码示例,参见章节三】 @Builder ScanKitView(){ XComponent({ id: 'componentId', type: XComponentType.SURFACE, controller: this.mXComponentController }) .onLoad(async () => {

    // 获取XComponent组件的surfaceId
    this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
    console.info(this.TAG, "Succeeded in getting surfaceId: " + this.surfaceId);

    this.startCamera();
    this.setFlashLighting();

  })
  .width(this.cameraWidth)
  .height(this.cameraHeight)
  .position({ x: this.cameraOffsetX, y: this.cameraOffsetY })

}

/**

  • 启动相机

  • / private startCamera() { this.isShowBack = false; this.scanResult = []; let viewControl: customScan.ViewControl = { width: this.cameraWidth, height: this.cameraHeight, surfaceId : this.surfaceId }; // 自定义启动第四步,请求扫码接口,通过Promise方式回调 try { customScan.start(viewControl)

    .then(async (result: Array<scanBarcode.ScanResult>) => {
      console.error(this.TAG, 'result: ' + JSON.stringify(result));
      if (result.length) {
        // 解析码值结果跳转应用服务页
        this.scanResult = result;
        this.isShowBack = true;
        // 获取到扫描结果后暂停相机流
        try {
          customScan.stop().then(() => {
            console.info(this.TAG, 'Succeeded in stopping scan by promise ');
          }).catch((error: BusinessError) => {
            console.error(this.TAG, 'Failed to stop scan by promise err: ' + JSON.stringify(error));
          });
        } catch (error) {
          console.error(this.TAG, 'customScan.stop err: ' + JSON.stringify(error));
        }
    
      }
    }).catch((error: BusinessError) => {
    console.error(this.TAG, 'customScan.start err: ' + JSON.stringify(error));

    }); } catch (err) { console.error(this.TAG, 'customScan.start err: ' + JSON.stringify(err)); } } 完成以上步骤后,就可以使用自定义扫码功能,进行二维码和条码的识别了。 三、示例源码:

ScanPage.ets 兼容折叠屏,Navigation。 import { customScan, scanBarcode, scanCore } from '@kit.ScanKit' import { hilog } from '@kit.PerformanceAnalysisKit' import { BusinessError } from '@kit.BasicServicesKit' import { abilityAccessCtrl, common } from '@kit.AbilityKit' import { display, promptAction, router } from '@kit.ArkUI'

@Builder export function ScanPageBuilder(name: string, param: object){ if(isLog(name, param)){ ScanPage() } }

function isLog(name: string, param: object){ console.log("ScanPageBuilder", " ScanPageBuilder init name: " + name); return true; }

@Entry @Component export struct ScanPage { private TAG: string = '[customScanPage]';

@State userGrant: boolean = false // 是否已申请相机权限 @State surfaceId: string = '' // xComponent组件生成id @State isShowBack: boolean = false // 是否已经返回扫码结果 @State isFlashLightEnable: boolean = false // 是否开启了闪光灯 @State isSensorLight: boolean = false // 记录当前环境亮暗状态 @State cameraHeight: number = 480 // 设置预览流高度,默认单位:vp @State cameraWidth: number = 300 // 设置预览流宽度,默认单位:vp @State cameraOffsetX: number = 0 // 设置预览流x轴方向偏移量,默认单位:vp @State cameraOffsetY: number = 0 // 设置预览流y轴方向偏移量,默认单位:vp @State zoomValue: number = 1 // 预览流缩放比例 @State setZoomValue: number = 1 // 已设置的预览流缩放比例 @State scaleValue: number = 1 // 屏幕缩放比 @State pinchValue: number = 1 // 双指缩放比例 @State displayHeight: number = 0 // 屏幕高度,单位vp @State displayWidth: number = 0 // 屏幕宽度,单位vp @State scanResult: Array<scanBarcode.ScanResult> = [] // 扫码结果 private mXComponentController: XComponentController = new XComponentController()

async onPageShow() { // 自定义启动第一步,用户申请权限 await this.requestCameraPermission(); // 自定义启动第二步:设置预览流布局尺寸 this.setDisplay(); // 自定义启动第三步,配置初始化接口 this.setScanConfig(); }

private setScanConfig(){ // 多码扫码识别,enableMultiMode: true 单码扫码识别enableMultiMode: false let options: scanBarcode.ScanOptions = { scanTypes: [scanCore.ScanType.ALL], enableMultiMode: true, enableAlbum: true } customScan.init(options); }

async onPageHide() { // 页面消失或隐藏时,停止并释放相机流 this.userGrant = false; this.isFlashLightEnable = false; this.isSensorLight = false; try { customScan.off('lightingFlash'); } catch (error) { hilog.error(0x0001, this.TAG, Failed to off lightingFlash. Code: ${error.code}, message: ${error.message}); } await customScan.stop(); // 自定义相机流释放接口 customScan.release().then(() => { hilog.info(0x0001, this.TAG, 'Succeeded in releasing customScan by promise.'); }).catch((error: BusinessError) => { hilog.error(0x0001, this.TAG, Failed to release customScan by promise. Code: ${error.code}, message: ${error.message}); }) }

/**

  • 用户申请权限

  • @returns

  • / async reqPermissionsFromUser(): Promise<number[]> { hilog.info(0x0001, this.TAG, 'reqPermissionsFromUser start'); let context = getContext() as common.UIAbilityContext; let atManager = abilityAccessCtrl.createAtManager(); let grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']); return grantStatus.authResults; }

    /**

  • 用户申请相机权限

  • / async requestCameraPermission() { let grantStatus = await this.reqPermissionsFromUser(); for (let i = 0; i < grantStatus.length; i++) { if (grantStatus[i] === 0) {

    // 用户授权,可以继续访问目标操作
    console.log(this.TAG, "Succeeded in getting permissions.");
    this.userGrant = true;

    } } }

    // 竖屏时获取屏幕尺寸,设置预览流全屏示例 setDisplay() { // 折叠屏无 or 折叠 if(display.getFoldStatus() == display.FoldStatus.FOLD_STATUS_UNKNOWN || display.getFoldStatus() == display.FoldStatus.FOLD_STATUS_FOLDED){ // 默认竖屏 let displayClass = display.getDefaultDisplaySync(); this.displayHeight = px2vp(displayClass.height); this.displayWidth = px2vp(displayClass.width);

    }else{ // 折叠屏展开 or 半展开 let displayClass = display.getDefaultDisplaySync(); let tempHeight = px2vp(displayClass.height); let tempWidth = px2vp(displayClass.width); console.info("debugDisplay", 'tempHeight: ' + tempHeight + " tempWidth: " + tempWidth); this.displayHeight = tempHeight + px2vp(8); this.displayWidth = ( tempWidth - px2vp(64) ) / 2;

    } console.info("debugDisplay", 'final displayHeight: ' + this.displayHeight + " displayWidth: " + this.displayWidth);

    let maxLen: number = Math.max(this.displayWidth, this.displayHeight); let minLen: number = Math.min(this.displayWidth, this.displayHeight); const RATIO: number = 16 / 9; this.cameraHeight = maxLen; this.cameraWidth = maxLen / RATIO; this.cameraOffsetX = (minLen - this.cameraWidth) / 2; }

    // toast显示扫码结果 async showScanResult(result: scanBarcode.ScanResult) { // 使用toast显示出扫码结果 promptAction.showToast({ message: JSON.stringify(result), duration: 5000 }); }

    /**

  • 启动相机

  • / private startCamera() { this.isShowBack = false; this.scanResult = []; let viewControl: customScan.ViewControl = { width: this.cameraWidth, height: this.cameraHeight, surfaceId : this.surfaceId }; // 自定义启动第四步,请求扫码接口,通过Promise方式回调 try { customScan.start(viewControl)

    .then(async (result: Array<scanBarcode.ScanResult>) => {
      console.error(this.TAG, 'result: ' + JSON.stringify(result));
      if (result.length) {
        // 解析码值结果跳转应用服务页
        this.scanResult = result;
        this.isShowBack = true;
        // 获取到扫描结果后暂停相机流
        try {
          customScan.stop().then(() => {
            console.info(this.TAG, 'Succeeded in stopping scan by promise ');
          }).catch((error: BusinessError) => {
            console.error(this.TAG, 'Failed to stop scan by promise err: ' + JSON.stringify(error));
          });
        } catch (error) {
          console.error(this.TAG, 'customScan.stop err: ' + JSON.stringify(error));
        }
    
      }
    }).catch((error: BusinessError) => {
    console.error(this.TAG, 'customScan.start err: ' + JSON.stringify(error));

    }); } catch (err) { console.error(this.TAG, 'customScan.start err: ' + JSON.stringify(err)); } }

    /**

  • 注册闪光灯监听接口

  • / private setFlashLighting(){ customScan.on('lightingFlash', (error, isLightingFlash) => { if (error) {

    console.info(this.TAG, "customScan lightingFlash error: " + JSON.stringify(error));
    return;

    } if (isLightingFlash) {

    this.isFlashLightEnable = true;

    } else {

    if (!customScan?.getFlashLightStatus()) {
      this.isFlashLightEnable = false;
    }

    } this.isSensorLight = isLightingFlash; }); }

    // 自定义扫码界面的顶部返回按钮和扫码提示 @Builder TopTool() { Column() { Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {

    Text('返回')
      .onClick(async () => {
        // router.back();
        this.mNavContext?.pathStack.removeByName("ScanPage");
      })

    }.padding({ left: 24, right: 24, top: 40 })

    Column() {

    Text('扫描二维码/条形码')
    Text('对准二维码/条形码,即可自动扫描')

    }.margin({ left: 24, right: 24, top: 24 }) } .height(146) .width('100%') }

    @Builder ScanKitView(){ XComponent({ id: 'componentId', type: XComponentType.SURFACE, controller: this.mXComponentController }) .onLoad(async () => {

    // 获取XComponent组件的surfaceId
    this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
    console.info(this.TAG, "Succeeded in getting surfaceId: " + this.surfaceId);
    
    this.startCamera();
    this.setFlashLighting();

    }) .width(this.cameraWidth) .height(this.cameraHeight) .position({ x: this.cameraOffsetX, y: this.cameraOffsetY }) }

    @Builder ScanView(){ Stack() {

    Column() {

    if (this.userGrant) {
      this.ScanKitView()
    }

    } .height('100%') .width('100%') .backgroundColor(Color.Red)

    Column() {

    this.TopTool()
    Column() {
    }
    .layoutWeight(1)
    .width('100%')
    
    Column() {
      Row() {
        // 闪光灯按钮,启动相机流后才能使用
        Button('FlashLight')
          .onClick(() => {
            // 根据当前闪光灯状态,选择打开或关闭闪关灯
            if (customScan.getFlashLightStatus()) {
              customScan.closeFlashLight();
              setTimeout(() => {
                this.isFlashLightEnable = this.isSensorLight;
              }, 200);
            } else {
              customScan.openFlashLight();
            }
          })
          .visibility((this.userGrant && this.isFlashLightEnable) ? Visibility.Visible : Visibility.None)
    
        // 扫码成功后,点击按钮后重新扫码
        Button('ReScan')
          .onClick(() => {
            try {
              customScan.rescan();
            } catch (error) {
              console.error(this.TAG, 'customScan.rescan err: ' + JSON.stringify(error));
            }
    
            // 点击按钮重启相机流,重新扫码
            this.startCamera();
          })
          .visibility(this.isShowBack ? Visibility.Visible : Visibility.None)
    
        // 跳转下个页面
        Button('点击跳转界面')
          .onClick(() => {
            router.pushUrl({
              url: "pages/Index1",
            })
          })
      }
    
      Row() {
        // 预览流设置缩放比例
        Button('缩放比例,当前比例:' + this.setZoomValue)
          .onClick(() => {
            // 设置相机缩放比例
            if (!this.isShowBack) {
              if (!this.zoomValue || this.zoomValue === this.setZoomValue) {
                this.setZoomValue = customScan.getZoom();
              } else {
                this.zoomValue = this.zoomValue;
                customScan.setZoom(this.zoomValue);
                setTimeout(() => {
                  if (!this.isShowBack) {
                    this.setZoomValue = customScan.getZoom();
                  }
                }, 1000);
              }
            }
          })
      }
      .margin({ top: 10, bottom: 10 })
    
      Row() {
        // 输入要设置的预览流缩放比例
        TextInput({ placeholder: '输入缩放倍数' })
          .type(InputType.Number)
          .borderWidth(1)
          .backgroundColor(Color.White)
          .onChange(value => {
            this.zoomValue = Number(value);
          })
      }
    }
    .width('50%')
    .height(180)

    }

    // 单码、多码扫描后,显示码图蓝点位置。点击toast码图信息 ForEach(this.scanResult, (item: scanBarcode.ScanResult, index: number) => {

    if (item.scanCodeRect) {
      Image($r("app.media.icon_select_dian"))
        .width(20)
        .height(20)
        .markAnchor({ x: 20, y: 20 })
        .position({
          x: (item.scanCodeRect.left + item?.scanCodeRect?.right) / 2 + this.cameraOffsetX,
          y: (item.scanCodeRect.top + item?.scanCodeRect?.bottom) / 2 + this.cameraOffsetY
        })
        .onClick(() => {
          this.showScanResult(item);
        })
    }

    }) } // 建议相机流设置为全屏 .width('100%') .height('100%') .onClick((event: ClickEvent) => { // 是否已扫描到结果 if (this.isShowBack) {

    return;

    } // 点击屏幕位置,获取点击位置(x,y),设置相机焦点 let x1 = vp2px(event.displayY) / (this.displayHeight + 0.0); let y1 = 1.0 - (vp2px(event.displayX) / (this.displayWidth + 0.0)); customScan.setFocusPoint({ x: x1, y: y1 }); hilog.info(0x0001, this.TAG, Succeeded in setting focusPoint x1: ${x1}, y1: ${y1}); // 设置连续自动对焦模式 setTimeout(() => {

    customScan.resetFocus();

    }, 200); }).gesture(PinchGesture({ fingers: 2 }) .onActionStart((event: GestureEvent) => {

    hilog.info(0x0001, this.TAG, 'Pinch start');

    }) .onActionUpdate((event: GestureEvent) => {

    if (event) {
      this.scaleValue = event.scale;
    }

    }) .onActionEnd((event: GestureEvent) => {

    // 是否已扫描到结果
    if (this.isShowBack) {
      return;
    }
    // 获取双指缩放比例,设置变焦比
    try {
      let zoom = customScan.getZoom();
      this.pinchValue = this.scaleValue * zoom;
      customScan.setZoom(this.pinchValue);
      hilog.info(0x0001, this.TAG, 'Pinch end');
    } catch (error) {
      hilog.error(0x0001, this.TAG, `Failed to setZoom. Code: ${error.code}, message: ${error.message}`);
    }

    })) }

    private mNavContext: NavDestinationContext | null = null;

    build() { NavDestination(){ this.ScanView() } .width("100%") .height("100%") .hideTitleBar(true) .onReady((navContext: NavDestinationContext)=>{ this.mNavContext = navContext; }) .onShown(()=>{ this.onPageShow(); }) .onHidden(()=>{ this.onPageHide(); }) } }

点赞
收藏
评论区
推荐文章
wnm wnm
3年前
万能码码上付给你一个惊喜(安全扫码专业委员会)
万能码码上付给你一个惊喜(安全扫码专业委员会)码上付的出现让整个码的实用性更强了,扫码收付款已经是常态,甚至可以说但凡经济发展不落后的地方都几乎是以手机扫码付款为主,即便是在农村,也正是因为这样的趋势,似乎在扫码收付款上出现了一个契机,而这个契机则被诸多人看在眼里,于是在市场上开始出现许多形式的手机扫码支付的方式,既然是盈利自然是要收取费用,因此大部分都属于
Wesley13 Wesley13
3年前
Android二维码扫码ZXing,barcodescanner和BGAQRCode
Android二维码扫码ZXing,barcodescanner和BGAQRCodeAndroid技术比较Android二维码扫描是一种常见的功能开发,但是技术选型不当会造成初期开发难度大、后期维护成本高。常见的Android二维码扫码解决方案很多,比如ZXing,barcodescanner和BGAQRCodeAndroid等等。以下给出这
GeorgeGcs GeorgeGcs
13小时前
【 HarmonyOS 5 入门系列 】鸿蒙HarmonyOS示例项目讲解
【HarmonyOS5入门系列】鸿蒙HarmonyOS示例项目讲解\鸿蒙开发能力HarmonyOSSDK应用服务鸿蒙金融类应用(金融理财一、前言:移动开发声明式UI框架的技术变革在移动操作系统的发展历程中,UI开发模式经历了从命令式到声明式的重大变革。根据
GeorgeGcs GeorgeGcs
13小时前
【HarmonyOS 5】AttributeModifier和AttributeUpdater区别详解
【HarmonyOS5】AttributeModifier和AttributeUpdater区别详解\鸿蒙开发能力HarmonyOSSDK应用服务鸿蒙金融类应用(金融理财一、AttributeModifier和AttributeUpdater的定义和作用1
鸿蒙小林 鸿蒙小林
6小时前
《仿盒马》app开发技术分享-- 扫一扫功能(35)
技术栈Appgalleryconnect开发准备随着app的逐渐完善,我们现在需要在细节处做更多的打磨,在首页我们添加了很多静态的按钮和组件,现在我们开始对这些组件进行功能的添加,这次首先实现的是首页头部的扫一扫功能,扫一扫我们实现扫码后跳转商品详情页功能
GeorgeGcs GeorgeGcs
4小时前
【HarmonyOS NEXT】一键扫码功能
鸿蒙开发能力HarmonyOSSDK应用服务鸿蒙金融类应用(金融理财前言鸿蒙在api10之后,对系统api的基础上,封装了较为复杂功能的开发工具包,统一称之为Kit。这些Kit根据功能定义的不同,划分为不同的种类Kit。如下图所示:其实可以理解为集成在系统
GeorgeGcs GeorgeGcs
4小时前
【HarmonyOS 5】桌面快捷方式功能实现详解
鸿蒙开发能力HarmonyOSSDK应用服务鸿蒙金融类应用(金融理财一、前言在移动应用开发中,如何让用户快速触达核心功能,是目前很常见的功能之一。鸿蒙系统提供的桌面快捷方式(Shortcuts)功能,允许开发者为应用内常用功能创建直达入口,用户通过长按应用
GeorgeGcs GeorgeGcs
4小时前
【HarmonyOS 5】使用openCustomDialog如何禁止手势关闭的方案
鸿蒙开发能力HarmonyOSSDK应用服务鸿蒙金融类应用(金融理财一、前言在HarmonyOS中使用openCustomDialog自定义弹框时,我们会遇到实现禁止手势关闭弹框的业务场景。虽然在HarmonyOSNext中,自定义Dialog默认可能继承
GeorgeGcs GeorgeGcs
2小时前
【HarmonyOS】鸿蒙应用实现调用系统地图导航或路径规划
鸿蒙开发能力HarmonyOSSDK应用服务鸿蒙金融类应用(金融理财前言在涉及地图业务中,调用地图导航和路径规划是三方应用中较为常见的功能。若只是子业务需要地图导航效果,整个APP内部集成地图去实现导航或者路径规划,会造成SDK集成冗余。毕竟很重。所以该效
陈杨 陈杨
2天前
鸿蒙5开发宝藏案例分享---快捷触达的骑行体验
鸿蒙宝藏案例详解:共享单车“丝滑”骑行体验的代码实现🚲💻大家好!上次分享了鸿蒙那个超棒的共享单车体验案例,很多朋友留言说想看代码细节。没问题!这就带大家深入代码层,看看那些“丝滑”的体验(扫码直达、实时状态窗、路径规划)到底是怎么敲出来的。官方文档有时
GeorgeGcs
GeorgeGcs
Lv1
男 · 金融头部企业 · 鸿蒙应用架构师
HarmonyOS认证创作先锋,华为HDE专家,鸿蒙讲师,作者。目前任职鸿蒙应用架构师。 历经腾讯,宝马,研究所,金融。 待过私企,外企,央企。 深耕大应用开发领域十年。 AAE,Harmony(OpenHarmony\HarmonyOS),MAE(Android\IOS),FE(H5\Vue\RN)。
文章
56
粉丝
1
获赞
2