实现横屏分栏模式下视频全屏播放
===============

#### 问题现象

平板在横屏分栏模式下，利用XComponent设置宽高100%只能实现当前页面全屏，无法实现整个页面全屏，如何解决？

#### 背景知识

*   [Navigation组件](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-navigation)的分栏模式由[mode](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-navigation#mode9)属性控制，包括单栏（Stack）、分栏（Split）和自适应（Auto）三个属性。当为折叠屏或平板时，默认分栏显示，在折叠状态或普通手机时可为单栏显示。
*   [XComponent组件](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/napi-xcomponent-guidelines)可以结合[AVPlayer](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/video-playback)播放视频，通过设置XComponent宽高100%以及窗口沉浸式即可实现视频全屏播放。

#### 解决方案

1.  利用Navigation的[navBarWidth](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-navigation#navbarwidth9)属性来控制横屏模式下导航栏的宽度。
    
    Navigation(this.navPathStack) {
      Column() {
        VideoComponent({ source: this.source });
        Button('跳转SubPage')
          .margin({ top: 20 })
          .onClick(() \=\> {
            this.navPathStack.pushPath({ name: 'SubPage' });
          });
      }
      .width('100%')
      .height('100%');
    }
    _//_ _先判断是否是分栏模式，如果不是则代表需要全屏，可根据来源区分是哪个页面全屏_
    .navBarWidth(this.isSplit ? '40%' : this.source \=== 'Index' ? '100%' : '0.00001%')
    .hideToolBar(true)
    .hideTitleBar(true)
    .height('100%')
    .width('100%');
    
2.  利用Button按钮点击来设置是否全屏以及页面来源。
    
    Row() {
      Button('Source=' \+ this.source).width(200);
      Button('全屏').width(100)
        .onClick(() \=\> {
          this.xh \= '100%';
          this.setFullScreen(true);
          AppStorage.set('isSplit', false);
          AppStorage.set('source', this.source);
        });
      Button('退出全屏').width(100)
        .onClick(() \=\> {
          this.xh \= '80%';
          this.setFullScreen(false);
          AppStorage.set('isSplit', true);
        });
    };
    

完整示例参考如下：

1.  在EntryAbility里面声明MainPage，MainPage主页面代码如下：
    
    import { VideoComponent } from './VideoComponent';
    
    @Entry
    @Component
    struct MainPage {
      navPathStack: NavPathStack \= new NavPathStack();
      @StorageLink('isSplit') isSplit: boolean \= true;
      @StorageLink('source') source: string \= 'Index';
    
      aboutToAppear(): void {
        AppStorage.set('isSplit', true);
        AppStorage.set('source', 'Index');
      }
    
      build() {
        Navigation(this.navPathStack) {
          Column() {
            VideoComponent({ source: this.source });
            Button('跳转SubPage')
              .margin({ top: 20 })
              .onClick(() \=\> {
                this.navPathStack.pushPath({ name: 'SubPage' });
              });
          }
          .width('100%')
          .height('100%');
        }
       _ // 先判断是否是分栏模式，如果不是则代表需要全屏，可根据来源区分是哪个页面全屏_
        .navBarWidth(this.isSplit ? '40%' : this.source \=== 'Index' ? '100%' : '0.00001%')
        .hideToolBar(true)
        .hideTitleBar(true)
        .height('100%')
        .width('100%');
      }
    }
    
2.  SubPage子页面：
    
    import { VideoComponent } from './VideoComponent';
    
    @Component
    struct SubPage {
      navPathStack: NavPathStack \= new NavPathStack();
      @StorageLink('isSplit') isSplit: boolean \= true;
      @StorageLink('source') source: string \= 'SubPage';
    
      aboutToAppear(): void {
        AppStorage.set('source', 'SubPage');
      }
    
      build() {
        NavDestination() {
          Column() {
            VideoComponent({ source: this.source });
            Button('跳转主页')
              .margin({ top: 20 })
              .onClick(() \=\> {
                this.navPathStack.clear();
              });
          }
          .height('100%')
          .width('100%');
        }.onReady((ctx: NavDestinationContext) \=\> {
          this.navPathStack \= ctx.pathStack;
        })
        .height('100%')
        .width('100%');
      }
    }
    
    @Builder
    export function SubPageBuilder() {
      SubPage();
    }
    
3.  VideoComponent播放器页面：
    
    import { media } from '@kit.MediaKit';
    import { common } from '@kit.AbilityKit';
    import { window } from '@kit.ArkUI';
    import { resourceManager } from '@kit.LocalizationKit';
    
    class AVPlayerManager {
      private avPlayer?: media.AVPlayer;
      private surfaceId: string \= '';
      private context: Context;
    
      constructor(context: Context) {
        this.context \= context;
      }
    
      async init() {
        try {
          this.avPlayer \= await media.createAVPlayer();
        } catch (err) {
          console.error(\`create avplayer failed: ${err}\`);
          return;
        }
        this.setEventListening();
      }
    
      setSurfaceId(surfaceId: string) {
        this.surfaceId \= surfaceId;
      }
    
      async setMediaAssetRF(fileName: string) {
        if (this.avPlayer \=== undefined) {
          console.error(\`set media asset, avplayer is undefined\`);
          return;
        }
        let resourceMgr: resourceManager.ResourceManager \= this.context.resourceManager;
        let videoFD: resourceManager.RawFileDescriptor | undefined \= undefined;
        try {
          videoFD \= await resourceMgr.getRawFd(fileName);
        } catch (err) {
          console.error(\`get raw file description failed: ${err}}\`);
          return;
        }
        let avFD: media.AVFileDescriptor \= {
          fd: videoFD.fd,
          offset: videoFD.offset,
          length: videoFD.length,
        };
        this.avPlayer.fdSrc \= avFD;
      }
    
      async release() {
        if (this.avPlayer !== undefined) {
          try {
            await this.avPlayer.release();
            this.avPlayer \= undefined;
          } catch (err) {
            console.error(\`failed to invoke avplayer release, error is ${err}\`);
          }
        }
      }
    
      private setEventListening() {
        this.avPlayer?.on('stateChange', async (state: media.AVPlayerState) \=\> {
          console.info(\`AVPlayer state change to ${state}\`);
          if (this.avPlayer \=== undefined) {
            console.error('AVPlayer is undefined when state change');
            return;
          }
          switch (state) {
            case 'idle':
              break;
            case 'initialized':
              this.avPlayer.surfaceId \= this.surfaceId;
              try {
                await this.avPlayer.prepare();
              } catch (err) {
                console.error(\`failed to invoke avplayer prepare, error is ${err}\`);
              }
              break;
            case 'prepared':
              try {
                let trackList \= await this.avPlayer.getTrackDescription();
                console.info(\`Tracks Info: ${JSON.stringify(trackList)}\`);
                await this.avPlayer.play();
              } catch (err) {
                console.error(\`failed to invoke avplayer play, error is ${err}\`);
              }
              break;
            case 'playing':
              break;
            case 'completed':
              await this.release();
              break;
            case 'paused':
              break;
            case 'stopped':
              break;
            case 'released':
              break;
            case 'error':
              await this.release();
              break;
            default:
              console.info(\`AVPlayer change to unknown state: ${state}\`);
              break;
          }
        });
      };
    }
    
    @Component
    export struct VideoComponent {
      _// 区分页面来源_
      source: string \= '';
      xComponentController: XComponentController \= new XComponentController();
      private avPlayerMgr?: AVPlayerManager;
      @State xh: string \= '80%';
    
      build() {
        Stack({ alignContent: Alignment.BottomEnd }) {
          XComponent({
            type: XComponentType.SURFACE,
            controller: this.xComponentController
          })
            .width('100%')
            .height(this.xh)
            .onLoad(async () \=\> {
              let context: common.UIAbilityContext \= this.getUIContext().getHostContext() as common.UIAbilityContext;
              this.avPlayerMgr \= new AVPlayerManager(context);
              await this.avPlayerMgr.init();
              this.avPlayerMgr!.setSurfaceId(this.xComponentController.getXComponentSurfaceId());
              this.avPlayerMgr!.setMediaAssetRF('video.mp4');
            })
            .align(Alignment.TopStart);
    
          Row() {
            Button('Source=' \+ this.source).width(200);
            Button('全屏').width(100)
              .onClick(() \=\> {
                this.xh \= '100%';
                this.setFullScreen(true);
                AppStorage.set('isSplit', false);
                AppStorage.set('source', this.source);
              });
            Button('退出全屏').width(100)
              .onClick(() \=\> {
                this.xh \= '80%';
                this.setFullScreen(false);
                AppStorage.set('isSplit', true);
              });
          };
        };
      }
    
      _// 设置沉浸式窗口_
      setFullScreen(isLayoutFullScreen: boolean) {
        window.getLastWindow(this.getUIContext().getHostContext()).then((win) \=\> {
          win.setWindowLayoutFullScreen(isLayoutFullScreen);
        }).catch((err: string) \=\> {
          console.info(\`setWindowOrientation： Failed to obtain the top window. Cause: ${JSON.stringify(err)}\`);
        });
      }
    }
    
4.  在src/main目录下的module.json5配置文件中的module字段里配置"routerMap": "$profile:router\_map"，并在src/main/resources/base/profile目录下新增router\_map.json。router\_map.json示例如下：
    
    {
      "routerMap": \[
        {
          "name": "SubPage",
          "pageSourceFile": "src/main/ets/pages/SubPage.ets",
          "buildFunction": "SubPageBuilder",
          "data": {
            "description": "this is SubPage"
          }
        }
      \]
    }