【愚公系列】保姆级教程带你实现HarmonyOS手语猜一猜元服务

🚀前言

最近HarmonyOS NEXT大火,这个纯血鸿蒙吸引力了大家的关注。虽然现在还没面向个人开发者开放,但我们可以基于最新的API9及开发工具来尝试开发鸿蒙新的应用形态------元服务。来体验下未来在HarmonyOS NEXT上实现的应用开发。

HarmonyOS是华为公司开发的操作系统,它的设计理念是面向未来的全场景智慧体验,可在各种设备上运行,包括手机、平板电脑、智能手表、智能音箱等。HarmonyOS采用分布式技术,可以将不同设备之间的计算资源连接起来,实现设备间的协同工作,提高系统的性能和稳定性。此外,HarmonyOS还拥有高度自适应的界面、多屏协同等特性,使用户能够在不同设备上实现无缝的体验。

本文主要是基于手语学习元服务的开发案例:主要的功能有

  • 元服务内部功能:

    • 1、提供专业手语翻译老师示范视频,包括基本手势、字母表、常用短语等。用户可以点击观看示范并模仿。
    • 2、提供类似答题模块,帮助用户巩固所学的手语知识,用户可以完成答题模块测试自己的进步。
  • 元服务卡片:

    • 1、卡片界面展示每日一题,并可以在卡片上进行答题学习

项目实现效果如下:

元服务内部功能视频:

元服务卡片功能视频:

🚀一、HarmonyOS元服务简介

🔎1.什么是元服务

元服务(原名为原子化服务)是HarmonyOS提供的一种面向未来的服务提供方式。它是一种新型应用程序形态,具有独立入口、免安装的特点,可以为用户提供一个或多个便捷服务。

以线上购物为例,传统购物应用需要先安装应用,打开应用查找商品,加入购物车,然后完成支付。而通过元服务的方式,可以将购物过程拆分为多个服务,例如"商品浏览"、"购物车"、"支付"等,无需安装应用,通过丰富的入口直达服务页面。例如,将心仪的商品页添加到桌面,可以实时掌握商品价格的变动。在秒杀时间点,可以直接进入购物车进行结算。

元服务基于HarmonyOS API开发,为用户在合适的场景、合适的设备上提供便捷的使用体验。相比传统的需要安装的应用形态,元服务更加轻量,同时提供更丰富的入口和更精准的分发。它为用户提供了更灵活、更高效的服务方式,提升了用户体验和便利性。

🔎2.元服务的独特价值

元服务的呈现形态之一是鸿蒙万能卡片,它是元服务最主要的展示方式之一(其他形态包括语音、图标等)。每一个万能卡片都是一个在桌面上"永远打开的"元服务/应用,以卡片的形式展示元服务/应用的重要信息,并通过轻量级的交互行为实现服务的直达。

元服务带来了体验上的变革,具体表现在以下几个方面:

  1. 免安装:元服务以更轻量化的方式将服务提供给用户,无需进行繁琐的应用安装过程,节省了用户的时间和存储空间。
  2. 一键服务直达:元服务将用户感兴趣的内容前置、外显,通过一键操作即可直接进入所需的服务页面,提供了更快捷、便利的服务体验。
  3. 跨端转移:元服务支持多终端设备间的无缝流转,用户可以在不同的设备上无缝切换使用元服务,提供了更连贯、一致的服务体验。
  4. 情景智能卡片推荐:元服务可以根据用户的需求和偏好进行情景智能卡片推荐,用户可以随心定制自己的服务卡片,并根据个人喜好进行个性化设置,从而更好地满足用户的需求。

通过以上的变革,元服务为用户带来了更轻便、更直观、更智能的服务体验,提升了用户的便利性和满意度。

🔎3.元服务的应用场景

🦋3.1负一屏

负一屏旨在提供更快速便捷的信息和服务,采用了宫格设计,将常用服务进行分类,涵盖了本地生活、智慧出行、购物娱乐、金融理财等多个场景。用户通过右滑进入发现页,即可轻松地获取所需的服务,无需下载繁琐的App。此外,负一屏还具备实时状态的新体验,用户使用服务后可以随时查看进度,掌握关键节点信息。

🦋3.2应用市场

打开华为应用市场,点击"应用"页签,进入"元服务"专区发现并使用元服务。

🦋3.3桌面

用户可以将元服务的卡片添加到桌面,便可在桌面随时随地查看元服务的重要信息,点击卡片即可直达所需服务。

🦋3.4碰一碰/扫一扫

用户首次"碰一碰"或者"扫一扫"识别设备上的NFC标签,系统引导用户连接设备,连接成功后,再次"碰一碰"或者"扫一扫"即可直接使用相应的元服务。

🚀二、开发环境搭建

🔎1.DevEco Studio

DevEco Studio是一款专门为鸿蒙(HarmonyOS)系统开发而设计的综合性开发工具IDE,开发者可以利用该工具进行鸿蒙应用的设计、开发、调试和发布。DevEco Studio集成了代码编辑器、模拟器、调试工具、图形用户界面设计器和应用管理工具等多个功能,方便开发者进行鸿蒙应用的开发与管理。

DevEco Studio支持多语言开发,包括Java、ArkTS、JavaScript等,同时还支持多种开发模式和框架。此外,它还支持多平台开发,包括手机、平板、智能手表、电视等不同终端设备。

DevEco Studio下载地址:HUAWEI DevEco Studio和SDK下载和升级 | HarmonyOS开发者

安装完成之后界面:

🔎2.配置环境变量

本文以window系统为例,具体操作步骤如下:

1、通过"设置 > 系统 > 系统信息 > 高级系统设置"进入"系统属性"页面的"高级"页签,点击"环境变量"

2、在"系统变量"中添加 HDC_SERVER_PORT和OHOS_HDC_SERVER_PORT 两个变量,变量值设置为未被占用的端口,例如7036和7037

3、在用户或者系统的path变量中,添加HDC工具的路径。

HDC工具路径为:HarmonyOS SDK安装目录/hmscore/{版本号}/toolchains。例如:C:\Users\XXXXX\AppData\Local\Huawei\Sdk\hmscore\3.1.0\toolchains

环境变量配置完成,重启DevEco Studio。

🔎3.诊断开发环境

1、打开项目,从欢迎页进入:底部菜单选择"Help > Diagnose Development Environment"

2、待自动检查完成。如果有检查未通过的项目,请根据检查项的描述和修复建议进行处理

🔎4.下载SDK

1、打开项目,从欢迎页进入:底部菜单选择"Configure > Settings"

🚀三、创建元服务项目

🔎1.创建元服务项目的步骤

1、登录AppGallery Connect, 点击"我的应用"。

首次进入需要签协议

2、在"HarmonyOS"页签,"类型"选择"元服务",可以查看创建的元服务。

🔎2.选择模板和配置项目属性

1、打开DevEco Studio,菜单选择"File > New > Create Project",创建一个新工程。

2、选择"Atomic Service",选择"Empty Ability"模板,点击"Next"。

3、配置工程基本信息。

  • Project name:设置"myProject"。
  • Bundle name:本样例以"com.huawei.myproject"为例。
  • Save location:选择工程存放路径。
  • Compile SDK:支持API 4~9,本样例选择"API 9"。
  • Model:应用支持的模式,API Version 4~8只支持FA模式。
  • Enable Super Visual:是否使用低代码开发模式,本样例不打开此开关。
  • Language:开发语言。
  • Device type:该工程模板支持的设备类型。本样例以手机设备为例。

点击"Finish",等待工程创建完成,即可进行代码编写。

🔎3.编写代码和调试运行

1、点击右侧的Previewer工具,预览页面效果。

2、新建details.ets页面

details.ets页面

less 复制代码
@Entry
@Component
struct Details {
  build() {
    //Flex容器组件
    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      //Text组件
      Text('我是跳转页面')
        .fontSize(60)
        .fontWeight(500)
    }
    //容器整体宽高
    .width('100%')
    .height('100%')
  }
}

main_pages.json页面

css 复制代码
{
  "src": [
    "pages/Index",
    "pages/details"
  ]
}

3、添加跳转按钮实现页面跳转

index.ets页面

typescript 复制代码
import router from '@ohos.router'
@Entry
@Component
struct Index {
  @State message: string = '愚公搬代码'

  build() {
    //Flex容器组件
    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      //Text组件
      Text('愚公搬代码')
        .fontSize(60)
        .fontWeight(500)
      //Button组件
      Button('跳转下一页')
        .fontSize(40)
        .fontWeight(500)
        .width(280)
        .height(60)
        //点击Button实现页面跳转
        .onClick(() => {
          console.info("跳转开始")
          router.pushUrl({
            url: 'pages/details' // 目标url
          }, (err) => {
            if (err) {
              console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
              return;
            }
            console.info('Invoke pushUrl succeeded.');
          });
        })
    }
    //容器整体宽高
    .width('100%')
    .height('100%')
  }
}

🚀四、元服务代码分析

🔎1.元服务代码结构介绍

  • AppScope中存放应用全局所需要的资源文件。
  • entry是应用的主模块,存放HarmonyOS应用的代码、资源等。
  • oh_modules是工程的依赖包,存放工程依赖的源文件。
  • build-profile.json5是工程级配置信息,包括签名、产品配置等。
  • hvigorfile.ts是工程级编译构建任务脚本,hvigor是基于任务管理机制实现的一款全新的自动化构建工具,主要提供任务注册编排,工程模型管理、配置管理等核心能力。
  • oh-package.json5是工程级依赖配置文件,用于记录引入包的配置信息。

🦋1.1 AppScope

  • element文件夹主要存放公共的字符串、布局文件等资源。
  • media存放全局公共的多媒体资源文件。

🦋1.2 entry

  • entryability用于当前ability应用逻辑和生命周期管理。
  • entryformability用于当前卡片生命周期管理。
  • pages存放UI界面相关代码文件,初始会生成一个Index页面。
  • ohosTest是单元测试目录。
  • build-profile.json5是模块级配置信息,包括编译构建配置项。
  • hvigorfile.ts文件是模块级构建脚本。
  • oh-package.json5是模块级依赖配置信息文件。
  • resources目录下存放模块公共的多媒体、字符串及布局文件等资源,分别存放在element、media文件夹中。

🔎2.开发选型

HarmonyOS 3.1及以上版本支持ArkTS、JS语言和Stage模型和FA模型两种模型,其中Stage模型为从HarmonyOS 3.1开始新增的模型,将是长期演进的模型。

HarmonyOS中的Stage模型是指应用程序的窗口管理器和图形引擎。Stage模型包含一个场景(Scene)和多个舞台(Stage),每个舞台表示一个窗口。在舞台上,可以添加多个UI元素(如Button、Label、Image等),通过对UI元素进行布局、组合和交互,实现应用程序的功能。

在HarmonyOS中,通过Stage模型,可以实现窗口管理、多任务处理、图形渲染和事件处理等功能。同时,Stage与HarmonyOS系统的分层架构紧密结合,可以自动适配不同屏幕、不同分辨率的设备,保证应用程序在各种设备上的兼容性和可用性。

一个应用包含一个或者多个Module,以下是Module与UIAbility组件关系示意图:

编译后的示意图:

🔎3.元服务中常用的API和类

  1. OHOS::AAFwk::Ability: 这是Ability的基类,提供了启动、停止和生命周期管理等能力。
  2. OHOS::AAFwk::Want: 跨应用程序组件之间传递的信息,可以指定Intent和Bundle等参数。
  3. OHOS::AppExecFwk::EventHandler: 用于在Ability生命周期内处理异步任务,支持延迟执行和定时执行。
  4. OHOS::EventFwk::EventRunner: 用于实现异步事件的执行和处理。
  5. OHOS::AGP::Window: 应用程序窗口,用于显示UI界面。
  6. OHOS::AGP::View: UI界面中的基本控件,如Button、TextView等。
  7. OHOS::DataAbility::DataAbilityHelper: 数据能力的基础类,用于管理数据的增删改查操作。
  8. OHOS::DistributedSchedule::DistributedSchedulPolicy: 分布式调度策略,用于实现分布式系统的任务调度。
  9. OHOS::Media::MediaPlayer: 用于播放音频和视频文件。
  10. OHOS::Security::Permission: 权限管理类,用于管理应用程序对系统资源的访问权限。

🚀五、案例展示:手语猜一猜

🔎1.案例背景

手语学习元服务的案例背景主要是为了帮助人们学习手语,提高对聋人的理解和尊重。聋人由于听力障碍,无法通过声音语言进行交流,而手语是聋人交流的主要方式。手语学习元服务通过数字化的方式提供了学习手语的机会,让更多的人能够了解手语,学习手语,以便更好地与聋人交流和理解聋人的需求。

随着社会的不断发展,聋人的教育和融入社会的问题日益重要。手语学习应用可以提供数字化的手语课程,让更多的人能够学习手语,提高对聋人的理解和尊重,促进聋人的教育和融入社会。

手语是一种非常有意义的语言,它不仅可以让聋人与人进行有效的交流,还可以帮助听力正常的人了解和尊重聋人的文化和生活习惯。手语学习应用也可以成为一个重要的手语文化传播平台,让更多的人了解和学习手语,促进手语文化的传播和发展。

🔎2.功能简介

元服务:

1、提供专业手语翻译老师示范视频,包括基本手势、字母表、常用短语等。用户可以点击观看示范并模仿。

2、提供类似答题模块,帮助用户巩固所学的手语知识,用户可以完成答题模块测试自己的进步。

元服务卡片:

1、卡片界面展示每日一题,并可以在卡片上进行答题学习

🔎3.案例实现流程

🦋3.1 内部应用功能

☀️3.1.1 首页功能

首页功能主要包含了4块功能:每日挑战、课程、测试、学习记录

🌈3.1.1.1 每日挑战

每日挑战主要的交互有2块功能:换一换和答题

换一换:主要实现题目和资源的切换(替换题目、替换视频链接)

答题:主要是根据选项遍历出Button按钮,在根据按钮点击事件判断点击的按钮type等于1则答题正确

源码片段如下:

scss 复制代码
Flex({ justifyContent: FlexAlign.SpaceBetween }){
  Text('每日挑战')
    .fontSize(20)
    .fontColor('#fff')
  .width('50%')
  Column() {
    Flex({ justifyContent: FlexAlign.End  }) {
      Image(this.ic_new)
        .width(20).height(20)
        .margin({
          right:5
        })
      Text('换一批')
        .fontSize(20)
        .fontColor('#fff')
        .onClick(()=>{
          this.anyArray=[{            name:'吃',            type:1          },{            name:'厲害',            type:0          }]
          this.videoSrc=$rawfile('quick.mp4')
          this.previewUri=$r('app.media.quick')
        })
    }.width('50%')
  }
}.padding(10)
 .backgroundColor('#938cf4')
 Column(){
   Column(){
     Flex({justifyContent:FlexAlign.SpaceBetween}){
       Text('根据视频所示,选择正确的答案')
         .fontSize(12)
         .fontColor('#fff')
       Text('40%的人答错')
         .fontSize(12)
         .fontColor('#fff')
     }.padding({
       top:10,
       left:10,
       right:10,
       bottom:10
     })
     .margin({
       top:0
     })

     Video({
       src: this.videoSrc,
       previewUri: this.previewUri,
       currentProgressRate: this.curRate,
       controller: this.controller
     }).width('100%').height(180)
       .padding(5)
       .borderRadius(1)
       .margin({
         top:0,
         left:10,
         right:10,
         bottom:10,
       })
     Flex({justifyContent:FlexAlign.SpaceBetween}){
       ForEach(this.anyArray,(item)=>{
         Button(item.name,{ type: ButtonType.Normal,})
           .borderRadius(0)
           .width('45%')
           .backgroundColor('#ff986ec8')
           .onClick(()=>{
               if(item.type==1){
                 if (this.dialogController != undefined) {
                   this.dialogController.open()
                   this.textValue='答对了'
                 }
               }else{
                 if (this.dialogController != undefined) {
                   this.dialogController.open()
                   this.textValue='答错了'
                 }
               }
           })
       })
     }.margin({
       top:0,
       left:10,
       right:10,
       bottom:10,
     })
   }.backgroundColor('#ffa49fea')
   .margin({
     top:0,
     left:10,
     right:10,
     bottom:10,
   })
   .borderRadius(10)
 }.backgroundColor('#938cf4')
🌈3.1.1.2 课程

这边主要是点击按钮跳转到课程页面一个功能

源码片段如下:

scss 复制代码
Column() {
  Flex({ alignItems: this.alignItems }) {
    Text('课程').width('50%')
      .fontSize(20)
      .fontWeight(FontWeight.Bold)
  }
  .size({width: '90%',})
  .padding({
    top:20,
    left:10,
    bottom:0
  })
  Flex({ justifyContent: FlexAlign.SpaceBetween }) {
    Text('初级课程,易上手').width('70%')
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor('#c4c2cf')
    Button('学习').width('30%')
      .backgroundColor('#fecc5b')
      .margin({
        top:-25,
      })
      .onClick(()=>{
        router.push({ url: 'pages/list' })
      })
  }
  .size({width: '100%', })
  .padding(
    { left: 30 ,top:10,right:30,bottom:20},
  )
  .border({
    radius:{bottomLeft: 15, bottomRight: 15}
  })
}.width('90%')
.backgroundColor('#fff')
.borderRadius(15)
.margin(20)
🌈3.1.1.3 测试

这边主要是点击按钮跳转到测试页面一个功能

源码片段如下:

scss 复制代码
Column() {
  Flex({ alignItems: this.alignItems }) {
    Text('课程').width('50%')
      .fontSize(20)
      .fontWeight(FontWeight.Bold)
  }
  .size({width: '90%',})
  .padding({
    top:20,
    left:10,
    bottom:0
  })
  Flex({ justifyContent: FlexAlign.SpaceBetween }) {
    Text('初级课程,易上手').width('70%')
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor('#c4c2cf')
    Button('学习').width('30%')
      .backgroundColor('#fecc5b')
      .margin({
        top:-25,
      })
      .onClick(()=>{
        router.push({ url: 'pages/list' })
      })
  }
  .size({width: '100%', })
  .padding(
    { left: 30 ,top:10,right:30,bottom:20},
  )
  .border({
    radius:{bottomLeft: 15, bottomRight: 15}
  })
}.width('90%')
.backgroundColor('#fff')
.borderRadius(15)
.margin(20)
🌈3.1.1.4 学习记录

这边主要是基于组件的形式在界面展示,功能点主要有:时间数据、学习课程数

  • 时间数据:已当前时间自动更新
  • 学习课程数:来源于已学课程的本地存储数据(存储来源在课程学习界面)

源码片段如下:

scss 复制代码
PersistentStorage.PersistProp('course', 0);

let date = new Date()
let time = date.getFullYear() + "年" + (date.getMonth() + 1) + "月" + date.getDate() + "日"
console.log(time);
@Component
export default struct studyCollect {
  alignItems : number = 0
  // @State timeDate:string = date
  @StorageLink('course') course: number = 0
  @State dayTime:string=time;
  build() {
    Column() {
      Flex({ alignItems: this.alignItems }) {
        Text('学习记录').width('50%')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
        Text(` ${this.dayTime}`).width('50%').height(30).textAlign(TextAlign.End)
      }
      .size({width: '90%', height: 50})
      .margin({
        top:10
      })
      .padding(10)
      Flex({ alignItems: this.alignItems }) {
        Text(`已学${this.course}课程`).width('50%').height(20).fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#fff')
        Text(`${this.course}分钟`).width('50%').height(20).textAlign(TextAlign.End)
          .fontColor('#fff')
      }
      .size({width: '100%', height: 50})
      .padding(
        { left: 40 ,top:15,right:40,},
      )
      .border({
        radius:{bottomLeft: 15, bottomRight: 15}
      })
      .backgroundColor('#938cf4')
    }.width('90%')
    .backgroundColor('#fff')
    .borderRadius(15)
    .margin({
      top:5
    })

  }
}

☀️3.1.2 课程功能

课程主页面主要是课程信息展示和跳转到具体详情页面

源码片段如下:

typescript 复制代码
import router from '@ohos.router';

import { arrImage } from '../common/json'
@Entry
@Component
struct list {
  scroller: Scroller = new Scroller();
  @State arrImage: Object[]=arrImage


  build() {
    Scroll(this.scroller) {
      Column() {
        Column({ space: 5 }) {
          Text('初级课程').fontSize(20).fontColor('#fff').width('90%')
            .padding(10)
          .backgroundColor('')
          Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
            ForEach(this.arrImage, (item) => {
              Column() {
                Image(item.image).width('100%').height(120)
                Text(item.name).lineHeight(30)
              }.width('45%').height(150).backgroundColor(0xF5DEB3)
              .margin(5)
              .onClick(()=>{
                router.pushUrl({
                    url:'pages/details',
                    params:{
                      text:item.name,
                      image:item.image,
                      video:item.video
                    }
                })
              })
            })

          }
          .width('100%')
          .padding(10)
          .backgroundColor(0xAFEEEE)
        }.width('100%').margin({ top: 5 })
      }.width('100%')
      .backgroundColor('#938cf4')
    }
    .backgroundColor(0xDCDCDC)
    .scrollable(ScrollDirection.Vertical) // 滚动方向纵向
    .scrollBar(BarState.On) // 滚动条常驻显示
    .scrollBarColor(Color.Gray) // 滚动条颜色
    .scrollBarWidth(5) // 滚动条宽度
    .edgeEffect(EdgeEffect.Spring) // 滚动到边沿后回弹
  }
}
🌈3.1.2.1 课程详情页

功能点主要有:路由数据、客户对课程的状态信息

  • 路由数据:手语答案、手语视频地址
  • 客户对课程的状态信息:点赞、收藏、转发相关状态变更

源码片段如下:

less 复制代码
import router from '@ohos.router'
import promptAction from '@ohos.promptAction'

import { CommonTitleBar } from '../common/CommonTitleBar';


@Entry
@Component
struct Index {
  @StorageLink('course') course: number = 0

  // @State good:Resource=$r('app.media.icon_good');
  @State goodStatus:boolean=true
  @State goodNumber:number=0

  @State startStatus:boolean=true
  @State startNumber:number=0

  @State shareStatus:boolean=true
  @State shareNumber:number=0

  @State text: string = router.getParams()['text']
  @State previewUri: Resource = router.getParams()['image']
  @State videoSrc: Resource = router.getParams()['video']
  @State curRate: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X
  controller: VideoController = new VideoController()
  alignItems : number = 0
  build() {
    Column(){
      // CommonTitleBar({attribute: {
      //   bg_color: '#ff2ad4b2',
      //   close_text: '返回',
      //   closeCallback: () => {
      //
      //   },
      //   title_text: '标题',
      //   menuCallback: () => {
      //
      //   }
      // }})
      Column(){
        Column(){
          Video({
            src: this.videoSrc,
            previewUri: this.previewUri,
            currentProgressRate: this.curRate,
            controller: this.controller
          }).width('100%').height(180)
            .padding(10)
            .borderRadius(10)
            .onFinish(() => {
              this.course += 1;
              console.info('onFinish')
            })
        }
        Flex(){
          Text('当前示例:')
            .fontSize(24)
          Text(`${this.text}`)
            .fontSize(24)
        }.padding(10)
        Flex({}){
          Flex(){
            Image(this.goodStatus ? $r("app.media.icon_good"):$r("app.media.active_icon_good")
            ).width(24).height(24)
            Text(`${this.goodNumber}`).height(25)
          }.width('20%')
          .onClick(()=>{
            this.goodStatus=!this.goodStatus;
            this.goodStatus ? this.goodNumber--:this.goodNumber++;
            promptAction.showToast({
              message: this.goodStatus ? '取消点赞' :'点赞成功',
              duration: 2000,
            });
          })
          Flex(){
            Image(this.startStatus ? $r("app.media.icon_star"):$r("app.media.active_icon_star")).width(24).height(24)
            Text(`${this.startNumber}`).height(25)
          }.width('20%')
          .onClick(()=>{
            this.startStatus=!this.startStatus;
            this.startStatus ? this.startNumber--:this.startNumber++;
            promptAction.showToast({
              message: this.startStatus ? '取消收藏' :'收藏成功',
              duration: 2000,
            });
          })
          Flex(){
            Image(this.shareStatus ? $r("app.media.icon_share"):$r("app.media.active_icon_share")).width(24).height(24)
            Text(`${this.shareNumber}`).height(25)
          }.width('20%')
          .onClick(()=>{
            this.shareStatus=!this.shareStatus;
            this.shareStatus ? this.shareNumber--:this.shareNumber++;
            promptAction.showToast({
              message: this.shareStatus ? '取消转发' :'转发成功',
              duration: 2000,
            });
          })
        }.padding(10)
      }
    }
    //容器整体宽高
    .width('100%')
    .height('100%')
    .backgroundColor('#ff0f2ff')
  }
}

☀️3.1.3 测试功能

测试功能页面主要功能是从json的题库取出10题,如果答题正确会自动切换到下一题。

源码片段如下:

less 复制代码
import router from '@ohos.router'

import promptAction from '@ohos.promptAction'
import { arrImage } from '../common/json'
@Entry
@Component
struct Index {
  @State onActive : number = 1

  @State arrImage: Object[]=arrImage

  @State videoSrc: Resource = $rawfile('hello.mp4')
  @State previewUri: Resource = $r("app.media.hello")
  @State curRate: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X
  controller: VideoController = new VideoController()
  alignItems : number = 0
  @State message: string = '手语猜猜看'
  @State studyName: string = '手语阅览'
  @State collectName: string = '手语挑战'
  @State testName: string = '测试'

  @State arr: string[] =['你好','出生','爱','晚饭','中午','明天']


  build() {
    Column(){
      Column(){
        Flex({alignItems:ItemAlign.Center,justifyContent:FlexAlign.Center}){
          // Text(`${this.arrImage[this.onActive]['name']}`)
          Text(`${this.onActive}`)
            .fontSize(28)
          Text('/')
            .fontSize(28)
          Text(`${this.arrImage.length}`)
            .fontSize(28)
        }
        .margin(10)
        Column(){
          Video({
            src: this.arrImage[this.onActive-1]['video'],
            previewUri: this.arrImage[this.onActive-1]['image'],
            currentProgressRate: this.curRate,
            controller: this.controller
          }).width('100%').height(180)
            .margin({
            top:0,
            bottom:0,
          })
        }
        Column({}){
          Text('在下列选项中选择正确答案?')
            .width('100%')
            .padding(10)
            .fontSize(20)
        }.backgroundColor('#fff')
        Flex({wrap: FlexWrap.Wrap }){
          ForEach(this.arrImage[this.onActive-1]['answer'],(item:string)=>{
            Button(item,{ type: ButtonType.Normal,})
              .margin(5)
              .borderRadius(5)
              .onClick(() => {
                console.log(item)
                if(this.onActive>=this.arrImage.length){
                  promptAction.showToast({
                    message: '闯关已结束',
                    duration: 2000,
                  });
                  router.back({url:'pages/index'});
                  return false;
                }
                if(item==this.arrImage[this.onActive-1]['name']){
                  promptAction.showToast({
                    message: '答对了,请继续下一题',
                    duration: 2000,
                  });
                  this.onActive=this.onActive+1;
                }else{
                  promptAction.showToast({
                    message: '答错了',
                    duration: 2000,
                  });
                }

            })
          })
        }.padding(10)
      }
    }
    //容器整体宽高
    .width('100%')
    .height('100%')
    .backgroundColor('#ff0f2ff')
  }
}

🦋3.2 卡片功能

这边和每日挑战功能类似,主要多了些动画效果,卡片主要的交互有2块功能:换一换和答题

  • 换一换:主要实现题目和资源的切换(替换题目、替换视频链接)
  • 答题:主要是根据选项遍历出Button按钮,在根据按钮点击事件判断点击的按钮type等于1则答题正确

源码片段如下:

css 复制代码
@Entry
@Component
struct WidgetCard {

  @State onActive:number=0
  @State arrImage: Object[] = [
    {
      name:'你好',
      image:$r("app.media.hello"),
      content:'一手食指指向对方。一手握拳,向上伸出拇指。',
      video:$rawfile('hello.mp4'),
      anyArray:[{
        name:'你好',
        type:1
      },{
        name:'谢谢',
        type:0
      }]
    },
    {
      name:'谢谢',
      image:$r('app.media.thank'),
      content:'一手伸出拇指,弯曲两下,表示向人感谢。',
      video:$rawfile('thank.mp4'),
      anyArray:[{
        name:'不好',
        type:0
      },{
        name:'谢谢',
        type:1
      }]
    },
    {
      name:'爱',
      image:$r('app.media.love'),
      content:'一手轻轻抚摩另一手拇指指背,表示一种"怜爱"的感情',
      video:$rawfile('love.mp4'),
      anyArray:[{
        name:'没有',
        type:0
      },{
        name:'爱',
        type:1
      }]
    },
    {
      name:'喜欢',
      image:$r('app.media.live'),
      video:$rawfile('like.mp4'),
      content:'一手拇、食指微曲,指尖抵于颌下,头微微点动一下。	',
      anyArray:[{
        name:'没有',
        type:0
      },{
        name:'喜欢',
        type:1
      }]
    },
    {
      name:'不喜欢',
      image:$r('app.media.dislike'),
      video:$rawfile('dislike.mp4'),
      content:'一手伸直,左右摆动几下。	一手拇、食指微曲,指尖抵于颌下,头微微点动一下。	',
      anyArray:[{
        name:'好晕',
        type:0
      },{
        name:'不喜欢',
        type:1
      }]
    },
    {
      name:'饭',
      image:$r('app.media.eat'),
      video:$rawfile('eat.mp4'),
      content:'(一)一手拇、食指相对,中间留有米粒大小距离。(二)一手伸食、中指象征筷子,作吃饭动作。',
      anyArray:[{
        name:'可爱',
        type:0
      },{
        name:'饭',
        type:1
      }]
    },
    {
      name:'快',
      image:$r('app.media.quick'),
      video:$rawfile('quick.mp4'),
      content:'一手拇、食指相捏,很快地从一侧向另一侧作快速挥动,象征物体运动速度很快。',
      anyArray:[{
        name:'慢',
        type:0
      },{
        name:'快',
        type:1
      }]
    },
    {
      name:'慢',
      image:$r('app.media.slow'),
      video:$rawfile('show.mp4'),
      content:'一手掌心向下,慢慢地上下微动几下,象征物体运动速度缓慢。',
      anyArray:[{
        name:'去',
        type:0
      },{
        name:'慢',
        type:1
      }]
    },
    {
      name:'没关系',
      image:$r('app.media.matter'),
      video:$rawfile('matter.mp4'),
      content:'一手拇、食、中指捻动,连续几次。	两手拇、食指搭成圆圈,互相套环。	',
      anyArray:[{
        name:'人类',
        type:0
      },{
        name:'没关系',
        type:1
      }]
    },
    {
      name:'厉害',
      image:$r('app.media.powerful'),
      video:$rawfile('powerful.mp4'),
      content:'一手打手指字母"L"的指式,并绕脸部转一圈。同时面部作出严厉的表情。	',
      anyArray:[{
        name:'好帅',
        type:0
      },{
        name:'厉害',
        type:1
      }]
    },
  ];

  /*
   * The max lines.
   */
  readonly MAX_LINES: number = 1;

  /*
   * The action type.
   */
  readonly ACTION_TYPE: string = 'router';

  /*
   * The message.
   */
  readonly MESSAGE: string = 'add detail';

  /*
   * The ability name.
   */
  readonly ABILITY_NAME: string = 'EntryAbility';

  /*
   * The with percentage setting.
   */
  readonly FULL_WIDTH_PERCENT: string = '100%';

  /*
   * The height percentage setting.
   */
  readonly FULL_HEIGHT_PERCENT: string = '100%';


  @State opacityAngle: number =  0.8
  @State mainFlag: boolean = false;
  @State flag: boolean = true;
  build() {
    Stack() {
      if(this.flag){
        Image(this.arrImage[this.onActive]['image'])
          .width(this.FULL_WIDTH_PERCENT)
          .height(this.FULL_HEIGHT_PERCENT)
          .objectFit(ImageFit.Cover)
          .opacity(this.opacityAngle)
          .transition({ type: TransitionType.Insert, translate: { x: 0, y: 0 } })
          .transition({ type: TransitionType.Delete, opacity: 0, scale: { x: 0, y: 0 } })
      }else{
        Image(this.arrImage[this.onActive]['image'])
          .width(this.FULL_WIDTH_PERCENT)
          .height(this.FULL_HEIGHT_PERCENT)
          .objectFit(ImageFit.Cover)
          .opacity(this.opacityAngle)
          .transition({ type: TransitionType.Insert, translate: { x: 0, y: 0 } })
          .transition({ type: TransitionType.Delete, opacity: 0, scale: { x: 0, y: 0 } })
      }
       Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.Center }){
         Image($r("app.media.ic_new")).width(11).height(11)
           .margin({
             left:10,
             top:3,
             right:5
           })
         Text("换一批").fontSize('8fp').width(40)
           .fontColor('#fff')
           .margin({
             top:3
           })
        }.backgroundColor('#fecc5b')
       .width(50)
       .height(15)
       .borderRadius(10)
       .onClick(()=>{
         animateTo({ duration: 1000 }, () => {
           this.flag = !this.flag;
           this.onActive=(this.onActive+1)%this.arrImage.length;
         })
       })
       .position({ x: 10, y: 10 })
      Column(){
        Flex({justifyContent:FlexAlign.SpaceBetween}){
          ForEach(this.arrImage[this.onActive]['anyArray'],(item)=>{
            Button(item.name,{ type: ButtonType.Normal,})
              .fontSize('8fp')
              .borderRadius(10)
              .height(20)
              .width('45%')
              .backgroundColor('#fecc5b')
              .onClick(()=>{
                console.log('44')
                if(item.type==1){
                  animateTo({ duration: 1000 }, () => {
                    this.flag = !this.flag;
                    this.onActive=(this.onActive+1)%this.arrImage.length;
                  })
                }else{

                }
              })
          })
        }
      }.margin({
        top:110,
        left:10,
        right:10
      })
      Text(this.arrImage[this.onActive]['content'])
        .fontSize('7fp')
        .opacity($r('app.float.detail_immersive_opacity'))
        .margin({ top: '70vp',left:'10vp',right:'10vp' })
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .fontColor('#000')
        .fontWeight('900')
        .maxLines(this.MAX_LINES)
    }
    .width(this.FULL_WIDTH_PERCENT)
    .height(this.FULL_HEIGHT_PERCENT)
    .onClick(() => {
      postCardAction(this, {
        "action": this.ACTION_TYPE,
        "abilityName": this.ABILITY_NAME,
        "params": {
          "message": this.MESSAGE
        }
      });
    })
  }
}

元服务项目源码:download.csdn.net/download/aa...

🚀总结

手语猜一猜元服务的实现借助了HarmonyOS的跨应用数据共享和功能交互特性,让用户可以通过手势输入手语,并进行识别和猜词游戏。这种应用方式不仅提供了娱乐和互动的体验,还促进了手语的学习和交流。通过使用该应用,我深刻体会到了HarmonyOS元服务的便利和灵活性。

开发使用HarmonyOS元服务可以带来许多好处,包括跨设备互联、数据共享和交互、灵活性和扩展性,以及性能优化。我鼓励大家积极尝试使用HarmonyOS元服务,开发出更好属于自己的应用。

🚀附录

相关推荐
腾讯TNTWeb前端团队4 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰7 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪7 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪7 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy8 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom9 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom9 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom9 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom9 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom9 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试