在移動應用開發中,畫中畫(Picture-in-Picture,PiP)功能已成為提升用户體驗的核心特性之一。無論是視頻App讓用户邊刷資訊邊追劇,會議軟件支持邊看文檔邊參會,還是直播平台允許用户邊互動邊觀看,畫中畫都能打破單一窗口的限制,實現多任務並行。鴻蒙系統通過@ohos.PiPWindow模塊提供了標準化、高擴展性的畫中畫解決方案,覆蓋手機、平板、PC、電視等多終端,支持API version 11及以上版本。本文將結合視頻播放、視頻通話、在線會議、直播四大核心場景,通過完整案例講解PiPWindow的深度集成與場景化優化技巧。
一、模塊核心能力與場景適配基礎
1.1 核心能力全景
@ohos.PiPWindow模塊的核心價值在於全場景適配+精細化控制,其核心能力可概括為:
- 跨設備兼容:API 20前支持手機、平板,API 20後新增PC/2in1設備支持,電視、穿戴設備同樣適配
- 多模板預設:提供視頻播放、通話、會議、直播4類模板,無需從零開發控制欄
- 全生命週期管理:支持啓動、停止、恢復、異常處理等完整狀態流轉
- 精細化控制:窗口尺寸調整、控制欄自定義、狀態監聽、自動啓動配置等
- 靈活擴展:支持自定義UI疊加、LocalStorage狀態同步、XComponent內容渲染等高級能力
1.2 場景-模板-控件組適配關係
不同場景對應不同的模板類型和控制組件,合理搭配能大幅提升開發效率,具體適配關係如下:
| 應用場景 | 模板類型(PiPTemplateType) | 核心控制組(PiPControlGroup) | 典型設備 |
|---|---|---|---|
| 視頻播放(影視、短視頻) | VIDEO_PLAY | 上一個/下一個、快進/後退 | 手機、平板、電視 |
| 視頻通話(一對一通話) | VIDEO_CALL | 麥克風開關、攝像頭開關、掛斷、靜音 | 手機、PC |
| 在線會議(多人協作) | VIDEO_MEETING | 掛斷、靜音、攝像頭開關、麥克風開關 | PC、平板、手機 |
| 視頻直播(電商直播、賽事直播) | VIDEO_LIVE | 播放/暫停、靜音 | 手機、平板、電視 |
1.3 開發前置準備
(1)環境配置
- 開發工具:DevEco Studio 4.0+(需安裝HarmonyOS SDK API 11及以上)
- 測試設備:HarmonyOS 3.1+真機或模擬器(建議使用API 12+版本以支持完整功能)
- 權限説明:無需額外申請懸浮窗權限,系統自動適配(部分設備需在設置中開啓"應用畫中畫權限")
(2)核心模塊導入
// 核心模塊導入
import {
PiPWindow, PiPTemplateType, PiPControlGroup, PiPController,
PiPState, PiPControlType, PiPControlStatus, PiPWindowInfo, PiPWindowSize
} from '@kit.ArkUI';
// 輔助模塊導入
import { BusinessError } from '@kit.BasicServicesKit';
import { XComponentController, XComponentType, UIContext, NodeController, BuilderNode } from '@kit.ArkUI';
import { Context, AbilityConstant } from '@kit.AbilityKit';
import { LocalStorage } from '@kit.ArkUI';
二、場景化開發實戰:四大核心場景完整案例
場景一:視頻播放場景(影視App)
場景需求
用户在觀看電影時,點擊Home鍵返回桌面或切換到其他應用,視頻自動縮小為畫中畫窗口繼續播放;支持在畫中畫窗口控制播放/暫停、快進/後退、切換視頻;返回原App時,畫中畫恢復為全屏播放。
實現步驟
1. 基礎配置與UI佈局
首先創建視頻播放頁面,包含視頻渲染容器(XComponent)、控制按鈕和畫中畫觸發按鈕:
@Entry
@Component
struct VideoPlayerPage {
// 視頻播放相關狀態
private videoUrl: string = 'https://example.com/movie.mp4';
private currentTime: number = 0; // 當前播放進度(秒)
private isPlaying: boolean = false;
// PiP相關實例
private xComponentController: XComponentController = new XComponentController();
private pipController: PiPWindow.PiPController | undefined;
private localStorage: LocalStorage = new LocalStorage({ 'playbackTime': 0 });
// 頁面導航ID(用於從PiP恢復時定位頁面)
private navId: string = 'video_player_page';
build() {
Column() {
// 視頻渲染容器:通過XComponent實現硬件加速渲染
XComponent({
id: 'video_surface',
type: XComponentType.SURFACE,
controller: this.xComponentController
})
.width('100%')
.height(300)
.onLoad(() => {
// 初始化視頻播放器,綁定XComponent的Surface
this.initVideoPlayer(this.xComponentController.getSurfaceId());
})
// 播放控制欄
Row() {
Button('播放/暫停')
.onClick(() => this.togglePlay())
Button('開啓畫中畫')
.marginLeft(20)
.onClick(() => this.startPiP())
Button('切換視頻')
.marginLeft(20)
.onClick(() => this.switchVideo('https://example.com/next-movie.mp4'))
}
.margin(20)
}
.padding(16)
.width('100%')
}
// 初始化視頻播放器(實際開發中需結合媒體播放模塊)
private initVideoPlayer(surfaceId: string) {
console.info(`綁定視頻渲染Surface:${surfaceId}`);
// 此處省略視頻播放器初始化邏輯,核心是將播放內容渲染到XComponent的Surface
}
private togglePlay() {
this.isPlaying = !this.isPlaying;
// 省略播放/暫停控制邏輯
}
}
2. PiP配置與控制器創建
創建適配視頻播放場景的PiPConfiguration,包含模板類型、控制組、狀態同步等配置:
// 初始化PiP配置
private initPiPConfig(): PiPWindow.PiPConfiguration {
return {
// 上下文環境:從UIContext獲取宿主上下文
context: this.getUIContext().getHostContext() as Context,
// XComponent控制器:關聯視頻渲染容器
componentController: this.xComponentController,
// 導航ID:用於從PiP恢復時回到當前播放頁面
navigationId: this.navId,
// 模板類型:視頻播放
templateType: PiPTemplateType.VIDEO_PLAY,
// 視頻原始尺寸(影響PiP窗口比例)
contentWidth: 1280,
contentHeight: 720,
// 控制組:快進/後退(與上一個/下一個互斥)
controlGroups: [PiPWindow.VideoPlayControlGroup.FAST_FORWARD_BACKWARD],
// LocalStorage:同步主窗口與PiP窗口的播放進度
localStorage: this.localStorage,
// 默認窗口大小:小窗(1=小窗,2=大窗,0=上次尺寸)
defaultWindowSizeType: 1
};
}
// 創建PiP控制器並啓動畫中畫
async startPiP() {
// 1. 兼容性檢測
if (!PiPWindow.isPiPEnabled()) {
Toast.show({ message: '當前設備不支持畫中畫功能' });
return;
}
// 2. 保存當前播放進度到LocalStorage
this.localStorage.setOrCreate('playbackTime', this.currentTime);
try {
// 3. 創建PiP控制器
const config = this.initPiPConfig();
this.pipController = await PiPWindow.create(config);
console.info('PiP控制器創建成功');
// 4. 啓動畫中畫
await this.pipController.startPiP();
console.info('畫中畫啓動成功');
// 5. 註冊監聽事件
this.registerPiPListeners();
// 6. 設置返回桌面時自動啓動PiP(可選)
this.pipController.setAutoStartEnabled(true);
} catch (err) {
const error = err as BusinessError;
console.error(`PiP啓動失敗:錯誤碼${error.code},消息${error.message}`);
this.handlePiPError(error.code);
}
}
3. 狀態監聽與業務聯動
畫中畫的核心價值在於與主應用的狀態同步,需監聽生命週期、控制欄操作、窗口尺寸變化三類事件:
private registerPiPListeners() {
if (!this.pipController) return;
// 1. 生命週期狀態監聽:處理啓動、停止、恢復等流轉
this.pipController.on('stateChange', (state: PiPState, reason: string) => {
switch (state) {
case PiPState.STARTED:
// PiP啓動成功:暫停主窗口視頻,避免音視頻衝突
this.isPlaying = false;
this.pauseVideo();
console.info(`PiP啓動,原因:${reason}`);
break;
case PiPState.STOPPED:
// PiP停止:恢復主窗口視頻播放(從LocalStorage讀取進度)
this.currentTime = this.localStorage.get<number>('playbackTime') || 0;
this.seekTo(this.currentTime);
this.isPlaying = true;
this.playVideo();
console.info(`PiP停止,原因:${reason}`);
// 移除監聽,釋放資源
this.removePiPListeners();
break;
case PiPState.ABOUT_TO_RESTORE:
// PiP即將恢復到主窗口:準備UI狀態
this.setPageState('restoring');
console.info(`PiP準備恢復,原因:${reason}`);
break;
case PiPState.ERROR:
// 異常處理:提示用户並恢復播放
Toast.show({ message: '畫中畫異常,已恢復原窗口播放' });
this.resumeVideo();
console.error(`PiP異常,原因:${reason}`);
break;
}
});
// 2. 控制欄操作監聽:響應播放/暫停、快進/後退
this.pipController.on('controlEvent', (param) => {
switch (param.controlType) {
case PiPControlType.VIDEO_PLAY_PAUSE:
if (param.status === PiPControlStatus.PLAY) {
this.playVideo(); // 播放
this.isPlaying = true;
// 更新主窗口狀態(可選)
this.updateMainWindowPlayState(true);
} else {
this.pauseVideo(); // 暫停
this.isPlaying = false;
this.updateMainWindowPlayState(false);
}
break;
case PiPControlType.FAST_FORWARD:
this.currentTime += 15; // 快進15秒
this.seekTo(this.currentTime);
this.localStorage.set('playbackTime', this.currentTime);
break;
case PiPControlType.FAST_BACKWARD:
this.currentTime = Math.max(0, this.currentTime - 15); // 後退15秒(不小於0)
this.seekTo(this.currentTime);
this.localStorage.set('playbackTime', this.currentTime);
break;
}
});
// 3. 窗口尺寸變化監聽(API 15+):適配不同尺寸的視頻渲染
this.pipController.on('pipWindowSizeChange', (size: PiPWindowSize) => {
console.info(`PiP窗口變化:寬${size.width}px,高${size.height}px,縮放比${size.scale}`);
// 調整視頻渲染比例,避免拉伸
this.adjustVideoAspectRatio(size.width, size.height);
});
}
// 移除監聽(避免內存泄漏)
private removePiPListeners() {
if (!this.pipController) return;
this.pipController.off('stateChange');
this.pipController.off('controlEvent');
this.pipController.off('pipWindowSizeChange');
}
4. 高級優化:自定義UI疊加
需求:在畫中畫窗口右上角顯示視頻時長和清晰度標識。通過customUIController實現自定義UI疊加:
// 1. 定義自定義UI構建器
@Builder
function CustomVideoOverlay(params: { duration: string, quality: string }) {
Row() {
Text(`${params.duration}`)
.fontSize(12)
.fontColor(Color.White)
.backgroundColor(Color.Black.opacity(0.6))
.padding(2)
.borderRadius(2)
Text(`${params.quality}`)
.fontSize(12)
.fontColor(Color.White)
.backgroundColor(Color.Black.opacity(0.6))
.padding(2)
.borderRadius(2)
.marginLeft(4)
}
.position({ right: 8, top: 8 })
}
// 2. 實現自定義NodeController
class VideoOverlayController extends NodeController {
private overlayNode: BuilderNode<[{ duration: string, quality: string }]> | null = null;
private params: { duration: string, quality: string };
constructor(duration: string, quality: string) {
super();
this.params = { duration, quality };
}
makeNode(context: UIContext): FrameNode | null {
this.overlayNode = new BuilderNode(context);
this.overlayNode.build(wrapBuilder<[{ duration: string, quality: string }]>(CustomVideoOverlay), this.params);
return this.overlayNode.getFrameNode();
}
// 更新自定義UI參數(如切換清晰度時)
updateParams(newParams: { duration: string, quality: string }) {
this.params = newParams;
this.overlayNode?.update(newParams);
}
}
// 3. 在PiP配置中添加自定義UI控制器
private initPiPConfig(): PiPWindow.PiPConfiguration {
// 初始化自定義UI控制器
const overlayController = new VideoOverlayController('01:32:45', '1080P');
return {
// ...其他配置
customUIController: overlayController, // 添加自定義UI疊加
};
}
場景二:視頻通話場景(辦公通訊App)
場景需求
用户在進行一對一視頻通話時,切換到郵件、文檔等應用查看內容,通話窗口縮小為畫中畫;支持在畫中畫窗口控制麥克風開關、攝像頭開關、靜音、掛斷;返回原App時恢復全屏通話狀態;通話結束時自動關閉畫中畫。
核心實現代碼
1. 通話場景PiP配置
@Component
struct VideoCallPage {
private xComponentController: XComponentController = new XComponentController();
private pipController: PiPWindow.PiPController | undefined;
private isMicOpen: boolean = true; // 麥克風狀態
private isCameraOpen: boolean = true; // 攝像頭狀態
private isMuted: boolean = false; // 靜音狀態
private callId: string = 'call_123456'; // 通話ID
// 初始化通話場景PiP配置
private initCallPiPConfig(): PiPWindow.PiPConfiguration {
return {
context: this.getUIContext().getHostContext() as Context,
componentController: this.xComponentController,
templateType: PiPTemplateType.VIDEO_CALL, // 通話模板
contentWidth: 720,
contentHeight: 1280, // 豎屏通話比例
// 通話核心控制組
controlGroups: [
PiPWindow.VideoCallControlGroup.MICROPHONE_SWITCH,
PiPWindow.VideoCallControlGroup.CAMERA_SWITCH,
PiPWindow.VideoCallControlGroup.MUTE_SWITCH,
PiPWindow.VideoCallControlGroup.HANG_UP_BUTTON
],
defaultWindowSizeType: 1 // 小窗啓動
};
}
// 啓動通話畫中畫
async startCallPiP() {
if (!PiPWindow.isPiPEnabled()) {
Toast.show({ message: '當前設備不支持畫中畫通話' });
return;
}
try {
const config = this.initCallPiPConfig();
this.pipController = await PiPWindow.create(config);
await this.pipController.startPiP();
this.registerCallPiPListeners();
} catch (err) {
const error = err as BusinessError;
console.error(`通話PiP啓動失敗:${error.code} - ${error.message}`);
}
}
2. 通話控制事件處理
private registerCallPiPListeners() {
if (!this.pipController) return;
// 控制欄操作監聽
this.pipController.on('controlEvent', (param) => {
switch (param.controlType) {
case PiPControlType.MICROPHONE_SWITCH:
// 切換麥克風狀態
this.isMicOpen = param.status === PiPControlStatus.OPEN;
this.setMicrophoneState(this.isMicOpen); // 調用原生API控制麥克風
break;
case PiPControlType.CAMERA_SWITCH:
// 切換攝像頭狀態
this.isCameraOpen = param.status === PiPControlStatus.OPEN;
this.setCameraState(this.isCameraOpen); // 調用原生API控制攝像頭
break;
case PiPControlType.MUTE_SWITCH:
// 切換靜音狀態
this.isMuted = param.status === PiPControlStatus.CLOSE;
this.setMuteState(this.isMuted);
break;
case PiPControlType.HANG_UP_BUTTON:
// 掛斷通話
this.endCall();
this.stopCallPiP();
break;
}
});
// 生命週期監聽
this.pipController.on('stateChange', (state, reason) => {
if (state === PiPState.STOPPED) {
// PiP停止時,同步更新主窗口通話狀態
this.syncCallState();
this.removePiPListeners();
}
});
}
// 停止通話畫中畫
async stopCallPiP() {
if (!this.pipController) return;
try {
await this.pipController.stopPiP();
} catch (err) {
const error = err as BusinessError;
console.error(`通話PiP停止失敗:${error.code} - ${error.message}`);
}
}
場景三:在線會議場景(協同辦公App)
場景需求
多人在線會議中,用户需要邊查看會議文檔邊參與討論,會議窗口縮小為畫中畫;支持靜音、關閉攝像頭、掛斷、打開麥克風等操作;會議主持人可強制關閉參會者的畫中畫(通過狀態同步);PC端支持調整畫中畫窗口大小。
關鍵實現要點
1. 會議模板配置與多實例同步
// 會議場景PiP配置
private initMeetingPiPConfig(): PiPWindow.PiPConfiguration {
// 會議狀態存儲:用於多實例同步(如主持人控制)
const meetingStorage = new LocalStorage({
isHost: true,
meetingStatus: 'ongoing'
});
return {
context: this.getUIContext().getHostContext() as Context,
componentController: this.xComponentController,
templateType: PiPTemplateType.VIDEO_MEETING, // 會議模板
contentWidth: 1920,
contentHeight: 1080, // PC端會議比例
controlGroups: [
PiPWindow.VideoMeetingControlGroup.HANG_UP_BUTTON,
PiPWindow.VideoMeetingControlGroup.MUTE_SWITCH,
PiPWindow.VideoMeetingControlGroup.CAMERA_SWITCH,
PiPWindow.VideoMeetingControlGroup.MICROPHONE_SWITCH
],
localStorage: meetingStorage, // 同步會議狀態
defaultWindowSizeType: 2 // PC端默認大窗
};
}
2. 主持人控制邏輯(狀態同步)
// 主持人關閉參會者PiP
private closeAttendeePiP(attendeeId: string) {
// 通過LocalStorage同步狀態
this.meetingStorage.set('forceClosePiP', attendeeId);
// 監聽參會者PiP狀態
this.meetingStorage.on('change', (key) => {
if (key === 'forceClosePiP' && this.attendeeId === this.meetingStorage.get<string>('forceClosePiP')) {
this.stopMeetingPiP();
Toast.show({ message: '主持人已關閉畫中畫模式' });
}
});
}
場景四:視頻直播場景(電商直播App)
場景需求
用户觀看電商直播時,可切換到商品詳情頁查看信息,直播窗口縮小為畫中畫;支持播放/暫停、靜音操作;畫中畫窗口顯示直播狀態(如"正在秒殺");返回直播頁面時恢復全屏。
核心實現代碼
@Component
struct LiveStreamingPage {
private xComponentController: XComponentController = new XComponentController();
private pipController: PiPWindow.PiPController | undefined;
private liveStatus: string = '秒殺中'; // 直播狀態
// 直播場景PiP配置
private initLivePiPConfig(): PiPWindow.PiPConfiguration {
// 自定義直播狀態UI
const liveOverlayController = new LiveStatusOverlayController(this.liveStatus);
return {
context: this.getUIContext().getHostContext() as Context,
componentController: this.xComponentController,
templateType: PiPTemplateType.VIDEO_LIVE, // 直播模板
contentWidth: 1280,
contentHeight: 720,
controlGroups: [
PiPWindow.VideoLiveControlGroup.VIDEO_PLAY_PAUSE,
PiPWindow.VideoLiveControlGroup.MUTE_SWITCH
],
customUIController: liveOverlayController, // 直播狀態疊加
defaultWindowSizeType: 1
};
}
// 直播控制事件處理
private registerLivePiPListeners() {
if (!this.pipController) return;
this.pipController.on('controlEvent', (param) => {
switch (param.controlType) {
case PiPControlType.VIDEO_PLAY_PAUSE:
this.toggleLivePlay(param.status === PiPControlStatus.PLAY);
break;
case PiPControlType.MUTE_SWITCH:
this.setLiveMute(param.status === PiPControlStatus.CLOSE);
break;
}
});
// 直播狀態更新(如從"秒殺中"改為"正常直播")
this.updateLiveStatus = (newStatus: string) => {
this.liveStatus = newStatus;
this.liveOverlayController.updateParams({ status: newStatus });
};
}
}
三、進階技巧:性能優化與用户體驗提升
3.1 性能優化要點
- 資源釋放:所有監聽事件(
stateChange、controlEvent等)在PiP停止後必須通過off()移除,避免內存泄漏; - 渲染優化:PiP啓動時暫停主窗口的視頻渲染和音頻播放,僅保留PiP窗口的媒體流;
- 尺寸適配:通過
updateContentSize()方法同步媒體源尺寸變化,避免PiP窗口拉伸; - 異步處理:所有PiP相關API(
create、startPiP、stopPiP等)均為異步操作,需通過Promise或async/await處理,避免阻塞主線程。
3.2 用户體驗優化技巧
-
自動啓動配置:通過
setAutoStartEnabled(true)設置返回桌面時自動啓動PiP,但需先通過getPiPSettingSwitch()(API 20+)檢查系統開關狀態,避免配置失效;// 檢查系統畫中畫開關狀態(API 20+) async checkSystemPiPSwitch() { if (!this.pipController) return false; try { const isSwitchOpen = await this.pipController.getPiPSettingSwitch(); return isSwitchOpen; } catch (err) { const error = err as BusinessError; console.error(`獲取系統PiP開關失敗:${error.code}`); return false; } } - 狀態提示:PiP啓動、恢復、異常時通過Toast或通知提示用户,如"畫中畫已啓動,可拖動窗口調整位置";
- 窗口拖動:系統默認支持PiP窗口拖動,無需額外開發,避免在窗口邊緣添加遮擋元素;
- 恢復記憶:從PiP恢復到主窗口時,保留原播放進度、通話狀態、會議設置等,提升連貫性。
3.3 錯誤處理與兼容性適配
(1)常見錯誤碼處理
| 錯誤碼 | 含義 | 處理方案 |
|---|---|---|
| 401 | 參數錯誤 | 檢查context、componentController是否為空;驗證controlGroups與templateType是否匹配;確保尺寸參數為正整數 |
| 801 | 設備不支持 | 提示用户當前設備不支持畫中畫功能,隱藏PiP入口 |
| 1300012 | PiP狀態異常 | 調用stopPiP()重置狀態,重新創建控制器 |
| 1300013 | 創建窗口失敗 | 檢查XComponent配置是否正確;確保應用無懸浮窗權限限制 |
(2)跨API版本適配
- API 11-12:基礎功能支持(創建、啓動、停止、基礎控制組),避免使用
customUIController、localStorage等高級特性; - API 15+:支持窗口尺寸監聽(
pipWindowSizeChange)、PiPWindowInfo獲取,可實現更精細的尺寸適配; - API 20+:支持PC/2in1設備、
getPiPSettingSwitch(),需適配橫屏場景的窗口比例。
四、實際開發避坑指南
- XComponent配置錯誤:XComponent的
type必須設置為SURFACE,且controller必須與PiPConfiguration中的componentController一致,否則會導致內容無法渲染到PiP窗口; - 控制組與模板不匹配:如視頻播放模板不能添加通話控制組(
MICROPHONE_SWITCH),否則會導致控制欄不顯示,需嚴格按照場景-模板-控制組適配關係配置; - 上下文獲取錯誤:
PiPConfiguration中的context必須通過getUIContext().getHostContext()獲取,不能直接使用AbilityContext,否則會導致權限異常; - 內存泄漏:忘記移除監聽事件是最常見的內存泄漏原因,需在
PiPState.STOPPED狀態或頁面銷燬時調用off()移除所有監聽; - 音視頻衝突:PiP窗口和主窗口同時播放音頻會導致聲音重疊,需在PiP啓動時暫停主窗口音頻,停止時恢復。
總結
鴻蒙@ohos.PiPWindow模塊通過模板化設計、全生命週期管理、跨設備兼容等特性,為開發者提供了高效的畫中畫開發方案。無論是視頻播放、通話、會議還是直播場景,都能通過合理配置模板和控制組,快速實現核心功能,再結合自定義UI、狀態同步、性能優化等進階技巧,打造出貼合用户需求的優質體驗。在實際開發中,需重點關注場景與模板的適配、狀態同步、錯誤處理和跨版本兼容,同時遵循用户體驗最佳實踐,讓畫中畫功能真正成為提升應用競爭力的加分項。