鸿蒙技术分享:敲鸿蒙木鱼,积____功德——鸿蒙元服务开发:从入门到放弃(1)...

本文是系列文章,其他文章见:
敲鸿蒙木鱼,积____功德🐶🐶🐶------鸿蒙元服务开发:从入门到放弃(2)
敲鸿蒙木鱼,积____功德🐶🐶🐶------鸿蒙元服务开发:从入门到放弃(3)

本文完整源码查看funny-widget

简介

因为工作需要,准备开发元服务,所以就想着搞一个电子木鱼的DEMO学习一下元服务以及桌面卡片的功能开发知识。

详细了解HarmonyOS的元服务,可查看官方介绍

涉及知识点

  • 元服务开发流程
  • 加载图片
  • 播放音频
  • 开发调试
  • 组件代码在卡片和元服务间共享
  • 数据在卡片和元服务间共享

元服务开发

创建项目

根据官方文档创建工程

必须要注册华为开发者账号。

开发电子木鱼页面

新建ElectronicWoodenFishPage页面。

为了方便后期共用布局和逻辑,将木鱼逻辑抽离出单独的Component,ElectronicWoodenFishComponent`。

typescript 复制代码
import { ElectronicWoodenFishComponent } from '../components/ElectronicWoodenFishComponent';
import { DataManager } from '../utils/DataManager';

@Entry
@Component
struct ElectronicWoodenFishPage {
  @State meritCount: number = 0;
  build() {
    RelativeContainer() {
      ElectronicWoodenFishComponent({
        meritCount: this.meritCount,
        onClickWoodenFish: () => {
          
        }
      })
        .id('ElectronicWoodenFishComponent')
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
    }
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    .height('100%')
    .width('100%')
  }
}
添加布局和点击功能

ElectronicWoodenFishComponent组件中添加木鱼图片布局。

typescript 复制代码
@Component
export struct ElectronicWoodenFishComponent {
  @Prop meritCount: number = 0;

  onClickWoodenFish: () => void = () => {}

  build() {
    Row() {
      Column() {
        Text(`功德:${this.meritCount}`)
          .fontColor(Color.White)
          .fontWeight(FontWeight.Bold)
        Stack() {
          Image($r('app.media.WoodenFish'))
            .size({ width: '80%' })
            .aspectRatio(1)
            .onClick(() => {
              if (this.onClickWoodenFish) {
                this.onClickWoodenFish()
              }
            })
        }
        .align(Alignment.Top)
      }
      .width('100%')
    }
    .height('100%')
    .backgroundColor(Color.Black)
  }
}
获取图片资源

baidu木鱼图片,现在baidu搜索图片支持智能抠图,方便了很多。

有需要的同学直接取用:

添加缩放动画和文本动画

ElectronicWoodenFishComponent组件中添加"+1"上浮动画文字和木鱼放大动画。

typescript 复制代码
@Component
export struct ElectronicWoodenFishComponent {
  @Prop meritCount: number = 0;

  @State imageScale: ScaleOptions = { x: 1, y: 1 }
  @State plusOneOffsetY: number = 40;
  @State plusOneOpacity: number = 0;

  onClickWoodenFish: () => void = () => {}

  build() {
    Row() {
      Column() {
        Text(`功德:${this.meritCount}`)
          .fontColor(Color.White)
          .fontWeight(FontWeight.Bold)
        Stack() {
          Image($r('app.media.WoodenFish'))
            .size({ width: '80%' })
            .aspectRatio(1)
            .scale(this.imageScale)
            .animation({
              curve: Curve.EaseInOut,
              playMode: PlayMode.AlternateReverse,
              duration: 500,
              onFinish: () => {
                this.imageScale = { x: 1, y: 1 }
              }
            })
            .onClick(() => {
              this.imageScale = { x: 1.2, y: 1.2 }
              this.plusOneOpacity = 1
              this.plusOneOffsetY = 0

              if (this.onClickWoodenFish) {
                this.onClickWoodenFish()
              }
            })
          Text('+1')
            .fontColor(Color.White)
            .opacity(this.plusOneOpacity)
            .offset({ y: this.plusOneOffsetY })
            .animation({
              curve: Curve.EaseInOut,
              playMode: PlayMode.Normal,
              duration: 960,
              onFinish: () => {
                this.plusOneOffsetY = 40
                this.plusOneOpacity = 0
              }
            })
        }
        .align(Alignment.Top)
      }
      .width('100%')
    }
    .height('100%')
    .backgroundColor(Color.Black)
  }
}

简单的使用关键帧动画完成放大和平移动画,不过之前测试遇到过动画onFinish会触发多次的问题,感觉还是不太稳定。

添加音效

修改ElectronicWoodenFishPage页面点击回调,增加音效播放。

typescript 复制代码
@Entry
@Component
struct ElectronicWoodenFishPage {
  build() {
    RelativeContainer() {
      ElectronicWoodenFishComponent({
        meritCount: this.meritCount,
        onClickWoodenFish: () => {
          AudioManager.shared.playOnlineSound()
          // ...
        }
      })
    }
  }
}

export class AudioManager {
  static shared = new AudioManager()

  isSeek: boolean = false
  count: number = 0
  setAVPlayerCallback(avPlayer: media.AVPlayer) {
    // seek操作结果回调函数
    avPlayer.on('seekDone', (seekDoneTime: number) => {
      console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
    })
    // error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程
    avPlayer.on('error', (err: BusinessError) => {
      hilog.info(0x0000, '音频', '%{public}s', `error回调:${err.message}`);
      console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
      avPlayer.reset(); // 调用reset重置资源,触发idle状态
    })
    // 状态机变化回调函数
    avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
      hilog.info(0x0000, '音频', '%{public}s', `stateChange回调:${state},${reason.toString()}`);
      switch (state) {
        case 'idle': // 成功调用reset接口后触发该状态机上报
          console.info('AVPlayer state idle called.');
          avPlayer.release(); // 调用release接口销毁实例对象
          break;
        case 'initialized': // avplayer 设置播放源后触发该状态上报
          console.info('AVPlayer state initialized called.');
          avPlayer.prepare();
          break;
        case 'prepared': // prepare调用成功后上报该状态机
          console.info('AVPlayer state prepared called.');
          avPlayer.play(); // 调用播放接口开始播放
          break;
        case 'playing': // play成功调用后触发该状态机上报
          console.info('AVPlayer state playing called.');
          if (this.count !== 0) {
            if (this.isSeek) {
              console.info('AVPlayer start to seek.');
              avPlayer.seek(avPlayer.duration); //seek到音频末尾
            } else {
              // 当播放模式不支持seek操作时继续播放到结尾
              console.info('AVPlayer wait to play end.');
            }
          } else {
            avPlayer.pause(); // 调用暂停接口暂停播放
          }
          this.count++;
          break;
        case 'paused': // pause成功调用后触发该状态机上报
          console.info('AVPlayer state paused called.');
          avPlayer.play(); // 再次播放接口开始播放
          break;
        case 'completed': // 播放结束后触发该状态机上报
          console.info('AVPlayer state completed called.');
          avPlayer.stop(); //调用播放结束接口
          break;
        case 'stopped': // stop接口成功调用后触发该状态机上报
          console.info('AVPlayer state stopped called.');
          avPlayer.reset(); // 调用reset接口初始化avplayer状态
          break;
        case 'released':
          console.info('AVPlayer state released called.');
          break;
        default:
          console.info('AVPlayer state unknown called.');
          break;
      }
    })
  }

  // 以下demo为通过url设置网络地址来实现播放直播码流的demo
  async playOnlineSound() {
    hilog.info(0x0000, '电子木鱼组件', '%{public}s', 'playOnlineSound');
    console.debug(`[电子木鱼组件]playOnlineSound`)
    // 创建avPlayer实例对象
    let avPlayer: media.AVPlayer = await media.createAVPlayer();
    // 创建状态机变化回调函数
    this.setAVPlayerCallback(avPlayer);
    avPlayer.url = 'https://clemmensen.top/static/muyu.mp3';
  }
}

1.这里的在线mp3音频地址使用的掘金其他老哥的资源,感谢;码上掘金实现电子木鱼

2.在线播放音频肯定是有延迟;至于为什么不用本地的,因为元服务播放本地音频失败[捂脸];

数据存储

简单封装了下preferences。

typescript 复制代码
import preferences from '@ohos.data.preferences';
import { AsyncCallback, Callback } from '@ohos.base';

export class DataManager {
  static shared = new DataManager()

  preferences: preferences.Preferences | null = null;

  init(context: Context) {
    this.preferences = preferences.getPreferencesSync(context, { name: 'myStore' });
  }

  get(key: string, defValue: preferences.ValueType, callback: AsyncCallback<preferences.ValueType>): void {
    this.preferences?.get(key, defValue, callback)
  }

  getSync(key: string, defValue: preferences.ValueType): preferences.ValueType {
    return this.preferences?.getSync(key, defValue) ?? defValue
  }

  put(key: string, value: preferences.ValueType, callback: AsyncCallback<void>): void {
    this.preferences?.put(key, value, callback)
  }

  putSync(key: string, value: preferences.ValueType): void {
    this.preferences?.putSync(key, value)
    this.preferences?.flush()
  }
}

修改ElectronicWoodenFishPage页面,增加数据存取逻辑。

typescript 复制代码
import { ElectronicWoodenFishComponent } from '../components/ElectronicWoodenFishComponent';
import { DataManager } from '../utils/DataManager';

@Entry
@Component
struct ElectronicWoodenFishPage {
  @State meritCount: number = 0;
  aboutToAppear(): void {
    DataManager.shared.init(getContext(this))
    this.meritCount = DataManager.shared.getSync('meritCount', 0) as number
  }
  build() {
    RelativeContainer() {
      ElectronicWoodenFishComponent({
        meritCount: this.meritCount,
        onClickWoodenFish: () => {
          let meritCount: number = DataManager.shared.getSync('meritCount', 0) as number
          DataManager.shared.putSync('meritCount', meritCount + 1)
          this.meritCount = meritCount + 1
        }
      })
    }
  }
}

本文完整源码查看funny-widget

相关推荐
richard_yuu2 小时前
鸿蒙心理测评模块实战|PHQ-9/GAD7双量表答题、实时计分与结果本地化存储
华为·harmonyos
不爱吃糖的程序媛5 小时前
2026年Electron 鸿蒙PC环境搭建指南
人工智能·华为·harmonyos
nashane5 小时前
HarmonyOS 6学习:长截图功能开发中的滚动拼接与权限处理实战
人工智能·华为·harmonyos
大师兄66686 小时前
从零开发一个 HarmonyOS 输入法——KikaInputMethod 完整拆解
harmonyos·服务卡片·harmonyos6·formkit
Python私教11 小时前
鸿蒙 NEXT 也能接 MCP?用 ArkTS 跑通 AI Agent 工具链
人工智能·华为·harmonyos
Swift社区14 小时前
分布式能力在鸿蒙 PC 上到底怎么用?
分布式·华为·harmonyos
nashane1 天前
HarmonyOS 6学习:外接键盘CapsLock与长截图功能的实战调试与完整解决方案
学习·华为·计算机外设·harmonyos
aqi001 天前
一文理清 HarmonyOS 6.0.2 涵盖的十个升级点
android·华为·harmonyos·鸿蒙·harmony
环信即时通讯云1 天前
环信Flutter UIKit适配鸿蒙实战指南
flutter·华为·harmonyos