HarmonyOS APP画中画:从 PiP 窗口创建到交互控制,打造沉浸式小窗体验

举报
Jack20 发表于 2026/06/20 15:06:05 2026/06/20
【摘要】 HarmonyOS APP画中画:从 PiP 窗口创建到交互控制,打造沉浸式小窗体验📌 核心要点:画中画(PiP)是"最小化但不中断"的窗口模式,核心在于生命周期管理、内容适配和交互控制一、背景与动机你一定有过这样的体验:正在看视频教程,突然来了条微信消息需要回复。切到微信?视频就停了。不切?消息又不能不看。这时候如果视频能自动缩成一个小窗口挂在角落,边回消息边看视频,那就完美了。这就是画...

HarmonyOS APP画中画:从 PiP 窗口创建到交互控制,打造沉浸式小窗体验

📌 核心要点:画中画(PiP)是"最小化但不中断"的窗口模式,核心在于生命周期管理、内容适配和交互控制


一、背景与动机

你一定有过这样的体验:正在看视频教程,突然来了条微信消息需要回复。切到微信?视频就停了。不切?消息又不能不看。这时候如果视频能自动缩成一个小窗口挂在角落,边回消息边看视频,那就完美了。

这就是画中画(Picture-in-Picture,PiP)的价值。它和悬浮窗很像,但有一个本质区别:PiP 是系统级能力,由系统管理窗口的生命周期和位置,开发者只需要声明"我要用 PiP"并配置参数,系统就会帮你处理窗口的创建、显示、移动和销毁。

PiP 最典型的两个场景:

1. 视频播放 PiP:看视频时切到别的应用,视频自动缩小到角落继续播放

2. 视频通话 PiP:打视频电话时切到别的应用,通话画面缩小到角落继续显示

这篇文章,咱们就把 PiP 开发从头到尾讲清楚。


二、核心原理

2.1 PiP 的工作机制

2.2 PiP 生命周期详解

PiP 有自己独立的生命周期,与 Activity/Ability 生命周期并行但不同步:

2.3 PiP 与悬浮窗的区别

特性

PiP

悬浮窗

窗口管理

系统管理

开发者管理

权限要求

声明PiP能力

SYSTEM_FLOAT_WINDOW 权限

窗口位置

系统决定(角落)

开发者指定

窗口大小

系统决定(有标准尺寸)

开发者指定

拖拽

系统内置

需要手动实现

控制按钮

系统内置

需要手动实现

适用场景

视频/通话

通用

审核要求

较宽松

较严格

简单来说:PiP 是"系统帮你管"的悬浮窗,适合视频和通话场景;悬浮窗是"你自己管"的通用浮窗,适合自定义场景。


三、代码实战

3.1 视频播放 PiP 完整实现

这是视频播放场景下 PiP 的完整实现,包括创建、控制、生命周期管理:

// VideoPipPlayer.ets
import { pip } from '@kit.ArkUI';
import { media } from '@kit.MediaKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct VideoPipPlayerPage {
// 视频播放器
private avPlayer: media.AVPlayer | null = null;
// PiP控制器
private pipController: pip.PipController | null = null;
// 播放状态
@State isPlaying: boolean = false;
@State videoTitle: string = 'HarmonyOS 开发教程';
@State currentPosition: number = 0;
@State duration: number = 0;
// PiP状态
@State isPipMode: boolean = false;

aboutToAppear(): void {
this.initPlayer();
this.initPip();
}

aboutToDisappear(): void {
this.releasePlayer();
this.releasePip();
}

/**
* 初始化视频播放器
*/
private async initPlayer(): Promise<void> {
try {
this.avPlayer = await media.createAVPlayer();

// 设置播放状态回调
this.avPlayer.on('stateChange', (state: string) => {
console.info(`[VideoPip] 播放器状态: ${state}`);
if (state === 'playing') {
this.isPlaying = true;
} else if (state === 'paused' || state === 'stopped') {
this.isPlaying = false;
}
});

// 设置进度回调
this.avPlayer.on('timeUpdate', (time: number) => {
this.currentPosition = time;
});

// 设置时长回调
this.avPlayer.on('durationUpdate', (duration: number) => {
this.duration = duration;
});

// 设置视频源
this.avPlayer.url = 'https://example.com/tutorial.mp4';
} catch (e) {
console.error('[VideoPip] 初始化播放器失败');
}
}

/**
* 初始化 PiP 控制器
*/
private async initPip(): Promise<void> {
try {
// 创建 PiP 控制器
this.pipController = pip.createPipController(
pip.PipContentType.VIDEO_PLAY,
this.getContext('videoPip')
);

// 注册 PiP 生命周期回调
this.pipController.on('stateChange', (state: pip.PipState, reason: string) => {
console.info(`[VideoPip] PiP状态变化: ${state}, 原因: ${reason}`);
this.handlePipStateChange(state, reason);
});

// 注册 PiP 控制事件回调
this.pipController.on('controlPanelAction', (action: pip.ControlPanelActionEvent) => {
this.handlePipControlAction(action);
});

console.info('[VideoPip] PiP控制器初始化成功');
} catch (e) {
const err = e as BusinessError;
console.error(`[VideoPip] 初始化PiP失败: ${err.code} - ${err.message}`);
}
}

/**
* 处理 PiP 状态变化
*/
private handlePipStateChange(state: pip.PipState, reason: string): void {
switch (state) {
case pip.PipState.STARTED:
this.isPipMode = true;
console.info('[VideoPip] PiP已启动');
break;
case pip.PipState.STOPPED:
this.isPipMode = false;
console.info('[VideoPip] PiP已停止');
break;
case pip.PipState.ERROR:
console.error(`[VideoPip] PiP错误: ${reason}`);
break;
default:
break;
}
}

/**
* 处理 PiP 控制面板动作
*/
private handlePipControlAction(action: pip.ControlPanelActionEvent): void {
switch (action.status) {
case pip.ControlPanelStatus.PLAY:
this.avPlayer?.play();
break;
case pip.ControlPanelStatus.PAUSE:
this.avPlayer?.pause();
break;
case pip.ControlPanelStatus.STOP:
this.avPlayer?.stop();
this.stopPip();
break;
default:
break;
}
}

/**
* 启动 PiP
*/
private async startPip(): Promise<void> {
if (!this.pipController) {
console.error('[VideoPip] PiP控制器未初始化');
return;
}

try {
// 配置 PiP 参数
const pipConfig: pip.PipConfiguration = {
// PiP窗口的宽高比(视频通常16:9)
ratio: 16 / 9,
// 控制面板配置
controlPanel: {
// 显示播放/暂停按钮
showPlayControl: true,
// 显示停止按钮
showStopControl: true,
}
};

// 启动 PiP
await this.pipController.startPip(pipConfig);
console.info('[VideoPip] PiP启动成功');
} catch (e) {
const err = e as BusinessError;
console.error(`[VideoPip] 启动PiP失败: ${err.code} - ${err.message}`);
}
}

/**
* 停止 PiP
*/
private async stopPip(): Promise<void> {
if (!this.pipController) return;
try {
await this.pipController.stopPip();
this.isPipMode = false;
} catch (e) {
console.error('[VideoPip] 停止PiP失败');
}
}

/**
* 释放播放器资源
*/
private releasePlayer(): void {
this.avPlayer?.release();
this.avPlayer = null;
}

/**
* 释放 PiP 资源
*/
private releasePip(): void {
this.pipController?.off('stateChange');
this.pipController?.off('controlPanelAction');
this.pipController = null;
}

build() {
Column({ space: 16 }) {
// 视频标题
Text(this.videoTitle)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ top: 60 })

// 视频播放区域(全屏时显示)
if (!this.isPipMode) {
Column() {
Text('🎬 视频播放区域')
.fontSize(16)
.fontColor('#888')
}
.width('90%')
.height(200)
.borderRadius(12)
.backgroundColor('#1a1a2e')
.justifyContent(FlexAlign.Center)
}

// 播放控制
Row({ space: 20 }) {
Button('⏮')
.onClick(() => { /* 上一段 */ })
Button(this.isPlaying ? '⏸' : '▶')
.onClick(() => {
if (this.isPlaying) {
this.avPlayer?.pause();
} else {
this.avPlayer?.play();
}
})
Button('⏭')
.onClick(() => { /* 下一段 */ })
}

// 进度条
Progress({
value: this.currentPosition,
total: this.duration,
type: ProgressType.Linear
})
.width('80%')
.color('#4CAF50')

// PiP 控制按钮
Button(this.isPipMode ? '退出画中画' : '进入画中画')
.width('80%')
.backgroundColor(this.isPipMode ? '#FF9800' : '#9C27B0')
.onClick(() => {
if (this.isPipMode) {
this.stopPip();
} else {
this.startPip();
}
})
}
.width('100%')
.height('100%')
.backgroundColor('#0d0d1a')
.foregroundColor('#fff')
.justifyContent(FlexAlign.Center)
}
}

3.2 视频通话 PiP 实现

视频通话的 PiP 和视频播放有所不同,关键区别在于:通话 PiP 需要同时显示远端和近端画面,且通话期间 PiP 不能被用户主动关闭(只能挂断)。

// VideoCallPip.ets
import { pip } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct VideoCallPipPage {
// PiP控制器
private pipController: pip.PipController | null = null;
// 通话状态
@State isCallActive: boolean = false;
@State callDuration: number = 0;
@State callerName: string = '张三';
@State isMuted: boolean = false;
@State isCameraOff: boolean = false;
@State isPipMode: boolean = false;

// 计时器
private callTimer: number = -1;

aboutToAppear(): void {
this.initCallPip();
this.startCall();
}

aboutToDisappear(): void {
this.endCall();
}

/**
* 初始化视频通话 PiP
*/
private async initCallPip(): Promise<void> {
try {
// 视频通话使用 VIDEO_LIVE 类型
this.pipController = pip.createPipController(
pip.PipContentType.VIDEO_LIVE,
this.getContext('callPip')
);

// 监听 PiP 状态变化
this.pipController.on('stateChange', (state: pip.PipState, reason: string) => {
console.info(`[CallPip] PiP状态: ${state}`);
if (state === pip.PipState.STARTED) {
this.isPipMode = true;
} else if (state === pip.PipState.STOPPED) {
this.isPipMode = false;
}
});

// 监听控制面板动作
this.pipController.on('controlPanelAction', (action: pip.ControlPanelActionEvent) => {
this.handleCallControlAction(action);
});

console.info('[CallPip] 通话PiP初始化成功');
} catch (e) {
console.error('[CallPip] 初始化PiP失败');
}
}

/**
* 处理通话控制动作
* 视频通话的控制比视频播放更复杂
*/
private handleCallControlAction(action: pip.ControlPanelActionEvent): void {
switch (action.status) {
case pip.ControlPanelStatus.PLAY:
// 恢复通话(取消静音)
this.isMuted = false;
console.info('[CallPip] 取消静音');
break;
case pip.ControlPanelStatus.PAUSE:
// 静音
this.isMuted = true;
console.info('[CallPip] 静音');
break;
case pip.ControlPanelStatus.STOP:
// 挂断通话
this.endCall();
break;
default:
break;
}
}

/**
* 开始视频通话
*/
private startCall(): void {
this.isCallActive = true;
this.callDuration = 0;

// 启动通话计时器
this.callTimer = setInterval(() => {
this.callDuration++;
}, 1000);

console.info('[CallPip] 视频通话开始');
}

/**
* 结束视频通话
*/
private endCall(): void {
this.isCallActive = false;

// 停止计时器
if (this.callTimer !== -1) {
clearInterval(this.callTimer);
this.callTimer = -1;
}

// 停止 PiP
this.stopCallPip();

console.info('[CallPip] 视频通话结束');
}

/**
* 启动通话 PiP
*/
private async startCallPip(): Promise<void> {
if (!this.pipController) return;

try {
const pipConfig: pip.PipConfiguration = {
// 通话画面通常1:1或4:3
ratio: 1,
controlPanel: {
showPlayControl: true, // 静音/取消静音
showStopControl: true, // 挂断
}
};

await this.pipController.startPip(pipConfig);
} catch (e) {
const err = e as BusinessError;
console.error(`[CallPip] 启动PiP失败: ${err.message}`);
}
}

/**
* 停止通话 PiP
*/
private async stopCallPip(): Promise<void> {
if (!this.pipController) return;
try {
await this.pipController.stopPip();
} catch (e) {
console.error('[CallPip] 停止PiP失败');
}
}

/**
* 格式化通话时长
*/
private formatDuration(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}

build() {
Column({ space: 20 }) {
// 通话信息
Column({ space: 8 }) {
Text(this.callerName)
.fontSize(24)
.fontWeight(FontWeight.Bold)

Text(this.formatDuration(this.callDuration))
.fontSize(16)
.fontColor('#4CAF50')

Text(this.isCallActive ? '通话中' : '已结束')
.fontSize(14)
.fontColor('#888')
}
.margin({ top: 80 })

// 视频画面区域
if (!this.isPipMode) {
Stack() {
// 远端画面(大)
Column() {
Text('📹 远端画面')
.fontSize(14)
.fontColor('#888')
}
.width('90%')
.height(300)
.borderRadius(16)
.backgroundColor('#1a1a2e')
.justifyContent(FlexAlign.Center)

// 近端画面(小窗)
Column() {
Text('📷 我')
.fontSize(12)
.fontColor('#888')
}
.width(100)
.height(140)
.borderRadius(12)
.backgroundColor('#2a2a3e')
.justifyContent(FlexAlign.Center)
.position({ x: '70%', y: 10 })
}
.width('90%')
.height(320)
}

// 通话控制按钮
Row({ space: 24 }) {
// 静音
Column({ space: 4 }) {
Text(this.isMuted ? '🔇' : '🎤')
.fontSize(28)
Text(this.isMuted ? '取消静音' : '静音')
.fontSize(11)
.fontColor('#888')
}
.onClick(() => {
this.isMuted = !this.isMuted;
})

// 挂断
Column({ space: 4 }) {
Text('📞')
.fontSize(28)
Text('挂断')
.fontSize(11)
.fontColor('#F44336')
}
.onClick(() => this.endCall())

// 摄像头
Column({ space: 4 }) {
Text(this.isCameraOff ? '📷' : '📹')
.fontSize(28)
Text(this.isCameraOff ? '开摄像头' : '关摄像头')
.fontSize(11)
.fontColor('#888')
}
.onClick(() => {
this.isCameraOff = !this.isCameraOff;
})
}

// PiP 按钮
Button('切换到画中画')
.width('80%')
.backgroundColor('#9C27B0')
.onClick(() => this.startCallPip())
}
.width('100%')
.height('100%')
.backgroundColor('#0d0d1a')
.foregroundColor('#fff')
}
}

3.3 PiP 交互控制与状态管理

这个示例展示 PiP 的交互控制细节,包括自定义控制面板、状态同步、恢复全屏等:

// PipInteractionControl.ets
import { pip } from '@kit.ArkUI';
import { emitter } from '@kit.BasicServicesKit';
import { BusinessError } from '@kit.BasicServicesKit';

// PiP 交互事件
const EVENT_PIP_STATE_CHANGE = 40001;
const EVENT_PIP_RESTORE_FULLSCREEN = 40002;

@Entry
@Component
struct PipInteractionControlPage {
// PiP 控制器
private pipController: pip.PipController | null = null;
// 播放状态
@State isPlaying: boolean = false;
@State isPipMode: boolean = false;
@State pipStateText: string = '未启动';
@State videoTitle: string = 'HarmonyOS PiP 教程';
@State playbackSpeed: number = 1.0;
// 播放进度
@State currentTime: number = 0;
@State totalTime: number = 300; // 5分钟

aboutToAppear(): void {
this.setupPipController();
this.setupEventListeners();
}

aboutToDisappear(): void {
this.cleanupPipController();
this.cleanupEventListeners();
}

/**
* 设置 PiP 控制器
*/
private async setupPipController(): Promise<void> {
try {
this.pipController = pip.createPipController(
pip.PipContentType.VIDEO_PLAY,
this.getContext('pipControl')
);

// 监听 PiP 状态变化
this.pipController.on('stateChange', (state: pip.PipState, reason: string) => {
this.handlePipStateChange(state, reason);
});

// 监听控制面板动作
this.pipController.on('controlPanelAction', (action: pip.ControlPanelActionEvent) => {
this.handleControlAction(action);
});

console.info('[PipControl] PiP控制器设置完成');
} catch (e) {
console.error('[PipControl] 设置PiP控制器失败');
}
}

/**
* 设置事件监听(与主窗口通信)
*/
private setupEventListeners(): void {
// 监听恢复全屏事件
emitter.on({ eventId: EVENT_PIP_RESTORE_FULLSCREEN }, () => {
this.isPipMode = false;
this.pipStateText = '已恢复全屏';
console.info('[PipControl] 恢复全屏');
});
}

/**
* 处理 PiP 状态变化
*/
private handlePipStateChange(state: pip.PipState, reason: string): void {
const stateMap: Record<number, string> = {
[pip.PipState.STARTED]: 'PiP运行中',
[pip.PipState.STOPPED]: 'PiP已停止',
[pip.PipState.ERROR]: 'PiP错误',
};

this.pipStateText = stateMap[state] || '未知状态';
this.isPipMode = state === pip.PipState.STARTED;

// 通知其他组件 PiP 状态变化
emitter.emit({ eventId: EVENT_PIP_STATE_CHANGE }, {
data: { isPipMode: this.isPipMode, state: this.pipStateText }
});

console.info(`[PipControl] 状态: ${this.pipStateText}, 原因: ${reason}`);
}

/**
* 处理控制面板动作
*/
private handleControlAction(action: pip.ControlPanelActionEvent): void {
switch (action.status) {
case pip.ControlPanelStatus.PLAY:
this.isPlaying = true;
console.info('[PipControl] PiP控制: 播放');
break;
case pip.ControlPanelStatus.PAUSE:
this.isPlaying = false;
console.info('[PipControl] PiP控制: 暂停');
break;
case pip.ControlPanelStatus.STOP:
this.isPlaying = false;
this.stopPip();
console.info('[PipControl] PiP控制: 停止');
break;
default:
break;
}
}

/**
* 启动 PiP(带详细配置)
*/
private async startPip(): Promise<void> {
if (!this.pipController) return;

try {
const pipConfig: pip.PipConfiguration = {
// 视频宽高比
ratio: 16 / 9,
// 控制面板配置
controlPanel: {
showPlayControl: true,
showStopControl: true,
}
};

await this.pipController.startPip(pipConfig);
console.info('[PipControl] PiP启动成功');
} catch (e) {
const err = e as BusinessError;
console.error(`[PipControl] 启动PiP失败: ${err.code}`);
}
}

/**
* 停止 PiP
*/
private async stopPip(): Promise<void> {
if (!this.pipController) return;
try {
await this.pipController.stopPip();
} catch (e) {
console.error('[PipControl] 停止PiP失败');
}
}

/**
* 更新 PiP 内容(切换视频时需要)
*/
private async updatePipContent(): Promise<void> {
if (!this.pipController || !this.isPipMode) return;
try {
// 更新 PiP 窗口中的内容
await this.pipController.updateContent();
console.info('[PipControl] PiP内容已更新');
} catch (e) {
console.error('[PipControl] 更新PiP内容失败');
}
}

/**
* 清理 PiP 控制器
*/
private cleanupPipController(): void {
if (this.pipController) {
this.pipController.off('stateChange');
this.pipController.off('controlPanelAction');
this.pipController = null;
}
}

/**
* 清理事件监听
*/
private cleanupEventListeners(): void {
emitter.off(EVENT_PIP_RESTORE_FULLSCREEN);
}

build() {
Column({ space: 16 }) {
// 标题和状态
Column({ space: 8 }) {
Text('PiP 交互控制')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin({ top: 60 })

// PiP 状态指示
Row({ space: 8 }) {
Circle({ width: 8, height: 8 })
.fill(this.isPipMode ? '#4CAF50' : '#666')
Text(this.pipStateText)
.fontSize(14)
.fontColor('#aaa')
}
}

// 视频信息卡片
Column({ space: 8 }) {
Text(this.videoTitle)
.fontSize(16)
.fontWeight(FontWeight.Medium)
Row() {
Text(`${this.formatTime(this.currentTime)} / ${this.formatTime(this.totalTime)}`)
.fontSize(12)
.fontColor('#888')
Blank()
Text(`${this.playbackSpeed}x`)
.fontSize(12)
.fontColor('#4CAF50')
}
.width('100%')
}
.width('85%')
.padding(16)
.borderRadius(12)
.backgroundColor('#1a1a2e')

// 播放控制
Row({ space: 16 }) {
Button('⏮').onClick(() => { this.currentTime = Math.max(0, this.currentTime - 10); })
Button(this.isPlaying ? '⏸' : '▶')
.width(56)
.height(56)
.borderRadius(28)
.backgroundColor('#4CAF50')
.onClick(() => { this.isPlaying = !this.isPlaying; })
Button('⏭').onClick(() => { this.currentTime = Math.min(this.totalTime, this.currentTime + 10); })
}

// 播放速度
Row({ space: 8 }) {
ForEach([0.5, 1.0, 1.5, 2.0], (speed: number) => {
Text(`${speed}x`)
.fontSize(13)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(6)
.backgroundColor(this.playbackSpeed === speed ? '#4CAF50' : '#333')
.onClick(() => { this.playbackSpeed = speed; })
}, (speed: number) => `${speed}`)
}

// PiP 操作按钮
Row({ space: 12 }) {
Button('启动 PiP')
.backgroundColor('#9C27B0')
.onClick(() => this.startPip())

Button('停止 PiP')
.backgroundColor('#F44336')
.onClick(() => this.stopPip())
}

// 更新 PiP 内容
Button('更新 PiP 内容')
.width('80%')
.backgroundColor('#2196F3')
.onClick(() => this.updatePipContent())
}
.width('100%')
.height('100%')
.backgroundColor('#0d0d1a')
.foregroundColor('#fff')
.justifyContent(FlexAlign.Center)
}

/**
* 格式化时间
*/
private formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
}


四、踩坑与注意事项

4.1 PiP 的设备支持

:在部分低端设备上调用 startPiP() 直接报错。

PiP 是系统级能力,不是所有设备都支持。使用前必须检查:

// ✅ 正确:先检查是否支持 PiP
private async checkPipSupport(): Promise<boolean> {
try {
// 尝试创建 PiP 控制器
const controller = pip.createPipController(
pip.PipContentType.VIDEO_PLAY,
this.getContext()
);
if (controller) {
// 创建成功说明支持
return true;
}
return false;
} catch (e) {
console.warn('[PipCheck] 设备不支持PiP');
return false;
}
}

4.2 PiP 窗口的内容更新

:切换视频后 PiP 窗口显示的还是旧内容。

PiP 窗口的内容需要主动更新,不会自动跟随播放器状态变化:

// ❌ 错误:切换视频后 PiP 内容不更新
this.avPlayer.url = 'new_video.mp4';
// PiP 窗口还是旧画面

// ✅ 正确:切换视频后主动更新 PiP 内容
this.avPlayer.url = 'new_video.mp4';
this.pipController?.updateContent();

4.3 PiP 与音频焦点

:进入 PiP 模式后,其他应用的音频和 PiP 的音频冲突。

PiP 运行时,你的应用仍然持有音频焦点。如果其他应用也需要播放音频,需要正确处理音频焦点:

// 进入 PiP 时降低音量(可选)
this.avPlayer.setVolume(0.5);

// 退出 PiP 时恢复音量
this.avPlayer.setVolume(1.0);

4.4 PiP 的宽高比设置

:PiP ratio 设置不当,导致画面变形。

// ❌ 错误:ratio 与视频实际宽高比不一致
pipConfig.ratio = 1; // 1:1,但视频是16:9

// ✅ 正确:ratio 与视频宽高比一致
pipConfig.ratio = 16 / 9; // 视频是16:9
pipConfig.ratio = 4 / 3; // 视频是4:3
pipConfig.ratio = 1; // 视频通话(正方形)

4.5 PiP 的生命周期与 Ability 生命周期

:Ability 销毁后 PiP 仍在运行,导致资源泄漏。

PiP 的生命周期独立于 Ability,但 PiP 的内容依赖于 Ability 的资源。如果 Ability 被销毁,PiP 窗口会变成黑屏或崩溃:

// ✅ 正确:Ability 销毁时停止 PiP
onWindowStageDestroy(): void {
this.pipController?.stopPip();
this.pipController = null;
}

// ✅ 正确:应用退到后台时保持 PiP
onBackground(): void {
// 不需要停止 PiP,PiP 本身就是为后台运行设计的
console.info('[Pip] 应用退到后台,PiP继续运行');
}


五、HarmonyOS 6 适配

5.1 API 变化

特性

HarmonyOS 5.0

HarmonyOS 6.0

PiP 内容类型

VIDEO_PLAY, VIDEO_LIVE

新增 AUDIO_LIVE(音乐PiP)

控制面板

播放/暂停/停止

新增自定义按钮、进度条

PiP 尺寸

系统固定

支持用户缩放

PiP

不支持

支持同时运行多个PiP

PiP 动画

无过渡动画

新增进入/退出过渡动画

PiP 位置

系统固定角落

支持用户拖拽到任意位置

5.2 迁移要点

1. 音乐 PiP:HarmonyOS 6 新增 AUDIO_LIVE 类型,音乐应用也可以使用 PiP 显示播放控制

2. 自定义控制面板:可以添加自定义按钮(如收藏、下一首),不再局限于播放/暂停/停止

3. 用户缩放:PiP 窗口支持用户双指缩放,需要确保内容在不同尺寸下都能正常显示

4. PiP:如果应用需要同时运行多个 PiP(如同时播放两个视频),需要使用不同的 controller

5.3 兼容性写法

// 兼容 HarmonyOS 5.0 和 6.0 的 PiP 配置
private getPipConfig(): pip.PipConfiguration {
const config: pip.PipConfiguration = {
ratio: 16 / 9,
controlPanel: {
showPlayControl: true,
showStopControl: true,
}
};

// HarmonyOS 6.0 新增:自定义控制按钮
// if (deviceInfo.osFullName >= '6.0.0') {
// config.controlPanel.customButtons = [
// { icon: 'heart', action: 'favorite' },
// { icon: 'next', action: 'nextTrack' },
// ];
// }

return config;
}


六、总结

知识点

核心内容

关键API

PiP 窗口创建

创建控制器、配置参数、启动PiP

pip.createPipController, startPip

PiP 生命周期

STARTED/STOPPED/ERROR状态

on('stateChange')

视频播放PiP

VIDEO_PLAY类型、16:9比例

PipContentType.VIDEO_PLAY

视频通话PiP

VIDEO_LIVE类型、1:1比例

PipContentType.VIDEO_LIVE

交互控制

播放/暂停/停止、自定义按钮

on('controlPanelAction')

内容更新

切换视频后主动更新PiP

updateContent

宽高比

ratio与视频实际比例一致

PipConfiguration.ratio

设备兼容

检查设备是否支持PiP

createPipController 异常捕获

资源管理

Ability销毁时停止PiP

stopPip, off

一句话总结:PiP 开发的核心是"三个正确"——类型选对(播放用 VIDEO_PLAY,通话用 VIDEO_LIVE)、比例设对(ratio 跟视频一致)、生命周期管对(Ability 销毁时停止 PiP)。系统帮你管窗口,你只需要管好内容和状态,这就是 PiP 的优雅之处。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。