🎉

HarmonyOS運動開発:ユーザーの運動歩数データをどのようにリスニングするか

に公開

HarmonyOS運動開発:ユーザーの運動歩数データをどのようにリスニングするか

前言

運動系アプリを開発する際、ユーザーの運動歩数を正確にリスニングし、記録することは重要な機能です。HarmonyOSは強力なセンサー・フレームワークを提供しており、開発者が簡単にデバイスの運動データを取得することができます。本稿では、HarmonyOSアプリで歩数リスニング機能を実装する方法を詳しく探り、開発プロセスで得た経験やノウハウを共有し、この機能をより良い理解と実装を助けることを目的としています。

  1. HarmonyOSセンサー・フレームワークを理解する

HarmonyOSは多種多様なセンサーを提供しており、その中でもPEDOMETER(ペダルメーター)センサーは、ユーザーの運動歩数を取得するためのコア・センサーです。このセンサーは、デバイス起動後の累積歩数を返すものであり、増分歩数ではありません。つまり、アプリのロジックで歩数の初期値と増分計算を処理する必要があります。

  1. 核心代码実装

2.1 センサーの初期化

始めに、必要な権限を申請し、センサーを初期化する必要があります。以下がセンサーの初期化コードです:

import { sensor } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
import { common } from '@kit.AbilityKit';

export class StepCounterService {
  private static instance: StepCounterService;
  private stepCount: number = 0; // 現在の累積歩数
  private initialStepCount: number = 0; // 初期歩数(デバイス起動後の累積歩数)
  private isMonitoring: boolean = false; // リスニング中かどうか
  private isPaused: boolean = false; // 一時停止中かどうか
  private listeners: Array<(steps: number) => void> = [];
  private context: common.UIAbilityContext;
  private pausedIntervals: Array<PauseCounter> = [];
  private deviceTotalSteps: number = 0;

  private constructor(context: common.UIAbilityContext) {
    this.context = context;
  }

  public static getInstance(context: common.UIAbilityContext): StepCounterService {
    if (!StepCounterService.instance) {
      StepCounterService.instance = new StepCounterService(context);
    }
    return StepCounterService.instance;
  }

  private async requestMotionPermission(): Promise<boolean> {
    const atManager = abilityAccessCtrl.createAtManager();
    try {
      const result = await atManager.requestPermissionsFromUser(
        this.context,
        ['ohos.permission.ACTIVITY_MOTION']
      );
      return result.permissions[0] === 'ohos.permission.ACTIVITY_MOTION' &&
        result.authResults[0] === 0;
    } catch (err) {
      console.error('権限申請失敗:', err);
      return false;
    }
  }

  public async startStepCounter(): Promise<void> {
    if (this.isMonitoring) return;

    const hasPermission = await this.requestMotionPermission();
    if (!hasPermission) {
      throw new Error('運動センサーの権限が与えられていません');
    }

    try {
      sensor.on(sensor.SensorId.PEDOMETER, (data: sensor.PedometerResponse) => {
        this.deviceTotalSteps = data.steps;

        if (this.initialStepCount === 0) {
          this.initialStepCount = data.steps;
        }

        const deltaSteps = this.deviceTotalSteps - this.initialStepCount;

        if (this.isPaused) {
          // 一時停止中はデバイスの総歩数のみ更新し、ビジネス歩数は変更しない
        } else {
          // すべての一時停止区間の総歩数を計算する
          const totalPausedSteps = this.pausedIntervals.reduce((sum, interval) => {
            return sum + (interval.end - interval.start);
          }, 0);

          this.stepCount = deltaSteps - totalPausedSteps;
          this.notifyListeners();
        }
      });

      this.isMonitoring = true;
    } catch (error) {
      const e = error as BusinessError;
      console.error(`ペダルメーターの購読に失敗しました: Code=${e.code}, Message=${e.message}`);
      throw error as Error;
    }
  }
}

2.2 一時停止と再開機能

運動中、ユーザーは運動を一時停止したり、再開したりする必要があるかもしれません。このような状況に対処するためには、一時停止と再開時の歩数を記録し、再開時に一時停止期間の歩数増分を計算する必要があります。以下が一時停止と再開機能の実装です:

public pauseStepCounter(): void {
  if (!this.isMonitoring || this.isPaused) return;
  this.pausedIntervals.push({
    start: this.deviceTotalSteps,
    end: 0
  });
  this.isPaused = true;
}

public resumeStepCounter(): void {
  if (!this.isMonitoring || !this.isPaused) return;

  const lastInterval = this.pausedIntervals[this.pausedIntervals.length - 1];
  lastInterval.end = this.deviceTotalSteps;

  this.isPaused = false;
}

2.3 リスニングの停止

ユーザーが運動を終了した後は、歩数のリスニングを停止し、関連するリソースをクリーンアップする必要があります。

public stopStepCounter(): void {
  if (!this.isMonitoring) return;

  this.stepCount = 0;
  this.initialStepCount = 0;
  this.isPaused = false;
  this.pausedIntervals = []; // すべての一時停止記録をクリアする
  try {
    sensor.off(sensor.SensorId.PEDOMETER);
    this.isMonitoring = false;
  } catch (error) {
    const e = error as BusinessError;
    console.error(`ペダルメーターの購読をキャンセルするに失敗しました: Code=${e.code}, Message=${e.message}`);
  }
}

3. リスナーに通知する

インターフェースがリアルタイムで歩数を更新できるように、歩数が変化したときにリスナーに通知する必要があります。通知が頻繁すぎないよう、デバウンスメカニズムを使用することができます。

```typescript
private debounceTimer?: number;

private notifyListeners(): void {
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
  this.debounceTimer = setTimeout(() => {
    this.listeners.forEach(listener => listener(this.stepCount));
  }, 500); // 500msごとに通知する
}
  1. 歩数をリセットする

ユーザーが歩数をリセットする必要がある場合があります。以下が歩数をリセットする実装です。

public resetStepCounter(): void {
  this.initialStepCount = this.deviceTotalSteps;
  this.pausedIntervals = [];
  this.stepCount = 0;
}
  1. 使用例

以下は、ページでStepCounterServiceを使用する方法の例です。


@Component
export struct KeepRunningPage {
  @State isRunning: boolean = false;
  @State stepCount: number = 0;

  private stepCounterService?: StepCounterService;

  build() {
    Column() {
      Text('ランニング・ペダルメーター')
        .fontSize(24)
        .textAlign(TextAlign.Center)
        .margin({ top: 20 })

      Text(`歩数: ${this.stepCount}`)
        .fontSize(18)
        .textAlign(TextAlign.Center)
        .margin({ top: 20 })

      Button('ランニングを開始')
        .onClick(() => this.startRunning())
        .margin({ top: 20 })

      Button('ランニングを一時停止')
        .onClick(() => this.pauseRunning())
        .margin({ top: 20 })

      Button('ランニングを再開')
        .onClick(() => this.resumeRunning())
        .margin({ top: 20 })

      Button('ランニングを停止')
        .onClick(() => this.stopRunning())
        .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }

  aboutToAppear(): void {
    this.stepCounterService = StepCounterService.getInstance(Application.getInstance().uiAbilityContext);
    this.stepCounterService.addStepListener((steps) => {
      this.stepCount = steps;
    });
  }

  aboutToDisappear(): void {
    this.stepCounterService!.removeStepListener((steps) => {
      this.stepCount = steps;
    });
  }

  async startRunning(): Promise<void> {
    if (!this.isRunning) {
      this.isRunning = true;
      await this.stepCounterService!.startStepCounter();
    }
  }

  pauseRunning(): void {
    if (this.isRunning) {
      this.stepCounterService!.pauseStepCounter();
    }
  }

  resumeRunning(): void {
    if (this.isRunning) {
      this.stepCounterService!.resumeStepCounter();
    }
  }

  stopRunning(): void {
    if (this.isRunning) {
      this.isRunning = false;
      this.stepCounterService!.stopStepCounter();
    }
  }
}
  1. 核心点の整理

6.1 権限申請

センサーを使用する前に、ohos.permission.ACTIVITY_MOTION権限を申請する必要があります。これはabilityAccessCtrl.createAtManagerを使用して実現できます。

6.2 初期歩数の処理

PEDOMETERセンサーは、デバイス起動後の累積歩数を返します。したがって、初めてデータを取得するときに初期歩数を記録する必要があります。

6.3 一時停止と再開のメカニズム

ユーザーが一時停止や再開の操作を行う可能性があるため、pausedIntervals配列を導入して、一時停止の開始と終了の歩数を記録します。この方法により、再開時に正確に一時停止期間の歩数増分を計算し、総歩数からその分を差し引くことができます。

6.4 デバウンスメカニズム

リスナーに通知する回数が多すぎることによるパフォーマンス問題を避けるために、notifyListenersメソッドにデバウンスメカニズムを導入しました。タイマーを設定することで、一定の時間間隔(たとえば500ms)以内にリスナーに一度だけ通知するようにし、不要な更新を減らします。

6.5 リセット機能

ユーザーが歩数カウンターをリセットする必要がある状況があります。resetStepCounterメソッドを使用することで、初期歩数を現在のデバイスの総歩数に設定し、すべての一時停止記録をクリアすることで、歩数をリセットできます。

  1. 開発上の注意点

7.1 権限管理

実際のアプリケーションでは、権限申請は無視できない要素です。ユーザーが権限申請を拒否した場合、アプリはユーザーにフレンドリーなメッセージを表示し、権限を再度申請するオプションを提供する必要があります。

7.2 リソース管理

センサーのリスニングは継続的なプロセスであり、適切に管理しないとリソースリークが発生する可能性があります。したがって、リスニングが必要ない場合は、必ずsensor.offメソッドを呼び出してリスニングをキャンセルする必要があります。

7.3 データの正確性

PEDOMETERセンサーは累積歩数を返すため、一時停止と再開の処理では、データの正確性に特に注意する必要があります。一時停止と再開時に歩数増分を正しく記録し、計算することで、データのずれを避ける必要があります。

  1. 結論

HarmonyOSアプリで歩数リスニング機能を実装する方法について詳しく探りました。センサーの初期化と権限申請、一時停止、再開、リスニングの停止の実装について説明しました。これらの内容が、運動系アプリの開発をよりスムーズに行うのに役立つことを願っています。

Discussion