HarmonyOS鸿蒙Next中求教:全屏模式下的自适应问题

HarmonyOS鸿蒙Next中求教:全屏模式下的自适应问题 在开发视频播放应用时,通过使用 Stack 布局包裹 Video组件和自定义控制栏实现了自定义控制栏(包含全屏按钮),当点击全屏按钮后出现以下两个问题:

  1. 调用 requestFullScreen()方法成功进入全屏,但自定义控制栏消失
  2. 手动切换设备横竖屏时,播放器状态(如进度、播放状态)被重置。

求教:

如何在全屏模式下保留自定义控制栏?横竖屏切换时如何保持播放状态?如何实现横屏/竖屏自适应全屏(根据视频宽高比动态切换)?

5 回复

不要使用requestFullScreen() 有更好的解决方案!

  1. 通过setPreferredOrientation方法设置窗口方向旋转模式为跟随传感器自动旋转。
  2. 监听横竖屏变化,获取设备当前定向。
aboutToAppear() {
  setOrientation(this.context);
  // 监听横竖屏变化
  this.windowClass = AppStorage.get('windowClass');
  this.windowClass?.on('windowSizeChange', () => {
    const DEFAULT_DISPLAY = display.getDefaultDisplaySync();
    this.orientation = DEFAULT_DISPLAY.orientation;
  });
}
export function setOrientation(context: UIContext) {
  try {
    window.getLastWindow(context.getHostContext(), (err, data) => { // 获取window实例
      if (err.code) {
        return;
      }
      let windowClass = data;
      let orientation = window.Orientation.AUTO_ROTATION_UNSPECIFIED; // 设置窗口方向旋转模式
      try {
        windowClass.setPreferredOrientation(orientation, (err) => {
          if (err.code) {
            return;
          }
        });
      } catch (exception) {
        // ...
      }
    });
  } catch (exception) {
    // ...
  }
}

cke_1008.gif

看这个代码:https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtyPub/011/111/111/0000000000011111111.20251027190854.94844233983151546040957953024698:50001231000000:2800:779C99B723FCD925C40F6F3EDCBE3A6096E6F801240C6461C0DDEF0C09038B65.zip?needInitFileName=true

还有一个官方项目:https://gitcode.com/HarmonyOS_Samples/avplayer-long-video

cke_12938.png

更多关于HarmonyOS鸿蒙Next中求教:全屏模式下的自适应问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


你说的对,试了有效,

开发者您好,针对您点击全屏按钮后出现两个问题,请参考如下:

  • 问题1:调用 requestFullScreen()方法成功进入全屏,但自定义控制栏消失。原因:Video组件自带的全屏功能仅将视频内容设为全屏,显示默认控制器,无法显示自定义标题或控制器。如需其他功能,用户需自行实现全屏功能。具体可参考官网文档:requestFullscreen说明。
  • 问题2: 手动切换设备横竖屏时,播放器状态(如进度、播放状态)被重置。我这边使用Mate X5 API 21未能复现您的问题,您方便的话麻烦提供下您的复现demo还有复现设备。下面是我的复现代码,注意视频资源替换为您的资源文件:
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';

const CONVERSION = 60;

@Entry
@Component
struct Index {
  @State orientation: number = 0;
  @State isPlaying: boolean = false;
  context: common.UIAbilityContext = this.getUIContext()?.getHostContext() as common.UIAbilityContext;
  windowClass: window.Window = (this.context as common.UIAbilityContext).windowStage.getMainWindowSync();
  @State curTime: number = 0;
  @State videoDuration: number = 0;
  private videoController: VideoController = new VideoController();
  @State isFullScreen: boolean = false;

  onPageHide(): void {
    this.isPlaying = false;
  }

  // 视频时间格式化
  timeFormat(time: number) {

    let hoursStr = Math.floor(time / (CONVERSION * CONVERSION)).toString();
    let minutesStr = Math.floor(time / CONVERSION).toString();
    let secondsStr = Math.floor(time % CONVERSION).toString();

    if (minutesStr.length === 1) {
      minutesStr = '0' + minutesStr;
    }
    if (secondsStr.length === 1) {
      secondsStr = '0' + secondsStr;
    }
    return `${hoursStr}:${minutesStr}:${secondsStr}`;
  }

  build() {
    Column() {
      Stack({ alignContent: Alignment.Bottom }) {
        Video({
          src: $rawfile('test.mp4'), // 请替换为您的视频文件
          controller: this.videoController
        })
          .controls(false)
          .autoPlay(true)
          .width('100%')
          .height('100%')
          .onPrepared((event) => {
            this.videoDuration = event.duration;
          })
          .onStart(() => {
            if (!this.isPlaying) {
              this.videoController.pause();
            }
          })
          .onUpdate((event) => {
            this.curTime = event.time;
          })
          .onFinish(() => {
            this.isPlaying = false;
          });
        // 控制栏
        Row() {
          Image(this.isPlaying ? $r('app.media.ic_pause') : $r('app.media.ic_play'))
            .width(24)
            .height(24)
            .onClick(() => {
              if (this.isPlaying) {
                this.videoController.pause();
                this.isPlaying = false;
              } else {
                this.videoController.start();
                this.isPlaying = true;
              }
            });
          Text(this.timeFormat(this.curTime))
            .fontSize(10)
            .fontColor($r('sys.color.white'))
            .margin({
              left: 12,
              right: 8
            });
          Slider({
            value: this.curTime,
            step: 1,
            min: 0,
            max: this.videoDuration,
            style: SliderStyle.OutSet
          })
            .layoutWeight(1)
            .trackThickness(5)
            .blockColor(Color.White)
            .blockSize({
              width: 12,
              height: 12
            })
            .trackColor($r('app.color.slider_track'))
            .selectedColor($r('app.color.slider_selected'))
            .onChange((value, mode) => {
              this.curTime = value;
              if (mode === SliderChangeMode.End) {
                this.videoController.setCurrentTime(value);
              }
            });
          Text(this.timeFormat(this.videoDuration))
            .fontSize(10)
            .fontColor($r('sys.color.white'))
            .margin({
              right: 12,
              left: 8
            });
          Image($r('app.media.ic_full'))
            .width(24)
            .height(24)
            .onClick(() => {
              if (!this.windowClass) {
                hilog.error(0x0000, 'testTag', 'this.windowClass is und');
                return;
              }
              if (!this.isFullScreen) {
                this.windowClass.setPreferredOrientation(window.Orientation.LANDSCAPE);
              } else {
                this.windowClass.setPreferredOrientation(window.Orientation.PORTRAIT);
              }
              this.isFullScreen = !this.isFullScreen;
            });
        }
        .padding({
          left: 12,
          right: 12,
          top: 8,
          bottom: 8
        })
        .backgroundColor($r('app.color.video_controller_background'));
      }
      .width('100%')
      .height('100%');
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('sys.color.white'))
    .padding({
      top: 16,
      left: 12,
      right: 12,
      bottom: 16
    });
  }
}

根据您描述中期望解决的问题,

  1. 如何在全屏模式下保留自定义控制栏:不使用requestFullScreen()方法成功进入全屏,原因请参考上面问题一解答。通过setPreferredOrientation方法设置窗口方向旋转模式为跟随传感器自动旋转,监听横竖屏变化,获取设备当前定向,具体可参考官网文档:视频窗口横竖屏切换。或者参考上面代码中切换横竖屏方法,实现全屏。
  2. 横竖屏切换时如何保持播放状态:请您方便的话麻烦提供下您的复现demo,或者参考上面代码修改您的播放逻辑。
  3. 如何实现横屏/竖屏自适应全屏(根据视频宽高比动态切换):通用属性onAreaChangeonSizeChange均可以监听组件的尺寸变化,并执行回调中的动作。Video的画面填充模式可以通过设置不同的objectFit属性值改变。通过onAreaChange获取外部容器的高度。Video组件设置objectFit属性为ImageFit.Contain,使画面内容保持宽高比,在边界内完全显示;同时设置组件的高度为随外部组件高度动态变化。

鸿蒙Next全屏模式下自适应可通过以下方式实现:

  1. 使用preferredOrientation配置应用支持的屏幕方向
  2. 通过displayCutout处理刘海屏适配
  3. 利用windowSizeClass响应式布局方案
  4. 采用GridRow/GridCol栅格系统适配不同屏幕尺寸
  5. 使用MediaQuery获取设备屏幕信息进行动态布局

关键API包括Window模块的getTopWindow()获取窗口属性,配合display模块的屏幕信息查询。建议使用自适应布局能力替代固定尺寸值。

针对你提到的两个问题,解决方案如下:

1. 全屏模式下保留自定义控制栏 自定义控制栏消失,是因为requestFullScreen()默认作用于调用它的组件及其子节点。当Video组件单独进入全屏时,外层的Stack布局和控制栏会被排除在外。 解决方法:将需要全屏显示的整个容器(包含Video和控制栏的Stack布局)作为requestFullScreen()的调用者。确保全屏请求作用于整个播放器容器,而非单独的Video组件。

2. 横竖屏切换时保持播放状态 状态重置是因为屏幕方向变化时,系统默认会重建UI(如销毁并重新创建Ability)。这会导致播放器组件被重新初始化。 解决方法

  • 状态持久化:在方向变化前(例如监听onConfigurationUpdate回调),将当前播放进度、播放状态等关键数据保存到AppStorage或本地临时变量中。
  • 状态恢复:在UI重新构建后(例如在aboutToAppear生命周期中),从存储中读取并恢复这些状态,重新设置给Video组件。
  • 避免重建:在module.json5中对应Ability的configChanges字段里添加"orientation"等配置,声明由应用自身处理屏幕方向变化,从而避免系统重建UI。这通常是首选的解决方案。

3. 实现横屏/竖屏自适应全屏 这通常不是通过简单的“全屏”API实现,而是需要根据视频宽高比和当前屏幕方向,动态调整播放器容器的尺寸和布局。 核心思路

  • 计算与判断:获取视频的原始宽高比(aspect ratio)。同时,通过媒体查询(@ohos.mediaquery)或窗口管理器(@ohos.window)获取当前窗口的实际尺寸和方向。
  • 动态布局:比较视频宽高比与当前屏幕区域的宽高比。
    • 若视频更“宽”(如16:9的电影在9:18.5的竖屏手机上),则采用“竖屏适配模式”:播放器高度撑满屏幕,宽度按比例计算,左右留黑边(或显示其他内容)。
    • 若视频更“高”或与屏幕比例匹配,则采用“横屏适配模式”:播放器宽度撑满屏幕,高度按比例计算,上下留黑边。
  • 这种“自适应”通常是在一个固定的全窗口容器内,通过动态计算并设置Video组件的widthheightobjectFit属性来实现,而不是调用系统级的requestFullScreen()。真正的“全屏”体验可以通过隐藏系统状态栏和导航栏来增强。

总结建议: 对于视频播放器场景,实现“沉浸式自适应播放”的常见做法是:

  1. 使用窗口管理器设置全窗口布局(隐藏状态栏/导航栏)。
  2. 通过configChanges接管屏幕方向变化,避免UI重建。
  3. 自行监听方向传感器或窗口尺寸变化,根据视频比例动态计算并应用播放器视图的尺寸和位置。
  4. 将播放状态、进度等数据与UI组件生命周期分离,进行持久化管理,确保在任何界面变化下都能正确恢复。

这样既能获得类似全屏的沉浸体验,又能完全掌控控制栏的显示和播放状态的连续性。

回到顶部