4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

AndroidAdvent Calendar 2022

Day 17

Jetpack Composeでスタバアプリのようなスティッキーヘッダーを実装したい

Last updated at Posted at 2022-12-17

きっかけ

スタバのアプリのホーム画面をスクロールした時に、ぬるぬるっと動いて固定されるスティッキヘッダーを見て感動したので、これをJetpack Composeで表現できないかなと思い立ちました。

実際に書いてみて、それっぽいのができたので将来の自分への忘備録として残しておこうと思います。

TL;DR

最終的なコード全体を確認したい場合は、次のリポジトリを参照してください。

LazyColumnとstickyHeader

スクロールしたときに、一番上に到達したらそのまま画面外にはみ出るのではなく、一番上で固定されるようなスティッキヘッダーを実装したい場合は、LazyListScopestickyHeader()を使います。

そして、その固定したい中身をstickyHeader()content引数を経由してラムダで渡します。

LazyColumn {
    stickyHeader {
        // ここにスティッキヘッダーの本体を書いていく
    }
}

スタバのアプリの一番上の部分

スタバのアプリでは、画面の一番上に「おはようございます」などのメッセージが表示されるので、その部分をTopAppBarを使って実装していきます。

LazyColumnの要素として一番上に配置させたいので、まずはitemを最初に呼び出します。そしてその中にSpacerTopAppBarを配置します。

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

スティッキヘッダーの本体

次に、スティッキヘッダーの本体の部分を実装していこうと思います。
スタバ本家のアプリでは、eTicketInboxが並んで左端に、設定アイコンがポツンと一番右端に表示されています。これを再現するために、今回は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))
        }
    }
}

TopAppBarstickyHeaderExperimentalなので、必ず@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)をコンポーザブルに指定してください。

完成したもの

実際に動かすと次のような感じになります。

名称未設定のデザイン (2).gif

まとめ

僕は昔、React Nativeでスティッキヘッダーを再現してみたことがあるのですが、そのときはスクロール位置を取得して、スティッキヘッダーコンポーネントをそれに連動してアニメーションさせることで実装するみたいなことをした記憶があります。結構大変だった。。。

ところが今回、Jetpack Composeを使って同じようなスティッキヘッダーを表現しようとしてみた結果、ものの30分くらいで同じような表現を実装することができました。
とりあえずやってみて思ったのが、Jetpack Composeすごい!!!ってことです。

ぜひみなさんも、この書きやすさと表現力の豊かさを体験してみてもらいたいです。

参考

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?