HarmonyOS APP画中画:从 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 的优雅之处。
- 点赞
- 收藏
- 关注作者
评论(0)