Slide 1

Slide 1 text

Harada Reo DroidKaigi 2023 UI/UX Master of NestedScroll

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

目次 / Index 実際のプロダクト開発で起こりうる ネストスクロール問題について 2 3 4 JetpackCompose時代における解決策 相互運用上の注意点 AndroidView時代のコードと比較 1

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Jetpack Compose時代における解決策 2

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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 )

Slide 26

Slide 26 text

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 )

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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 }

Slide 49

Slide 49 text

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 }

Slide 50

Slide 50 text

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 }

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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 }

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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 }

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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 }

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

AndroidView時代のコードと比較 3

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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) ...

Slide 101

Slide 101 text

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) ...

Slide 102

Slide 102 text

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) ... ページを切り替えていると みなすスクロール量

Slide 103

Slide 103 text

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で取得できる

Slide 104

Slide 104 text

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がどちらにスクロールされたか

Slide 105

Slide 105 text

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) ... これ以上スクロールできるか

Slide 106

Slide 106 text

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() } } ...

Slide 107

Slide 107 text

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() } } ...

Slide 108

Slide 108 text

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() } } ...

Slide 109

Slide 109 text

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() } ...

Slide 110

Slide 110 text

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) ...

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

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 {...} }

Slide 130

Slide 130 text

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 {...} }

Slide 131

Slide 131 text

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 {...} }

Slide 132

Slide 132 text

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 {...} }

Slide 133

Slide 133 text

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 {...} }

Slide 134

Slide 134 text

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 {...} }

Slide 135

Slide 135 text

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 {...} }

Slide 136

Slide 136 text

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

Slide 137

Slide 137 text

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

Slide 138

Slide 138 text

相互運用上の注意点 4

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

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

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

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

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

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

Slide 148

Slide 148 text

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

Slide 149

Slide 149 text

相互運用上の注意点 スクロール消費量と消費されなかった量を送る 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), ... )

Slide 150

Slide 150 text

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

Slide 151

Slide 151 text

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

Slide 152

Slide 152 text

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

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

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

Slide 155

Slide 155 text

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

Slide 156

Slide 156 text

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

Slide 157

Slide 157 text

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

Slide 158

Slide 158 text

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

Slide 159

Slide 159 text

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

Slide 160

Slide 160 text

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

Slide 161

Slide 161 text

ゴールの振り返り

Slide 162

Slide 162 text

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

Slide 163

Slide 163 text

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

Slide 164

Slide 164 text

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

Slide 165

Slide 165 text

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

Slide 166

Slide 166 text

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

Slide 167

Slide 167 text

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

Slide 168

Slide 168 text

Thank you