スロットシステムは、AliPlayerKit のコアアーキテクチャ設計です。コンポーネント化により、プレーヤー UI を独立したプラガブルコンポーネントに分解します。統一されたシステムがこれらのコンポーネントを管理し、UI のデカップリング、柔軟な構成、拡張可能なカスタマイズを実現します。
概念の紹介
スロットシステムとは
スロットシステムは、AliPlayerKit のコアアーキテクチャ設計の 1 つであり、UI コンポーネント向けのプラグイン管理メカニズムです。
開発者は、プレーヤーインターフェイスの任意の位置で UI コンポーネントを動的に追加、削除、または置き換えることで、高度にカスタマイズ可能なプレーヤー UI を実現できます。
スロットとは
スロットは予約済みのビューコンテナであり、関連する 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 |
[再生/一時停止] ボタン | ✓ | ✓ | ✓ | ✓ | ✗ |
プログレスバー | ドラッグ可能 | ドラッグ不可 | ドラッグ可能 | ドラッグ不可 | ✗ |
時間表示 | ✓ | ✓ | ✓ | ✓ | ✗ |
[更新] ボタン | ✗ | ✓ | ✗ | ✗ | ✗ |
[フルスクリーン] ボタン | ✓ | ✓ | ✓ | ✓ | ✗ |
基本的な使用方法
スロットシステムには 3 つの使用戦略があり、開発者はニーズに応じて適切な方法を選択できます。
戦略 | 説明 | 適用シナリオ |
デフォルトインターフェイスの使用 | 最も簡単な方法です。プレーヤーコンポーネントはデフォルトインターフェイスを使用します。 | 迅速な統合、標準的な再生シナリオ。 |
一部のスロットをカスタマイズ | 特定のスロットのみをカスタマイズし、その他はデフォルトインターフェイスを使用します。 | 部分的なカスタマイズ、デフォルトのインタラクションの維持。 |
インターフェイスを完全にカスタマイズ | すべてのスロットをカスタマイズして、完全にパーソナライズされたプレーヤーインターフェイスを作成します。 | 詳細なカスタマイズ、ブランド固有の UI。 |
戦略 1: デフォルトインターフェイスの使用
最も簡単なアプローチです。組み込みのデフォルトインターフェイスを直接使用します:
// 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);戦略 2: 一部のスロットのカスタマイズ
特定のスロットのみをカスタマイズし、その他はデフォルトインターフェイスを維持します。例えば、トップバーのみをカスタマイズする場合:
// 1. レジストリを作成します
SlotRegistry registry = new SlotRegistry();
// 2. カスタマイズするスロットのみを登録します
registry.register(SlotType.TOP_BAR, parent -> new MyTopBarSlot(parent.getContext()));
// 3. アタッチ時にレジストリを渡します (登録されていないスロットはデフォルトの実装を使用します)
playerView.attach(controller, model, registry);戦略 3: インターフェイスの完全なカスタマイズ
すべてのスロットをカスタマイズして、完全にパーソナライズされたプレーヤーインターフェイスを作成します:
// 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 は、カスタムスロットを実装するための 2 つの方法を提供します。開発者は要件に応じて適切な方法を選択できます。
方法 1: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); } }登録と使用
SlotRegistry registry = new SlotRegistry(); registry.register(SlotType.COVER, parent -> new MyCoverSlot(parent.getContext())); playerView.attach(controller, model, registry);
方法 2: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 セットアップロジック } }登録と使用
SlotRegistry registry = new SlotRegistry(); registry.register(SlotType.PLAYER_SURFACE, parent -> new MySurfaceSlot(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 // ウォーターマークスロット
シナリオ 2:公式スロットは提供されているが不要
公式のプレーヤーコンポーネントがスロットを提供しているものの、ご自身のシナリオでは不要な場合は、次の 2 つの方法のいずれかで対応できます:
方法 1:設定による無効化 (推奨)
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());方法 2:物理的な削除 (非推奨)
対応する Slot クラスファイルとその関連リソースを直接削除します。この方法は推奨されません。
シナリオ 3:公式スロットの 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 変更を行う場合は、将来の AliPlayerKit のアップグレード時に競合を避けるため、カスタムスロット戦略の使用を推奨します:
公式のレイアウトファイルや Slot クラスを直接変更しないでください。
V2 バージョンを作成します (例:
TopBarSlotV2.javaおよびlayout_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 スロットに加えて、UI をレンダリングせずにビジネスロジックを実装するヘッドレススロットを通じて機能を拡張できます。
例: フルスクリーン管理スロット
公式の FullscreenSlot は典型的なヘッドレススロットです。フルスクリーン切り替えロジックのみを管理し、UI レンダリング機能はありません。
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 スロットによる対等なコラボレーション
ベストプラクティス
ライフサイクル管理
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 の切り替え | アニメーションが必要なシナリオに適しています。 |
空のスロットの切り替え | 音声のみの再生に適しています。 |
サンプルの実行
デモアプリで [スロットシステム] サンプルを選択すると、効果を確認できます。
API リファレンス
コアインターフェイス
インターフェイス / クラス | 説明 |
| ライフサイクルを定義するスロットのインターフェイス。 |
| 共通ロジックをカプセル化するスロットの基底クラス。 |
| ビルダーを管理するサービスレジストリ。 |
| 再構築とクエリ機能を提供するマネージャー。 |
BaseSlot メソッド
メソッド | 説明 |
| レイアウトのリソース ID を返します。 |
| スロットのホストを取得します。 |
| 可視性を制御します。 |
| イベントを発行します。 |
技術原理
ライフサイクル
スロットはデュアルライフサイクルシステムを使用します:
ライフサイクル | 説明 |
ビューライフサイクル |
|
データライフサイクル |
|
Android ライフサイクルとの関係:
スロットのライフサイクルは、Android のアクティビティやフラグメントのライフサイクルとは独立しています。アプリがフォアグラウンドとバックグラウンドを切り替える際に特定の操作 (アニメーションの一時停止やポーリングの停止など) を実行するには、次のように処理します:
方法 1:再生状態イベントのリッスン
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(); // アニメーションを再開します
}
}
}方法 2:アクティビティ/フラグメントでの制御
アクティビティの 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();
}
}単一方向データフロー
スロットシステムは、単一方向データフローアーキテクチャを採用しています。スロットはコントローラーへの直接参照を保持しません。代わりに、ホストが提供するインターフェイスを通じて疎結合な方法で相互作用します:
コントローラー → 状態/イベント → スロット (状態は下に流れ、読み取り専用)
スロット → コマンド → コントローラー (コマンドは上に流れ、送信専用)状態は下に流れる:状態を取得するには 2 つの方法があります。
能動的なクエリ:
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 の形式は ClassName.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 の後にのみアクセスできます
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 がすでにデタッチ状態になっている可能性があります。
エラー 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() でサブスクライブするイベントを宣言します。宣言されていない場合はサブスクライブしません。