Jetpack ComposeでSwipe to Refreshのインジケータの表示位置をずらす

これは エキサイトホールディングス Advent Calendar 2021 19日目の記事です。

こんにちは。エキサイト株式会社 Androidエンジニアの克です。

今回は、AndroidJetpack ComposeでのSwipe to Refreshについてのお話です。

Swipe to Refreshとは

GoogleMaterial Designには、画面上の表示を最新の状態に更新するための仕組みとしてSwipe to Refreshというものが存在します。

Swipe-to-Refresh

AndroidのViewとしてはSwiperefreshlayoutが存在しますが、Jetpack Compose本体には相当するものが無いため今回はAccompanistSwipe Refreshを使用します。

Swipe to Refreshとリスト表示を実装する

まずはSwipe to Refresh本体と、セットになることが多いリスト表示を実装していきます。

今回はインジケータの調整が目的のため、実装内容については公式のドキュメントを参照してください。

@Composable
private fun Screen() {
    var isRefreshing by remember { mutableStateOf(false) }

    SwipeRefresh(
        state = rememberSwipeRefreshState(isRefreshing),
        onRefresh = { isRefreshing = true },
    ) {
        LazyColumn {
            items(30) { index ->
                ListItem(id = index + 1)
            }
        }
    }
}

@Composable
private fun ListItem(id: Int) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(48.dp)
            .background(color = if (id % 2 == 0) Color.LightGray else Color.Gray),
        contentAlignment = Alignment.Center,
    ) {
        Text(text = "item $id")
    }
}

下記はこのコードの動作イメージです。

固定のコンテンツとリストを組み合わせる

追加の要件として、画面上部にスクロールに左右されない固定表示のコンテンツを追加します。

@Composable
private fun Screen() {
    var isRefreshing by remember { mutableStateOf(false) }

    val contentHeight = 128.dp
    val contentPadding = 16.dp

    SwipeRefresh(
        state = rememberSwipeRefreshState(isRefreshing),
        onRefresh = { isRefreshing = true },
    ) {
        Content(
            height = contentHeight,
            contentPadding = contentPadding,
            modifier = Modifier.zIndex(1F),
        )

        LazyColumn(
            contentPadding = PaddingValues(top = contentHeight),
        ) {
            items(30) { index ->
                ListItem(id = index + 1)
            }
        }
    }
}

@Composable
private fun Content(
    height: Dp,
    contentPadding: Dp,
    modifier: Modifier = Modifier,
) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .height(height = height)
            .padding(all = contentPadding),
        backgroundColor = MaterialTheme.colors.primary,
        contentColor = MaterialTheme.colors.onPrimary,
    ) {
        Box(
            contentAlignment = Alignment.Center,
        ) {
            Text(text = "Content")
        }
    }
}

リストアイテムの先頭がコンテンツの下部に位置するように、LazyColumncontentPaddingにコンテンツの高さ分を指定しています。

また、コンテンツがリストよりも上のレイヤーとなるようにコンテンツに対して zIndexを指定しています。

下記はこのコードの動作イメージです。

インジケータがコンテンツの裏側に入り込んでしまっているのがわかるでしょうか。

こちらを対応するのが今回の目的となります。

インジケータの表示位置を変更する

とはいえ、インジケータの位置を変更するのは非常に簡単です。

LazyColumnのアイテム位置をcontentPaddingで変更したのと同様に、SwipeRefreshにもindicatorPaddingというパラメータが存在するのでこれを設定するだけです。

@Composable
private fun Screen() {
    var isRefreshing by remember { mutableStateOf(false) }

    val contentHeight = 128.dp
    val contentPadding = 16.dp

    SwipeRefresh(
        state = rememberSwipeRefreshState(isRefreshing),
        onRefresh = { isRefreshing = true },
        indicatorPadding = PaddingValues(top = contentHeight - contentPadding),
    ) {
        Content(
            height = contentHeight,
            contentPadding = contentPadding,
            modifier = Modifier.zIndex(1F),
        )

        LazyColumn(
            contentPadding = PaddingValues(top = contentHeight),
        ) {
            items(30) { index ->
                ListItem(id = index + 1)
            }
        }
    }
}

コンテンツには余白が設定されているので、コンテンツの高さからコンテンツの余白分(下部のみ)を引いた値にしました。

下記はこのコードの動作イメージです。

インジケータがコンテンツの下部からきれいに現れていることが確認できますね。

まとめ

Swipe to Refreshのインジケータの表示位置は、indicatorPaddingで設定することができます。

基本的にはデフォルトの設定で問題はないかと思いますが、レイアウトによっては直感的でより指に馴染むアプリになるのでぜひお試しください。