Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ユーザーも開発者も悩ませない TV アプリ開発 ~Compose の内部実装から学ぶフォーカス制御~

Avatar for Daichi Takeya Daichi Takeya
September 11, 2025

ユーザーも開発者も悩ませない TV アプリ開発 ~Compose の内部実装から学ぶフォーカス制御~

Avatar for Daichi Takeya

Daichi Takeya

September 11, 2025
Tweet

More Decks by Daichi Takeya

Other Decks in Programming

Transcript

  1. 自己紹介 • Daichi Takeya (X:@taked_oO, GitHub:@taked137) ◦ 2023/04 ~ 株式会社サイバーエージェント

    ◦ 2023/05 ~ 株式会社AbemaTV - Native Mobileチーム ◦ 2024/05 ~ 株式会社AbemaTV - Native AndroidTVチーム
  2. 目次 1. Compose for TV とは? 2. TV アプリ向けの style

    設定 3. フォーカス移動の制御 a. 自動制御 b. 手動制御 4. スクロールの制御 5. 実際に導入してみた感想
  3. 目次 1. Compose for TV とは? 2. TV アプリ向けの style

    設定 3. フォーカス移動の制御 a. 自動制御 b. 手動制御 4. スクロールの制御 5. 実際に導入してみた感想
  4. • リモコンを利用した操作 TV アプリの操作 "TV Navigation Controller" by Android Developers,

    licensed under CC BY 2.5. ・フォーカスされている  Itemを目立たせる ・フォーカスに伴って  自動でスクロール する Mobile アプリほど直感的な操作が行えない フォーカスの操作性 がユーザー体験に大きく関係する
  5. • Compose for TV は二つのライブラリが存在 ◦ tv-material (stableになった🎉) ◦ tv-foundation

    (stable な “compose-foundation” にマイグレーション🚛) Leanback から Compose for TV への移行 Leanback Compose 何がフォーカスされているか分からないし 縦スクロールもガタガタしてるよ 自分でなんとかする必要があります \ナンテコッタイ / 今日 完全に理解しましょう Compose for TV の時代が来た!?
  6. 目次 1. Compose for TV とは? 2. TV アプリ向けの style

    設定 3. フォーカス移動の制御 a. 自動制御 b. 手動制御 4. スクロールの制御 5. 実際に導入してみた感想
  7. TVアプリ向けの style 適用 • まずはフォーカス中のアイテムを目立たせる androidx.tv.material3 の Composable を使うと簡潔に設定可能 e.g.)

    Surface, Card, etc… フォーカス中のアイテムが見つけやすくなった 🎉 Modifier.onFocusChanged で頑張ってもいいけど ... 見やすくなったところで本題に入っていきます
  8. 目次 1. Compose for TV とは? 2. TV アプリ向けの style

    設定 3. フォーカス移動の制御 a. 自動制御 b. 手動制御 4. スクロールの制御 5. 実際に導入してみた感想
  9. デフォルトのフォーカス制御ロジックの深掘り フォーカス中の View よりも “右側” にあり “左側” にはみ出ていないことが 選出条件 ❌

    フォーカス中の View と “Y座標” が重なっていると 優先的に選出 ⭕ ⭕ 他の View は “左右” というよりは “上下” に位置していると感じられる👏 • なぜ “→” 押下で、一番右上のアイテム (青色) にフォーカスが当たるのか
  10. デフォルトのフォーカス制御ロジックの深掘り フォーカス中の View よりも “右側” にあり “左側” にはみ出ていないことが 選出条件 ❌

    フォーカス中の View と “Y座標” が重なっていると 優先的に選出 ⭕ ⭕ 他の View は “左右” というよりは “上下” に位置していると感じられる👏 • なぜ “→” 押下で、一番右上のアイテム (青色) にフォーカスが当たるのか この他にも多くの精巧なロジックによって フォーカス制御が行われている
  11. デフォルトのフォーカス制御の限界 • 純粋に View の位置関係だけに基づいたフォーカス移動 ◦ Column や Row といった

    Component の単位を考慮していない 先頭のアイテムが 最初にフォーカスされない Master/Detail Flow な画面 別タブに 戻ってしまう クイズで利用した画面 “→” 押下時の フォーカス対象を変えたい
  12. フォーカス制御のアプローチ 🤖 自動制御 ・数行追加するだけで完結する ・Compose 側がロジックを隠蔽してくれる ・多くのケースでは自動制御で事足りる ・対応可能な問題には限界がある ・Compose のバージョンによってはバグが存在

    🎯 手動制御 ・(おそらく) 全てのケースに対応できる ・適切に管理できないとすぐ予期せぬ挙動が生じる ・可読性が著しく低下する 適切なアプローチを採用して ユーザ と 開発者 の両方に優しいアプリを目指そう
  13. 目次 1. Compose for TV とは? 2. TV アプリ向けの style

    設定 3. フォーカス移動の制御 a. 自動制御 b. 手動制御 4. スクロールの制御 5. 実際に導入してみた感想
  14. (余談) デフォルトで focusGroup である Composable • LazyColumn, LazyRow などはデフォルトで focusGroup

    もし focusGroup ではなかったら... LazyRow のフォーカス移動 描画されていない子要素 もフォーカス対象として考慮 → 番組表がスクロールできる🙌 描画中の要素 の位置関係に基づいてフォーカス対象を選出 → 番組表がスクロールできない😢
  15. Modifier.focusRestorer --- フォーカス位置の保存/復元 • focusGroup の子要素に対してフォーカス位置の保存/復元を行う ・Modifier に追加するだけ ・compose-ui 1.8.0

    にて遂に stable に LazyColumn は既に focusGroup なので focusRestorer だけを指定 Column は “focusRestorer → focusGroup” の順に適用
  16. • focusRestorer の状態を初期化したい ◦ UI の差分更新だけでは初期化されない compose.runtime.key --- コンポジションの再構築を誘発 左側の別のタブが選択されたら

    コンポジションツリーごと再生成 リストの更新に伴って focusRestorer の状態も 初期化される👍 リストの更新直後は 表示位置に基づいて フォーカス対象が選出される😑 更新直後は先頭のアイテムが フォーカスされるようにしたい
  17. 自動制御まとめ 子要素を「フォーカス探索のまとまり」にグルーピング ・グループ内で優先的にフォーカス対象を選出 ・グループ単位の制御 を実現 (フォーカス対象の復元など ) ・Lazy Layout は

    デフォルトで focusGroup Modifier.focusGroup Modifier.focusRestorer 同じグループ内でフォーカス対象を復元 ・Modifier.focusGroup とセットで扱う ・UIが変わったらフォーカス情報をリセット (keyを利用) ・FocusRequester を使用して fallback 先を指定
  18. 自動制御まとめ 子要素を「フォーカス探索のまとまり」にグルーピング ・グループ内で優先的にフォーカス対象を選出 ・グループ単位の制御 を実現 (フォーカス対象の復元など ) ・Lazy Layout は

    デフォルトで focusGroup Modifier.focusGroup Modifier.focusRestorer 同じグループ内でフォーカス対象を復元 ・Modifier.focusGroup とセットで扱う ・UIが変わったらフォーカス情報をリセット (keyを利用) ・FocusRequester を使用して fallback 先を指定 この他にも もっと柔軟なフォーカス制御を行いたい!
  19. 目次 1. Compose for TV とは? 2. TV アプリ向けの style

    設定 3. フォーカス移動の制御 a. 自動制御 b. 手動制御 4. スクロールの制御 5. 実際に導入してみた感想
  20. 手動制御が必要なケース 意図しない状態破棄の回避 Composable をフォーカス不可能に変更 AnimatedVisibility で非表示になると focusRestorer の状態も破棄 Button で

    `enabled = false` にしても フォーカスできてしまう フォーカスの移動先を変更 ❌ “→” キー押下時の移動先は Compose が決定 とある Modifier で全て解決
  21. Modifier.focusProperties --- フォーカス不可能に設定 • 内部で focusable に設定される Composable もフォーカス不可能に上書きする disabled

    な Button も フォーカス可能 😢 focusProperties の canFocus により フォーカス先の選出候補から外れる 🎉
  22. Modifier.focusProperties --- focusGroup に対するフォーカス挙動の制御 • focusGroup に対して enter/exit 時のフォーカス先を設定 (頑張れば)

    focusRestorer を代替可能 • “フォーカス状態” を任意のグループで管理できる ◦ key で CompositionTree 全体を破棄しなくて良い ◦ AnimatedVisibility の外で保存することも可能 enter/onEnter exit/onExit
  23. 手動制御 (特に focusProperties) の注意点 focusProperties で 実現してみる コード量が約 2倍に 😫

    安易に導入せず プロダクト要件として重要度を 考慮した上で利用する!
  24. 手動制御まとめ • Modifier.focusProperties を最終手段として使用 👍 細かい挙動まで制御可能 • Composable をフォーカス不可能に設定 •

    フォーカスの移動先をカスタマイズ • focusRestorer の代替 ◦ “UI” と “状態” を独立して管理 🥶 実装の複雑化 • 実装者ですらロジックの解読が困難 • バグの混入リスク フォーカスの移動先をカスタマイズ Composable のフォーカス可否を上書き可能
  25. 目次 1. Compose for TV とは? 2. TV アプリ向けの style

    設定 3. フォーカス移動の制御 a. 自動制御 b. 手動制御 4. スクロールの制御 5. 実際に導入してみた感想
  26. TV アプリにおけるスクロール フォーカス移動に伴って 自動で必要な量だけ スクロール (タッチ操作が可能な端末を使えば ) スワイプ操作でのスクロール も可能 “フォーカス移動

    ” と “スワイプ操作 ” で 異なるスクロールロジックが実行される スクロールを無効化したつもりが “スワイプ操作” のスクロールだけ無効化されていた 😔こんなこともしばしば、、、
  27. スクロール可能なコンテナ化 • Modifier.verticalScroll / Modifier.horizontalScroll でスクロール可能になる Mobile & TV 向けのロジックを両方含む

    • 📱スワイプ操作によるスクロールの有効化 • 📺フォーカス移動に伴うスクロールの有効化 focusGroup も自動で適用される • ScrollableNode は FocusGroupNode と同様に FocusTargetModifierNode に委譲
  28. スクロールの制御まとめ スクロール量とアニメーションを変更 ・CompositionLocal で配布 ・公式で紹介されている位置ベースのロジックが便利 ・アニメーションは compose 1.8.x では無視される ・リスト全体のスクロールに影響あり

    BringIntoViewSpec 子のフォーカスに伴うスクロールロジックを上書き ・子からのスクロール要求を受け取って挙動を変更可能 ・親全体が見えるように変更すると見切れない ・特定の item にだけ適用可能 BringIntoViewModifierNode 65% 親全体を 見せる 子の座標は 無視
  29. 目次 1. Compose for TV とは? 2. TV アプリ向けの style

    設定 3. フォーカス移動の制御 a. 自動制御 b. 手動制御 4. スクロールの制御 5. 実際に導入してみた感想
  30. 導入して得られた開発効率の向上 RowsSupportFragment DetailsSupportFragment PresenterSelector ListRow VerticalGridView WindowAlignment SingleRow etc… Leanback

    の学習コストが不要に 使い方が分からず Fragment in Fragment で無理やり 表示させた過去も...😢 命令的な状態更新に起因するバグ根絶 ViewHolder が再利用された際に 別の item 向けに設定していた状態が 意図せず使いまわされる
  31. 導入して感じた注意点 • UI とフォーカス制御の分離が難しい • FocusRequester ベースの処理が読みづらい ◦ FocusRequester の

    attach 先を発見する労力が必要 • attach する前に利用するとすぐクラッシュする (していた) ◦ Lazy Layout で陥りやすい 🕹 FocusRequester の取り扱いの難しさ • Experimental な機能が多く、挙動がすぐ変わる ◦ スクロールアニメーションが突如無視される ◦ Exception を throw していた処理が println になる • stable な機能もバグが放置されていたりする ◦ e.g.) LazyVerticalGrid でフォーカスが飛ぶ (link) 🐝 技術的な不安定性 扱いづらいところはあるものの 開発スピードの向上, バグ混入リスクの低減, 容易な機能拡張 など Compose for TV の導入で開発効率は向上した と感じてます
  32. まとめ • TV アプリはフォーカス操作の快適さがユーザー体験に大きく影響 ◦ Leanback ライブラリはユーザー体験が向上するためのロジックが自動で適用される ◦ Compose for

    TV では自前で実装していく必要がある • フォーカス制御を行う API はいくつかあるのでトレードオフを考慮して選択 ◦ 自動制御:focusGroup, focusRestorer ◦ 手動制御:focusProperties, focusRequester • 「フォーカス移動」に伴う自動スクロールの挙動は変更可能 ◦ CompositionLocal による BringIntoViewSpec の提供で基準位置を変更 ◦ BringIntoViewModifierNode を使ってロジックを上書き Compose for TV はいいぞ!