HarmonyOS应用六之应用程序进阶二

目录:

一、进度条通知

typescript 复制代码
/*
 * Copyright (c) 2023 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { notificationManager } from '@kit.NotificationKit';
import { createWantAgent, publishNotification, openNotificationPermission } from '../common/utils/NotificationUtil';
import { getStringByRes } from '../common/utils/ResourseUtil';
import Logger from '../common/utils/Logger';
import CommonConstants, { DOWNLOAD_STATUS } from '../common/constants/CommonConstants';

@Entry
@Component
struct MainPage {
  @State downloadStatus: number = DOWNLOAD_STATUS.INITIAL;
  @State downloadProgress: number = 0;
  private context = getContext(this) as common.UIAbilityContext;
  private isSupport: boolean = true;
  private notificationTitle: string = '';
  private wantAgentObj: object = new Object();
  private interval: number = -1;

  aboutToAppear() {
  //打开通知权限
    openNotificationPermission();
    let bundleName = this.context.abilityInfo.bundleName;
    let abilityName = this.context.abilityInfo.name;
    //创建want行为意图
    createWantAgent(bundleName, abilityName).then(want => {
      this.wantAgentObj = want;
    }).catch((err: Error) => {
      Logger.error(`getWantAgent fail, err: ${JSON.stringify(err)}`);
    });
    //判断当前系统知否支持下载通知模版
    notificationManager.isSupportTemplate('downloadTemplate').then(isSupport => {
      if (!isSupport) {
        promptAction.showToast({
          message: $r('app.string.invalid_button_toast')
        })
      }
      this.isSupport = isSupport;
    });
  }
//此方法监听页面返回,此处是页面返回后调用取消下载的方法
  onBackPress() {
    this.cancel();
  }

  build() {
    Column() {
      Text($r('app.string.title'))
        .fontSize($r('app.float.title_font_size'))
        .fontWeight(CommonConstants.FONT_WEIGHT_LAGER)
        .width(CommonConstants.TITLE_WIDTH)
        .textAlign(TextAlign.Start)
        .margin({
          top: $r('app.float.title_margin_top'),
          bottom: $r('app.float.title_margin_top')
        })
      Row() {
        Column() {
          Image($r('app.media.ic_image'))
            .objectFit(ImageFit.Fill)
            .width($r('app.float.card_image_length'))
            .height($r('app.float.card_image_length'))
        }
        .layoutWeight(CommonConstants.IMAGE_WEIGHT)
        .height(CommonConstants.FULL_LENGTH)
        .alignItems(HorizontalAlign.Start)

        Column() {
          Row() {
            Text(CommonConstants.DOWNLOAD_FILE)
              .fontSize($r('app.float.name_font_size'))
              .textAlign(TextAlign.Center)
              .fontWeight(CommonConstants.FONT_WEIGHT_LAGER)
              .lineHeight($r('app.float.name_font_height'))
            Text(`${this.downloadProgress}%`)
              .fontSize($r('app.float.normal_font_size'))
              .lineHeight($r('app.float.name_font_height'))
              .opacity(CommonConstants.FONT_OPACITY)
          }
          .justifyContent(FlexAlign.SpaceBetween)
          .width(CommonConstants.FULL_LENGTH)

          Progress({
            value: this.downloadProgress,
            total: CommonConstants.PROGRESS_TOTAL
          })
            .width(CommonConstants.FULL_LENGTH)
          Row() {
            Text(CommonConstants.FILE_SIZE)
              .fontSize($r('app.float.normal_font_size'))
              .lineHeight($r('app.float.name_font_height'))
              .opacity(CommonConstants.FONT_OPACITY)
            if (this.downloadStatus === DOWNLOAD_STATUS.INITIAL) {
              this.customButton($r('app.string.button_download'), (): Promise<void> => this.start())
            } else if (this.downloadStatus === DOWNLOAD_STATUS.DOWNLOADING) {
              Row() {
                this.cancelButton()
                this.customButton($r('app.string.button_pause'), (): Promise<void> => this.pause())
              }
            } else if (this.downloadStatus === DOWNLOAD_STATUS.PAUSE) {
              Row() {
                this.cancelButton()
                this.customButton($r('app.string.button_resume'), (): Promise<void> => this.resume())
              }
            } else {
              this.customButton($r('app.string.button_finish'), (): void => this.open())
            }
          }
          .width(CommonConstants.FULL_LENGTH)
          .justifyContent(FlexAlign.SpaceBetween)
        }
        .layoutWeight(CommonConstants.CARD_CONTENT_WEIGHT)
        .height(CommonConstants.FULL_LENGTH)
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .width(CommonConstants.CARD_WIDTH)
      .height($r('app.float.card_height'))
      .backgroundColor(Color.White)
      .borderRadius($r('app.float.card_border_radius'))
      .justifyContent(FlexAlign.SpaceBetween)
      .padding($r('app.float.card_padding'))
    }
    .width(CommonConstants.FULL_LENGTH)
    .height(CommonConstants.FULL_LENGTH)
    .backgroundColor($r('app.color.index_background_color'))
  }

  /**
   * Start the timer and send notification.(下载逻辑)
   */
  download() {
    this.interval = setInterval(async () => {
      if (this.downloadProgress === CommonConstants.PROGRESS_TOTAL) {
        this.notificationTitle = await getStringByRes($r('app.string.notification_title_finish'), this);
        this.downloadStatus = DOWNLOAD_STATUS.FINISHED;
        clearInterval(this.interval);
      } else {
        this.downloadProgress += CommonConstants.PROGRESS_SPEED;
      }
      if (this.isSupport) {
        publishNotification(this.downloadProgress, this.notificationTitle, this.wantAgentObj);
      }
    }, CommonConstants.UPDATE_FREQUENCY);
  }

  /**
   * Click to download.(开始下载)
   */
  async start() {
    this.notificationTitle = await getStringByRes($r('app.string.notification_title_download'), this);
    this.downloadStatus = DOWNLOAD_STATUS.DOWNLOADING;
    this.downloadProgress = 0;
    this.download();
  }

  /**
   * Click pause.(暂停逻辑)
   */
  async pause() {
    this.notificationTitle = await getStringByRes($r('app.string.notification_title_pause'), this);
    clearInterval(this.interval);
    this.downloadStatus = DOWNLOAD_STATUS.PAUSE;
    if (this.isSupport) {
      publishNotification(this.downloadProgress, this.notificationTitle, this.wantAgentObj);
    }
  }

  /**
   * Click resume.(继续下载逻辑)
   */
  async resume() {
    this.notificationTitle = await getStringByRes($r('app.string.notification_title_download'), this);
    this.download();
    this.downloadStatus = DOWNLOAD_STATUS.DOWNLOADING;
  }

  /**
   * Click cancel.(取消逻辑)
   */
  async cancel() {
    this.downloadProgress = 0;
    clearInterval(this.interval);
    this.downloadStatus = DOWNLOAD_STATUS.INITIAL;
    notificationManager.cancel(CommonConstants.NOTIFICATION_ID);
  }

  /**
   * Open file
   */
  open() {
    promptAction.showToast({
      message: $r('app.string.invalid_button_toast')
    })
  }

  @Builder
  customButton(textResource: Resource, click: Function = () => {
  }) {
    Button(textResource)
      .backgroundColor($r('app.color.button_color'))
      .buttonsStyle()
      .onClick(() => {
        click();
      })
  }

  @Builder
  cancelButton() {
    Button($r('app.string.button_cancel'))
      .buttonsStyle()
      .backgroundColor($r('app.color.cancel_button_color'))
      .fontColor($r('app.color.button_color'))
      .margin({ right: $r('app.float.button_margin') })
      .onClick(() => {
        this.cancel();
      })
  }
}

@Extend(Button)
function buttonsStyle() {
  .constraintSize({ minWidth: $r('app.float.button_width') })
  .height($r('app.float.button_height'))
  .borderRadius($r('app.float.button_border_radius'))
  .fontSize($r('app.float.button_font_size'))
}

NotificationUtil.ets:

typescript 复制代码
/*
 * Copyright (c) 2023 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { wantAgent } from '@kit.AbilityKit';
import { notificationManager } from '@kit.NotificationKit';
import CommonConstants from '../constants/CommonConstants';
import Logger from '../utils/Logger';

/**
 * Obtains the WantAgent of an application.
 *
 * @returns WantAgent of an application.
 */
 //创建行为意图并拉起拉起UIAbility
export function createWantAgent(bundleName: string, abilityName: string): Promise<object> {
  let wantAgentInfo = {
    wants: [
      {
        bundleName: bundleName,
        abilityName: abilityName
      }
    ],
    operationType: wantAgent.OperationType.START_ABILITY,
    requestCode: 0,
    wantAgentFlags: [wantAgent.WantAgentFlags.CONSTANT_FLAG]
  } as wantAgent.WantAgentInfo;
  return wantAgent.getWantAgent(wantAgentInfo);
}

/**
 * Publish notification.
 *
 * @param progress Download progress
 * @param title Notification title.
 * @param wantAgentObj The want of application.
 */
 //发布下载通知模版
export function publishNotification(progress: number, title: string, wantAgentObj: object) {
  let template:notificationManager.NotificationTemplate = {
    name: 'downloadTemplate',
    data: {
      title:  `${title}`,
      fileName:  `${title}:${CommonConstants.DOWNLOAD_FILE}`,
      progressValue: progress,
      progressMaxValue: CommonConstants.PROGRESS_TOTAL,
      isProgressIndeterminate: false
    }
  };
  let notificationRequest: notificationManager.NotificationRequest = {
    id: CommonConstants.NOTIFICATION_ID,
    notificationSlotType: notificationManager.SlotType.CONTENT_INFORMATION,
    // Construct a progress bar template. The name field must be set to downloadTemplate.
    template: template,
    content: {
      notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
      normal: {
        title: `${title}:${CommonConstants.DOWNLOAD_FILE}`,
        text: ' ',
        additionalText: `${progress}%`
      }
    },
    wantAgent: wantAgentObj
  };
  notificationManager.publish(notificationRequest).catch((err: Error) => {
    Logger.error(`[ANS] publish failed,message is ${err}`);
  });
}

/**
 * open notification permission
 */
 //请求通知权限
export function openNotificationPermission() {
  notificationManager.requestEnableNotification().then(() => {
    Logger.info('Enable notification success');
  }).catch((err:Error) => {
    Logger.error('Enable notification failed because ' + JSON.stringify(err));
  });
}

二、闹钟提醒

2.1、在module.json5配置文件中开启权限

typescript 复制代码
"module": {
  // ...
  "requestPermissions": [
    {
     "name": "ohos.permission.PUBLISH_AGENT_REMINDER"
    }
  ]
}

2.2、导入后台代理提醒reminderAgentManager模块,将此模块命名为reminderAgentManager

typescript 复制代码
import { reminderAgentManager } from '@kit.BackgroundTasksKit';

2.3、如果是新增提醒,实现步骤如下:

  • 用reminderAgent.ReminderRequest类定义提醒实例。
typescript 复制代码
import { reminderAgentManager } from '@kit.BackgroundTasksKit';
// ...

export class ReminderService {
  public addReminder(alarmItem: ReminderItem, callback?: (reminderId: number) => void) {
    let reminder = this.initReminder(alarmItem);
    reminderAgentManager.publishReminder(reminder, (err, reminderId) => {
      if (callback != null) {
        callback(reminderId);
      }
    });
  }
   
  private initReminder(item: ReminderItem): reminderAgentManager.ReminderRequestAlarm {
    return {
      reminderType: item.remindType,
      hour: item.hour,
      minute: item.minute,
      daysOfWeek: item.repeatDays,
      title: item.name,
      ringDuration: item.duration * CommonConstants.DEFAULT_TOTAL_MINUTE,
      snoozeTimes: item.intervalTimes,
      timeInterval: item.intervalMinute * CommonConstants.DEFAULT_TOTAL_MINUTE,
      actionButton: [
        {
          title: '关闭',
          type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE
        },
        // ...
      ],
      wantAgent: {
        pkgName: CommonConstants.BUNDLE_NAME,
        abilityName: CommonConstants.ABILITY_NAME
      },
      notificationId: item.notificationId,
      // ...
    }
  }
    
  // ...
}
  • 发布提醒。
typescript 复制代码
import { reminderAgentManager } from '@kit.BackgroundTasksKit';
// ...

export class ReminderService {
  public addReminder(alarmItem: ReminderItem, callback?: (reminderId: number) => void) {
    let reminder = this.initReminder(alarmItem);
    reminderAgentManager.publishReminder(reminder, (err, reminderId) => {
      if (callback != null) {
        callback(reminderId);
      }
    });
  }
   
  private initReminder(item: ReminderItem): reminderAgentManager.ReminderRequestAlarm {
    // ...
  }
    
  // ...
}

3、Native C++交互

  • 设置模块注册信息

ArkTS侧import native模块时,会加载其对应的so。加载so时,首先会调用napi_module_register方法,将模块注册到系统中,并调用模块初始化函数。

napi_module有两个关键属性:一个是.nm_register_func,定义模块初始化函数;另一个是.nm_modname,定义模块的名称,也就是ArkTS侧引入的so库的名称,模块系统会根据此名称来区分不同的so。

typescript 复制代码
// entry/src/main/cpp/hello.cpp

// 准备模块加载相关信息,将上述Init函数与本模块名等信息记录下来。
static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "entry",
    .nm_priv = nullptr,
    .reserved = {0},
};

// 加载so时,该函数会自动被调用,将上述demoModule模块注册到系统中。
extern "C" __attribute__((constructor)) void RegisterDemoModule() { 
    napi_module_register(&demoModule);
 }
  • 模块初始化

实现ArkTS接口与C++接口的绑定和映射。

typescript 复制代码
// entry/src/main/cpp/hello.cpp
EXTERN_C_START
// 模块初始化
static napi_value Init(napi_env env, napi_value exports) {
    // ArkTS接口与C++接口的绑定和映射
    napi_property_descriptor desc[] = {
        {"callNative", nullptr, CallNative, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"nativeCallArkTS", nullptr, NativeCallArkTS, nullptr, nullptr, nullptr, napi_default, nullptr},
    };
    // 在exports对象上挂载CallNative/NativeCallArkTS两个Native方法
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

// 模块基本信息
static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "entry",
    .nm_priv = nullptr,
    .reserved = {0},
};
  • 在index.d.ts文件中,提供JS侧的接口方法。
typescript 复制代码
// entry/src/main/cpp/types/libentry/index.d.ts
export const callNative: (a: number, b: number) => number;
export const nativeCallArkTS: (cb: (a: number) => number) => number;
在oh-package.json5文件中将index.d.ts与cpp文件关联起来。

{
  "name": "libentry.so",
  "types": "./index.d.ts",
  "version": "",
  "description": "Please describe the basic information."
}
  • 在CMakeLists.txt文件中配置CMake打包参数。
typescript 复制代码
# entry/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
project(MyApplication2)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include)

# 添加名为entry的库
add_library(entry SHARED hello.cpp)
# 构建此可执行文件需要链接的库
target_link_libraries(entry PUBLIC libace_napi.z.so)
  • 实现Native侧的CallNative以及NativeCallArkTS接口。具体代码如下:
typescript 复制代码
// entry/src/main/cpp/hello.cpp
static napi_value CallNative(napi_env env, napi_callback_info info)
{
    size_t argc = 2;
    // 声明参数数组
    napi_value args[2] = {nullptr};

    // 获取传入的参数并依次放入参数数组中
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    // 依次获取参数
    double value0;
    napi_get_value_double(env, args[0], &value0);
    double value1;
    napi_get_value_double(env, args[1], &value1);

    // 返回两数相加的结果
    napi_value sum;
    napi_create_double(env, value0 + value1, &sum);
    return sum;
}

static napi_value NativeCallArkTS(napi_env env, napi_callback_info info)
{    
    size_t argc = 1;
    // 声明参数数组
    napi_value args[1] = {nullptr};

    // 获取传入的参数并依次放入参数数组中
    napi_get_cb_info(env, info, &argc, args , nullptr, nullptr);

    // 创建一个int,作为ArkTS的入参
    napi_value argv = nullptr;    
    napi_create_int32(env, 2, &argv );

    // 调用传入的callback,并将其结果返回
    napi_value result = nullptr;
    napi_call_function(env, nullptr, args[0], 1, &argv, &result);
    return result;
}
  • ArkTS侧调用C/C++方法实现

ArkTS侧通过import引入Native侧包含处理逻辑的so来使用C/C++的方法。

typescript 复制代码
// entry/src/main/ets/pages/Index.ets
// 通过import的方式,引入Native能力。
import nativeModule from 'libentry.so'

@Entry
@Component
struct Index {
  @State message: string = 'Test Node-API callNative result: ';
  @State message2: string = 'Test Node-API nativeCallArkTS result: ';
  build() {
    Row() {
      Column() {
        // 第一个按钮,调用add方法,对应到Native侧的CallNative方法,进行两数相加。
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.message += nativeModule.callNative(2, 3);
            })
        // 第二个按钮,调用nativeCallArkTS方法,对应到Native的NativeCallArkTS,在Native调用ArkTS function。
        Text(this.message2)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.message2 += nativeModule.nativeCallArkTS((a: number)=> {
                return a * 2;
            });
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

具体的和c++交互的流程如上,具体的c++代码可以自行参考去实现。

4、第三方库的基本使用

  • 安装@ohos/lottie

通过ohpm执行对应的指令,将lottie安装到项目中。

typescript 复制代码
ohpm install @ohos/lottie
  • 卸载@ohos/lottie

通过ohpm执行卸载指令,将lottie从项目中删除,其程序包和配置信息将会从项目中移除。

typescript 复制代码
ohpm uninstall @ohos/lottie
  • 构建Canvas画布

@ohos/lottie解析JSON动画文件的数据需要基于Canvas 画布进行2D渲染,所以在加载JSON动画之前,要先初始化渲染上下文,并在画面中创建Canvas画布区域,将对应的渲染上下文renderingContext传递给Canvas。

typescript 复制代码
// 初始化渲染上下文  
private renderingSettings: RenderingContextSettings = new RenderingContextSettings(true) // 设置开启抗锯齿
private renderingContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.renderingSettings)  // 创建2D渲染上下文

// 加载Canvas画布   
Canvas(this.renderingContext)
  ...
  • 使用@ohos/lottie加载JSON动画
    加载JSON动画需要用到loadAnimation方法,在方法中需配置相应的初始设置,包括渲染上下文、渲染方式以及JSON动画资源的路径等。可以直接使用lottie.loadAnimation方法,也可以用一个animationItem实例来接收返回的animationItem对象。
typescript 复制代码
// 用animationItem实例接收
let animationItem = lottie.loadAnimation({
            container: this.renderingContext,            // 渲染上下文
            renderer: 'canvas',                          // 渲染方式
            loop: true,                                  // 是否循环播放,默认true
            autoplay: true,                              // 是否自动播放,默认true
            path: 'common/lottie/data.json',             // json路径
          })      
    lottie.loadAnimation({                               // 或者直接使用
            container: this.renderingContext,            // 渲染上下文
            renderer: 'canvas',                          // 渲染方式
            loop: true,                                  // 是否循环播放,默认true
            autoplay: true,                              // 是否自动播放,默认true
            path: 'common/lottie/data.json',             // json路径
          })
  • @ohos/lottie控制动画

@ohos/lottie内封装了包括状态控制,进度控制,播放设置控制和属性控制等多个API,用户可以利用这些API完成对动画的控制,实现更加灵活的交互效果。

typescript 复制代码
// 播放、暂停、停止、销毁  可以使用lottie,也可以使用animationItem实例进行控制
lottie.play();        // 从目前停止的帧开始播放
lottie.stop();        // 停止播放,回到第0帧
lottie.pause();       // 暂停该动画,在当前帧停止并保持
lottie.togglePause(); // 切换暂停/播放状态
lottie.destroy();     // 删除该动画,移除相应的元素标签等。在unmount的时候,需要调用该方法

// 播放进度控制
animationItem.goToAndStop(value, isFrame); // 跳到某个时刻/帧并停止。isFrame(默认false)指示value表示帧还是时间(毫秒)
animationItem.goToAndPlay(value, isFrame); // 跳到某个时刻/帧并进行播放
animationItem.goToAndStop(30, true);       // 例:跳转到第30帧并停止
animationItem.goToAndPlay(300);            // 例:跳转到第300毫秒并播放

// 控制帧播放
animationItem.setSegment(5,15);             // 限定动画资源播放时的整体帧范围,即设置动画片段
animationItem.resetSegments(5,15);          // 重置播放的动画片段
animationItem.playSegments(arr, forceFlag); // arr可以包含两个数字或者两个数字组成的数组,forceFlag表示是否立即强制播放该片段
animationItem.playSegments([10,20], false); // 例:播放完之前的片段,播放10-20帧
animationItem.playSegments([[5,15],[20,30]], true); //例: 直接播放5-15帧和20-30帧

// 动画基本属性控制
lottie.setSpeed(speed);         // 设置播放速度,speed为1表示正常速度
lottie.setDirection(direction); // 设置播放方向,1表示正向播放,-1表示反向播放

// 获取动画帧数属性
animationItem.getDuration();    //获取动画时长

通过上述的引用第三方库后就可以使用第三方库里面的一些函数方法了,引用第三方库都可以使用这个方法去调用第三方库的函数。

相关推荐
鸿蒙程序媛3 小时前
【鸿蒙开发】第十一章 Stage模型应用组件-任务Mission
harmonyos
lqj_本人9 小时前
鸿蒙next版开发:相机开发-预览(ArkTS)
数码相机·华为·harmonyos
柯南二号11 小时前
HarmonyOS ArkTS 下拉列表组件
前端·javascript·数据库·harmonyos·arkts
帅比九日13 小时前
【HarmonyOS NEXT】实战——登录页面
前端·学习·华为·harmonyos
鸿蒙开天组●13 小时前
鸿蒙进阶篇-属性动画-animateTo&转场动画
华为·harmonyos
howard200515 小时前
鸿蒙实战:页面跳转
华为·harmonyos·页面跳转
lqj_本人16 小时前
鸿蒙next版开发:ArkTS组件通用属性(禁用控制)
华为·harmonyos
火柴就是我16 小时前
Harmony AttributeModifier 基本使用
harmonyos
智汇云校乐乐老师16 小时前
千帆启航,人才先行 | 讯方技术HarmonyOS人才训练营
华为认证·harmonyos·华为harmonyos认证
lqj_本人16 小时前
鸿蒙next版开发:使用HiChecker检测问题(ArkTS)
华为·harmonyos