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

できる!ComposeでCollapsingToolbar

Tomoya Miwa
November 08, 2022

 できる!ComposeでCollapsingToolbar

Tomoya Miwa

November 08, 2022
Tweet

More Decks by Tomoya Miwa

Other Decks in Technology

Transcript

  1. 10

  2. 14

  3. パターン3: LazyColumnごとスクロール Collapsing Toolbar in Jetpack Compose | Problem, solutions

    and alternatives で紹介さ れているアイデア LazyColumnは全画面表示で、ツールバーをその上に被せる ツールバーを開いている状態では、LazyColumnにy方向オフセットを指定して下に移動 しておく ツールバー開閉は、LazyColumnへのスクロール操作に対して、ツールバー内部と LazyColumnのy方向オフセットなどを変更して実現 24
  4. 3つのパターンの比較 LazyColumnに ContentPadding(top) Material3の TopAppBar LazyColumnごとス クロール 理解しやすさ 普通 簡単

    少し難しい 実装難易度 普通 簡単 少し難しい recomposition 少ない 多い 少ない scrollToItem() 問題あり 問題なし 問題なし 27
  5. 使用例 // TopAppBar + LazyColumn の(ほぼ同じ)引数に対応 LazyColumnWithToolbar( title = {

    }, navigationIcon = { }, actions = { }, ) { items(100) { index -> Text( text = "I'm item $index", style = MaterialTheme.typography.h3, modifier = Modifier .fillMaxWidth() .padding(16.dp), ) } } 33
  6. @Composable fun LazyColumnWithToolbar( ... ) { // これの中身が重要 ToolbarWithScrollableContent( modifier

    = modifier, toolbarState = toolbarState, title = title, navigationIcon = navigationIcon, actions = actions, stickyContent = stickyContent, isReachedTop = { listState.isReachedTop() }, ) { LazyColumn(...) } } 34
  7. @Composable fun ToolbarWithScrollableContent(...) { ... Box(modifier = modifier .fillMaxSize() .nestedScroll(nestedScrollConnection))

    { CollapsingToolbar(...) // ツールバー部分 Column(...) { // ツールバーの開閉に応じて上下に移動 // ツールバー下部に常に表示するコンテンツ stickyContent?.invoke() // LazyColumn content() } } } 35
  8. @Composable fun CollapsingToolbar( ... ) { ... CompositionLocalProvider( LocalContentColor provides

    contentColor, ) { Box(modifier = modifier.height(toolbarMaxHeight)) { // 背景を表示、開閉状態に応じて高さを変更 Background(...) // navigationIcon とactions ToolbarWithoutTitle(...) // タイトルを表示、開閉状態に応じて位置を移動 ToolbarTitle(...) } } } 36
  9. class ToolbarNestedScrollConnection(...) : NestedScrollConnection { // ユーザーがースクロール操作した場合、 // LazyColumn がコンテンツを実際にスクロールする前に呼ばれる

    override fun onPreScroll(...): Offset { // LazyColumn のスクロール位置が0 なら、 // ツールバーの開閉にスクロール量を消費する } ... // ユーザーがフリンジ操作した場合、 // LazyColumn がフリンジ操作をハンドリングした後に呼ばれる override suspend fun onPostFling(...): Velocity { // LazyColumn のスクロール位置が0 かつツールバーがまだ開けるなら、 // ツールバーを開く } } 41
  10. @Stable class CollapsingToolbarState(...) { private val maxOffset: Float = (maxHeight

    - minHeight).toFloat() private var _offset: Float by mutableStateOf(offset.coerceIn(0f, maxOffset)) private var _collapseFraction: Float by mutableStateOf(calcCollapseFraction()) // how much the toolbar extends from its collapsed state val offset: Float get() = _offset // fraction of collapse progress, 1 -> fully collapsed, 0 -> fully expanded val collapseFraction: Float get() = _collapseFraction var navigationIconWidth by mutableStateOf(0) var actionsWidth by mutableStateOf(0) fun consumeScrollOffset(available: Offset): Offset { ... } } 45
  11. 改善前 @Composable ToolbarWithScrollableContent( ... isReachedTop = listState.isReachedTop(), ) { ...

    } 改善後:ラムダで包む @Composable ToolbarWithScrollableContent( ... isReachedTop = { listState.isReachedTop() }, ) { ... } 47
  12. 改善前 @Composable Background( ... toolbarStateHeight = toolbarState.minHeight + toolbarState.offset, )

    改善後:@StableなStateクラスを渡す @Composable Background( ... toolbarState = toolbarState, ) 48
  13. 改善前 @Composable private fun Background( modifier: Modifier = Modifier, backgroundColor:

    Color, toolbarState: CollapsingToolbarState, ) { val toolbarHeight = with(LocalDensity.current) { (toolbarState.minHeight + toolbarState.offset).toDp() } Box( modifier = modifier .background(backgroundColor) .fillMaxWidth() .height(toolbarHeight) ) } 50
  14. 改善版 @Composable private fun Background( modifier: Modifier = Modifier, backgroundColor:

    Color, toolbarState: CollapsingToolbarState, ) { Box( modifier = modifier .fillMaxSize() .drawWithCache { onDrawBehind { drawRect( color = backgroundColor, size = Size( width = size.width, height = toolbarState.minHeight + toolbarState.offset, ), ) } }, ) } 51
  15. ラムダ版のModifierを使う Defer reads as long as possible の中で触れられている Modifier が変わる

    = recomposition が発生する なので、可能な限りラムダバージョン を使用する 52
  16. 改善前 Row( modifier = modifier .height(toolbarMinHeight) .graphicsLayer( translationX = toolbarState.navigationIconWidth

    * toolbarState.collapseFraction, translationY = toolbarState.offset, ), verticalAlignment = Alignment.CenterVertically, ) {...} 53
  17. 改善後 Row( modifier = modifier .height(toolbarMinHeight) .graphicsLayer { translationX =

    toolbarState.navigationIconWidth * toolbarState.collapseFraction translationY = toolbarState.offset }, verticalAlignment = Alignment.CenterVertically, ) {...} 54
  18. 改善前 Row( ... ) { Layout(content = title) { measurables,

    constraints -> // cause a lot of recomposition!! val maxWidth = if (toolbarState.offset < toolbarState.minHeight) { constraints.maxWidth - toolbarState.navigationIconWidth - toolbarState.actionsWidth } else { constraints.maxWidth } ... } } 56
  19. 改善後 val almostCollapsed: Boolean by remember { derivedStateOf { toolbarState.offset

    < toolbarState.minHeight } } Row( ... ) { Layout(content = title) { measurables, constraints -> val maxWidth = if (almostCollapsed) { constraints.maxWidth - toolbarState.navigationIconWidth - toolbarState.actionsWidth } else { constraints.maxWidth } ... } } 57