Jetpack Composeで動的なスタンプカードを実装する

2024.04.29

本記事のiOS版は「SwiftUIで動的なスタンプカードを実装する」をご覧ください。

パンに付いてくるシールを集めて白いお皿と交換するなど日常的な商取引でもスタンプカードを利用していることは多い。

コンシューマ向けアプリを開発していると、それと同様に来店回数やランキングによってスタンプを付与し、一定数のスタンプと商品とを交換する機能を実装することがある。たとえば、フィットネスアプリの場合、ユーザーが定められた運動目標を達成するごとにスタンプを付与し、一定数を集めるとヘルスケア製品の割引などの報酬を得られる。

この記事では、LazyVerticalGridを使用して簡単なスタンプカードを実装する方法を紹介する。

スタンプカードの実装

スタンプカードのようにスタンプを等間隔で並べて表示するにはLazyVerticalGridが適している。

以下はスタンプをグリッドレイアウトで表示する画面の実装例である。現在16個中2個のスタンプを獲得した状態を表現している。

スタンプのViewを実装する

各スタンプは色でアクティブか非アクティブかを区別する。まず、アクティブなスタンプViewを実装する。指定された色で塗りつぶされた円をユーザーが獲得したスタンプとする。

@Composable
fun ActiveStampView(text: String, stampColor: Color) {
    Box(
        modifier = Modifier
            .size(50.dp)
            .clip(CircleShape)
            .background(stampColor)
    ) {
        Text(
            text = text,
            color = Color.White,
            modifier = Modifier.align(
                Alignment.Center
            )
        )
    }
}

次に、非アクティブなスタンプViewを実装する。このViewはスタンプ未獲得状態を破線の円で示す。

@Composable
fun InactiveStampView(text: String, stampColor: Color) {
    Box(
        modifier = Modifier
            .size(50.dp)
            .background(Color.Transparent, CircleShape)
    ) {
        Canvas(modifier = Modifier.matchParentSize()) {
            val center = this.center
            val radius = size.minDimension / 2
            drawCircle(
                color = stampColor,
                radius = radius,
                center = center,
                style = Stroke(
                    width = 2.dp.toPx(),
                    pathEffect = PathEffect.dashPathEffect(floatArrayOf(35f, 10f))
                )
            )
        }
        Text(
            text = text,
            color = stampColor,
            modifier = Modifier.align(
                Alignment.Center
            )
        )
    }
}

スタンプ画面の実装

16個中2個のスタンプを獲得のような状態を表現するために、num: 2required: 16をパラメータとする StampScreen を実装した。

@Composable
fun StampScreen(
    num: Int, required: Int,
    activeStampColor: Color = Color.Red,
    inactiveStampColor: Color = Color.Gray
) {
    LazyVerticalGrid(
        columns = GridCells.Fixed(7),
        contentPadding = PaddingValues(all = 8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        item(span = { GridItemSpan(maxCurrentLineSpan) }) {
            Column {
                Text(
                    text = "$num / $required",
                    fontSize = 20.sp,
                    fontWeight = FontWeight.Bold,
                    color = Color.Black,
                    textAlign = TextAlign.Center,
                    modifier = Modifier.align(Alignment.CenterHorizontally)
                )
                Spacer(modifier = Modifier.height(16.dp))
            }
        }
        items((0 until required).toList()) { index ->
            if (index < num) {
                ActiveStampView(
                    text = "${index + 1}",
                    stampColor = activeStampColor
                )
            } else {
                InactiveStampView(
                    text = "${index + 1}",
                    stampColor = inactiveStampColor
                )
            }
        }
    }
}

上記の StampScreen を表示させるプレビューを実装する。

@Preview(showBackground = true)
@Composable
fun StampScreenPreview() {
    SampleStamp3Theme {
        StampScreen(2, 16)
    }
}

プレビューウィンドウにスタンプカード画面が表示された。

ActiveStampViewInactiveStampViewの実装を変更することで、ハート型や星形など様々な形状のViewへの対応や、アプリバイナリにスタンプ画像をリソースとして組み込んでおき利用したり、サーバーからの画像をダウンロードして表示させたりすることも実現できるだろう。

まとめ

Jetpack Composeを使用することで、LazyVerticalGridLazyHorizontalGridを活用して、簡単にグリッドレイアウトのViewを表示することが可能である。スタンプカードのようなにスタンプを規則正しく並べて表示するなどの用途に向いている。またサンプルコードで示したようにitemitemsを組み込むことで、1列のアイテムと7列のアイテムを混在させて並べられる。おそらくRecyclerViewGridLayoutManagerを使った実装よりも直感的に扱うことができるだろう。

関連リンク