插槽系統(Slot System)是AliPlayerKit的核心架構設計,通過組件化與可插拔機制將播放器UI拆分為獨立插槽組件,由統一系統管理,實現播放器UI解耦、組合與靈活擴充。
概念介紹
什麼是插槽系統?
插槽系統 (Slot System) 是 AliPlayerKit 的核心架構設計之一,是一套 UI 組件的可插拔管理機制。
它允許開發人員在播放器介面的任意位置動態添加、移除或替換 UI 組件,實現高度可定製的播放器介面。
什麼是插槽?
插槽 (Slot) 是一個預留的視圖容器,用於承載一組相關的 UI 組件。
每個插槽都有獨立的生命週期、顯隱邏輯和事件管理,可以根據播放狀態、情境類型等條件自動控制內部組件的顯示或隱藏。
功能特性
解決問題
UI 組件與播放器邏輯耦合。
自訂介面需要修改架構源碼。
不同情境下組件組合缺乏靈活性。
組件生命週期管理困難。
核心價值
插槽系統將播放器UI組件獨立拆解,開發人員可自主選擇使用方式。
使用方式 | 說明 | 優勢 |
使用預設介面 | 播放器組件使用官方預設 UI | 簡化接入流程,降低接入成本,低代碼接入 |
自訂使用 | 替換部分或全部插槽實現 | 滿足各類豐富的 UI 需求 |
不使用插槽 | 僅使用播放能力 | 純播放情境,無 UI 依賴 |
架構優勢:
解耦:UI組件與播放器核心邏輯分離,職責清晰。
靈活:運行時動態組合、替換UI組件。
可擴充:無需修改架構即可自訂UI,擴充性強。
關注點分離:UI樣式(XML布局)與商務邏輯(Slot類處理)分離。
核心能力
能力 | 說明 |
動態組合 | 運行時自由組合 UI 組件。 |
熱替換 | 無需重啟即可替換組件實現。 |
情境適配 | 不同播放情境自動切換 UI 行為。 |
內建群組件
AliPlayerKit 提供了一組開箱即用的內建群組件,覆蓋常見的播放器 UI 需求。
插槽的類型
插槽類型 | 說明 | 預設實現 |
PLAYER_SURFACE | 視頻畫面顯示,支援多種渲染視圖 | DisplayViewSlot / SurfaceViewSlot / TextureViewSlot |
FULLSCREEN | 全屏管理,處理螢幕方向切換 | FullscreenSlot |
GESTURE_CONTROL | 手勢控制,處理單擊/雙擊/長按/拖動 | GestureControlSlot |
LANDSCAPE_HINT | 橫屏觀看提示,引導使用者全屏 | LandscapeHintSlot |
COVER | 封面圖,播放前顯示視頻封面 | CoverSlot |
CENTER_DISPLAY | 中心顯示,顯示倍速/亮度/音量等狀態 | CenterDisplaySlot |
PLAY_STATE | 播放狀態,顯示載入中/錯誤提示等 | PlayStateSlot |
LOG_PANEL | 日誌面板,調試時顯示播放器日誌 | LogPanelSlot |
TOP_BAR | 頂部控制欄,顯示返回/標題/設定等 | TopBarSlot |
BOTTOM_BAR | 底部控制欄,顯示播放控制/進度條等 | BottomBarSlot |
SETTING_MENU | 設定菜單,顯示倍速/清晰度/鏡像等設定(無 UI 事件橋接層,實際 UI 由 | SettingMenuSlot |
OPTION_PANEL | 選項面板,橫屏情境下顯示倍速/清晰度等獨立選擇面板 | OptionPanelSlot |
情境適配
AliPlayerKit定義了5種播放情境,不同情境下插槽行為自動適配:
情境 | 說明 | 典型應用 |
VOD | 點播情境,支援所有功能。 | 常規視頻播放。 |
LIVE | 直播情境,禁用時間軸操作。 | 即時直播流。 |
VIDEO_LIST | 列表播放情境,禁用垂直手勢。 | 資訊流、短視頻列表。 |
RESTRICTED | 受限播放情境,限制跳躍播放。 | 教育培訓、考試監控。 |
MINIMAL | 最小化播放情境,僅顯示視頻畫面。 | 背景視頻、裝飾視頻。 |
插槽可見度規則:
插槽 | VOD | LIVE | VIDEO_LIST | RESTRICTED | MINIMAL |
PLAYER_SURFACE | ✓ | ✓ | ✓ | ✓ | ✓ |
FULLSCREEN | ✓ | ✓ | ✓ | ✓ | ✗ |
GESTURE_CONTROL | ✓ | ✓ | ✓ | ✓ | ✗ |
LANDSCAPE_HINT | ✓ | ✓ | ✓ | ✓ | ✗ |
COVER | ✓ | ✗ | ✓ | ✗ | ✗ |
CENTER_DISPLAY | ✓ | ✓ | ✓ | ✓ | ✗ |
PLAY_STATE | ✓ | ✓ | ✓ | ✓ | ✗ |
LOG_PANEL | 可配置 | 可配置 | 可配置 | 可配置 | ✗ |
TOP_BAR | ✓ | ✓ | ✓ | ✓ | ✗ |
BOTTOM_BAR | ✓ | ✓ | ✓ | ✓ | ✗ |
SETTING_MENU | ✓ | ✓ | ✓ | ✓ | ✗ |
OPTION_PANEL | ✓ | ✓ | ✓ | ✓ | ✗ |
手勢行為差異:
手勢 | VOD | LIVE | VIDEO_LIST | RESTRICTED | MINIMAL |
單擊 | 顯示/隱藏控制欄 | 顯示/隱藏控制欄 | 顯示/隱藏控制欄 | 顯示/隱藏控制欄 | 禁用 |
雙擊 | 切換播放/暫停 | 切換播放/暫停 | 切換播放/暫停 | 切換播放/暫停 | 禁用 |
長按 | 2倍速播放 | 禁用 | 2倍速播放 | 禁用 | 禁用 |
水平拖動 | 進度跳轉 | 禁用 | 進度跳轉 | 禁用 | 禁用 |
左側垂直拖動 | 亮度調節 | 亮度調節 | 禁用 | 亮度調節 | 禁用 |
右側垂直拖動 | 音量大小 | 音量大小 | 禁用 | 音量大小 | 禁用 |
底部控制欄差異:
控制項 | VOD | LIVE | VIDEO_LIST | RESTRICTED | MINIMAL |
播放/暫停按鈕 | ✓ | ✓ | ✓ | ✓ | ✗ |
進度條 | 可拖拽 | 不可拖拽 | 可拖拽 | 不可拖拽 | ✗ |
時間顯示 | ✓ | ✓ | ✓ | ✓ | ✗ |
重新整理按鈕 | ✗ | ✓ | ✗ | ✗ | ✗ |
全屏按鈕 | ✓ | ✓ | ✓ | ✓ | ✗ |
基礎使用
插槽系統提供三種使用原則,開發人員可根據需求選擇合適的方式:
策略 | 說明 | 適用情境 |
使用預設介面 | 最簡單的使用方式,播放器組件將使用預設介面。 | 快速整合、標準播放情境。 |
自訂部分插槽 | 只自訂特定插槽,其他使用預設介面。 | 局部定製、保留預設互動。 |
完全自訂介面 | 自訂所有插槽,建立完全個人化的播放器介面。 | 深度定製、品牌專屬UI。 |
策略一:使用預設配置
最簡單的使用方式,直接使用內建的插槽配置:
// 1. 建立控制器並配置播放資料
AliPlayerController controller = new AliPlayerController(this);
controller.configure(new AliPlayerModel.Builder()
.videoSource(videoSource)
.build());
// 2. 擷取播放器視圖並綁定(自動使用預設插槽)
AliPlayerView playerView = findViewById(R.id.player_view);
playerView.attach(controller);策略二:自訂部分插槽
只自訂特定插槽,其他使用預設介面。例如,只自訂頂部欄:
// 1. 建立控制器並配置播放資料
AliPlayerController controller = new AliPlayerController(this);
controller.configure(new AliPlayerModel.Builder()
.videoSource(videoSource)
.build());
// 2. 擷取播放器視圖
AliPlayerView playerView = findViewById(R.id.player_view);
// 3. 通過 SlotManager 註冊需要自訂的插槽(覆蓋預設實現)
SlotManager slotManager = playerView.getSlotManager();
slotManager.register(SlotType.TOP_BAR, parent -> new MyTopBarSlot(parent.getContext()));
// 4. 綁定控制器
playerView.attach(controller);策略三:完全自訂介面
自訂所有插槽,建立完全個人化的播放器介面:
// 1. 建立控制器並配置播放資料
AliPlayerController controller = new AliPlayerController(this);
controller.configure(new AliPlayerModel.Builder()
.videoSource(videoSource)
.build());
// 2. 擷取播放器視圖
AliPlayerView playerView = findViewById(R.id.player_view);
// 3. 清除預設插槽,從空白開始
SlotManager slotManager = playerView.getSlotManager();
slotManager.clearAll();
// 4. 註冊所有插槽
slotManager.register(SlotType.PLAYER_SURFACE, parent -> new MySurfaceSlot(parent.getContext()));
slotManager.register(SlotType.COVER, parent -> new MyCoverSlot(parent.getContext()));
slotManager.register(SlotType.TOP_BAR, parent -> new MyTopBarSlot(parent.getContext()));
slotManager.register(SlotType.BOTTOM_BAR, parent -> new MyBottomBarSlot(parent.getContext()));
// ... 註冊其他插槽
// 5. 綁定控制器
playerView.attach(controller); 進階使用
如何?動態切換插槽?
運行時動態切換插槽實現:
// 擷取管理器
SlotManager slotManager = playerView.getSlotManager();
// 切換 Surface 類型
slotManager.register(SlotType.PLAYER_SURFACE,
parent -> new TextureViewSlot(parent.getContext()));
slotManager.rebuildSlots(); 如何?自訂插槽?
AliPlayerKit 提供兩種方式實現自訂插槽,開發人員可根據需求選擇合適的方式。
方式一:繼承 BaseSlot(推薦)
繼承 BaseSlot 是最簡單的方式,架構已封裝好生命週期管理、事件訂閱等通用邏輯。
適用情境:大多數 UI 插槽,如封面、控制欄、狀態顯示等。
建立布局檔案。
在
res/layout/目錄下建立布局檔案:<!-- res/layout/my_cover_layout.xml --> <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/cover_image" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" /> </FrameLayout>建立 Slot 類。
繼承
BaseSlot並重寫必要方法:public class MyCoverSlot extends BaseSlot { public MyCoverSlot(@NonNull Context context) { super(context); } @Override protected int getLayoutId() { return R.layout.my_cover_layout; // 返回布局 ID } @Override public void onBindData(@NonNull AliPlayerModel model) { // 綁定資料 ImageView coverImage = findViewByIdCompat(R.id.cover_image); Glide.with(getContext()).load(model.getCoverUrl()).into(coverImage); } @Override public void onUnbindData() { // 清理資源 ImageView coverImage = findViewByIdCompat(R.id.cover_image); Glide.with(getContext()).clear(coverImage); } }註冊使用。
SlotManager slotManager = playerView.getSlotManager(); slotManager.register(SlotType.COVER, parent -> new MyCoverSlot(parent.getContext())); playerView.attach(controller);
方式二:實現 ISlot 介面
直接實現 ISlot 介面可以獲得更高的靈活性,但需要自行處理生命週期。
適用情境:需要完全控制視圖層級,或需要繼承特定 View 類型(如 SurfaceView、TextureView)。
建立布局檔案。
在
res/layout/目錄下建立布局檔案:<!-- res/layout/my_cover_layout.xml --> <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/cover_image" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" /> </FrameLayout>建立 Slot 類。
實現
ISlot介面,並通過組合SlotBehavior獲得插槽能力:public class MySurfaceSlot extends FrameLayout implements ISlot, ISurfaceProvider { // 通過組合獲得插槽核心能力 private final SlotBehavior slotBehavior = new SlotBehavior(); private SlotHost host; public MySurfaceSlot(@NonNull Context context) { super(context); View.inflate(context, R.layout.my_surface_layout, this); } @Override public void onAttach(@NonNull SlotHost host) { // 1. 委託給 SlotBehavior 處理生命週期 slotBehavior.attach(host); // 2. 儲存宿主引用 this.host = host; // 3. 設定 Surface setupSurfaceProvider(host); } @Override public void onDetach() { if (host != null) { onSurfaceDestroyed(host); } slotBehavior.detach(); host = null; } @Override public void onBindData(@NonNull AliPlayerModel model) { // 資料繫結邏輯 } @Override public void onUnbindData() { // 資料清理邏輯 } @Override public void setupSurfaceProvider(@Nullable SlotHost host) { // Surface 設定邏輯 } }註冊使用。
SlotManager slotManager = playerView.getSlotManager(); slotManager.register(SlotType.COVER, parent -> new MyCoverSlot(parent.getContext())); playerView.attach(controller);
方式對比
特性 | 繼承 BaseSlot | 實現 ISlot 介面 |
代碼量 | 少,只需關注商務邏輯 | 多,需手動處理生命週期 |
靈活性 | 中等,繼承 FrameLayout | 高,可繼承任意 View 類型 |
生命週期 | 架構自動管理 | 需手動委託給 SlotBehavior |
推薦情境 | 大多數 UI 插槽 | 需要特殊檢視類型 |
如何? UI 還原?
在實際專案中需要還原播放器UI時,以下是不同情境下的處理方式:
情境一:官方未提供所需插槽
如果播放器組件官方未提供您需要的插槽類型,需要自行實現:
建立自訂插槽。
建議將自訂插槽放在
ui/slots/custom目錄下,與官方實現區分:your-module/src/main/java/com/yourpackage/ └── ui/ └── slots/ └── custom/ ├── MyDanmakuSlot.java // 彈幕插槽 ├── MySubtitleSlot.java // 字幕插槽 └── MyWatermarkSlot.java // 浮水印插槽
情境二:官方提供了插槽但不需要
如果播放器組件官方提供了插槽,但您的情境不需要,有兩種處理方式:
方式一:配置禁用(推薦)。
在
SlotConstants的createDefaultConfigs()方法中刪除對應的配置項:// 修改前:SlotManager 會注入預設組件 configs.add(new SlotConfig.Builder() .type(SlotType.LOG_PANEL) .excludeScenes(createSet(SceneType.MINIMAL)) .condition(AliPlayerKit::isLogPanelEnabled) .build()); // 修改後:注釋或刪除該配置,SlotManager 將不再注入預設組件 // configs.add(new SlotConfig.Builder() // .type(SlotType.LOG_PANEL) // ... // .build());方式二:物理刪除(不推薦)。
直接刪除對應的 Slot 類檔案及其相關資源,不建議使用。
情境三:官方插槽 UI 樣式不滿足需求
若官方插槽的UI樣式不滿足需求,可直接修改XML布局檔案實現UI還原。得益於樣式與邏輯分離的設計,布局修改不影響商務邏輯。
找到對應的布局檔案。
布局檔案位於
playerkit/src/main/res/layout/目錄,命名規則為layout_{slot_type}.xml:插槽類型
布局檔案
TOP_BAR
layout_top_bar_slot.xmlBOTTOM_BAR
layout_bottom_bar_slot.xmlCOVER
layout_cover_slot.xmlPLAY_STATE
layout_play_state_slot.xml…
…
修改布局檔案。
直接修改 XML 檔案中的樣式屬性,注意保持控制項 ID 不變:
<!-- 修改前:官方預設樣式 --> <LinearLayout android:background="#80000000" android:padding="8dp"> <ImageView android:id="@+id/iv_back" android:layout_width="40dp" android:layout_height="40dp" /> </LinearLayout> <!-- 修改後:自訂樣式(保持 ID 不變) --> <LinearLayout android:background="@drawable/custom_top_bar_bg" android:padding="12dp"> <ImageView android:id="@+id/iv_back" android:layout_width="48dp" android:layout_height="48dp" android:src="@drawable/ic_custom_back" /> </LinearLayout>架構約束。
通過以下約束保證自訂代碼與官方代碼不衝突:
控制項ID需固定,Slot類通過ID尋找控制項並綁定事件。
布局結構可調整,但核心控制項必須保留。
商務邏輯無需修改,自動適配新布局。
升級相容建議。
如果需要對 UI 進行大量修改,我們建議採用 自訂 Slot 策略 來避免後續升級 AliPlayerKit 時的衝突:
請勿直接修改官方布局檔案或Slot類。
建立V2版本(如
TopBarSlotV2.java和layout_top_bar_slot_v2.xml)通過
SlotManager註冊 或 修改SlotConstants替換預設實現
public class TopBarSlotV2 extends BaseSlot { @Override protected int getLayoutId() { return R.layout.layout_top_bar_slot_v2; } }
如何?無頭插槽?
插槽系統不僅實現UI組件化,更支援商務邏輯組件化。除UI插槽外,還可通過無頭插槽(Headless Slot)不渲染任何UI、僅實現商務邏輯完成功能擴充。
樣本:全屏管理插槽
官方提供的FullscreenSlot是無頭插槽的典型樣本:它僅負責全屏切換邏輯管理,無介面渲染功能。
public class FullscreenSlot extends BaseSlot {
@Override
protected int getLayoutId() {
return 0; // 返回 0 表示無布局,不渲染 UI
}
@Override
public void onAttach(@NonNull SlotHost host) {
super.onAttach(host);
// 監聽全屏切換事件
}
@Override
protected void onEvent(@NonNull PlayerEvent event) {
if (event instanceof FullscreenEvents.Toggle) {
// 處理全屏切換邏輯
toggleFullscreen();
}
}
private void toggleFullscreen() {
// 純邏輯處理:修改 Activity 方向、調整系統 UI 等
}
}實現方式:
方式 | 說明 | 適用情境 |
繼承 | 簡單,繼承 FrameLayout 但不渲染。 | 大多數無頭插槽。 |
實現 | 靈活,完全不繼承 View。 | 純邏輯組件。 |
設計價值:
無頭插槽使商務邏輯同樣獲得插槽系統的優勢:
統一生命週期管理。
與播放器狀態自動同步。
可插拔替換。
與UI插槽平等協作。
細粒度控制
AliPlayerKit 提供了Element Registry架構,允許開發人員在保留預設插槽 UI 的前提下,按元素層級隱藏子組件或禁用手勢。
架構概述
細粒度控制採用 雙模式 設計:
模式 | 適用情境 | 使用方式 |
靜態元素註冊 | View 類型的 UI 元素(按鈕、文本等) |
|
行為元素查詢 | 手勢、事件驅動的功能 |
|
生命週期:
onAttach(host) ← 子類初始化 View
↓
dispatchRegisterElements() ← 架構注入 SlotType + 觸發 onRegisterElements()
↓
onBindData(model) ← 架構:緩衝隱藏配置 → 應用可見度;子類:處理資料
↓
onUnbindData() ← 架構:重設可見度
↓
onDetach() ← 架構:清理註冊表 + SlotTypeonRegisterElements() 由架構在 onAttach() 返回後立即自動調用(通過 dispatchRegisterElements() 包級方法)。架構在調用前會自動注入 SlotType,子類無需覆寫 getSlotType() 方法—SlotType 由架構在 attachSlot() 時根據註冊表自動確定。此階段屬於 Setup/Attach 生命週期階段,與 Data 生命週期階段(onBindData / onUnbindData)職責分離。
基本用法
聲明隱藏配置(使用者視角):
SlotManager slotManager = playerView.getSlotManager();
slotManager.hideElements(SlotType.TOP_BAR, SlotElements.TopBar.BACK, SlotElements.TopBar.SNAPSHOT);
slotManager.hideElements(SlotType.GESTURE_CONTROL, SlotElements.GestureControl.DOUBLE_TAP);
playerView.attach(controller);自訂插槽中使用
靜態元素註冊(繼承 BaseSlot):
public class MyTopBarSlot extends BaseSlot {
@Overridepublic void onAttach(@NonNull SlotHost host) {
super.onAttach(host);
// 初始化 View ...
mBtnBack = findViewByIdCompat(R.id.btn_back);
mTvTitle = findViewByIdCompat(R.id.tv_title);
}
@Overrideprotected void onRegisterElements() {
// 架構在 onAttach 後自動調用,聲明式註冊 架構自動管理可見度// SlotType 由架構自動注入,子類無需關心
registerElement(SlotElements.TopBar.BACK, mBtnBack);
registerElement(SlotElements.TopBar.TITLE, mTvTitle);
}
// onBindData 中無需手動處理可見度,架構全自動
}架構優勢:子類零配置獲得 SlotType。registerElement(key, view) 便捷重載一行註冊,內部自動封裝為 view.setVisibility(visible ? VISIBLE : GONE)。view 參數可為 null(靜默跳過,不崩潰),適配不同布局下控制項可能不存在的情況。
新舊風格對比:
// 簡化前(舊風格) 已廢棄@Nullable @Overrideprotected SlotType getSlotType() { return SlotType.TOP_BAR; }
@Overrideprotected void onRegisterElements() {
registerElement(SlotElements.TopBar.BACK, new SlotElementHandle() {
@Overridepublic void setVisible(boolean visible) {
mIvBack.setVisibility(visible ? View.VISIBLE : View.GONE);
}
});
}
// 簡化後(新風格) 推薦@Overrideprotected void onRegisterElements() {
registerElement(SlotElements.TopBar.BACK, mIvBack);
registerElement(SlotElements.TopBar.TITLE, mTvTitle);
}行為元素查詢(手勢/事件驅動):
public class MyGestureSlot extends BaseSlot {
private void handleDoubleTap() {
// 運行時查詢:如果被隱藏則跳過if (!isElementVisible(SlotElements.GestureControl.DOUBLE_TAP)) {
return;
}
// 處理雙擊邏輯...
}
}自訂 SlotElementHandle(控制多個 View 或非標準行為時使用):
當需要控制多個 View 或需要非標準的可見度行為時,仍使用 SlotElementHandle 方式:
// 進度條控制兩個 View
registerElement(SlotElements.BottomBar.PROGRESS, new SlotElementHandle() {
@Overridepublic void setVisible(boolean visible) {
mSeekBar.setVisibility(visible ? View.VISIBLE : View.GONE);
mTvTime.setVisibility(visible ? View.VISIBLE : View.GONE);
}
});重設機制
架構在 onUnbindData() 時自動將所有登入元素恢複為可見狀態。當切換視訊源(重新調用 onBindData())時,會根據新配置重新應用可見度,確保狀態不會殘留。
可用元素常量
所有可控元素定義在 SlotElements 類中:
插槽類型 | 常量類 | 可用元素 |
TOP_BAR |
| BACK, TITLE, DOWNLOAD, SNAPSHOT, PIP, SETTINGS |
BOTTOM_BAR |
| PLAY, PROGRESS, REFRESH, SUBTITLE, FULLSCREEN, SPEED, QUALITY |
SETTING_MENU |
| VOLUME, BRIGHTNESS, SPEED, TRACK_INFO, LOOP, MUTE, MIRROR_MODE, ROTATE_MODE, SCALE_MODE, SUBTITLE |
CENTER_DISPLAY |
| VOLUME, BRIGHTNESS, SPEED |
PLAY_STATE |
| ERROR_ICON, ERROR_MESSAGE |
GESTURE_CONTROL |
| SINGLE_TAP, DOUBLE_TAP, LONG_PRESS, HORIZONTAL_DRAG, LEFT_VERTICAL_DRAG, RIGHT_VERTICAL_DRAG |
層級控制
AliPlayerKit 的插槽系統採用顯式層級控制機制,通過 order 值定義每個插槽在視圖樹中的渲染順序。內建插槽以 10 為間距遞增,為自訂插槽預留了充足的插入空間。
設計理念
播放器介面由多個插槽層疊組成,層級順序直接決定了 UI 的遮蓋關係。架構通過以下設計實現靈活且可控的層級管理:
顯式定義:每個插槽通過
order值聲明自身的層級位置,語義清晰。間距預留:內建插槽間距為 10,開發人員可在任意兩個內建插槽之間插入自訂插槽。
統一排序:所有插槽(內建 + 自訂)在
SlotHostLayout重建時統一按order值排序渲染。
內建插槽層級表
插槽類型 | Order 值 | 層級說明 | 功能描述 |
PLAYER_SURFACE | 10 | 底層 | 視頻畫面渲染 |
FULLSCREEN | 15 | 邏輯層 | 全屏切換管理(無 UI) |
GESTURE_CONTROL | 20 | 互動層 | 手勢識別與控制 |
LANDSCAPE_HINT | 30 | 提示層 | 橫屏觀看引導 |
COVER | 40 | 覆蓋層 | 視頻封面圖 |
CENTER_DISPLAY | 50 | 資訊層 | 倍速/亮度/音量狀態 |
PLAY_STATE | 60 | 狀態層 | 載入中/錯誤提示 |
LOG_PANEL | 70 | 調試層 | 日誌面板 |
TOP_BAR | 80 | 控制層 | 頂部控制欄 |
BOTTOM_BAR | 90 | 控制層 | 底部控制欄 |
SETTING_MENU | 100 | 菜單層 | 設定菜單浮層 |
OPTION_PANEL | 110 | 菜單層 | 選項面板(橫屏倍速/清晰度) |
層級規則:order 值越小越靠底層(先渲染、先被遮蓋),order 值越大越靠頂層(後渲染、後遮蓋其他)。
自訂插槽層級
通過 CustomSlotType 類,開發人員可以建立自訂插槽並指定其層級位置。
CustomSlotType 構造參數:
參數 | 類型 | 說明 |
key | String | 插槽唯一標識,用於區分不同的自訂插槽 |
order | int | 層級順序值,決定渲染位置 |
使用樣本:建立浮水印插槽
// 1. 定義自訂插槽類型(order=25,位於 GESTURE_CONTROL(20) 和 LANDSCAPE_HINT(30) 之間)
CustomSlotType WATERMARK = new CustomSlotType("watermark", 25);
// 2. 通過 SlotManager 註冊構建器
SlotManager slotManager = playerView.getSlotManager();
slotManager.register(WATERMARK, parent -> new WatermarkSlot(parent.getContext()));
// 3. 綁定到播放器視圖
playerView.attach(controller);自訂插槽實現樣本:
public class WatermarkSlot extends BaseSlot {
public WatermarkSlot(@NonNull Context context) {
super(context);
}
@Overrideprotected int getLayoutId() {
return R.layout.layout_watermark_slot;
}
@Overridepublic void onBindData(@NonNull AliPlayerModel model) {
// 設定浮水印內容
TextView tvWatermark = findViewByIdCompat(R.id.tv_watermark);
tvWatermark.setText(model.getWatermarkText());
}
@Overridepublic void onUnbindData() {
// 清理
}
}層級定序
SlotHostLayout 在執行 rebuildSlots() 時,會將所有登入的插槽(內建 + 自訂)合并,並按照以下規則排序渲染:
規則 | 說明 |
按 order 值升序 | order 值從小到大排列,值小的先添加到視圖樹(位於底層) |
相同 order 值 | 按註冊順序排列(先註冊的在下,後註冊的在上) |
無限制範圍 | 自訂插槽可使用任意整數值,包括負數或大於 100 的值 |
設計原則
為什麼選擇間距 10?
內建插槽的 order 值以 10 為步長遞增,這意味著每兩個相鄰內建插槽之間有 9 個可用的整數值。例如:
LANDSCAPE_HINT(30) 和 COVER(40) 之間可插入 order 為 31~39 的自訂插槽
TOP_BAR(80) 和 BOTTOM_BAR(90) 之間可插入 order 為 81~89 的自訂插槽
這種設計在滿足大多數情境的同時,保持了 order 值的可讀性。
如何選擇合適的 order 值?
需求 | 推薦 order 值 | 說明 |
視頻畫面之上的背景層 | 11~19 | 位於 PLAYER_SURFACE 之上、GESTURE_CONTROL 之下 |
封面之上的浮水印/字幕 | 41~49 | 位於 COVER 之上、CENTER_DISPLAY 之下 |
控制欄附近的彈幕 | 81~89 | 位於 TOP_BAR 之上、BOTTOM_BAR 之下 |
最頂層的全域浮層 | >110 | 位於所有內建插槽之上 |
向後相容保證:
內建插槽的 order 值是架構契約的一部分,不會在後續版本中變更
新增內建插槽時,會使用當前間隙中的值(如 15、25 等),不影響已有的 order 分配
自訂插槽的 order 值由開發人員完全控制,架構不會幹預
完整樣本
以下樣本展示了如何定義和註冊多個自訂插槽,構建一個包含浮水印、彈幕和字幕的播放器介面:
// ==================== 定義自訂插槽類型 ====================// 浮水印插槽:位於 GESTURE_CONTROL(20) 和 LANDSCAPE_HINT(30) 之間public static final CustomSlotType WATERMARK = new CustomSlotType("watermark", 25);
// 字幕插槽:位於 CENTER_DISPLAY(50) 和 PLAY_STATE(60) 之間public static final CustomSlotType SUBTITLE = new CustomSlotType("subtitle", 55);
// 彈幕插槽:位於 TOP_BAR(80) 和 BOTTOM_BAR(90) 之間public static final CustomSlotType DANMAKU = new CustomSlotType("danmaku", 85);
// 全域浮層:位於所有內建插槽之上public static final CustomSlotType GLOBAL_OVERLAY = new CustomSlotType("global_overlay", 120);
// ==================== 註冊插槽 ====================
SlotManager slotManager = playerView.getSlotManager();
// 註冊自訂插槽
slotManager.register(WATERMARK, parent -> new WatermarkSlot(parent.getContext()));
slotManager.register(SUBTITLE, parent -> new SubtitleSlot(parent.getContext()));
slotManager.register(DANMAKU, parent -> new DanmakuSlot(parent.getContext()));
slotManager.register(GLOBAL_OVERLAY, parent -> new GlobalOverlaySlot(parent.getContext()));
// 也可以同時自訂內建插槽
slotManager.register(SlotType.TOP_BAR, parent -> new MyTopBarSlot(parent.getContext()));
// ==================== 綁定到播放器 ====================
playerView.attach(controller);
// 最終渲染順序(從底到頂):// PLAYER_SURFACE(10) → FULLSCREEN(15) → GESTURE_CONTROL(20) → WATERMARK(25)// → LANDSCAPE_HINT(30) → COVER(40) → CENTER_DISPLAY(50) → SUBTITLE(55)// → PLAY_STATE(60) → LOG_PANEL(70) → TOP_BAR(80) → DANMAKU(85) → BOTTOM_BAR(90)// → SETTING_MENU(100) → OPTION_PANEL(110) → GLOBAL_OVERLAY(120)最佳實務
生命週期管理
public class MySlot extends BaseSlot {
@Override
public void onAttach(@NonNull SlotHost host) {
super.onAttach(host);
// 初始化視圖
}
@Override
public void onDetach() {
// 清理資源
super.onDetach();
}
}事件訂閱
@Override
protected List<Class<? extends PlayerEvent>> observedEvents() {
return Arrays.asList(PlayerEvents.StateChanged.class);
}
@Override
protected void onEvent(@NonNull PlayerEvent event) {
if (event instanceof PlayerEvents.StateChanged) {
// 處理狀態變化
}
}Surface選擇
情境 | 推薦類型 | 原因 |
普通播放 | SurfaceView | 效能更好 |
需要動畫 | TextureView | 支援變換 |
後台播放 | 無 Surface | 節省資源 |
樣本參考
專案提供了完整的樣本,位於 playerkit-examples/example-slot-system。
樣本功能
功能 | 說明 |
SurfaceView 切換 | 適合普通播放情境 |
TextureView 切換 | 適合需要動畫的情境 |
空插槽切換 | 適合純音頻播放 |
運行樣本
在 Demo App 中選擇 Slot System 樣本查看效果。
API參考
核心介面
介面/類 | 說明 |
| 插槽介面,定義生命週期 |
| 插槽基類,封裝通用邏輯 |
| 管理器,提供註冊、可見度控制、重建和查詢 |
BaseSlot 方法
方法 | 說明 |
| 返回布局資源 ID |
| 擷取插槽宿主 |
| 控制可見度 |
| 發布事件 |
技術原理
生命週期
插槽採用雙生命週期系統:
生命週期 | 說明 |
View 生命週期 |
|
Data 生命週期 |
|
與 Android 生命週期的關係:
插槽生命週期獨立於Android Activity/Fragment生命週期。若需在應用前後台切換時執行特定操作(如暫停動畫、停止輪詢),可通過以下方式處理:
方式一:監聽播放狀態事件
通過 observedEvents() 監聽播放狀態變化,當播放器暫停時停止動畫:
@Override
protected List<Class<? extends PlayerEvent>> observedEvents() {
return Arrays.asList(PlayerEvents.StateChanged.class);
}
@Override
protected void onEvent(@NonNull PlayerEvent event) {
if (event instanceof PlayerEvents.StateChanged) {
PlayerState state = ((PlayerEvents.StateChanged) event).newState;
if (state == PlayerState.PAUSED || state == PlayerState.STOPPED) {
stopAnimation(); // 暫停動畫
} else if (state == PlayerState.PLAYING) {
startAnimation(); // 恢複動畫
}
}
}方式二:在 Activity/Fragment 中控制
在 Activity 的 onPause()/onResume() 中通過插槽管理器擷取插槽並調用方法:
@Override
protected void onPause() {
super.onPause();
MyAnimationSlot slot = playerView.getSlotManager().getSlot(SlotType.CUSTOM);
if (slot != null) {
slot.pauseAnimation();
}
}
@Override
protected void onResume() {
super.onResume();
MyAnimationSlot slot = playerView.getSlotManager().getSlot(SlotType.CUSTOM);
if (slot != null) {
slot.resumeAnimation();
}
}單向資料流
插槽系統採用單向資料流架構,插槽不直接持有控制器引用,而是通過宿主提供的介面進行解耦互動:
Controller → State / Event → Slot(狀態下行,唯讀)
Slot → Command → Controller(命令上行,只發)狀態下行:兩種方式擷取狀態。
主動查詢:通過
getPlayerStateStore()查詢目前狀態。被動監聽:通過訂閱事件接收狀態變化通知。
命令上行:插槽通過
postEvent()發送命令,由控制器執行,插槽不關心執行細節。
主動查詢狀態:通過 host.getPlayerStateStore() 擷取播放器狀態的唯讀訪問:
@Override
public void onAttach(@NonNull SlotHost host) {
super.onAttach(host);
// 查詢播放狀態
PlayerState state = host.getPlayerStateStore().getPlayState();
// 查詢當前播放位置和總時間長度
long position = host.getPlayerStateStore().getCurrentPosition();
long duration = host.getPlayerStateStore().getDuration();
}被動監聽狀態:通過訂閱事件接收狀態變化:
@Override
protected List<Class<? extends PlayerEvent>> observedEvents() {
return Arrays.asList(
PlayerEvents.StateChanged.class, // 播放狀態變化
PlayerEvents.Info.class // 播放進度更新
);
}
@Override
protected void onEvent(@NonNull PlayerEvent event) {
if (event instanceof PlayerEvents.StateChanged) {
// 處理播放狀態變化
updatePlayPauseIcon(((PlayerEvents.StateChanged) event).newState);
} else if (event instanceof PlayerEvents.Info) {
// 處理播放進度更新
PlayerEvents.Info info = (PlayerEvents.Info) event;
updateProgress(info.currentPosition, info.bufferedPosition, info.duration);
}
}發送命令:通過 postEvent() 發送命令事件觸發播放器行為:
// 播放/暫停切換
postEvent(new PlayerCommand.Toggle(mPlayerId));
// 跳轉到指定位置
postEvent(new PlayerCommand.Seek(mPlayerId, 30000));
// 設定播放速度
postEvent(new PlayerCommand.SetSpeed(mPlayerId, 1.5f));
// 截圖
postEvent(new PlayerCommand.Snapshot(mPlayerId));單向資料流架構徹底解耦插槽與控制器—插槽無需持有控制器引用,只需關注狀態查詢與命令發送,實現關注點分離。
插槽間的橫向隔離與事件規範
為保障架構的穩定性,插槽之間嚴禁直接擷取對方執行個體或進行點對點通訊。
當插槽A(如設定浮層插槽)觸發狀態變更需影響插槽B(如底部控制欄插槽)時:
事件上浮:插槽A通過
postEvent()將事件傳遞至控制器Controller。狀態下沉:由控制器
Controller或StateStore統一處理狀態變更,廣播新狀態至所有監聽插槽。
通過此U型鏈路(事件上浮,狀態下沉)避免網狀耦合與死結風險。
事件攔截提示:覆蓋頁面表層的插槽容器需處理手勢事件下發,避免遮擋底部GestureControlSlot的手勢檢測。
常見問題
onAttach 什麼時候調用?
調用 playerView.attach() 或 slotManager.rebuildSlots() 時觸發。
自訂插槽注意事項?
在
onAttach中調用super.onAttach(host)。在
onDetach前清理資源。
如何調試?
使用 LogHub 查看日誌,TAG 格式:類名.BaseSlot。
高頻崩潰錯例
以下是客戶回函中最易引發崩潰的問題,請嚴格規避:
錯例 1:未調用super.onAttach()導致生命週期斷層
錯誤碼:
public class MySlot extends BaseSlot {
@Override
public void onAttach(@NonNull SlotHost host) {
// 忘記調用 super.onAttach(host)
// 直接初始化視圖
ImageView iv = findViewByIdCompat(R.id.iv_icon);
iv.setOnClickListener(v -> postEvent(...)); // NullPointerException!
}
} 正確代碼:
@Override
public void onAttach(@NonNull SlotHost host) {
super.onAttach(host); // 必須首先調用
// 然後再初始化視圖
ImageView iv = findViewByIdCompat(R.id.iv_icon);
iv.setOnClickListener(v -> postEvent(...));
} 崩潰原因:若未調用super.onAttach(host),則無法初始化slotBehavior、訂閱事件及設定Surface,導致getHost()返回null、postEvent()失敗、事件訂閱無效。
錯例 2:自訂 XML 時 ID 覆蓋導致 NullPointerException
錯誤碼:
<!-- 自訂布局時,ID 與官方預設 ID 衝突 -->
<LinearLayout ...>
<!-- 官方使用 @+id/iv_back,你覆蓋了它 -->
<ImageView
android:id="@+id/iv_back"
android:src="@drawable/my_icon" /> <!-- 官方期望的是返回按鈕 -->
</LinearLayout> 正確做法:
保持核心控制項ID不變:Slot類使用的控制項ID在布局中必須保留。
控制項類型匹配:ID對應的控制項類型需與代碼中的類型一致。
<!-- 保持核心 ID 不變 -->
<LinearLayout ...>
<ImageView
android:id="@+id/iv_back" <!-- 保持 ID,但可以改樣式 -->
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/custom_back" /> <!-- 可以換表徵圖 -->
</LinearLayout> 崩潰原因:若布局中缺失Slot類所需的控制項ID(R.id.iv_back)或控制項類型錯誤,findViewByIdCompat()將引發強轉失敗或點擊事件綁定錯誤。
錯例 3:在建構函式中調用 getHost()
錯誤碼:
public class MySlot extends BaseSlot {
public MySlot(@NonNull Context context) {
super(context);
// 建構函式中 getHost() 返回 null
SlotHost host = getHost(); // null!
}
}正確代碼:
@Override
public void onAttach(@NonNull SlotHost host) {
super.onAttach(host);
// 在 onAttach 之後才能訪問 host
SlotHost host = getHost(); // 正常擷取
}崩潰原因:插槽執行個體構造時尚未掛載到宿主,getHost()將返回null。
錯例 4:onDetach 未清理資源導致記憶體流失
錯誤碼:
public class MySlot extends BaseSlot {
private Handler handler = new Handler();
@Override
public void onAttach(@NonNull SlotHost host) {
super.onAttach(host);
handler.postDelayed(() -> updateUI(), 1000); // 延遲任務
}
@Override
public void onDetach() {
// 忘記移除延遲任務
super.onDetach();
}
}正確代碼:
@Override
public void onDetach() {
handler.removeCallbacksAndMessages(null); // 清理所有延遲任務
super.onDetach();
}崩潰原因:若延遲任務持有Slot引用,將導致記憶體無法被回收(記憶體流失);且任務執行時Slot可能已處於detach狀態。
錯例 5:事件訂閱未在 observedEvents() 中聲明
錯誤碼:
public class MySlot extends BaseSlot {
@Override
public void onAttach(@NonNull SlotHost host) {
super.onAttach(host);
// 期望收到事件,但未在 observedEvents() 中聲明
}
@Override
protected void onEvent(@NonNull PlayerEvent event) {
// 永遠不會被調用!
}
}正確代碼:
@Override
protected List<Class<? extends PlayerEvent>> observedEvents() {
return Arrays.asList(
PlayerEvents.StateChanged.class, // 聲明要訂閱的事件
PlayerEvents.Info.class
);
}
@Override
protected void onEvent(@NonNull PlayerEvent event) {
// 現在可以正常收到事件了
}崩潰原因:BaseSlot通過observedEvents()聲明需訂閱的事件,未聲明則不會訂閱。