STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

約30行でできる!Jetpack Composeで作るサイン画面

はじめに

こんにちは! STORES 決済 でAndroidアプリの開発をしているchukaです。

最近は美味しいパンを食べることにハマっています。

美味しいパン屋さんを知っている方は、ぜひ教えてください 🥐 🍞 🥖 🥯

Jetpack Composeでサインをしよう

みなさんは、クレジットカードでの支払い時にサインを求められた経験はありますか? STORES 決済 でも、クレジットカードで決済されたとき、アプリ上でサインをしてもらう場合があります。*1

でも、Jetpack Composeでサイン画面を作るのってなんか難しそう・・・と思いませんか?(私は思っていました)

実はそのサイン画面、Jetpack Composeでとてもお手軽に実装できるのです!

今回は、そんなサイン画面の実装方法について解説していきます。

1. サインを描画する

まずは、一番シンプルな「線を描く」処理の実装例から紹介します。

@Composable
private fun SignatureField(modifier: Modifier = Modifier) {
    var path by remember { mutableStateOf(Path()) }

    Canvas(
        modifier =
            modifier
                .fillMaxSize()
                .pointerInput(Unit) {
                    detectDragGestures(
                        onDragStart = { offset ->
                            path.moveTo(offset.x, offset.y)
                        },
                        onDrag = { change, _ ->
                            path.lineTo(change.position.x, change.position.y)
                            // pathを更新して再描画をトリガーする
                            path = Path().apply { addPath(path) }
                        },
                    )
                },
    ) {
        drawPath(
            path = path,
            color = Color.Black,
            style =
                Stroke(
                    width = STROKE_WIDTH,
                    cap = StrokeCap.Round,
                    join = StrokeJoin.Round,
                ),
        )
    }
}

動作例

なんと、これだけでサイン機能が完成しました!

CanvaspointerInput()を使用することで、ユーザーの指の動きに合わせて線を描くことができます。

実装のポイントは、path = Path().apply { addPath(path) }で明示的にpathを更新することです。 path.moveTopath.lineTo でpathの内部状態は変化していますが、 インスタンス自体は変わっていないため、Compose側が変更を検知できずに再描画が走りません。 そのため、毎回pathを新しく生成して代入し直すことで、Compose側に変更を通知し、再描画をトリガーします。

2. サインをなめらかにする

1でサインを描くことはできましたが、ちょっと線がカクカクしていますね 🤔

path.lineTo は直線を描画するので、点同士が直線で繋がれてカクカクしてしまいます。 より滑らかな描画を実現するには、2点間を曲線(ベジェ曲線)でつなぐのがおすすめです。

path = Path().apply {
    addPath(path)
    quadraticTo(
        lastPosition.x, 
        lastPosition.y,
        (lastPosition.x + change.position.x) / 2,
        (lastPosition.y + change.position.y) / 2
    )
}

quadraticTo()ベジェ曲線を描画します。

前回の点と現在の点の中間点を制御点とすることで、なめらかな線になります!

なめらかになった!

gifではわかりにくいと思うので、比較画像もお見せします。

左: lineTo()で描画した線, 右: quadraticTo()で描画した線

左が1のlineTo()で描画した線で、右が2のquadraticTo()で描画した線です。 カーブの部分を見ていただくと、その差がよくわかるかと思います。

3. サインを消す

最後に、サインを消す機能もつけてみましょう!

@Composable
private fun SignatureField(modifier: Modifier = Modifier) {
    var path by remember { mutableStateOf(Path()) }
    var lastPosition by remember { mutableStateOf<Offset?>(null) }

    Box(
        modifier = modifier.fillMaxSize(),
    ) {
        Canvas(
            modifier =
                Modifier
                    .fillMaxSize()
                    .pointerInput(Unit) {
                        detectDragGestures(
                            onDragStart = { offset ->
                                path.moveTo(offset.x, offset.y)
                                lastPosition = offset
                            },
                            onDrag = { change, _ ->
                                lastPosition?.let { lastPosition ->
                                    // pathを更新して再描画をトリガーする
                                    path =
                                        Path().apply {
                                            addPath(path)
                                            quadraticTo(
                                                lastPosition.x,
                                                lastPosition.y,
                                                (lastPosition.x + change.position.x) / 2,
                                                (lastPosition.y + change.position.y) / 2,
                                            )
                                        }
                                }
                                lastPosition = change.position
                            },
                        )
                    },
        ) {
            drawPath(
                path = path,
                color = Color.Black,
                style =
                    Stroke(
                        width = STROKE_WIDTH,
                        cap = StrokeCap.Round,
                        join = StrokeJoin.Round,
                    ),
            )
        }

        Button(
            modifier = Modifier.align(Alignment.BottomCenter).padding(16.dp),
            onClick = {
                path = Path() // パスを初期化
                lastPosition = null
            },
        ) {
            Text("サインを消す!")
        }
    }
}

サインを消したいときは、pathを初期化してあげましょう。

たったこれだけでサインを描き直すことができるようになりました!

描き直しができるようになった!

おわりに

今回はJetpack Composeを使ったサイン画面の実装について紹介しました。

とても簡単に実装できて感動しませんか!?(私はしました)

このような自由に線を描くUIは、サイン以外の用途でも活躍します。

もし手描きUI難しそう・・・と思っていた方がいれば、この記事を読んで手描きメモやお絵描きアプリなどを作ってみるきっかけになれたら幸いです☺️

*1:2025年3月に、クレジットカードのPINバイパス(暗証番号の入力をスキップしてサインで本人認証を行う取引)は原則廃止されました(https://support.coiney.com/hc/ja/articles/360001040491)。