插槽系统

更新时间:
复制为 MD 格式

插槽系统(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

设置菜单,显示倍速/清晰度/镜像等设置

SettingMenuSlot

场景适配

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

手势行为差异

手势

VOD

LIVE

VIDEO_LIST

RESTRICTED

MINIMAL

单击

显示/隐藏控制栏

显示/隐藏控制栏

显示/隐藏控制栏

显示/隐藏控制栏

禁用

双击

切换播放/暂停

切换播放/暂停

切换播放/暂停

切换播放/暂停

禁用

长按

2倍速播放

禁用

2倍速播放

禁用

禁用

水平拖动

进度跳转

禁用

进度跳转

禁用

禁用

左侧垂直拖动

亮度调节

亮度调节

禁用

亮度调节

禁用

右侧垂直拖动

音量调节

音量调节

禁用

音量调节

禁用

底部控制栏差异

控件

VOD

LIVE

VIDEO_LIST

RESTRICTED

MINIMAL

播放/暂停按钮

进度条

可拖拽

不可拖拽

可拖拽

不可拖拽

时间显示

刷新按钮

全屏按钮

基础使用

插槽系统提供三种使用策略,开发者可根据需求选择合适的方式:

策略

说明

适用场景

使用默认界面

最简单的使用方式,播放器组件将使用默认界面。

快速集成、标准播放场景。

自定义部分插槽

只自定义特定插槽,其他使用默认界面。

局部定制、保留默认交互。

完全自定义界面

自定义所有插槽,创建完全个性化的播放器界面。

深度定制、品牌专属UI。

策略一:使用默认配置

最简单的使用方式,直接使用内置的插槽配置:

// 1. 获取播放器视图
AliPlayerView playerView = findViewById(R.id.player_view);

// 2. 创建控制器和播放数据
AliPlayerController controller = new AliPlayerController(this);
AliPlayerModel model = new AliPlayerModel.Builder()
        .videoSource(videoSource)
        .build();

// 3. 绑定到视图(自动使用默认插槽)
playerView.attach(controller, model);

策略二:自定义部分插槽

只自定义特定插槽,其他使用默认界面。例如,只自定义顶部栏:

// 1. 创建注册表
SlotRegistry registry = new SlotRegistry();

// 2. 只注册需要自定义的插槽
registry.register(SlotType.TOP_BAR, parent -> new MyTopBarSlot(parent.getContext()));

// 3. 绑定时传入注册表(未注册的插槽使用默认实现)
playerView.attach(controller, model, registry);

策略三:完全自定义界面

自定义所有插槽,创建完全个性化的播放器界面:

// 1. 创建注册表
SlotRegistry registry = new SlotRegistry();

// 2. 注册所有插槽
registry.register(SlotType.PLAYER_SURFACE, parent -> new MySurfaceSlot(parent.getContext()));
registry.register(SlotType.COVER, parent -> new MyCoverSlot(parent.getContext()));
registry.register(SlotType.TOP_BAR, parent -> new MyTopBarSlot(parent.getContext()));
registry.register(SlotType.BOTTOM_BAR, parent -> new MyBottomBarSlot(parent.getContext()));
// ... 注册其他插槽

// 3. 绑定时传入注册表
playerView.attach(controller, model, registry);     

进阶使用

如何实现动态切换插槽

运行时动态切换插槽实现:

// 获取管理器
ISlotManager slotManager = playerView.getSlotManager();

// 切换 Surface 类型
registry.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. 注册使用。

    SlotRegistry registry = new SlotRegistry();
    registry.register(SlotType.COVER, parent -> new MyCoverSlot(parent.getContext()));
    playerView.attach(controller, model, registry);

方式二:实现 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. 注册使用。

    SlotRegistry registry = new SlotRegistry();
    registry.register(SlotType.COVER, parent -> new MyCoverSlot(parent.getContext()));
    playerView.attach(controller, model, registry);

方式对比

特性

继承 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() 方法中删除对应的配置项:

    // 修改前:SlotRegistry 会注入默认组件
    configs.add(new SlotConfig.Builder()
            .type(SlotType.LOG_PANEL)
            .excludeScenes(createSet(SceneType.MINIMAL))
            .condition(AliPlayerKit::isLogPanelEnabled)
            .build());
    
    // 修改后:注释或删除该配置,SlotRegistry 将不再注入默认组件
    // 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.javalayout_top_bar_slot_v2.xml

    • 通过SlotRegistry注册或修改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 等
    }
}

实现方式

方式

说明

适用场景

继承 BaseSlotgetLayoutId() 返回 0

简单,继承 FrameLayout 但不渲染。

大多数无头插槽。

实现 ISlot 接口 + 组合 SlotBehavior

灵活,完全不继承 View。

纯逻辑组件。

设计价值:

无头插槽使业务逻辑同样获得插槽系统的优势:

  • 统一生命周期管理。

  • 与播放器状态自动同步。

  • 可插拔替换。

  • UI插槽平等协作。

最佳实践

生命周期管理

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

插槽基类,封装通用逻辑

SlotRegistry

注册中心,管理构建器

ISlotManager

管理器,提供重建和查询

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. 状态下沉:由控制器ControllerStateStore统一处理状态变更,广播新状态至所有监听插槽。

通过此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()声明需订阅的事件,未声明则不会订阅。