HarmonyOS鸿蒙Next中如何监听应用前后台切换?

HarmonyOS鸿蒙Next中如何监听应用前后台切换? **问题描述:**视频播放时切到后台需暂停,回到前台继续播放。

5 回复

【背景知识】 要实现视频播放切换到后台暂停,切换到前台时要自动播放的功能,需要在了解Video组件的基础上,还要了解自定义页面的生命周期相关知识:自定义组件的生命周期

【解决方案】 通过对自定义视频播放组件的生命周期管理来实现。

示例代码如下:

@Entry
@Component
struct VideoPlayerPage {
  @State videoUrl: ResourceStr = ''
  @State previewUri: Resource = $r('app.media.preview');
  controller: VideoController = new VideoController();

  // 未硬编码时,需要提前加载视频路径
  aboutToAppear() {
    this.videoUrl = $rawfile('videoTest.mp4') // 初始化视频
  }

  // 页面显示,继续播放
  onPageShow() {
    this.controller.start();
  }

  // 页面隐藏,暂停播放
  onPageHide() {
    this.controller.pause();
  }

  build() {
    Row() {
      Column() {
        Video({
          src: this.videoUrl,
          previewUri: this.previewUri,
          controller: this.controller
        })
          .width('100%')
          .height(600)
          .autoPlay(false)
          .controls(true)
          .loop(false)
          .objectFit(ImageFit.Contain)
      }
      .width('100%')
    }
    .height('100%')
  }
}
  • 跳转回该页面时加载指定路径视频: 一般而言,在页面的aboutToAppear()时期,对视频路径变量this.videoUrl根据传回的参数重新赋值即可。 示例代码如下:
aboutToAppear() {
  // 如果是采用的单例模式跳转,由于不会重新创建页面,建议在该生命时期加载视频路径
  this.videoUrl = $rawfile('videoTest.mp4') // 初始化视频
  this.controller.start();
}

但如果是单例模式跳转,例如Navigation跳转时采用LaunchModePOP_TO_SINGLETON或者MOVE_TO_TOP_SINGLETON模式时,由于页面跳转规则是: 如果栈内已经存在该页面实例,则不会重新创建页面。而是将站内找到的实例页面推送到栈顶显示或则将该页面上的所有页面删除显示,所以该页面不会重复执行aboutToAppear(),只会执行onShown()。 此时若在aboutToAppear()时期接收参数或者更换视频路径,则会获取失败。单例模式下,采用在onShown()内执行相关操作即可。 若是单例模式下获取视频资源则删除上述代码的aboutToAppear()事件,做如下更改即可:

NavDestination() {
}.title('PageOne')
.onShown(() => {
  console.log('执行了PageOne-onShown');
})

【常见FAQ】 Q:Video组件视频播放时只有声音没有图像? A:建议先检查视频格式,是否是Video支持的mp4、mkv、TS格式。若格式没有问题,则需要考虑视频获取方式是否有问题。若以上方式都没问题,那需要检查获取视频的方式是否是在特定的生命周期内获取失败。

更多关于HarmonyOS鸿蒙Next中如何监听应用前后台切换?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


通过使用Emitter模块实现监听应用前后台切换;

【背景知识】

  • UIAbility是包含UI界面的应用组件,当应用首次启动到前台或者从后台转入到前台时,系统触发UIAbility.onForeground回调;当应用从前台转入到后台时,系统触发UIAbility.onBackground回调。
  • Emitter模块提供了在同一进程不同线程间,或同一进程同一线程内,发送和处理事件的能力,包括持续订阅事件、单次订阅事件、取消订阅事件,以及发送事件到事件队列的能力。

【解决方案】

使用Emitter模块实现onForeground方法调用。

  1. 在EntryAbility.ets中的onForeground和onBackground事件中发送指定事件。

    onForeground(): void {
      // Ability has brought to foreground
      emitter.emit('onForegroundEvent')
      hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
    }
    
    onBackground(): void {
      // Ability has back to background
      emitter.emit('onBackgroundEvent')
      hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
    }
    
  2. 在你所需的页面的aboutToAppear中使用emitter.on实现事件持续订阅。

    import { emitter } from '@kit.BasicServicesKit';
    
    @Entry
    @Component
    struct VideoPage {
      @State status: string = '处于前台状态';
    
      aboutToAppear(): void {
        let onForegroundCallback: Callback<emitter.EventData> = (eventData: emitter.EventData) => {
          console.log('onForeground called')
          this.status = '处于前台状态'
          // 播放视频代码
        }
        // 收到eventId为"onForegroundEvent"的事件后执行回调函数
        emitter.on(`onForegroundEvent`, onForegroundCallback);
    
        let onBackgroundCallback: Callback<emitter.EventData> = (eventData: emitter.EventData) => {
          console.log('onBackground called')
          this.status = '处于后台状态'
          // 暂停视频代码
        }
        // 收到eventId为"onBackgroundEvent"的事件后执行回调函数
        emitter.on(`onBackgroundEvent`, onBackgroundCallback);
      }
    
      aboutToDisappear(): void {
        emitter.off(`onForegroundEvent`);
        emitter.off(`onBackgroundEvent`);
      }
    
      build() {
        Column() {
          Text(this.status)
            .fontSize($r('app.float.page_text_font_size'))
            .fontWeight(FontWeight.Bold)
        }
        .height('100%')
        .width('100%')
      }
    }
    

**详细回答:**重写 UIAbility 的 onForeground/onBackground 生命周期。

✅ 正确做法

// EntryAbility.ts
export default class EntryAbility extends UIAbility {
 onForeground() {
   // 通知页面恢复播放
   AppStorage.SetOrCreate('appState', 'foreground');
 }

 onBackground() {
   // 通知页面暂停播放
    AppStorage.SetOrCreate('appState', 'background');
  }
}

自定义VideoPlayer.ets

import { StandardTitleBar } from '../components/StandardTitleBar';

/**
 * @author J.query
 * @date 2025/12/24 8:43
 * @email j-query@foxmail.com
 Description:
 */

@Entry
@Component
struct VideoPlayer {
  @State [@Watch](/user/Watch)('onAppStateChange')
  appState: string = AppStorage.Get<string>('appState') || 'foreground';

  private controller: VideoController = new VideoController();
  private videoSrc: string = ''; // 初始化为空字符串,稍后设置为网络视频或应用沙箱路径
  private previewUri: string = ''; // 初始化为空字符串
  private isPlaying: boolean = false;
  private currentTime: number = 0;
  private duration: number = 0;
  private volume: number = 0.8;
  private isMute: boolean = false;
  private speedOptions: string[] = ['0.75x', '1.0x', '1.25x', '1.5x', '2.0x'];

  aboutToAppear() {
    // 页面即将出现时的初始化
    this.isPlaying = false;
    // 设置默认视频源,可以是网络视频或应用沙箱路径
    this.videoSrc = 'https://example.com/sample.mp4'; // 示例网络视频,实际使用时替换为真实URL
  }

  resumeVideo() {
    if (this.controller) {
      this.controller.start();
      this.isPlaying = true;
    }
  }

  pauseVideo() {
    if (this.controller) {
      this.controller.pause();
      this.isPlaying = false;
    }
  }

  build() {
    Column() {
      // 标题栏
      StandardTitleBar({
        title: '视频播放器',
        showBack: true
      })

      // 视频播放区域
      Column() {
        // 视频组件
        Video({
          src: this.videoSrc,
          previewUri: this.previewUri,
          controller: this.controller
        })
        .width('100%')
        .height(300)
        .autoPlay(false)
        .controls(true) // 显示默认控制栏
        .onPrepared((preparedInfo: PreparedInfo) => {
          this.duration = preparedInfo.duration;
        })
        .onUpdate((playbackInfo: PlaybackInfo) => {
          this.currentTime = playbackInfo.time;
        })
        .onFinish(() => {
          this.isPlaying = false;
        })
        .onError(() => {
          console.error('视频播放出错');
        })
      }
      .width('100%')
      .padding({ top: 10, bottom: 10 })
      .backgroundColor('#000000')

      // 控制面板
      Column() {
        // 播放/暂停按钮
        Row() {
          Button(this.isPlaying ? '⏸️ 暂停' : '▶️ 播放')
            .width('40%')
            .height(50)
            .backgroundColor('#45B7D1')
            .fontColor('#FFFFFF')
            .borderRadius(8)
            .onClick(() => {
              if (this.isPlaying) {
                this.pauseVideo();
              } else {
                this.resumeVideo();
              }
            })

          // 停止按钮
          Button('⏹️ 停止')
            .width('40%')
            .height(50)
            .backgroundColor('#FF6B6B')
            .fontColor('#FFFFFF')
            .borderRadius(8)
            .margin({ left: '5%' })
            .onClick(() => {
              if (this.controller) {
                this.controller.stop();
                this.isPlaying = false;
              }
            })
        }
        .width('100%')
        .margin({ bottom: 15 })

        // 进度条
        Column() {
          Text(`${this.formatTime(this.currentTime)} / ${this.formatTime(this.duration)}`)
            .fontSize(12)
            .fontColor('#666666')
            .width('100%')
            .textAlign(TextAlign.Start)

          Slider({
            value: this.currentTime,
            min: 0,
            max: this.duration > 0 ? this.duration : 100,
            step: 1
          })
          .width('100%')
          .showSteps(false)
          .onChange((value: number) => {
            this.currentTime = value;
            if (this.controller) {
              this.controller.setCurrentTime(value);
            }
          })
        }
        .width('100%')
        .margin({ bottom: 15 })

        // 音量控制
        Row() {
          Text('🔈')
            .fontSize(20)
            .width('10%')

          Slider({
            value: this.isMute ? 0 : this.volume * 100,
            min: 0,
            max: 100,
            step: 1
          })
          .width('80%')
          .showSteps(false)
          .onChange((value: number) => {
            this.volume = value / 100;
            this.isMute = (value === 0);
            // HarmonyOS VideoController 没有直接的 volume 属性
            // 可以通过其他方式实现音量控制,这里暂时保留变量
          })

          Text(this.isMute ? '🔇' : '🔊')
            .fontSize(20)
            .width('10%')
            .textAlign(TextAlign.Center)
        }
        .width('100%')
        .margin({ bottom: 15 })

        // 倍速播放
        Row() {
          Text('播放倍速:')
            .fontSize(14)
            .width('30%')
            .textAlign(TextAlign.Start)

          Row() {
            ForEach(this.speedOptions, (speed: string) => {
              Button(speed)
                .width(60)
                .height(35)
                .backgroundColor(this.getCurrentSpeed() === parseFloat(speed) ? '#45B7D1' : '#F0F0F0')
                .fontColor(this.getCurrentSpeed() === parseFloat(speed) ? '#FFFFFF' : '#333333')
                .borderRadius(6)
                .fontSize(12)
                .margin({ right: 10 })
                .onClick(() => {
                  this.setPlaybackSpeed(parseFloat(speed));
                })
            })
          }
          .width('70%')
          .justifyContent(FlexAlign.Start)
        }
        .width('100%')
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  // 格式化时间显示 (毫秒转换为 mm:ss)
  formatTime(time: number): string {
    const totalSeconds = Math.floor(time / 1000);
    const minutes = Math.floor(totalSeconds / 60);
    const seconds = totalSeconds % 60;
    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  }

  // 获取当前播放倍速
  getCurrentSpeed(): number {
    // 这里需要根据实际控制器状态获取当前播放速度
    return 1.0; // 默认值,实际实现中需要从控制器获取
  }

  // 设置播放倍速
  setPlaybackSpeed(speed: number) {
    if (this.controller) {
    }
  }

  onAppStateChange(changedPropertyName: string) {
    if (this.appState === 'background') {
      this.pauseVideo();
    } else {
      this.resumeVideo();
    }
  }
}

cke_4677.png

⚠️ 避坑指南

📡 onBackground 中不能执行耗时操作(系统 10 秒内杀进程)。

🔄 页面需用 @Watch 监听 AppStorage 变化。

🎵 后台播放音频需申请 ohos.permission.RUNNING_LOCK。

🎯 效果

应用切后台自动暂停视频,切回前台无缝续播,符合用户预期。

在HarmonyOS Next中,监听应用前后台切换可使用UIAbilityonWindowStageCreateonWindowStageDestroy生命周期回调。
应用进入前台时触发onWindowStageCreate,进入后台时触发onWindowStageDestroy
也可通过@State装饰器结合AppStorage中的AppState来管理应用状态。
具体实现需在UIAbility文件中定义这些回调方法。

在HarmonyOS Next中,监听应用前后台切换,可以通过订阅UIAbility的生命周期回调或使用windowStage的事件来实现。以下是针对你视频播放场景的两种核心方法:

方法一:在UIAbility中监听生命周期(推荐)

在承载页面的UIAbility(例如EntryAbility)中,重写onWindowStageCreateonWindowStageDestroy等方法,它们对应窗口的前后台状态。

// EntryAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';

export default class EntryAbility extends UIAbility {
  // 当窗口创建(应用进入前台)
  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 窗口已创建,应用进入前台
    // 在这里触发视频播放逻辑
    console.info('App进入前台 - 播放视频');
    // 例如:向页面发送事件或调用播放方法
  }

  // 当窗口销毁(应用退到后台)
  onWindowStageDestroy(): void {
    // 窗口已销毁,应用进入后台
    // 在这里触发视频暂停逻辑
    console.info('App进入后台 - 暂停视频');
    // 例如:向页面发送事件或调用暂停方法
  }
}

方法二:在页面中通过windowStage监听

如果需要在具体页面(如视频播放页)中更精细地控制,可以获取windowStage并订阅其事件。

// VideoPlayerPage.ets
import window from '@ohos.window';
import common from '@ohos.app.ability.common';

@Entry
@Component
struct VideoPlayerPage {
  // 获取UIAbility上下文
  private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
  private windowStage: window.WindowStage | null = null;

  aboutToAppear(): void {
    // 获取windowStage并监听
    this.windowStage = window.getLastWindowStage(this.context);
    // 可以通过windowStage的状态或事件来判断前后台,但更直接的方式是结合UIAbility生命周期。
    // 通常,页面通过AppStorage或Emitter接收UIAbility生命周期变化的事件来操作视频。
  }

  // 视频播放组件
  build() {
    // 你的视频播放组件
  }
}

实际场景实现建议

对于“后台暂停,前台播放”的需求,通常的架构是:

  1. 前后台状态管理:在UIAbility生命周期回调中,通过AppStorageEmitter全局事件机制,通知页面状态变化。
  2. 视频控制:在视频播放页面监听该全局状态,并执行播放/暂停。

例如,在UIAbility中:

// 使用AppStorage存储前后台状态
AppStorage.SetOrCreate<boolean>('isAppInForeground', true);

onWindowStageCreate() {
  AppStorage.Set('isAppInForeground', true);
}
onWindowStageDestroy() {
  AppStorage.Set('isAppInForeground', false);
}

在页面中:

// 监听AppStorage状态变化
@StorageLink('isAppInForeground') isForeground: boolean = true;

// 观察状态变化并控制视频
watch('isForeground', (newValue) => {
  if (newValue) {
    // 播放视频
  } else {
    // 暂停视频
  }
});

注意事项

  • 权限:不需要额外权限。
  • 多窗口场景:如果应用支持多窗口,需要根据业务细化逻辑,上述方法监听的是整个应用的前后台。
  • 精准控制:对于视频播放,建议结合onHide/onShow等页面生命周期作为补充,确保及时暂停/播放。

这种方法能可靠地实现应用前后台切换的监听,并适用于你的视频播放场景。

回到顶部