📱

[Android]手軽にスクロールに合わせたAppBarを作りたい

に公開

Collapsing App Bar

Jetpack Composeではマテリアル3のコンポーネントで

  • TopAppBar
  • CenterAlignedTopAppBar
  • MediumTopAppBar
  • LargeTopAppBar

上記4種類が用意されていて、決められた挙動でCollapsing App Barを実装できる
https://developer.android.com/develop/ui/compose/components/app-bars

しかし、欲しい動きはそうじゃないというのがあると思う。
それをできるだけお手軽に実装してみたい

1.ジャンプ+方式

デフォルトだとNavigation Icon以外は透明にしておく、itemのindexが1以上かつちょうど良い感じのoffsetを設定する
それ対応したalpha値のStateを用意する
スクロールによって徐々にalpha値が変化する

val scrollState = rememberLazyListState()
val appBarAlpha = remember(scrollState) {
    derivedStateOf {
        val scrollOffset = when {
            scrollState.firstVisibleItemIndex > 0 -> 1f // 1つ目のアイテムが見えなくなったら完全に表示
            else -> {
                // スクロールオフセットに基づいて0〜1の値を計算
                // 例: 200pxスクロールしたら完全に表示する場合
                val maxOffset = 220f
                (scrollState.firstVisibleItemScrollOffset / maxOffset).coerceIn(0f, 1f)
            }
        }
        scrollOffset
    }
}
Box {
    CenterAlignedTopAppBar(
        modifier = Modifier.zIndex(1f),
        title = {
            Text("テキスト", modifier = Modifier
                .graphicsLayer {
                    alpha = appBarAlpha.value
                }
            )
        },
        navigationIcon = {
            IconButton(
                onClick = { /* Handle back navigation */ },
            ) {
                Icon(
                    imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                    contentDescription = "戻る"
                )
            }
        },
        colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
            containerColor = Color.White.copy(alpha = appBarAlpha.value)
        )
    )
    LazyColumn(
        modifier = Modifier
            .fillMaxSize()
            .padding(innerPadding),
        state = scrollState
    ) {
        item {
            Image(
                painter = painterResource(id = R.drawable.img),
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(220.dp)
            )
        }
        items(100) {
            Text("Item $it", modifier = Modifier.fillMaxWidth())
        }
    }
}

2.ジャンプTOON方式

LargeTopAppBarを利用してこの動きを利用しつつ画像を重ねてcollapsedFractionに合わせて画像サイズを変えていく

Scaffold(
   modifier = Modifier
       .fillMaxSize()
       .nestedScroll(scrollBehavior.nestedScrollConnection),
   topBar = {
       Box {
           Image(
               painter = painterResource(id = R.drawable.img),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier
                   .fillMaxWidth()
                   .height(220.dp * (1f - scrollBehavior.state.collapsedFraction))
                   .graphicsLayer {
                       alpha = 1f - scrollBehavior.state.collapsedFraction
                   }
           )

           Box(
               modifier = Modifier
                   .padding(top = statusBarHeight)
                   .height(64.dp)
                   .fillMaxWidth(),
               contentAlignment = Alignment.Center
           ) {
               Text(
                   "テキスト",
                   modifier = Modifier
                       .graphicsLayer {
                           alpha = (scrollBehavior.state.collapsedFraction - 0.5f) * 2
                       }
               )
           }

           LargeTopAppBar(
               scrollBehavior = scrollBehavior,
               expandedHeight = 220.dp,
               title = {},
               navigationIcon = {
                   IconButton(
                       onClick = { /* Handle back navigation */ },
                   ) {
                       Icon(
                           imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                           contentDescription = "戻る"
                       )
                   }
               },
               colors = TopAppBarDefaults.largeTopAppBarColors(
                   containerColor = Color.Transparent,
                   scrolledContainerColor = Color.Transparent
               )
           )
       }
   }
) { innerPadding ->
   LazyColumn(
       modifier = Modifier
           .fillMaxSize()
           .padding(innerPadding),
       state = scrollState
   ) {
       items(100) {
           Text("Item $it", modifier = Modifier.fillMaxWidth())
       }
   }
}

Discussion