きっかけ
スタバのアプリのホーム画面をスクロールした時に、ぬるぬるっと動いて固定されるスティッキヘッダーを見て感動したので、これをJetpack Composeで表現できないかなと思い立ちました。
実際に書いてみて、それっぽいのができたので将来の自分への忘備録として残しておこうと思います。
TL;DR
最終的なコード全体を確認したい場合は、次のリポジトリを参照してください。
LazyColumnとstickyHeader
スクロールしたときに、一番上に到達したらそのまま画面外にはみ出るのではなく、一番上で固定されるようなスティッキヘッダーを実装したい場合は、LazyListScope
のstickyHeader()
を使います。
そして、その固定したい中身をstickyHeader()
のcontent
引数を経由してラムダで渡します。
LazyColumn {
stickyHeader {
// ここにスティッキヘッダーの本体を書いていく
}
}
スタバのアプリの一番上の部分
スタバのアプリでは、画面の一番上に「おはようございます」などのメッセージが表示されるので、その部分をTopAppBar
を使って実装していきます。
LazyColumn
の要素として一番上に配置させたいので、まずはitem
を最初に呼び出します。そしてその中にSpacer
とTopAppBar
を配置します。
LazyColumn {
item {
Spacer(modifier = Modifier.height(32.dp))
TopAppBar(
title = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text(
"おはようございます",
style = MaterialTheme.typography.headlineLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
)
Spacer(modifier = Modifier.height(8.dp))
}
}
スティッキヘッダーの本体
次に、スティッキヘッダーの本体の部分を実装していこうと思います。
スタバ本家のアプリでは、eTicketとInboxが並んで左端に、設定アイコンがポツンと一番右端に表示されています。これを再現するために、今回はRow
を組み合わせて再現しようと思います。
stickyHeader {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(
horizontal = 18.dp,
vertical = 18.dp
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row() {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = null
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "eTicket",
style = MaterialTheme.typography.titleMedium
)
}
Spacer(modifier = Modifier.width(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Email,
contentDescription = null
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "Inbox",
style = MaterialTheme.typography.titleMedium
)
}
}
Icon(
imageVector = Icons.Filled.Settings,
contentDescription = null
)
}
}
今回は、アイコンなどを押したからといって、特定のアクションを実行したいわけではないのでIconButton
は使いませんでした。
スクロール対象のコンテンツ
本家のアプリでは、ヘッダーの下にはバナー広告やリワードが表示されていますが、今回は単純にエミュレーターでスクロールできるまでLazyColumn
の高さをスケールさせたいので、簡単なリストを準備してそれをコンテンツ代わりに表示したいと思います。
val dataSource by remember {
mutableStateOf(
listOf(
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10"
)
)
}
...
LazyColumn(
state = listState
) {
...
items(dataSource) { item ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 8.dp
)
.height(120.dp)
.background(MaterialTheme.colorScheme.primaryContainer),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = item,
)
}
Spacer(modifier = Modifier.height(8.dp))
}
}
そして、完成した実際のコンポーザブルは次のようになります。
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun StickyTopAppBarScreen() {
val listState = rememberLazyListState()
val dataSource by remember {
mutableStateOf(
listOf(
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10"
)
)
}
LazyColumn(
state = listState
) {
item {
Spacer(modifier = Modifier.height(32.dp))
TopAppBar(
title = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text(
"おはようございます",
style = MaterialTheme.typography.headlineLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
)
Spacer(modifier = Modifier.height(8.dp))
}
stickyHeader {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(
horizontal = 18.dp,
vertical = 18.dp
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row() {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = null
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "eTicket",
style = MaterialTheme.typography.titleMedium
)
}
Spacer(modifier = Modifier.width(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Email,
contentDescription = null
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "Inbox",
style = MaterialTheme.typography.titleMedium
)
}
}
Icon(
imageVector = Icons.Filled.Settings,
contentDescription = null
)
}
}
items(dataSource) { item ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 8.dp
)
.height(120.dp)
.background(MaterialTheme.colorScheme.primaryContainer),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = item,
)
}
Spacer(modifier = Modifier.height(8.dp))
}
}
}
TopAppBar
とstickyHeader
はExperimental
なので、必ず@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
をコンポーザブルに指定してください。
完成したもの
実際に動かすと次のような感じになります。
まとめ
僕は昔、React Nativeでスティッキヘッダーを再現してみたことがあるのですが、そのときはスクロール位置を取得して、スティッキヘッダーコンポーネントをそれに連動してアニメーションさせることで実装するみたいなことをした記憶があります。結構大変だった。。。
ところが今回、Jetpack Composeを使って同じようなスティッキヘッダーを表現しようとしてみた結果、ものの30分くらいで同じような表現を実装することができました。
とりあえずやってみて思ったのが、Jetpack Composeすごい!!!ってことです。
ぜひみなさんも、この書きやすさと表現力の豊かさを体験してみてもらいたいです。
参考