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

本文完整源码查看funny-widget

简介

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

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

涉及知识点

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

元服务开发

创建项目

根据官方文档创建工程
必须要注册华为开发者账号。

开发电子木鱼页面

新建ElectronicWoodenFishPage页面。

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

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组件中添加木鱼图片布局。

@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”上浮动画文字和木鱼放大动画。

@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页面点击回调,增加音效播放。

@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。

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页面,增加数据存取逻辑。

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

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐