鸿蒙开发中如何实现画中画

画中画是把视频类内容(视频播放、视频通话、视频会议、直播)以小窗形式悬浮在屏幕上,切到小窗后用户还能干别的。它是系统级窗口,比应用主窗口层级高,退到桌面后依然可见。

1. 画中画与普通浮窗的区别

维度 画中画 PiP 闪控窗 FloatView 应用内子窗口
内容类型 仅视频 / 音视频 任意 UI 任意 UI
系统级置顶 ✅ 系统级小窗 ✅ ❌ 仅应用内
后台可见 ✅ 退桌面仍显示 ✅ ❌ 退后台即失效
自带交互 拖动 / 缩放 / 隐藏 / 删除 / 控制条 拖动 / 缩放 / 闪控球 需自己实现
是否需申请权限 无需受限权限(后台播放需长时任务) 需 FLOAT_VIEW(前台授权) 无需
渲染方式 绑定视频 Surface 加载页面 加载页面

画中画是系统为 "视频小窗" 定制的专用窗口,官方推荐用于播放 / 通话 / 会议 / 直播;如果浮窗里放的是任意内容(不是视频),建议用闪控窗FloatView。

2. 画中画的三种开发方式怎么选

官方提供 3 种接入方式:

方式 适用场景 是否需要自行管理页面
XComponent 应用用 Navigation 管理页面,或 Ability 单页面 ❌ 无需管理页面,最省事
typeNode 所有场景,官方推荐 ✅ 需自行管理页面,灵活
NDK (C/C++) 依赖 NDK 接口开发的应用 ✅ 需自行管理

3. 核心接口

使用时需要导入PiPWindow类。import { PiPWindow } from '@kit.ArkUI'; 主要接口如下:

主要接口 作用
isPiPEnabled(): boolean 判断当前系统是否开启画中画
create(config, contentNode) 创建画中画控制器(typeNode 方式)
startPiP() / stopPiP() 启动 / 停止画中画
setAutoStartEnabled(true) 应用退后台时自动拉起画中画(关键)
updateContentSize(w, h) 媒体源切换(如横竖屏切换)时更新尺寸
on('stateChange', cb) 监听生命周期状态
on('controlEvent', cb) 监听控制面板按钮点击
updatePiPControlStatus(type, status) 更新控制按钮状态(如播放 / 暂停)
setPiPControlEnabled(type, enabled) 设置按钮使能 / 禁用

画中画PiP 依赖系统能力,部分设备 / 系统版本不支持。启动前必须调用PiPWindow.isPiPEnabled() 判断,不支持时要做好降级(比如退后台继续用悬浮窗或直接停留原页),不能假设一定可用。另外,用户可在「设置 > 系统 > 智慧多窗」里关闭 "自动启动画中画" 开关。即使你 setAutoStartEnabled(true),用户关掉后依然不会自动拉起。这是系统对用户的兜底,别写成 "一定能自动切小窗"。

4. 完整实现(typeNode 推荐方式)

第 1 步:创建视频节点(XComponent + AVPlayer 绑定)

复制代码
// PipManager.ets ------ 画中画管理器(单例)
import { PiPWindow, typeNode, NodeController, FrameNode, UIContext,
         XComponentController, XComponentType } from '@kit.ArkUI';
import { AVPlayer } from '@kit.MediaKit';
import { common } from '@kit.AbilityKit';

export class CustomXComponentController extends XComponentController {
  onSurfaceCreated(surfaceId: string): void {
    // 把 AVPlayer 的渲染目标绑定到 XComponent 的 Surface
    PipManager.getInstance().setSurfaceId(surfaceId);
  }
}

export class PipManager {
  private static instance: PipManager = new PipManager();
  static getInstance(): PipManager { return PipManager.instance; }

  player: AVPlayer | undefined = undefined;
  pipController: PiPWindow.PiPController | undefined = undefined;
  private xcController: CustomXComponentController = new CustomXComponentController();
  private xComponent: typeNode.XComponent | undefined = undefined;

  // 创建自由节点(不加入布局,仅在画中画中显示,最简单)
  makeTypeNode(ctx: UIContext): void {
    this.xComponent = typeNode.createNode(ctx, 'XComponent', {
      type: XComponentType.SURFACE,
      controller: this.xcController,
    });
  }

  // 创建画中画控制器
  async createPipController(ctx: Context): Promise {
    if (!PiPWindow.isPiPEnabled()) {
      console.error('当前系统未开启画中画');
      return;
    }
    const config: PiPWindow.PiPConfiguration = {
      context: ctx,
      componentController: this.xcController,   // 关联视频 surfaceId
      templateType: PiPWindow.PiPTemplateType.VIDEO_PLAY,
      contentWidth: 1920,                        // 原始内容宽高
      contentHeight: 1080,
      // 可选:控制层控件组
      controlGroups: [PiPWindow.VideoPlayControlGroup.VIDEO_PREVIOUS_NEXT],
    };
    this.pipController = await PiPWindow.create(config, this.xComponent);
    // 关键:退后台自动拉起
    this.pipController.setAutoStartEnabled(true);
    // 注册生命周期 + 控制事件
    this.pipController.on('stateChange', (state, reason) => this.onStateChange(state, reason));
    this.pipController.on('controlEvent', (ctrl) => this.onControlEvent(ctrl));
  }

  startPip(): void {
    this.pipController?.startPiP();
  }

  stopPip(): void {
    this.pipController?.stopPiP();
  }

  updateContentSize(w: number, h: number): void {
    this.pipController?.updateContentSize(w, h);
  }
}

第 2 步:在 EntryAbility 里初始化节点

复制代码
// EntryAbility.ets
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { PipManager } from '../manager/PipManager';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.getMainWindow().then((win) => {
      const ctx = win.getUIContext();
      // 用主窗口 UIContext 创建 typeNode
      PipManager.getInstance().makeTypeNode(ctx);
    });
  }
}

第 3 步:页面里放视频预览 + 启动按钮

复制代码
// pages/PlayerPage.ets
@Entry
@Component
struct PlayerPage {
  build() {
    Column() {
      // 主页视频预览区(挂 AVPlayer 渲染的 XComponent)
      NodeContainer(PipManager.getInstance().getNodeController())
        .width('100%').height(400)

      Row({ space: 20 }) {
        Button('启动画中画').onClick(() => PipManager.getInstance().startPip())
        Button('停止').onClick(() => PipManager.getInstance().stopPip())
      }
    }
    .width('100%').height('100%')
  }
}

如果页面用 Navigation/Router 管理,需要在生命周期里做节点管理(ABOUT_TO_START 时移除节点、ABOUT_TO_RESTORE 时跳回原页)。另外,画中画PiP 窗口大小用的是 Surface 的宽高比,和渲染分辨率无关,给大一点或小一点都能渲染,所以不要试图用窗口尺寸去控制画质。XComponent 和 Surface 的大小是独立的,想精确控制 PiP 内画面区域,用 XComponent 的 setXComponentSurfaceRect() 单独设 Surface 矩形,而不是靠调 XComponent 本身。

横竖屏 / 媒体源切换时是出问题的重灾区,切横竖屏、分辨率变化时画面常会被拉伸或留黑边,正确做法是要监听 avPlayer.on('stateChange'),在 prepared 状态拿到视频真实宽高,动态更新 XComponent Surface 区域,并调用 updateContentSize(w, h) 通知 PiP 控制器。播放器 objectFit 设成 Auto或Contain(保持宽高比缩放),别用默认拉伸模式。视频节点要正确绑定 Surface,AVPlayer 是靠 XComponent 的 onSurfaceCreated(surfaceId) 拿到的 surfaceId 渲染的。节点被移除 / 重新添加(PiP 进出)时,要处理好 surfaceId 的复用和 onSurfaceDestroyed 释放,避免黑屏或画面丢失。

5. 生命周期状态机

复制代码
ABOUT_TO_START → STARTED → (用户拖动到边缘隐藏) → HIDDEN → 点击图标恢复 → STARTED
      │                │
      │                └── 点击"关闭" 或 拖到垃圾桶热区 → ABOUT_TO_STOP → STOPPED
      │
      └── 点击"恢复全屏" → ABOUT_TO_RESTORE → RESTORED(回到应用原界面)

必须在 on('stateChange') 回调里处理这些状态:

状态 含义 你需要做的事
ABOUT_TO_START 即将进入画中画 从主页面移除视频节点、可返回上级页
STARTED 已在画中画中 正常播放
ABOUT_TO_RESTORE 即将还原全屏 若之前返回了上级页,需跳回原界面
ABOUT_TO_STOP 即将关闭 把视频节点重新加回布局
STOPPED 已关闭 清理资源;

申请了长时任务的要关掉任务 / 进程 |

示例代码如下:

复制代码
onStateChange(state: PiPWindow.PiPState, reason: string): void {
  switch (state) {
    case PiPWindow.PiPState.ABOUT_TO_START:
      // 移除主页节点,进入画中画
      PipManager.getInstance().getNodeController()?.removeNode();
      break;
    case PiPWindow.PiPState.ABOUT_TO_RESTORE:
      // 还原时跳回原界面(如果 START 时 back 了)
      this.getUIContext().getRouter().pushUrl({ url: 'pages/PlayerPage' });
      break;
    case PiPWindow.PiPState.STOPPED:
      // 关闭长时任务、释放 AVPlayer
      break;
  }
}

需要后台持续播放时,必须配长时任务(continuousTask),PiP 窗口能后台渲染,但应用进程要防挂起,需申请长时任务。另外,STOPPED 时必须主动释放长时任务,画中画被关闭 / 删除(比如用户拖到底部垃圾桶)后,官方明确要求:申请了长时任务的应用要监听STOPPED,主动停掉任务或进程。不释放会被系统判定为不合理后台占用,导致耗电、被挂起甚至被系统杀掉。

6. 交互与控制面板

系统内置交互(无需自己写代码):

• 单击:显示 / 隐藏控制层(控制层 3 秒自动隐藏)

• 双击:放大 / 缩小窗口

• 拖动:移到屏幕任意位置;拖到左右边缘自动隐藏,边缘出现小图标,点击恢复

• 拖拽四边 / 四角:缩放窗口(有最小 / 最大档,超界回弹)

• 拖到底部垃圾桶热区:删除画中画

控制面板控件(按模板类型可选,controlGroups 配置):

模板 可用控件
VIDEO_PLAY 播放 / 暂停、上一集 / 下一集
VIDEO_CALL 麦克风切换、挂断、摄像头切换
VIDEO_MEETING 麦克风切换、挂断、摄像头切换
VIDEO_LIVE 播放 / 暂停、静音
复制代码
// 监听控制按钮点击
this.pipController.on('controlEvent', (ctrl: PiPWindow.ControlEventParam) => {
  switch (ctrl.controlType) {
    case PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE:
      // 切换播放/暂停,并同步按钮状态
      this.pipController.updatePiPControlStatus(
        PiPWindow.PiPControlType.VIDEO_PLAY_PAUSE,
        PiPWindow.PiPControlStatus.PLAY);
      break;
    case PiPWindow.PiPControlType.VIDEO_CALL_HANG_UP:
      this.stopPip();
      break;
  }
});

自定义 UI 不响应任何交互,PiPConfiguration.customUIController 里的自定义 UI 只是 "显示" 在内容上方,无法点击、无交互。想要按钮就得用系统 controlGroups 控件,别把交互逻辑塞进自定义 UI。视频通话、视频会议模板默认不存在任何按钮,点击窗口即还原。要显示按钮必须显式配置controlGroups。另外,按钮状态要主动同步,点了暂停、切了上一集,要调updatePiPControlStatus / setPiPControlEnabled 更新控制面板按钮,否则按钮状态和实际不一致。PiP 与闪控窗 / 闪控球是互斥的,同一个应用启动了画中画,就不能再启动闪控窗,需先 stopPiP。如果业务既要视频小窗又要任意内容浮窗,得设计好切换逻辑。

7. 关键注意事项

• 退后台自动拉起:出于安全,应用在后台时不允许直接 startPiP。想实现 "退出页面 → 自动切小窗" 必须用 setAutoStartEnabled(true),它会在主窗退后台时自动拉起画中画。

• 后台继续播放:画中画能在后台渲染视频,但涉及后台任务时需配合长时任务(continuousTask),并在STOPPED 时主动关闭任务 / 进程,否则会被系统回收。

• 媒体源切换:横竖屏切换、分辨率变化时调 updateContentSize 更新,否则画面会被拉伸。

最后,画中画是系统级视频小窗,只放视频,无需受限权限,退后台自动保持;如果你要浮的是 "任意内容"(面板、按钮、客服球),那建议不要使用用画中画,走闪控窗或子窗口。

相关推荐
donk13143 小时前
React Native for OpenHarmony 实战:三方库 react-native-toast-message 的鸿蒙化适配指南
react native·react.js·音视频·harmonyos
Flutter_OH3 小时前
Flutter OHOS Dart 堆栈符号化
harmonyos
Flutter_OH3 小时前
Flutter OHOS Flutter 多设备自适应 DPI 指导文档
harmonyos
SuperHeroWu74 小时前
【HarmonyOS 7】 应用 Agent 接入云 A2A 与端 A2A 的实践详解
ai·harmonyos·小艺
用户29469405448164 小时前
个人开发者上架鸿蒙 AI 应用,最后倒在安全评估实名制这一格
harmonyos·deepseek
Keya5 小时前
HarmonyOS 开发实战:用 util.Aspect 玩转 AOP 切面编程
harmonyos
威哥爱编程5 小时前
HarmonyOS 7 Agent 开发实战 03:Tool Calling 让 AI 查实时订单与库存
agent·harmonyos
威哥爱编程6 小时前
HarmonyOS 7 Agent 开发实战 02:Agent 工程脚手架与对话 UI 骨架
agent·harmonyos
Georgewu14 小时前
【HarmonyOS 7】 HarmonyOS 应用 Skill创建和编排详解
harmonyos