全部產品
Search
文件中心

ApsaraVideo VOD:插槽系統

更新時間:Sep 10, 2026

插槽系統(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 由 SettingMenuDialogFragment 彈窗承載)

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 插槽,如封面、控制欄、狀態顯示等。

  1. 建立布局檔案。

    在 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>
  2. 建立 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);
        }
    }
  3. 註冊使用。

    SlotManager slotManager = playerView.getSlotManager();
    slotManager.register(SlotType.COVER, parent -> new MyCoverSlot(parent.getContext()));
    playerView.attach(controller);

方式二:實現 ISlot 介面

直接實現 ISlot 介面可以獲得更高的靈活性,但需要自行處理生命週期。

適用情境:需要完全控制視圖層級,或需要繼承特定 View 類型(如 SurfaceView、TextureView)。

  1. 建立布局檔案。

    在 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>
  2. 建立 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 設定邏輯
        }
    }
  3. 註冊使用。

    SlotManager slotManager = playerView.getSlotManager();
    slotManager.register(SlotType.COVER, parent -> new MyCoverSlot(parent.getContext()));
    playerView.attach(controller);

方式對比

特性

繼承 BaseSlot

實現 ISlot 介面

代碼量

少,只需關注商務邏輯

多,需手動處理生命週期

靈活性

中等,繼承 FrameLayout

高,可繼承任意 View 類型

生命週期

架構自動管理

需手動委託給 SlotBehavior

推薦情境

大多數 UI 插槽

需要特殊檢視類型

如何? UI 還原?

在實際專案中需要還原播放器UI時,以下是不同情境下的處理方式:

情境一:官方未提供所需插槽

如果播放器組件官方未提供您需要的插槽類型,需要自行實現:

  1. 建立自訂插槽。

    建議將自訂插槽放在 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還原。得益於樣式與邏輯分離的設計,布局修改不影響商務邏輯。

  1. 找到對應的布局檔案。

    布局檔案位於 playerkit/src/main/res/layout/ 目錄,命名規則為 layout_{slot_type}.xml:

    插槽類型

    布局檔案

    TOP_BAR

    layout_top_bar_slot.xml

    BOTTOM_BAR

    layout_bottom_bar_slot.xml

    COVER

    layout_cover_slot.xml

    PLAY_STATE

    layout_play_state_slot.xml

    …

    …

  2. 修改布局檔案。

    直接修改 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>
  3. 架構約束。

    通過以下約束保證自訂代碼與官方代碼不衝突:

    1. 控制項ID需固定,Slot類通過ID尋找控制項並綁定事件。

    2. 布局結構可調整,但核心控制項必須保留。

    3. 商務邏輯無需修改,自動適配新布局。

  4. 升級相容建議。

    如果需要對 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 等
    }
}

實現方式:

方式

說明

適用情境

繼承 BaseSlot,getLayoutId() 返回 0

簡單,繼承 FrameLayout 但不渲染。

大多數無頭插槽。

實現 ISlot 介面 + 組合 SlotBehavior

靈活,完全不繼承 View。

純邏輯組件。

設計價值:

無頭插槽使商務邏輯同樣獲得插槽系統的優勢:

  • 統一生命週期管理。

  • 與播放器狀態自動同步。

  • 可插拔替換。

  • 與UI插槽平等協作。

細粒度控制

AliPlayerKit 提供了Element Registry架構,允許開發人員在保留預設插槽 UI 的前提下,按元素層級隱藏子組件或禁用手勢。

架構概述

細粒度控制採用 雙模式 設計:

模式

適用情境

使用方式

靜態元素註冊

View 類型的 UI 元素(按鈕、文本等)

registerElement(key, view)

行為元素查詢

手勢、事件驅動的功能

isElementVisible(key)

生命週期:

onAttach(host)              ← 子類初始化 View
    ↓
dispatchRegisterElements()  ← 架構注入 SlotType + 觸發 onRegisterElements()
    ↓
onBindData(model)           ← 架構:緩衝隱藏配置 → 應用可見度;子類:處理資料
    ↓
onUnbindData()              ← 架構:重設可見度
    ↓
onDetach()                  ← 架構:清理註冊表 + SlotType

onRegisterElements() 由架構在 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

SlotElements.TopBar

BACK, TITLE, DOWNLOAD, SNAPSHOT, PIP, SETTINGS

BOTTOM_BAR

SlotElements.BottomBar

PLAY, PROGRESS, REFRESH, SUBTITLE, FULLSCREEN, SPEED, QUALITY

SETTING_MENU

SlotElements.SettingMenu

VOLUME, BRIGHTNESS, SPEED, TRACK_INFO, LOOP, MUTE, MIRROR_MODE, ROTATE_MODE, SCALE_MODE, SUBTITLE

CENTER_DISPLAY

SlotElements.CenterDisplay

VOLUME, BRIGHTNESS, SPEED

PLAY_STATE

SlotElements.PlayState

ERROR_ICON, ERROR_MESSAGE

GESTURE_CONTROL

SlotElements.GestureControl

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參考

核心介面

介面/類

說明

ISlot

插槽介面,定義生命週期

BaseSlot

插槽基類,封裝通用邏輯

SlotManager

管理器,提供註冊、可見度控制、重建和查詢

BaseSlot 方法

方法

說明

getLayoutId()

返回布局資源 ID

getHost()

擷取插槽宿主

show() / hide() / gone()

控制可見度

postEvent(event)

發布事件

技術原理

生命週期

插槽採用雙生命週期系統:

生命週期

說明

View 生命週期

onAttach → onDetach

Data 生命週期

onBindData → onUnbindData

與 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(如底部控制欄插槽)時:

  1. 事件上浮:插槽A通過postEvent()將事件傳遞至控制器Controller。

  2. 狀態下沉:由控制器Controller或StateStore統一處理狀態變更,廣播新狀態至所有監聽插槽。

通過此U型鏈路(事件上浮,狀態下沉)避免網狀耦合與死結風險。

事件攔截提示:覆蓋頁面表層的插槽容器需處理手勢事件下發,避免遮擋底部GestureControlSlot的手勢檢測。

常見問題

onAttach 什麼時候調用?

調用 playerView.attach() 或 slotManager.rebuildSlots() 時觸發。

自訂插槽注意事項?

  1. 在 onAttach 中調用 super.onAttach(host)。

  2. 在 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>      

正確做法:

  1. 保持核心控制項ID不變:Slot類使用的控制項ID在布局中必須保留。

  2. 控制項類型匹配: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()聲明需訂閱的事件,未聲明則不會訂閱。