鸿蒙应用框架开发【画中画效果实现】 UI框架

画中画效果实现

介绍

本示例通过@kit.ArkUI、@kit.MediaKit等接口,实现了视频播放、手动和自动拉起画中画、画中画窗口控制视频播放和暂停等功能。

效果预览

1

使用说明

  1. 在主界面,可以点击对应视频按钮进入视频播放页面
  2. 视频播放页面点击开启,应用拉起画中画,点击关闭关闭画中画
  3. 视频播放页面点击自动开启画中画,在返回桌面时会自动拉起画中画
  4. 视频播放页面会显示一些回调信息

具体实现

  • 整个示例用Navigation构建页面,主页面放置五个可点击视频框,点击之后进入视频播放页面。
  • 进入视频播放页面后,有三块区域,最上方的XComponent,中间的画中画控制按钮以及下方的回调信息显示框
  • 点击开启后,应用手动拉起画中画,视频在画中画播放,返回桌面视频依旧画中画播放;点击关闭后,画中画播放的视频返回XComponent播放,同时返回桌面不会拉起画中画。
  • 点击自动拉起画中画后,返回桌面时应用自动拉起画中画,视频画中画播放。
  • 在播放页面进行画中画播放时,XComponent框会提示当前视频正在以画中画播放
  • 回调信息显示框会显示当前状态错误原因以及按钮事件和状态,参考:[VideoPlay.ets]。
/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { PiPWindow } from '@kit.ArkUI';
import { JSON } from '@kit.ArkTS';
import { Constants } from '../constants/Constants';
import { AVPlayer } from './AVPlayer';
import Logger from '../utils/Logger';

const TAG = Constants.NAV_DESTINATION_NAME;

@Extend(Text)
function textType() {
  .padding({ left: $r('app.integer.other_padding') })
  .fontWeight(FontWeight.Bold)
  .fontSize($r('app.integer.text_size'))
  .alignSelf(ItemAlign.Start)
}

@Extend(Text)
function msgType() {
  .padding({ left: $r('app.integer.other_padding') })
  .fontSize($r('app.integer.text_size'))
  .fontColor($r('app.color.Message_color'))
  .alignSelf(ItemAlign.Start)
}

@Component
export struct PlayVideo {
  @Consume('pageInfos') pageInfos: NavPathStack;
  @State curState: string = '';
  @State curError: string = '';
  @State buttonAction: string = '';
  @State isAutoPull: boolean = false;
  @State isLightBackground: boolean = false;
  @State hintMsgVisibility: boolean = false;
  @State pipTypeString: string = '';
  mXComponentController = new XComponentController();
  surfaceId = '';
  navigationId: string = '';
  player?: AVPlayer;
  pipController?: PiPWindow.PiPController;
  eventHub = getContext().eventHub;
  private scrollerForScroll: Scroller = new Scroller()

  aboutToAppear(): void {
    this.eventHub.on('onStateChange', (fg: boolean) => {
      if (fg && this.curState === 'STARTED') {
        this.stopPip();
      }
    });
  }

  async startPip() {
    if (!this.pipController) {
      await this.createPipController();
    }
    if (!this.pipController) {
      Logger.info(`[${TAG}] pipController create error`);
      return;
    }
    await this.pipController.startPiP();
  }

  async stopPip() {
    if (!this.pipController) {
      Logger.info(`[${TAG}] pipController is not exist`);
      return;
    }
    await this.pipController.stopPiP();
  }

  async createPipController() {
    this.pipController = await PiPWindow.create({
      context: getContext(this),
      componentController: this.mXComponentController,
      navigationId: this.navigationId,
      templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY
    });
    this.pipController.on('stateChange', (state: PiPWindow.PiPState, reason: string) => {
      this.onStateChange(state, reason);
    });
    this.pipController.on('controlPanelActionEvent', (event: PiPWindow.PiPActionEventType, status?: number) => {
      this.onActionEvent(event, status);
    });
  }

  destroyPipController() {
    if (!this.pipController) {
      return;
    }
    this.pipController.off('stateChange');
    this.pipController.off('controlPanelActionEvent');
    this.pipController = undefined;
  }

  onStateChange(state: PiPWindow.PiPState, reason: string) {
    switch (state) {
      case PiPWindow.PiPState.ABOUT_TO_START:
        this.curState = 'ABOUT_TO_START';
        this.curError = Constants.ERROR_BY_DEFAULT;
        break;
      case PiPWindow.PiPState.STARTED:
        this.curState = 'STARTED';
        this.curError = Constants.ERROR_BY_DEFAULT;
        break;
      case PiPWindow.PiPState.ABOUT_TO_STOP:
        this.curState = 'ABOUT_TO_STOP';
        this.curError = Constants.ERROR_BY_DEFAULT;
        break;
      case PiPWindow.PiPState.STOPPED:
        this.player?.updatePlayStatus(true);
        this.player?.play();
        this.curState = 'STOPPED';
        this.curError = Constants.ERROR_BY_DEFAULT;
        break;
      case PiPWindow.PiPState.ABOUT_TO_RESTORE:
        this.curState = 'ABOUT_TO_RESTORE';
        this.curError = Constants.ERROR_BY_DEFAULT;
        break;
      case PiPWindow.PiPState.ERROR:
        this.curState = 'ERROR';
        this.curError = reason;
        break;
      default:
        break;
    }
    Logger.info(`[${TAG}] onStateChange: ${this.curState}, reason: ${reason}`);
  }

  onActionEvent(event: PiPWindow.PiPActionEventType, status: number | undefined) {
    switch (event) {
      case 'playbackStateChanged':
        if (status === 0) {
          this.player?.updatePlayStatus(false);
          this.player?.pause();
        } else {
          this.player?.updatePlayStatus(true);
          this.player?.play();
        }
        break;
      default:
        break;
    }
    this.buttonAction = event + `-status:${status}`;
    Logger.info(`[${TAG}] onActionEvent: ${this.buttonAction} status:${status}}`);
  }

  build() {
    Stack() {
      NavDestination() {
        Column({ space: Constants.SPACE }) {
          Stack() {
            Text($r('app.string.current_video_pip_play'))
              .fontColor($r('app.color.XComponent_text_color'))
              .margin({ bottom: $r('app.integer.x_component_marg_bottom') })
              .visibility(this.hintMsgVisibility ? Visibility.Visible : Visibility.Hidden)
            XComponent({ id: 'video', type: 'surface', controller: this.mXComponentController })
              .onLoad(() => {
                Logger.info(`[${TAG}] XComponent onLoad`);
                this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
                this.player = new AVPlayer(this.surfaceId, Constants.AVPLAYER_TYPE);
                this.player.avPlayerFdSrc();
              })
              .onDestroy(() => {
                this.player?.stopAvPlayer();
                Logger.info(`[${TAG}] XComponent onDestroy`);
              })
              .size({ width: Constants.X_COMPONENT_WIDTH, height: $r('app.float.x_component_height') })
              .margin({ top: $r('app.integer.x_component_marg_top') })
              .backgroundColor(Color.Transparent)
              .align(Alignment.Bottom)
              .id('x_component')
          }
          .size({ width: Constants.X_COMPONENT_WIDTH, height: $r('app.float.x_component_height') })
          .alignContent(Alignment.Bottom)
          .backgroundColor($r('app.color.XComponent_backgroundColor'))

          Scroll(this.scrollerForScroll) {
            Column({ space: Constants.SPACE }) {
              this.ControlPip()
              this.AutoPip()
              this.CallbackMessage()
            }
            .width(Constants.NAV_DESTINATION_WIDTH)
          }
          .layoutWeight(Constants.SCROLL_LAY_OUT_WEIGHT)
          .scrollable(ScrollDirection.Vertical)
          .scrollBar(BarState.Off)
          .edgeEffect(EdgeEffect.Spring)
        }
        .width(Constants.NAV_DESTINATION_WIDTH)
        .height(Constants.NAV_DESTINATION_HEIGHT)
      }
      .hideTitleBar(true)
      .backgroundColor($r('app.color.Play_backgroundColor'))
      .onBackPressed(() => {
        // Eject the top-of-the-stack element of the routing stack.
        const popDestinationInfo = this.pageInfos.pop();
        Logger.info('pop' + 'return value' + JSON.stringify(popDestinationInfo));
        return true;
      })
    }
  }

  @Builder
  ControlPip() {
    Row({ space: Constants.SPACE }) {
      Button($r('app.string.start'))
        .width($r('app.integer.control_button_width'))
        .onClick(() => {
          this.startPip();
          this.hintMsgVisibility = true;
        })
      Button($r('app.string.stop'))
        .width($r('app.integer.control_button_width'))
        .onClick(() => {
          this.stopPip();
          this.hintMsgVisibility = false;
        })
    }
    .size({ width: Constants.CONTROL_WIDTH, height: $r('app.integer.control_height') })
    .justifyContent(FlexAlign.SpaceAround)
    .id('pip_control')
  }

  @Builder
  AutoPip() {
    Row() {
      Text($r('app.string.auto'))
        .width($r('app.integer.auto_text_width'))
        .fontSize($r('app.integer.text_size'))
        .fontWeight(FontWeight.Bold)
        .padding({ left: $r('app.integer.other_padding') })

      Toggle({ type: ToggleType.Switch, isOn: this.isAutoPull })
        .width($r('app.integer.auto_button_width'))
        .height($r('app.integer.auto_button_height'))
        .selectedColor($r('app.color.Toggle_selectedColor'))
        .padding({ right: $r('app.float.toggle_padding') })
        .onChange(async (isOn: boolean) => {
          this.isAutoPull = isOn;
          if (!this.pipController) {
            await this.createPipController();
          }
          this.pipController?.setAutoStartEnabled(this.isAutoPull);
          this.hintMsgVisibility = true;
        })
    }
    .width(Constants.AUTO_PIP_WIDTH)
    .height($r('app.integer.auto_pip_height'))
    .borderRadius($r('app.integer.auto_button_board_radius'))
    .justifyContent(FlexAlign.SpaceBetween)
    .backgroundColor($r('app.color.start_window_background'))
  }

  @Builder
  CallbackMessage() {
    Column({ space: Constants.SPACE }) {
      Text($r('app.string.callback_message'))
        .fontColor($r('app.color.Text_color'))
        .padding({ right: $r('app.integer.callback_text_padding') })
      Column() {
        Text($r('app.string.current_status'))
          .textType()
        Text(this.curState)
          .msgType()
      }
      .size({
        width: Constants.CONTROL_WIDTH,
        height: $r('app.integer.control_height')
      })
      .backgroundColor($r('app.color.Callback_message_backgroundColor'))
      .borderRadius($r('app.integer.auto_button_board_radius'))
      .justifyContent(FlexAlign.SpaceAround)
      .id('current_state')

      Column() {
        Text($r('app.string.current_error'))
          .textType()
        Text(this.curError)
          .msgType()
      }
      .size({
        width: Constants.CONTROL_WIDTH,
        height: $r('app.integer.control_height')
      })
      .backgroundColor($r('app.color.Callback_message_backgroundColor'))
      .borderRadius($r('app.integer.auto_button_board_radius'))
      .justifyContent(FlexAlign.SpaceAround)
      .id('current_error')

      Column() {
        Text($r('app.string.current_action'))
          .textType()
        Text(this.buttonAction)
          .msgType()
      }
      .size({
        width: Constants.CONTROL_WIDTH,
        height: $r('app.integer.control_height')
      })
      .backgroundColor($r('app.color.Callback_message_backgroundColor'))
      .borderRadius($r('app.integer.auto_button_board_radius'))
      .justifyContent(FlexAlign.SpaceAround)
      .id('current_action')
    }
  }
}

以上就是本篇文章所带来的鸿蒙开发中一小部分技术讲解;想要学习完整的鸿蒙全栈技术。可以在结尾找我可全部拿到!
下面是鸿蒙的完整学习路线,展示如下:
1

除此之外,根据这个学习鸿蒙全栈学习路线,也附带一整套完整的学习【文档+视频】,内容包含如下

内容包含了:(ArkTS、ArkUI、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、鸿蒙南向开发、鸿蒙项目实战)等技术知识点。帮助大家在学习鸿蒙路上快速成长!

鸿蒙【北向应用开发+南向系统层开发】文档

鸿蒙【基础+实战项目】视频

鸿蒙面经

在这里插入图片描述

为了避免大家在学习过程中产生更多的时间成本,对比我把以上内容全部放在了↓↓↓想要的可以自拿喔!谢谢大家观看!

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值