$30 off During Our Annual Pro Sale. View Details »

Master of NestedScroll

Swimmy
September 13, 2023
2.7k

Master of NestedScroll

DroidKaigi 2023での登壇資料になります。
https://2023.droidkaigi.jp/timetable/494286/

Swimmy

September 13, 2023
Tweet

Transcript

  1. Harada Reo
    DroidKaigi 2023 UI/UX
    Master of NestedScroll

    View Slide

  2. 自己紹介 / Introduction
    Harada Reo
    CyberAgent Inc.
    Ameba
    どすこい塾
    DroidKaigi Staff
    RunningReo(X)

    View Slide

  3. Master of NestedScrollのゴールに対する指標
    プロダクト開発において
    ネストスクロールで
    困らないようになる

    View Slide

  4. Master of NestedScrollのゴールに対する指標
    1. ネストスクロールを実装する前に気を付けるべき点が
    分かっている状態
    2. ネストスクロールで期待しない動作が起きた時に
    原因究明する思考フローが身についている
    3. スクロールをカスタムする方法を知る

    View Slide

  5. Master of NestedScrollのゴールに対する指標
    事前共有
    「こんな経験をした」「こんな時はどうする?」
    といったコメント大歓迎です!
    (みんなでネストスクロールで困らない世界にしましょう)

    View Slide

  6. Master of NestedScrollのゴールに対する指標
    コンポーネントの並びを図示したものが出てきますが
    上から親コンポーネントの順に並んでいます。
    Son Component
    Parent Component
    Child Component

    View Slide

  7. 目次 / Index
    実際のプロダクト開発で起こりうる
    ネストスクロール問題について



    JetpackCompose時代における解決策
    相互運用上の注意点
    AndroidView時代のコードと比較

    View Slide

  8. 実際のプロダクト開発で起こりうる
    ネストスクロール問題について

    View Slide

  9. 実際のプロダクト開発で起こりうるネストスクロール問題について
    そもそもネストスクロールとは?
    一つのスクロール動作に複数のコンポーネントが関わること
    例えば...
    「スクロールに応じて折りたたまれるツールバー」
    「スクロールに応じて閉じるボトムシート」
    「横スワイプ内に配置された横向きリストの組み合わせ」

    View Slide

  10. 実際のプロダクト開発で起こりうるネストスクロール問題について
    そもそもネストスクロールとは?
    一つのスクロール動作に複数のコンポーネントが関わること
    例えば...
    「スクロールに応じて折りたたまれるツールバー」
    「スクロールに応じて閉じるボトムシート」
    「横スワイプ内に配置された横向きリストの組み合わせ」

    View Slide

  11. 折りたたみツールバー
    AppBarLayout
    CoordinatorLayout
    CollaptingToolbar
    ConstraintLayout
    NestedScrollView
    TextView
    実際のプロダクト開発で起こりうるネストスクロール問題について
     Toolbar

    View Slide

  12. 実際のプロダクト開発で起こりうるネストスクロール問題について
    シンプルなケースだと...
    AndroidXやマテリアルコンポーネントで
    提供されているコンポーネントを活用することで
    低レイヤーな部分を深く考えずに実装できる

    View Slide

  13. 実際のプロダクト開発で起こりうるネストスクロール問題について
    しかし、期待しない動作になる場合がある
    子のスクロールが動作しない ネストスクロールをしたくない場合

    View Slide

  14. 実際のプロダクト開発で起こりうるネストスクロール問題について
    スクロールの仕組みがイメージしにくい
    Jetpack Compose / AndroidView / 相互運用
    Jetpack ComposeだとAndroidViewほど
    なぜ難しいのか?
    それぞれスクロールの仕様が異なる
    コンポーネントが充実していない

    View Slide

  15. 実際のプロダクト開発で起こりうるネストスクロール問題について
    Jetpack Compose / AndroidView / 相互運用
    Jetpack Composeでネストスクロールを
    そこで・・・
    それぞれのスクロールの仕組みをイメージ化
    カスタムする方法を学ぶ

    View Slide

  16. Jetpack Compose時代における解決策

    View Slide

  17. JetpackCompose時代における解決策
    Jetpack Composeのスクロール環境
    ・Modifierで簡単にスクロールを適用可能
    ・TopAppBarでcollapsing対応が可能
    ・デフォルトでネストスクロールをサポート
    ・NestedSc
    rollConnectionの仕組み

    View Slide

  18. JetpackCompose時代における解決策
    Jetpack Composeのスクロール環境
    ・Modifierで簡単にスクロールを適用可能
    ・TopAppBarでcollapsing対応が可能
    ・デフォルトでネストスクロールをサポート
    ・NestedSc
    rollConnectionの仕組み

    View Slide

  19. JetpackCompose時代における解決策
    Modifierで簡単にスクロール可能にできる
    // ScrollViewのように振る舞う
    val scrollState = rememberScrollState()
    Column(modifier = Modifier.verticalScroll(scrollState)) { ... }
    // offsetしないので注意
    Column(
    modifier = Modifier.scrollable(
    orientation = Vertical,
    state = rememberScrollState { delta -> delta },
    )
    ) { ... }

    View Slide

  20. JetpackCompose時代における解決策
    スクロール可能なコンポーネントにできる
    // ScrollViewのように振る舞う
    val scrollState = rememberScrollState()
    Column(modifier = Modifier.verticalScroll(scrollState)) { ... }
    // offsetしないので注意
    Column(
    modifier = Modifier.scrollable(
    orientation = Vertical,
    state = rememberScrollState { delta -> delta },
    )
    ) { ... }

    View Slide

  21. JetpackCompose時代における解決策
    スクロール可能なコンポーネントにできる
    // ScrollViewのように振る舞う
    val scrollState = rememberScrollState()
    Column(modifier = Modifier.verticalScroll(scrollState)) { ... }
    // offsetしないので注意
    Column(
    modifier = Modifier.scrollable(
    orientation = Vertical,
    state = rememberScrollState { delta -> delta },
    )
    ) { ... }

    View Slide

  22. JetpackCompose時代における解決策
    注意点
    ・同一方向のスクロール可能なコンポーネントを
     ネストするとRuntimeエラーが起きる
    LazyXXXのDSL(item/items)を用いる
    ・scrollableはoffsetしないのでネストスクロールは
     サポートされない(基本的にscrollStateで良い)

    View Slide

  23. JetpackCompose時代における解決策
    Scrollableはoffsetされない
    ※ もっと良い活用事例があれば教えてください
    スクロールに応じて背景色を変更する

    View Slide

  24. JetpackCompose時代における解決策
    Jetpack Composeのスクロール環境
    ・Modifierで簡単にスクロールを適用可能
    ・TopAppBarでcollapsing対応が可能
    ・デフォルトでネストスクロールをサポート
    ・NestedSc
    rollConnectionの仕組み

    View Slide

  25. JetpackCompose時代における解決策
    TopAppBarでCollapsing対応が可能
    @ExperimentalMaterial3Api
    @Composable
    fun TopAppBar(
    title: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    navigationIcon: @Composable () -> Unit = {},
    actions: @Composable RowScope.() -> Unit = {},
    windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
    colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
    scrollBehavior: TopAppBarScrollBehavior? = null
    )

    View Slide

  26. JetpackCompose時代における解決策
    TopAppBarでCollapsing対応が可能
    @ExperimentalMaterial3Api
    @Composable
    fun TopAppBar(
    title: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    navigationIcon: @Composable () -> Unit = {},
    actions: @Composable RowScope.() -> Unit = {},
    windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
    colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
    scrollBehavior: TopAppBarScrollBehavior? = null
    )

    View Slide

  27. JetpackCompose時代における解決策
    親のModifier#nestedScrollとTopAppBarに適用する
    @Composable
    fun EnterAlwaysTopAppBar() {
    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
    Scaffold(
    modifier = Modifier.nestedScroll(behavior.nestedScrollConnection),
    topAppBar = {
    TopAppBar(
    ...
    scrollBehavior = scrollBehavior,
    )
    },
    ...

    View Slide

  28. JetpackCompose時代における解決策
    親のModifier#nestedScrollとTopAppBarに適用する
    @Composable
    fun EnterAlwaysTopAppBar() {
    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
    Scaffold(
    modifier = Modifier.nestedScroll(behavior.nestedScrollConnection),
    topAppBar = {
    TopAppBar(
    ...
    scrollBehavior = scrollBehavior,
    )
    },
    ...

    View Slide

  29. JetpackCompose時代における解決策
    親のModifier#nestedScrollとTopAppBarに適用する
    @Composable
    fun EnterAlwaysTopAppBar() {
    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
    Scaffold(
    modifier = Modifier.nestedScroll(behavior.nestedScrollConnection),
    topAppBar = {
    TopAppBar(
    ...
    scrollBehavior = scrollBehavior,
    )
    },
    ...

    View Slide

  30. JetpackCompose時代における解決策
    親のModifier#nestedScrollとTopAppBarに適用する
    @Composable
    fun EnterAlwaysTopAppBar() {
    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
    Scaffold(
    modifier = Modifier.nestedScroll(behavior.nestedScrollConnection),
    topAppBar = {
    TopAppBar(
    ...
    scrollBehavior = scrollBehavior,
    )
    },
    ...

    View Slide

  31. 各Behaviorの挙動
    JetpackCompose時代における解決策
    enterAlways existUntilCollapsed pinned ※state更新あり

    View Slide

  32. Material3のTopAppBarでCollapsing対応はできるが
    レイアウトを自由に変えることが難しい
    例えば・・・
    実際に高さを変えようとするが
    TopAppBar自体の高さは固定なので
    レイアウトの変更が難しい
    JetpackCompose時代における解決策

    View Slide

  33. JetpackCompose時代における解決策
    Jetpack Composeのスクロール環境
    ・Modifierで簡単にスクロールを適用可能
    ・TopAppBarでcollapsing対応が可能
    ・デフォルトでネストスクロールをサポート
    ・NestedSc
    rollConnectionの仕組み

    View Slide

  34. Parant Compose
    Child Compose
    JetpackCompose時代における解決策
    デフォルトでネストスクロールをサポート
    ※ AndroidViewはサポートしていない
    消費されなかったスクロール量を親に渡す

    View Slide

  35. JetpackCompose時代における解決策
    例)HorizontalPager + LazyRow
    LazyRowのスクロールと
    HorizontalPagerのスワイプが
    地続きになっている

    View Slide

  36. JetpackCompose時代における解決策
    Jetpack Composeのスクロール環境
    ・Modifierで簡単にスクロールを適用可能
    ・TopAppBarでcollapsing対応が可能
    ・デフォルトでネストスクロールをサポート
    ・NestedSc
    rollConnectionの仕組み

    View Slide

  37. スクロール動作をカスタムできる
    JetpackCompose時代における解決策
    // 子のネストスクロールに参加できるようになる
    fun Modifier.nestedScroll(
    connection: NestedScrollConnection,
    dispatcher: NestedScrollDispatcher? = null
    ): Modifier
    // スクロール消費量などの伝播を制御することが可能
    interface NestedScrollConnection
    // ネストスクロールシステムに消費量などを送る
    class NestedScrollDispatcher

    View Slide

  38. 子のスクロールに参加する
    JetpackCompose時代における解決策
    // 子のネストスクロールに参加できるようになる
    fun Modifier.nestedScroll(
    connection: NestedScrollConnection,
    dispatcher: NestedScrollDispatcher? = null
    ): Modifier
    // スクロール消費量などの伝播を制御することが可能
    interface NestedScrollConnection
    // 親のスクロールシステムに消費量などを通知
    class NestedScrollDispatcher

    View Slide

  39. スクロールの消費量を取得する
    JetpackCompose時代における解決策
    // 子のネストスクロールに参加できるようになる
    fun Modifier.nestedScroll(
    connection: NestedScrollConnection,
    dispatcher: NestedScrollDispatcher? = null
    ): Modifier
    // スクロール消費量などの伝播を制御することが可能
    interface NestedScrollConnection
    // 親のスクロールシステムに消費量などを通知
    class NestedScrollDispatcher

    View Slide

  40. 子のスクロール消費量などを親に送る
    JetpackCompose時代における解決策
    // 子のネストスクロールに参加できるようになる
    fun Modifier.nestedScroll(
    connection: NestedScrollConnection,
    dispatcher: NestedScrollDispatcher? = null
    ): Modifier
    // スクロール消費量などの伝播を制御することが可能
    interface NestedScrollConnection
    // 親のスクロールシステムに消費量などを通知
    class NestedScrollDispatcher

    View Slide

  41. JetpackCompose時代における解決策
    例)HorizontalPager + LazyRow
    LazyRowのスクロールと
    HorizontalPagerのスワイプが
    地続きになっている

    View Slide

  42. JetpackCompose時代における解決策
    NestedScrollConnectionの実装を渡してあげると解消
    // 余ったx軸方向のスクロールをLazyRowで吸収する
    val nestedScrollConnection = object : NestedScrollConnection {
    override fun onPostScroll(...) = Offset(available.x, 0F)
    }
    HorizontalPager {
    ...
    LazyRow(Modifier.nestedScroll(nestedScrollConnection)) { ... }
    }

    View Slide

  43. JetpackCompose時代における解決策
    NestedScrollConnectionの実装を渡してあげると解消
    // 余ったx軸方向のスクロールをLazyRowで吸収する
    val nestedScrollConnection = object : NestedScrollConnection {
    override fun onPostScroll(...) = Offset(available.x, 0F)
    }
    HorizontalPager {
    ...
    LazyRow(Modifier.nestedScroll(nestedScrollConnection)) { ... }
    }

    View Slide

  44. JetpackCompose時代における解決策
    NestedScrollConnectionの実装を渡してあげると解消
    // 余ったx軸方向のスクロールをLazyRowで吸収する
    val nestedScrollConnection = object : NestedScrollConnection {
    override fun onPostScroll(...) = Offset(available.x, 0F)
    }
    HorizontalPager {
    ...
    LazyRow(Modifier.nestedScroll(nestedScrollConnection)) { ... }
    }

    View Slide

  45. JetpackCompose時代における解決策
    NestedScrollConnectionの実装を渡してあげると解消
    // 余ったx軸方向のスクロールをLazyRowで吸収する
    val nestedScrollConnection = object : NestedScrollConnection {
    override fun onPostScroll(...) = Offset(available.x, 0F)
    }
    HorizontalPager {
    ...
    LazyRow(Modifier.nestedScroll(nestedScrollConnection)) { ... }
    }

    View Slide

  46. Child Composable
    Parent Composable (Modifier.nestedScroll(...))
    JetpackCompose時代における解決策
    NestedScrollConnectionの仕組み
    Modifier #nestedScrollを適用したComposeは
    子のスクロールに参加してスクロールイベントをインターセプトできる
    join dispatch

    View Slide

  47. JetpackCompose時代における解決策
    例)HorizontalPager + LazyRow
    子のスクロール量を
    全て消費し切ることで
    HorizontalPagerが連動して
    スワイプされることがなくなる

    View Slide

  48. JetpackCompose時代における解決策
    NestedScrollConnectionの内部実装
    // 引数など簡略化してます
    // returnした値は親のcomposable関数が吸収する
    interface NestedScrollConnection {
    fun onPreScroll(...): Offset = Offset.Zero
    fun onPostScroll(...):Offset = Offset.Zero
    suspend fun onPreFling(...): Velocity = Velocity.Zero
    suspend fun onPostFling(...): Velocity = Velocity.Zero
    }

    View Slide

  49. JetpackCompose時代における解決策
    NestedScrollConnectionの内部実装
    // 引数など簡略化してます
    // returnした値は親のcomposable関数が吸収する
    interface NestedScrollConnection {
    fun onPreScroll(...): Offset = Offset.Zero
    fun onPostScroll(...):Offset = Offset.Zero
    suspend fun onPreFling(...): Velocity = Velocity.Zero
    suspend fun onPostFling(...): Velocity = Velocity.Zero
    }

    View Slide

  50. JetpackCompose時代における解決策
    NestedScrollConnectionの内部実装
    // 引数など簡略化してます
    // returnした値は親のcomposable関数が吸収する
    interface NestedScrollConnection {
    fun onPreScroll(...): Offset = Offset.Zero
    fun onPostScroll(...):Offset = Offset.Zero
    suspend fun onPreFling(...): Velocity = Velocity.Zero
    suspend fun onPostFling(...): Velocity = Velocity.Zero
    }

    View Slide

  51. JetpackCompose時代における解決策
    ① NestedScrollConnection #onPreScroll
    // スクロール前に消費量を制御することが可能
    // availableは消費量, NestedScrollSourceはスクロール操作の種別
    // 活用事例: 縦スクロールの端に到達するまで伝播させたくない場合
    fun onPreScroll(available:Offset,source:NestedScrollSource): Offset
    = Offset.Zero
    // スクロールの端に達している時にFABを表示させるケース
    isVisible = (available.y == 0 && source == NestedScrollSource.Drag)

    View Slide

  52. JetpackCompose時代における解決策
    ① NestedScrollConnection #onPreScroll
    // スクロール前に消費量を制御することが可能
    // availableは消費量, NestedScrollSourceはスクロール操作の種別
    // 活用事例: 縦スクロールの端に到達するまで伝播させたくない場合
    fun onPreScroll(available:Offset,source:NestedScrollSource): Offset
    = Offset.Zero
    // スクロールの端に達している時にFABを表示させるケース
    isVisible = (available.y == 0 && source == NestedScrollSource.Drag)

    View Slide

  53. JetpackCompose時代における解決策
    ① NestedScrollConnection #onPreScroll
    // スクロール前に消費量を制御することが可能
    // availableは消費量, NestedScrollSourceはスクロール操作の種別
    // 活用事例: 縦スクロールの端に到達するまで伝播させたくない場合
    fun onPreScroll(available:Offset,source:NestedScrollSource): Offset
    = Offset.Zero
    // スクロールの端に達している時にFABを表示させるケース
    isVisible = (available.y == 0 && source == NestedScrollSource.Drag)

    View Slide

  54. JetpackCompose時代における解決策
    ① NestedScrollConnection #onPreScroll
    // スクロール前に消費量を制御することが可能
    // availableは消費量, NestedScrollSourceはスクロール操作の種別
    // 活用事例: 縦スクロールの端に到達するまで伝播させたくない場合
    fun onPreScroll(available:Offset,source:NestedScrollSource): Offset
    = Offset.Zero
    // スクロールの端に達している時にFABを表示させるケース
    isVisible = (available.y == 0 && source == NestedScrollSource.Drag)

    View Slide

  55. JetpackCompose時代における解決策
    ① NestedScrollConnection #onPreScroll
    // スクロール前に消費量を制御することが可能
    // availableは消費量, NestedScrollSourceはスクロール操作の種別
    // 活用事例: 縦スクロールの端に到達するまで伝播させたくない場合
    fun onPreScroll(available:Offset,source:NestedScrollSource): Offset
    = Offset.Zero
    // スクロールの端に達している時にFABを表示させるケース
    isVisible = (available.y == 0 && source == NestedScrollSource.Drag)

    View Slide

  56. JetpackCompose時代における解決策
    ① NestedScrollConnection #onPreScroll
    // スクロール前に消費量を制御することが可能
    // availableは消費量, NestedScrollSourceはスクロール操作の種別
    // 活用事例: 縦スクロールの端に到達するまで伝播させたくない場合
    fun onPreScroll(available:Offset,source:NestedScrollSource): Offset
    = Offset.Zero
    // スクロールの端に達している時にFABを表示させるケース
    isVisible = (available.y == 0 && source == NestedScrollSource.Drag)

    View Slide

  57. JetpackCompose時代における解決策
    NestedScrollSourceの主な種別
    Drag Fling

    View Slide

  58. JetpackCompose時代における解決策
    NestedScrollConnectionの内部実装
    // 引数など簡略化してます
    // returnした値は親のcomposable関数が吸収する
    interface NestedScrollConnection {
    fun onPreScroll(...): Offset = Offset.Zero
    fun onPostScroll(...):Offset = Offset.Zero
    suspend fun onPreFling(...): Velocity = Velocity.Zero
    suspend fun onPostFling(...): Velocity = Velocity.Zero
    }

    View Slide

  59. JetpackCompose時代における解決策
    ② NestedScrollConnection #onPostScroll
    // スクロール
    後に消費されなかったスクロール量をどうするか決められる
    // consumedは消費量, availableは消費されなかったスクロール量
    fun onPostScroll(
    consumed: Offset,
    available:Offset,
    source:NestedScrollSource,
    ): Offset = Offset.Zero
    // 伝播させず自身でスクロール量を吸収する
    ... return available

    View Slide

  60. JetpackCompose時代における解決策
    ② NestedScrollConnection #onPostScroll
    // スクロール後に消費されなかったスクロール量をどうするか決められる
    // consumedは消費量, availableは消費されなかったスクロール量
    fun onPostScroll(
    consumed: Offset,
    available:Offset,
    source:NestedScrollSource,
    ): Offset = Offset.Zero
    // 伝播させず自身でスクロール量を吸収する
    ... return available

    View Slide

  61. JetpackCompose時代における解決策
    ② NestedScrollConnection #onPostScroll
    // スクロール
    後に消費されなかったスクロール量をどうするか決められる
    // consumedは消費量, availableは消費されなかったスクロール量
    fun onPostScroll(
    consumed: Offset,
    available:Offset,
    source:NestedScrollSource,
    ): Offset = Offset.Zero
    // 伝播させず自身でスクロール量を吸収する
    ... return available

    View Slide

  62. JetpackCompose時代における解決策
    ② NestedScrollConnection #onPostScroll
    // スクロール
    後に消費されなかったスクロール量をどうするか決められる
    // consumedは消費量, availableは消費されなかったスクロール量
    fun onPostScroll(
    consumed: Offset,
    available:Offset,
    source:NestedScrollSource,
    ): Offset = Offset.Zero
    // 伝播させず自身でスクロール量を吸収する
    ... return available

    View Slide

  63. JetpackCompose時代における解決策
    ② NestedScrollConnection #onPostScroll
    // スクロール
    後に消費されなかったスクロール量をどうするか決められる
    // consumedは消費量, availableは消費されなかったスクロール量
    fun onPostScroll(
    consumed: Offset,
    available:Offset,
    source:NestedScrollSource,
    ): Offset = Offset.Zero
    // 伝播させず自身でスクロール量を吸収する
    ... return available

    View Slide

  64. JetpackCompose時代における解決策
    ② NestedScrollConnection #onPostScroll
    // スクロール
    後に消費されなかったスクロール量をどうするか決められる
    // consumedは消費量, availableは消費されなかったスクロール量
    fun onPostScroll(
    consumed: Offset,
    available:Offset,
    source:NestedScrollSource,
    ): Offset = Offset.Zero
    // 伝播させず自身でスクロール量を吸収する
    ... return available

    View Slide

  65. JetpackCompose時代における解決策
    NestedScrollConnectionの内部実装
    // 引数など簡略化してます
    // returnした値は親のcomposable関数が吸収する
    interface NestedScrollConnection {
    fun onPreScroll(...): Offset = Offset.Zero
    fun onPostScroll(...):Offset = Offset.Zero
    suspend fun onPreFling(...): Velocity = Velocity.Zero
    suspend fun onPostFling(...): Velocity = Velocity.Zero
    }

    View Slide

  66. JetpackCompose時代における解決策
    ③ NestedScrollConnection #onPreFling
    // スクロールで指を離した時の慣性速度
    // availableは発生した慣性速度
    fun onPreFling(consumed: Velocity): Velocity = Velocity.Zero
    // 縦にFlingさせないようにする(慎重に閲覧できる状態)
    fun onPreFling(consumed: Velocity): Velocity =
    Velocity(0F, consumed.y)

    View Slide

  67. JetpackCompose時代における解決策
    ③ NestedScrollConnection #onPreFling
    // スクロールで指を離した時の慣性速度
    // availableは発生した慣性速度
    fun onPreFling(consumed: Velocity): Velocity = Velocity.Zero
    // 縦にFlingさせないようにする(慎重に閲覧できる状態)
    fun onPreFling(consumed: Velocity): Velocity =
    Velocity(0F, consumed.y)

    View Slide

  68. JetpackCompose時代における解決策
    ③ NestedScrollConnection #onPreFling
    // スクロールで指を離した時の慣性速度
    // availableは発生した慣性速度
    fun onPreFling(available: Velocity): Velocity = Velocity.Zero
    // 縦にFlingさせないようにする(慎重に閲覧できる状態)
    fun onPreFling(consumed: Velocity): Velocity =
    Velocity(0F, consumed.y)

    View Slide

  69. JetpackCompose時代における解決策
    ③ NestedScrollConnection #onPreFling
    // スクロールで指を離した時の慣性速度
    // availableは発生した慣性速度
    fun onPreFling(available: Velocity): Velocity = Velocity.Zero
    // 縦にFlingさせないようにする(慎重に閲覧できる状態)
    fun onPreFling(consumed: Velocity): Velocity =
    Velocity(0F, consumed.y)

    View Slide

  70. JetpackCompose時代における解決策
    ③ NestedScrollConnection #onPreFling
    // スクロールで指を離した時の慣性速度
    // availableは発生した慣性速度
    fun onPreFling(available: Velocity): Velocity = Velocity.Zero
    // 縦にFlingさせないようにする(慎重に閲覧できる状態)
    fun onPreFling(consumed: Velocity): Velocity =
    Velocity(0F, consumed.y)

    View Slide

  71. JetpackCompose時代における解決策
    NestedScrollConnectionの内部実装
    // 引数など簡略化してます
    // returnした値は親のcomposable関数が吸収する
    interface NestedScrollConnection {
    fun onPreScroll(...): Offset = Offset.Zero
    fun onPostScroll(...):Offset = Offset.Zero
    suspend fun onPreFling(...): Velocity = Velocity.Zero
    suspend fun onPostFling(...): Velocity = Velocity.Zero
    }

    View Slide

  72. JetpackCompose時代における解決策
    ④ NestedScrollConnection #onPostFling
    // スクロール完了後の慣性速度
    // consumedは消費された慣性速度, availableは消費されなかった速度
    fun onPostFling(consumed: Velocity, available: Velocity): Velocity
    = Offset.Zero

    View Slide

  73. JetpackCompose時代における解決策
    ④ NestedScrollConnection #onPostFling
    // スクロール完了後の慣性速度
    // con
    sumedは消費された慣性速度, availableは消費されなかった速度
    fun onPostFling(consumed: Velocity, available: Velocity): Velocity
    = Offset.Zero

    View Slide

  74. JetpackCompose時代における解決策
    ④ NestedScrollConnection #onPostFling
    // スクロール完了後の慣性速度
    // consumedは消費された慣性速度, availableは消費されなかった速度
    fun onPostFling(consumed: Velocity, available: Velocity): Velocity
    = Offset.Zero

    View Slide

  75. JetpackCompose時代における解決策
    ④ NestedScrollConnection #onPostFling
    // スクロール完了後の慣性速度
    // consumedは消費された慣性速度, availableは消費されなかった速度
    fun onPostFling(consumed: Velocity, available: Velocity): Velocity
    = Offset.Zero

    View Slide

  76. JetpackCompose時代における解決策
    ④ NestedScrollConnection #onPostFling
    // スクロール完了後の慣性速度
    // consumedは消費された慣性速度, availableは消費されなかった速度
    fun onPostFling(consumed: Velocity, available: Velocity): Velocity
    = Offset.Zero

    View Slide

  77. JetpackCompose時代における解決策
    NestedScrollConnectionを活用して
    以下のような実装が可能
    FABの表示切り替え stickyレイアウト
    アニメーション

    View Slide

  78. JetpackCompose時代における解決策
    可変にしたい部分をStateにする
    // 初期値を定義する
    val paddingState by remember { mutableStateOf(16.dp) }
    // 描画時に初期値を取得する
    modifier = Modifier.onGloballyPositioned {
    heightState = it.size.height
    }
    ...
    modifier = Modifier.onSizeChanged {
    widthState = it.width
    }

    View Slide

  79. JetpackCompose時代における解決策
    可変にしたい部分をStateにする
    // 初期値を定義する
    val paddingState by remember { mutableStateOf(16.dp) }
    // 描画時に初期値を取得する
    modifier = Modifier.onGloballyPositioned {
    heightState = it.size.height
    }
    ...
    modifier = Modifier.onSizeChanged {
    widthState = it.width
    }

    View Slide

  80. JetpackCompose時代における解決策
    可変にしたい部分をStateにする
    // 初期値を定義する
    val paddingState by remember { mutableStateOf(16.dp) }
    // 描画時に初期値を取得する
    modifier = Modifier.onGloballyPositioned {
    heightState = it.size.height
    }
    ...
    modifier = Modifier.onSizeChanged {
    widthState = it.width
    }

    View Slide

  81. JetpackCompose時代における解決策
    可変にしたい部分をStateにする
    // 初期値を定義する
    val paddingState by remember { mutableStateOf(16.dp) }
    // 描画時に初期値を取得する
    modifier = Modifier.onGloballyPositioned {
    heightState = it.size.height
    }
    ...
    modifier = Modifier.onSizeChanged {
    widthState = it.width
    }

    View Slide

  82. JetpackCompose時代における解決策
    まとめ
    NestedScrollConnectionを用いることで
    スクロールに応じてコンポーネントの状態を更新できる
    現時点でalpha版やExperimentalAPIの
    sticky headerやMotion Layoutといった
    複雑なネストスクロールも実装可能
    ただし、実装が複雑になりがちなので注意したい

    View Slide

  83. AndroidView時代のコードと比較

    View Slide

  84. AndroidView時代のコードと比較
    AndroidViewのスクロール環境
    ・デフォルトでネストスクロールをサポートしていない
    ・onInterceptTouchEvent/NestedScrollViewで
    子のタッチイベントをインターセプトできる
    ・MotionLayoutとCoordinatorLayoutで
    複雑なネストス
    クロールを実装できる
    ・NestedScrollingParent3/NestedScrollingChild3
    を実装するとネストスクロールをカスタムできる

    View Slide

  85. AndroidView時代のコードと比較
    AndroidViewのスクロール環境
    ・デフォルトでネストスクロールをサポートしていない
    ・onInterceptTouchEvent/NestedScrollViewで
    子のタッチイベントをインターセプトできる
    ・MotionLayoutとCoordinatorLayoutで
    複雑なネストス
    クロールを実装できる
    ・NestedScrollingParent3/NestedScrollingChild3
    を実装するとネストスクロールをカスタムできる

    View Slide

  86. ネストしないスクロールは動作する
    AndroidView時代のコードと比較
    ConstraintLayout
    ScrollView ScrollView ScrollView
    TextView TextView TextView

    View Slide

  87. スクロール可能なViewを重ねると
    子のスクロールが動作しなくなる
    AndroidView時代のコードと比較
    ScrollView
    ScrollView ScrollView ScrollView
    TextView TextView TextView

    View Slide

  88. AndroidView時代のコードと比較
    AndroidViewのスクロール環境
    ・デフォルトでネストスクロールをサポートしていない
    ・onInterceptTouchEvent/NestedScrollViewで
    子のタッチイベントをインターセプトできる
    ・MotionLayoutとCoordinatorLayoutで
    複雑なネストス
    クロールを実装できる
    ・NestedScrollingParent3/NestedScrollingChild3
    を実装するとネストスクロールをカスタムできる

    View Slide

  89. NestedScrollViewを用いると子の
    スクロールをインターセプトできる
    AndroidView時代のコードと比較
    ScrollView
    NestedScrollView NestedScrollView NestedScrollView
    TextView TextView TextView

    View Slide

  90. AndroidView時代のコードと比較
    タッチイベントの流れ
    TextView
    ScrollView
    ネストスクロールできるか確認する(※後述)
    NestedScrollView
    ACTION_DOWNは伝播させる / ACTION_MOVEは奪う

    View Slide

  91. ViewPager2 + 横並びRecyclerView
    ViewPager2のスワイプと横並びのRecyclerViewの
    スクロールが競合してしまう
    ネストスクロールをサポートしていないので
    ViewPager2のタッチイベントが優先される
    AndroidView時代のコードと比較

    View Slide

  92. Github: android/platform-samples
    androidx.viewpager2.integration.testapp.NestedScrollableHost.kt 
    の実装をベースに見ていく
    Appach License 2.0
    https://www.apache.org/licenses/LICENSE-2.0
    AndroidView時代のコードと比較

    View Slide

  93. AndroidView時代のコードと比較
    ViewPager2とRecyclerViewのスクロール競合を解消








    View Slide

  94. AndroidView時代のコードと比較
    onInterceptTouchEventでスクロールの競合を解消








    View Slide

  95. AndroidView時代のコードと比較
    onInterceptTouchEventでスクロールの競合を解消
    class NestedScrollableHost : FrameLayout {
    ...
    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
    handleInterceptTouchEvent(e)
    return super.onInterceptTouchEvent(e)
    }
    private fun handleInterceptTouchEvent(e: MotionEvent) {
    ...
    }
    }

    View Slide

  96. AndroidView時代のコードと比較
    onInterceptTouchEventをオーバーライド
    class NestedScrollableHost : FrameLayout {
    ...
    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
    handleInterceptTouchEvent(e)
    return super.onInterceptTouchEvent(e)
    }
    private fun handleInterceptTouchEvent(e: MotionEvent) {
    ...
    }
    }

    View Slide

  97. AndroidView時代のコードと比較
    onInterceptTouchEventをオーバーライド
    class NestedScrollableHost : FrameLayout {
    ...
    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
    handleInterceptTouchEvent(e)
    return super.onInterceptTouchEvent(e)
    }
    private fun handleInterceptTouchEvent(e: MotionEvent) {
    ...
    }
    }

    View Slide

  98. AndroidView時代のコードと比較
    親ビューがタッチイベントを受け取らないようにする
    class NestedScrollableHost : FrameLayout {
    ...
    private fun handleInterceptTouchEvent(e: MotionEvent) {
    if (e.action == MotionEvent.ACTION_DOWN) {
    ...
    parent.requestDisallowInterceptTouchEvent(true)
    }
    ...
    }
    }

    View Slide

  99. AndroidView時代のコードと比較
    親ビューがタッチイベントを受け取らないようにする
    RecyclerView上を操作しても
    ViewPager2が動作しなくなる

    View Slide

  100. AndroidView時代のコードと比較
    スクロールの端に到達した場合、親に伝播させる
    class NestedScrollableHost : FrameLayout {
    ...
    if (scaledDx > touchSlop || scaledDy > touchSlop) {
    if (isVpHorizontal == (scaledDy > scaledDx)) {
    ...
    } else {
    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
    ...
    } else {
    parent.requestDisallowInterceptTouchEvent(false)
    ...

    View Slide

  101. AndroidView時代のコードと比較
    RecyclerViewのタッチイベントを邪魔しない
    class NestedScrollableHost : FrameLayout {
    ...
    if (scaledDx > touchSlop || scaledDy > touchSlop) {
    if (isVpHorizontal == (scaledDy > scaledDx)) {
    ...
    } else {
    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
    ...
    } else {
    parent.requestDisallowInterceptTouchEvent(false)
    ...

    View Slide

  102. AndroidView時代のコードと比較
    RecyclerViewのタッチイベントを邪魔しない
    class NestedScrollableHost : FrameLayout {
    ...
    if (scaledDx > touchSlop || scaledDy > touchSlop) {
    if (isVpHorizontal == (scaledDy > scaledDx)) {
    ...
    } else {
    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
    ...
    } else {
    parent.requestDisallowInterceptTouchEvent(false)
    ...
    ページを切り替えていると
    みなすスクロール量

    View Slide

  103. AndroidView時代のコードと比較
    RecyclerViewのタッチイベントを邪魔しない
    class NestedScrollableHost : FrameLayout {
    ...
    if (scaledDx > touchSlop || scaledDy > touchSlop) {
    if (isVpHorizontal == (scaledDy > scaledDx)) {
    ...
    } else {
    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
    ...
    } else {
    parent.requestDisallowInterceptTouchEvent(false)
    ...
    Viewシステムがスクロールとみなす距離
    ViewConfigurationで取得できる

    View Slide

  104. AndroidView時代のコードと比較
    縦スクロールか横スクロールかの判定
    class NestedScrollableHost : FrameLayout {
    ...
    if (scaledDx > touchSlop || scaledDy > touchSlop) {
    if (isVpHorizontal == (scaledDy > scaledDx)) {
    ...
    } else {
    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
    ...
    } else {
    parent.requestDisallowInterceptTouchEvent(false)
    ...
    ViewPagerが横スワイプ可能
    RecyclerViewがどちらにスクロールされたか

    View Slide

  105. AndroidView時代のコードと比較
    縦スクロールか横スクロールかの判定
    class NestedScrollableHost : FrameLayout {
    ...
    if (scaledDx > touchSlop || scaledDy > touchSlop) {
    if (isVpHorizontal == (scaledDy > scaledDx)) {
    ...
    } else {
    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
    ...
    } else {
    parent.requestDisallowInterceptTouchEvent(false)
    ...
    これ以上スクロールできるか

    View Slide

  106. AndroidView時代のコードと比較
    子がスクロールできるかどうか確認する
    class NestedScrollableHost : FrameLayout {
    ...
    private fun canChildScroll(orientation: Int, delta: Float): Boolean {
    val direction = -delta.sign.toInt()
    return when (orientation) {
    0 -> child?.canScrollHorizontally(direction) ?: false
    1 -> child?.canScrollVertically(direction) ?: false
    else -> throw IllegalArgumentException()
    }
    }
    ...

    View Slide

  107. AndroidView時代のコードと比較
    子がスクロールできるかどうか確認する
    class NestedScrollableHost : FrameLayout {
    ...
    private fun canChildScroll(orientation: Int, delta: Float): Boolean {
    val direction = -delta.sign.toInt()
    return when (orientation) {
    0 -> child?.canScrollHorizontally(direction) ?: false
    1 -> child?.canScrollVertically(direction) ?: false
    else -> throw IllegalArgumentException()
    }
    }
    ...

    View Slide

  108. AndroidView時代のコードと比較
    子がスクロールできるかどうか確認する
    class NestedScrollableHost : FrameLayout {
    ...
    private fun canChildScroll(orientation: Int, delta: Float): Boolean {
    val direction = -delta.sign.toInt()
    return when (orientation) {
    0 -> child?.canScrollHorizontally(direction) ?: false
    1 -> child?.canScrollVertically(direction) ?: false
    else -> throw IllegalArgumentException()
    }
    }
    ...

    View Slide

  109. AndroidView時代のコードと比較
    子がスクロールできるかどうか確認する
    class NestedScrollableHost : FrameLayout {
    ...
    private fun canChildScroll(orientation: Int, delta: Float): Boolean {
    val direction = -delta.sign.toInt()
    return when (orientation) {
    0 -> child?.canScrollHorizontally(direction) ?: false
    1 -> child?.canScrollVertically(direction) ?: false
    else -> throw IllegalArgumentException()
    }
    ...

    View Slide

  110. AndroidView時代のコードと比較
    子がスクロールできない場合、親がインターセプトする
    class NestedScrollableHost : FrameLayout {
    ...
    if (scaledDx > touchSlop || scaledDy > touchSlop) {
    if (isVpHorizontal == (scaledDy > scaledDx)) {
    ...
    } else {
    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
    ...
    } else {
    parent.requestDisallowInterceptTouchEvent(false)
    ...

    View Slide

  111. AndroidView時代のコードと比較
    タッチイベントの流れ
    RecyclerView
    ViewPager
    ACTION_DOWNの時は伝播させない / 子がスクロールできない時は伝播させる
    NestedScrollableHost
    スクロールできるか確認

    View Slide

  112. AndroidView時代のコードと比較
    AndroidViewはスクロールのカスタムが難しい
    ・ネストスクロールをサポートしていないので
    タッチイベントの競合を解消する必要がある
    ・親にスクロールを伝播するときに子がスクロール可能か
    どうかも確認する必要がある

    View Slide

  113. AndroidView時代のコードと比較
    AndroidViewのスクロール環境
    ・デフォルトでネストスクロールをサポートしていない
    ・onInterceptTouchEvent/NestedScrollViewで
    子のタッチイベントをインターセプトできる
    ・MotionLayoutとCoordinatorLayoutで
    複雑なネストス
    クロールを実装できる
    ・NestedScrollingParent3/NestedScrollingChild3
    を実装するとネストスクロールをカスタムできる

    View Slide

  114. CoordinatorLayout + CollapsingToolbarで
    シンプルな折りたたみツールバーをサポートできる
    AndroidView時代のコードと比較
    enterAlways enterAlwaysCollapsed exitUntilCollapsed

    View Slide

  115. MotionLayoutでより高度なアニメーションをつける
    ※ Jetpack Composeでも提供されている
    AndroidView時代のコードと比較

    View Slide

  116. AndroidView時代のコードと比較
    AndroidViewのスクロール環境
    ・デフォルトでネストスクロールをサポートしていない
    ・onInterceptTouchEvent/NestedScrollViewで
    子のタッチイベントをインターセプトできる
    ・MotionLayoutとCoordinatorLayoutで
    複雑なネストス
    クロールを実装できる
    ・NestedScrollingParent3/NestedScrollingChild3
    を実装するとネストスクロールをカスタムできる

    View Slide

  117. NestedScrollingParentとは?
    子と連携してネストスクロールを制御するためのinterface
    CoordinatorLayout / MotionLayoutなどで使われている
    AndroidView時代のコードと比較

    View Slide

  118. NestedScrollingChildとは?
    ネストスクロールを親に適切に通知するためのinterface
    NestedScrollViewやRecyclerViewなどで実装されている
    AndroidView時代のコードと比較

    View Slide

  119. AndroidView時代のコードと比較
    NestedScrollingParent/Childの大まかな流れ
    NestedScrollingParent3 NestedScrollingChild3
    #startNestedScroll
    #onStartNestedScroll
    #onNestedScrollAccepted
    #dispatchNestedPreScroll
    #onNestedPreScroll
    #dispatchNestedScroll
    #onNestedScroll
    #stopNestedScroll
    #onStopNestedScroll

    View Slide

  120. AndroidView時代のコードと比較
    ACTION_DOWNを受け取るとネストスクロールを開始する
    NestedScrollingParent3 NestedScrollingChild3
    #startNestedScroll
    #onStartNestedScroll
    #onNestedScrollAccepted
    #dispatchNestedPreScroll
    #onNestedPreScroll
    #dispatchNestedScroll
    #stopNestedScroll
    #onNestedScroll
    #onStopNestedScroll

    View Slide

  121. AndroidView時代のコードと比較
    ネストスクロールをサポートするかどうか決める
    NestedScrollingParent3 NestedScrollingChild3
    #startNestedScroll
    #onStartNestedScroll
    #onNestedScrollAccepted
    #dispatchNestedPreScroll
    #onNestedPreScroll
    #dispatchNestedScroll
    #stopNestedScroll
    #onNestedScroll
    #onStopNestedScroll

    View Slide

  122. AndroidView時代のコードと比較
    ネストスクロールが有効であることを確認する
    NestedScrollingParent3 NestedScrollingChild3
    #startNestedScroll
    #onStartNestedScroll
    #onNestedScrollAccepted
    #dispatchNestedPreScroll
    #onNestedPreScroll
    #dispatchNestedScroll
    #stopNestedScroll
    #onNestedScroll
    #onStopNestedScroll

    View Slide

  123. AndroidView時代のコードと比較
    ACTION_MOVE:子Viewが親にスクロール量を送る
    NestedScrollingParent3 NestedScrollingChild3
    #startNestedScroll
    #onStartNestedScroll
    #onNestedScrollAccepted
    #dispatchNestedPreScroll
    #onNestedPreScroll
    #dispatchNestedScroll
    #onNestedScroll
    #stopNestedScroll
    #onStopNestedScroll

    View Slide

  124. AndroidView時代のコードと比較
    子が消費する前に親に伝播させるスクロール量を決める
    NestedScrollingParent3 NestedScrollingChild3
    #startNestedScroll
    #onStartNestedScroll
    #onNestedScrollAccepted
    #dispatchNestedPreScroll
    #onNestedPreScroll
    #dispatchNestedScroll
    #onNestedScroll
    #stopNestedScroll
    #onStopNestedScroll

    View Slide

  125. AndroidView時代のコードと比較
    子でスクロールした消費量を渡す
    NestedScrollingParent3 NestedScrollingChild3
    #startNestedScroll
    #onStartNestedScroll
    #onNestedScrollAccepted
    #dispatchNestedPreScroll
    #onNestedPreScroll
    #dispatchNestedScroll
    #onNestedScroll
    #stopNestedScroll
    #onStopNestedScroll

    View Slide

  126. AndroidView時代のコードと比較
    ACTION_UP:タッチイベントの終了
    NestedScrollingParent3 NestedScrollingChild3
    #startNestedScroll
    #onStartNestedScroll
    #onNestedScrollAccepted
    #dispatchNestedPreScroll
    #onNestedPreScroll
    #dispatchNestedScroll
    #onNestedScroll
    #stopNestedScroll
    #onStopNestedScroll

    View Slide

  127. AndroidView時代のコードと比較
    NestedScrollingChildの実装例
    Github: takahirom/webview-in-coordinatorlayout
    NestedWebView.javaの実装をベースに見ていく
    Appach License 2.0
    https://www.apache.org/licenses/LICENSE-2.0

    View Slide

  128. NestedWebView
    AppBarLayout
    CoordinatorLayout
    CollaptingToolbar
    NestedWebView
    Toolbar
    AndroidView時代のコードと比較

    View Slide

  129. AndroidView時代のコードと比較
    NestedWebViewの実装(簡略化する)
    class NestedWebView (...) : WebView, NestedScrollingChild {
    private val mChildHelper = NestedScrollingChildHelper(this)
    override fun onTouchEvent(...): Boolean {...}
    override fun dispatch
    override fun setNestedScrollingEnabled(...) {...}
    override fun isNestedScrollingEnabled(...): Boolean {...}
    override fun startNestedScroll(...): Boolean {...}
    override fun stopNestedScroll(...) {...}
    override fun hasNestedScrollingParent(...): Boolean {...}
    override fun dispatchNestedScroll(...): Boolean {...}
    override fun dispatchNestedPreScroll(...): Boolean {...}
    override fun dispatchNestedFling(...): Boolean {...
    override fun dispatchNestedPreFling(...): Boolean {...}
    }

    View Slide

  130. AndroidView時代のコードと比較
    NestedScrollingChildを実装する
    class NestedWebView (...) : WebView, NestedScrollingChild {
    private val mChildHelper = NestedScrollingChildHelper(this)
    override fun onTouchEvent(...): Boolean {...}
    override fun setNestedScrollingEnabled(...) {...}
    override fun isNestedScrollingEnabled(...): Boolean {...}
    override fun startNestedScroll(...): Boolean {...}
    override fun stopNestedScroll(...) {...}
    override fun hasNestedScrollingParent(...): Boolean {...}
    override fun dispatchNestedScroll(...): Boolean {...}
    override fun dispatchNestedPreScroll(...): Boolean {...}
    override fun dispatchNestedFling(...): Boolean {...
    override fun dispatchNestedPreFling(...): Boolean {...}
    }

    View Slide

  131. AndroidView時代のコードと比較
    NestedScrollingChildHelperを介してイベントを送る
    class NestedWebView (...) : WebView, NestedScrollingChild {
    private val mChildHelper = NestedScrollingChildHelper(this)
    override fun onTouchEvent(...): Boolean {...}
    override fun setNestedScrollingEnabled(...) {...}
    override fun isNestedScrollingEnabled(...): Boolean {...}
    override fun startNestedScroll(...): Boolean {...}
    override fun stopNestedScroll(...) {...}
    override fun hasNestedScrollingParent(...): Boolean {...}
    override fun dispatchNestedScroll(...): Boolean {...}
    override fun dispatchNestedPreScroll(...): Boolean {...}
    override fun dispatchNestedFling(...): Boolean {...
    override fun dispatchNestedPreFling(...): Boolean {...}
    }

    View Slide

  132. AndroidView時代のコードと比較
    ACTION_DOWNでネストスクロールを開始する
    class NestedWebView (...) : WebView, NestedScrollingChild {
    private val mChildHelper = NestedScrollingChildHelper(this)
    override fun onTouchEvent(...): Boolean {...}
    override fun setNestedScrollingEnabled(...) {...}
    override fun isNestedScrollingEnabled(...): Boolean {...}
    override fun startNestedScroll(...): Boolean {...}
    override fun stopNestedScroll(...) {...}
    override fun hasNestedScrollingParent(...): Boolean {...}
    override fun dispatchNestedScroll(...): Boolean {...}
    override fun dispatchNestedPreScroll(...): Boolean {...}
    override fun dispatchNestedFling(...): Boolean {...
    override fun dispatchNestedPreFling(...): Boolean {...}
    }

    View Slide

  133. AndroidView時代のコードと比較
    ネストスクロールを有効化 / 確認
    class NestedWebView (...) : WebView, NestedScrollingChild {
    private val mChildHelper = NestedScrollingChildHelper(this)
    override fun onTouchEvent(...): Boolean {...}
    override fun setNestedScrollingEnabled(...) {...}
    override fun isNestedScrollingEnabled(...): Boolean {...}
    override fun startNestedScroll(...): Boolean {...}
    override fun stopNestedScroll(...) {...}
    override fun hasNestedScrollingParent(...): Boolean {...}
    override fun dispatchNestedScroll(...): Boolean {...}
    override fun dispatchNestedPreScroll(...): Boolean {...}
    override fun dispatchNestedFling(...): Boolean {...
    override fun dispatchNestedPreFling(...): Boolean {...}
    }

    View Slide

  134. AndroidView時代のコードと比較
    NestedScrollConnectionと同様にスクロール量を送る
    class NestedWebView (...) : WebView, NestedScrollingChild {
    private val mChildHelper = NestedScrollingChildHelper(this)
    override fun onTouchEvent(...): Boolean {...}
    override fun setNestedScrollingEnabled(...) {...}
    override fun isNestedScrollingEnabled(...): Boolean {...}
    override fun startNestedScroll(...): Boolean {...}
    override fun stopNestedScroll(...) {...}
    override fun hasNestedScrollingParent(...): Boolean {...}
    override fun dispatchNestedScroll(...): Boolean {...}
    override fun dispatchNestedPreScroll(...): Boolean {...}
    override fun dispatchNestedFling(...): Boolean {...
    override fun dispatchNestedPreFling(...): Boolean {...}
    }

    View Slide

  135. AndroidView時代のコードと比較
    ACTION_UP / ACTION_CANCELでスクロールを終了する
    class NestedWebView (...) : WebView, NestedScrollingChild {
    private val mChildHelper = NestedScrollingChildHelper(this)
    override fun onTouchEvent(...): Boolean {...}
    override fun setNestedScrollingEnabled(...) {...}
    override fun isNestedScrollingEnabled(...): Boolean {...}
    override fun startNestedScroll(...): Boolean {...}
    override fun stopNestedScroll(...) {...}
    override fun hasNestedScrollingParent(...): Boolean {...}
    override fun dispatchNestedScroll(...): Boolean {...}
    override fun dispatchNestedPreScroll(...): Boolean {...}
    override fun dispatchNestedFling(...): Boolean {...
    override fun dispatchNestedPreFling(...): Boolean {...}
    }

    View Slide

  136. AndroidView時代のコードと比較
    AndroidViewの場合デフォルトでネストスクロールを
    サポートしていないので考慮すべきことが多い
    ・オーバーライドするメソッドが多い
    ・ネストスクロールが有効になっているかの確認
    ・タッチイベントが終了した時
    ・MotionEventという汎用的なデータを受け取るので
    内部で値の操作が多く発生する

    View Slide

  137. AndroidView時代のコードと比較
    まとめ
    Jetpack Composeと比較してコンポーネントが豊富
    カスタムしないといけないケースは少ない
    スクロールをカスタムする時は
    Jetpack Composeの方がやりやすい印象
    特にデフォルトでネストスクロールをサポートしていない分
    考慮しないといけない部分が多い

    View Slide

  138. 相互運用上の注意点

    View Slide

  139. Android View
    Child Compose
    相互運用上の注意点
    相互運用する時のネストスクロール
    〜子のViewからCompose化していく時〜
    ??????

    View Slide

  140. CoordinatorLayout
    LazyColumn
    相互運用上の注意点
    相互運用する時のネストスクロール
    ネストスクロールが機能しない

    View Slide

  141. Android View : NestedScrollingParent3
    Child Compose
    #Modifier.nestedScroll(rememberNestedScrollInteropConnection)
    相互運用上の注意点
    親のAndroidViewが協力的な場合
    NestedScrollInteropConnectionを渡してあげる
    ネストスクロールが機能する

    View Slide

  142. 相互運用上の注意点
    協力的な親Viewとは?
    NestedScrollingParent3を実装しているView
    ・NestedScrollView
    ・CoordinatorLayout
    ・MotionLayout
    ・SwipeRefreshLayout

    View Slide

  143. 相互運用上の注意点
    NestedScrollInteropConnectionは何をしているのか?
    internal class NestedScrollInteropConnection(...) : NestedScrollConnection() {
    private val nestedScrollChildHelper = NestedScrollingChildHelper(...)
    ...
    override fun onPreScroll(...): Offset {
    if(nestedScrollChildHelper.startNestedScroll(...)) {
    ...
    nestedScrollChildHelper.dispatchNestedScroll(...)
    return toOffset(consumedScrollCache, available)
    }
    }

    View Slide

  144. 相互運用上の注意点
    NestedScrollConnectionを実装
    internal class NestedScrollInteropConnection(...) : NestedScrollConnection() {
    private val nestedScrollChildHelper = NestedScrollingChildHelper(...)
    ...
    override fun onPreScroll(...): Offset {
    if(nestedScrollChildHelper.startNestedScroll(...)) {
    ...
    nestedScrollChildHelper.dispatchNestedScroll(...)
    return toOffset(consumedScrollCache, available)
    }
    }

    View Slide

  145. 相互運用上の注意点
    AndroidView同様にHelperクラスを介する
    internal class NestedScrollInteropConnection(...) : NestedScrollConnection() {
    private val nestedScrollChildHelper = NestedScrollingChildHelper(...)
    ...
    override fun onPreScroll(...): Offset {
    if(nestedScrollChildHelper.startNestedScroll(...)) {
    ...
    nestedScrollChildHelper.dispatchNestedScroll(...)
    return toOffset(consumedScrollCache, available)
    }
    }

    View Slide

  146. 相互運用上の注意点
    スクロールする前の制御
    internal class NestedScrollInteropConnection(...) : NestedScrollConnection() {
    private val nestedScrollChildHelper = NestedScrollingChildHelper(...)
    ...
    override fun onPreScroll(...): Offset {
    if(nestedScrollChildHelper.startNestedScroll(...)) {
    ...
    nestedScrollChildHelper.dispatchNestedScroll(...)
    return toOffset(consumedScrollCache, available)
    }
    }

    View Slide

  147. 相互運用上の注意点
    ネストスクロールができるか確認
    internal class NestedScrollInteropConnection(...) : NestedScrollConnection() {
    private val nestedScrollChildHelper = NestedScrollingChildHelper(...)
    ...
    override fun onPreScroll(...): Offset {
    if(nestedScrollChildHelper.startNestedScroll(...)) {
    ...
    nestedScrollChildHelper.dispatchNestedScroll(...)
    return toOffset(consumedScrollCache, available)
    }
    }

    View Slide

  148. 相互運用上の注意点
    親にスクロール消費量を送る
    internal class NestedScrollInteropConnection(...) : NestedScrollConnection() {
    private val nestedScrollChildHelper = NestedScrollingChildHelper(...)
    ...
    override fun onPreScroll(...): Offset {
    if(nestedScrollChildHelper.startNestedScroll(...)) {
    ...
    nestedScrollChildHelper.dispatchNestedScroll(...)
    return toOffset(consumedScrollCache, available)
    }
    }

    View Slide

  149. 相互運用上の注意点
    スクロール消費量と消費されなかった量を送る
    internal class NestedScrollInteropConnection(...) : NestedScrollConnection() {
    private val nestedScrollChildHelper = NestedScrollingChildHelper(...)
    ...
    override fun onPreScroll(...): Offset {
    if(nestedScrollChildHelper.startNestedScroll(...)) {
    ...
    nestedScrollChildHelper.dispatchNestedScroll(
    composeToViewOffset(consumed.x),
    composeToViewOffset(consumed.y),
    composeToViewOffset(available.x),
    composeToViewOffset(available.y),
    ...
    )

    View Slide

  150. CoordinatorLayout
    Child Compose
    相互運用上の注意点
    NestedScrollInteropConnectionによりうまく動作する
    スクロールを伝播

    View Slide

  151. Parent Compose
    Child Android View
    相互運用上の注意点
    相互運用する時のネストスクロール
    〜親のViewをCompose化する時〜
    ??????

    View Slide

  152. 相互運用上の注意点
    Composable関数のAndroidViewを用いる
    Box(Modifier.nestedScroll(nestedScrollConnection)) {
    AndroidViewBinding(
    factory = InteropNestedScrollBinding::inflate,
    update = {
    recyclerview.layoutManager = LinearLayoutManager(root.context)
    recyclerview.adapter = AndroidViewViewPagerAdapter(...)
    },
    ...
    )
    TopAppBar(...)
    }

    View Slide

  153. 相互運用上の注意点
    Composable関数のAndroidViewを用いる
    Box(Modifier.nestedScroll(nestedScrollConnection)) {
    AndroidViewBinding(
    factory = InteropNestedScrollBinding::inflate,
    update = {
    recyclerview.layoutManager = LinearLayoutManager(root.context)
    recyclerview.adapter = AndroidViewViewPagerAdapter(...)
    },
    ...
    )
    TopAppBar(...)
    }

    View Slide

  154. 相互運用上の注意点
    Composable関数のAndroidViewを用いる
    Box(Modifier.nestedScroll(nestedScrollConnection)) {
    AndroidViewBinding(
    factory = InteropNestedScrollBinding::inflate,
    update = {
    recyclerview.layoutManager = LinearLayoutManager(root.context)
    recyclerview.adapter = AndroidViewViewPagerAdapter(...)
    },
    ...
    )
    TopAppBar(...)
    }

    View Slide

  155. 相互運用上の注意点
    Composable関数のAndroidViewを用いる
    Box(Modifier.nestedScroll(nestedScrollConnection)) {
    AndroidViewBinding(
    factory = InteropNestedScrollBinding::inflate,
    update = {
    recyclerview.layoutManager = LinearLayoutManager(root.context)
    recyclerview.adapter = AndroidViewViewPagerAdapter(...)
    },
    ...
    )
    TopAppBar(...)
    }

    View Slide

  156. 相互運用上の注意点
    Composable関数のAndroidViewを用いる
    Box(Modifier.nestedScroll(nestedScrollConnection)) {
    AndroidViewBinding(
    factory = InteropNestedScrollBinding::inflate,
    update = {
    recyclerview.layoutManager = LinearLayoutManager(root.context)
    recyclerview.adapter = AndroidViewViewPagerAdapter(...)
    },
    ...
    )
    TopAppBar(...)
    }

    View Slide

  157. 相互運用上の注意点
    Composable関数のAndroidViewを用いる
    Box(Modifier.nestedScroll(nestedScrollConnection)) {
    AndroidViewBinding(
    factory = InteropNestedScrollBinding::inflate,
    update = {
    recyclerview.layoutManager = LinearLayoutManager(root.context)
    recyclerview.adapter = AndroidViewViewPagerAdapter(...)
    },
    ...
    )
    TopAppBar(...)
    }

    View Slide

  158. 相互運用上の注意点
    Composable関数のAndroidViewの全体像
    Child AndroidView
    Parent Compose
    NestedScrollDispatcherでスクロール量を送る
    @composable Android View
    NestedScrollingParent3によりコンテナとして機能

    View Slide

  159. RecyclerView
    Parent Compose
    @composable Android View
    相互運用上の注意点
    AndroidViewによりネストスクロールが機能

    View Slide

  160. 相互運用上の注意点
    まとめ
    Jetpack ComposeとAndroidViewを接続する実装を
    用いることでネストスクロール問題を解決できる
    Jetpack Composeがネストスクロールのカスタムを
    しやすいので移行を考えるのも一つ

    View Slide

  161. ゴールの振り返り

    View Slide

  162. ゴールの振り返り
    プロダクト開発において
    ネストスクロールで
    困らないようになる

    View Slide

  163. ゴールの振り返り
    1. ネストスクロールを実装する前に気を付けるべき点が
    分かっている状態
    2. ネストスクロールで期待しない動作が起きた時に
    原因究明する思考フローが身についている
    3. スクロールをカスタムする方法を知る

    View Slide

  164. ゴールの振り返り
    Jetpack Composeのまとめ
    ・デフォルトでネストスクロールをサポートいる
    ・NestedScrollConnectionを活用したUiStateの更新で
    複雑なネストスクロールの挙動も実現できる

    View Slide

  165. ゴールの振り返り
    AndroidViewのまとめ
    ・ネストスクロールはサポートしていない
    ・NestedScrollView/onInterceptTouchEventを
    用いることでタッチイベントのカスタムが可能
    ・とはいえコンポーネントが充実している
    ・カスタムはJetpack Composeよりも考慮すべきことが  
    多い

    View Slide

  166. ゴールの振り返り
    相互運用の場合
    ・NestedScrollingParent3を実装している親View
    であればNestedScrollConnectionを繋げることは可能
    ・AndroidView(Composable関数)を用いることで
    ネストスクロールを接続させることが可能

    View Slide

  167. ゴールの振り返り
    最後に
    「こんな経験をした」「こんな時はどうする?」
    といったコメントオフィスアワーやXにて大歓迎です!
    (みんなでネストスクロールで困らない世界にしましょう)

    View Slide

  168. Thank you

    View Slide