💠

Composeで作る: スワイプ可能なダイアログ

2023/06/28に公開

はじめに

こんにちは😃
この記事ではJetpack Composeでスワイプ可能なダイアログを作成する方法について紹介します。
以前まではAccompanistのPager layoutsを用いての実装が主流だったと思いますが、Compose1.4以降ではandroidx.compose.foundation.pagerに統合されました。
そのため、今回は新しい方法について簡略的な実装手順を追っていきたいと思います。

インジケーター付きダイアログ

以下のような、スワイプ出来てインジケーターも付いているダイアログを作りたいと思います。

実装方法

AccompanistのPager layoutsの実装とほとんど同じように実装する事が出来るようです。
公式のガイドがありますので、そちらを参考にしました。

Android StudioのNew Project→Empty Activityでプロジェクトを新規作成した場合、Composeのバージョンを1.4.0以上にする必要がある為CompilerExtensionVersionやbuild.gradleのcompose-bomバージョンを1.4.0以上に対応するようにしましょう。(2023/6/29現在)
compose-bomバージョンの詳細はBOM to library version mappingで確認することが出来ます。

build.gradle
~省略~
composeOptions {
        kotlinCompilerExtensionVersion '1.4.0'
    }
~省略~

dependencies {
   ~省略~
-   implementation platform('androidx.compose:compose-bom:2022.12.00')   
+   implementation platform('androidx.compose:compose-bom:2023.03.00')
    implementation 'androidx.compose.ui:ui'
    implementation 'androidx.compose.ui:ui-graphics'
   ~省略~
-   androidTestImplementation platform('androidx.compose:compose-bom:2022.12.00')
+   androidTestImplementation platform('androidx.compose:compose-bom:2023.03.00')
    ~省略~
}

横スワイプはHorizontalPager、インジケーターはPager Stateを用いて選択されているページを取得し描画する事で実現が可能です。
以下の例ではダイアログ内のボタン押下でも次のページへスクロールするようにし、最後のページのボタン押下でダイアログを閉じるよう簡易的な実装をしています。

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SwipeDialog(closeDialog: () -> Unit) {
    var scrollToPageInt by remember { mutableStateOf(0) }
    val composableScope = rememberCoroutineScope()
    val pageCount = 6

    Dialog(
        onDismissRequest = {}
    ) {
        Surface(shape = RoundedCornerShape(30.dp), color = Color.Transparent) {
            val pagerState = rememberPagerState(initialPage = 0)
            Column(
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier
                    .fillMaxWidth()
            ) {
	        // ドットのインジケーターをダイアログの下に表示したいので入れ子にしている
                Surface(shape = RoundedCornerShape(30.dp)) {
                    Column(
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally,
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(0.dp, 10.dp)
                    ) {
                        HorizontalPager(state = pagerState, pageCount = pageCount) { page ->
                            Column(
                                verticalArrangement = Arrangement.Center,
                                horizontalAlignment = Alignment.CenterHorizontally,
                                modifier = Modifier
                                    .fillMaxWidth()
                            ) {
                                Text(
                                    text = "Page: $page",
                                    textAlign = TextAlign.Center,
                                    modifier = Modifier
                                        .fillMaxWidth()
                                        .padding(0.dp, 30.dp)
                                )
                                Button(
                                    onClick = {
                                        if (page == pageCount - 1) {
                                            closeDialog()
                                        } else {
                                            scrollToPageInt = pagerState.currentPage + 1
					    composableScope.launch {
                                                pagerState.animateScrollToPage(scrollToPageInt)
					    }
                                        }
                                    }
                                ) {
                                    Text(text = "Button $page")
                                }
                            }
                        }
                    }
                }
		// ドットのインジケーターを描画
                Row(
                    Modifier
                        .height(30.dp)
                        .fillMaxWidth(),
                    horizontalArrangement = Arrangement.Center,
                    verticalAlignment = Alignment.Bottom
                ) {
                    repeat(pageCount) { iteration ->
                        val color =
                            if (pagerState.currentPage == iteration) Color.DarkGray else Color.LightGray
                        Box(
                            modifier = Modifier
                                .padding(2.dp)
                                .clip(CircleShape)
                                .background(color)
                                .size(5.dp)
                        )
                    }
                }
            }
        }
    }
}

サンプルプロジェクトを作成しましたので、実際の動きの参考などにどうぞ。
以上で、composeでのスワイプ付きダイアログについて紹介しました。
とはいえ、プロジェクトの都合などでcomposeのバージョンを上げられない場合もあるでしょう。
そんな場合は引き続きAccompanistを用いるのが簡易的に実装出来て良いと思います。

Accompanistについて

Accompanistは、Jetpack Composeでより多機能なUIを実現するためのライブラリです。Pager layoutsもその一つで、スワイプによる画面遷移を容易に実現することができます。

ただし、現在では androidx.compose.foundation.pager にこれらの機能が統合されているため、新規のプロジェクトではこちらを利用することが推奨されています。

AccompanistのPager Layoutsを用いたHorizontalPagerIndicatorのIndicatorsの方がcomposeのものより、スワイプ時にもIndicatorが動いていて個人的にはAccompanistの方が好きです。(composeでも同様の実装はできるのでしょうが、良い実装があったら教えてください!)

まとめ

この記事では、androidx.compose.foundation.pagerまたはAccompanistを用いたスワイプ可能なダイアログの作成方法について紹介しました。
ダイアログなど、新規に実装していく箇所では積極的にJetpack Composeを採用していますが、大きなプロジェクトになると思いもよらない箇所に影響があったりするので慎重に実装していきたいですね!

Voicyテックブログ

Discussion