Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

【Android】Jetpack ComposeでCanvasを使って凝ったUIを表現しよう

こんにちは

Androidエンジニアのなかむらです。

ミラティブでリリースされた視聴ミッションの実装に関してお話ししていこうと思います。

視聴ミッションは視聴開始直後からメーターが進み始めるという機能です。 このメーターをかっこよく表現したいよねということでデザイナーやPMと悩んだ末に生まれたUIがコチラ

めちゃめちゃかっこいい。

でもこのUI組むの結構大変そうだなー。。。

どうやって組もうかなぁ。。。

色々と悩みました。

きっと自分以外にもそんな悩みを抱えている方が他にもいるはず(多分)ということで本記事では、そんな悩めるAndroidエンジニアに向けて実装方法を解説していきたいと思います。

このメーター部分のUIについて解説します

ちなみに、最初に言っておくと実装自体はとてもシンプルです。

何も難しいことはありません。

では何に苦戦したのか。

それはこのUIの作り方をどう調べたらいいのかわからんということでした。

このUI自体を表現する言葉がパッと思いつかなかったんです。扇形?扇子みたいなデザイン?速度メーターみたいなUI?このような感じで、的確に言い当てる語彙が見つかりません。 エンジニアにとって検索にかける単語が思いつかないことがかなり危機的状況なのは共感してもらえるのではないでしょうか。

結局色々と考えてもスマートな解決策は思いつかなかったので、あれこれ探し回った結果この図形は

arc

という単語で表現できることがわかりました。そしてなんとAndroidのCanvasにはarcを描画してくれる機能が存在していたんです。いやー本当にありがたい。

ちなみにarcの語源は弓(bow)から来ているそうで、言われてみればrainbow(虹)も同じ形をしていますね。

それでは、ここからは具体的にどう実装していけば良いか順を追って見ていきましょう。

実装方法

それでは、Canvasを用意します。

Canvas(
        modifier = Modifier
            .size(200.dp)
            .padding(8.dp),
        onDraw = {}
    )

onDrawはレシーバ付き関数であり、ラムダ内でDrawScopeをthisとして扱えるようになっています。 DrawScopeにはdrawArcというメソッドが用意されているので、今回はそれを使用します。

    Canvas(
        modifier = Modifier
            .size(200.dp)
            .padding(8.dp),
        onDraw = {
            drawArc(
                color = Color.Black,
                startAngle = 270f,
                sweepAngle = 180f,
                useCenter = false,
                size = Size(size.width, size.height)
            )
        }
    )

この結果描画されたUIがこちら

まだ目的の速度メーターのような図形とは程遠いですが、半円が描画されました。

ここでポイントになるのはstartAnglesweepAngleです。 startAngleでどこから描画するかを決めます。0が右、90が下、180が左、270が上からとなっています。 次にsweepAngleを使い、先ほどstartAngleで指定したポイントからどの角度まで描画するかを指定します。360なら一周しますし、180なら半周です。 上に載せたコードではstartAngleが270のため上から描画が始まり、sweepAngleで180を指定したため半周分の角度が描画されて上弦の月のような図形が描画されたというわけですね。

次にスタイルをつけてみましょう。drawArcの引数のstyleにStrokeを渡してみます。

            drawArc(
                color = Color.Black,
                startAngle = 270f,
                sweepAngle = 180f,
                useCenter = false,
                size = Size(size.width, size.height),
                style = Stroke(width = 20f), // これを追加する
            )

するとこのような図形が描画されました。(Jetpack Composeのプレビュー機能ほんと便利だな)

Strokeのスタイルを指定することにより、円周部分のみが描画されるようになりました。

少しずつ目的の形に近づいているのではなないでしょうか。

さらに角を丸くする設定を加え、startAnglesweepAngleの値も少し調整してみます。

            drawArc(
                color = Color.Black,
                startAngle = 150f, // 調整
                sweepAngle = 240f, // 調整
                useCenter = false,
                size = Size(size.width, size.height),
                style = Stroke(
                    width = 20f,
                    cap = StrokeCap.Round, // 角を丸くする
                ),
            )

その結果がこちら。

いい感じになってきました。

それでは最後にこの図形をうまく重ねて、メーターのUIを仕上げていきましょう。 重ねるのは背景となる図形、外枠を表現する図形、進捗表現をする図形の3つです。 全てdrawArcで実装します。

    val circleAngle = 360f // 円の角度(360度なので固定値)
    val max = 300f // 進捗の最大値
    val angle = 240f // メーターの角度
    val progress = 40f // メーターの進捗
    val progressWidth = 14.dp // メーターの幅
    val backgroundWidth = 18.dp // 外枠を描画する図形の幅
    val startAngle = (circleAngle / 4) + ((circleAngle - angle) / 2) // どこから描画を始めるかを計算する
    Canvas(
        modifier = Modifier
            .size(200.dp)
            .padding(10.dp),
        onDraw = {
            // 外枠を描画
            drawArc(
                color = Color.Black,
                startAngle = startAngle,
                sweepAngle = angle,
                useCenter = false,
                style = Stroke(
                    width = backgroundWidth.toPx(),
                    cap = StrokeCap.Round
                ),
                size = Size(size.width, size.height)
            )
            // 背景を描画
            drawArc(
                color = Color.DarkGray,
                startAngle = startAngle,
                sweepAngle = angle,
                useCenter = false,
                style = Stroke(width = progressWidth.toPx(), cap = StrokeCap.Round),
                size = Size(size.width, size.height)
            )
            // 進捗を描画
            drawArc(
                color = Color.Cyan,
                startAngle = startAngle,
                sweepAngle = angle / max * progress,
                useCenter = false,
                style = Stroke(width = progressWidth.toPx(), cap = StrokeCap.Round),
                size = Size(size.width, size.height)
            )
        }
    )

とてもシンプルなコードで形にすることができました。

実際の視聴ミッションではprogressangleなどの値は引数から渡せるようになっており、他にも少し違う実装になっている箇所がありますが、今回はわかりやすくするためにこのような形で記述してみました。

val startAngle = (circleAngle / 4) + ((circleAngle - angle) / 2)この辺りがポイントで、デザインの都合でメーターの角度を変更したくなった場合にangleだけ変更すれば描画の開始位置を計算できるようにしています。

またsweepAngle = angle / max * progressで進捗の割合を描画の角度に落としこんでいます。progressの値が毎秒更新されることで、メーターが進捗するという仕組みです。

まとめ

さて、今回は視聴ミッションで表示されるメーターのUIをどう実装しているか解説してみました。

ミラティブでは新規画面は基本的にJetpack Composeで実装する方針をとっています。今のところ大きな問題もなく、アニメーションを含めた高機能なプレビューができる等、多くのメリットがあると感じています。学習コストもそれほど高いわけではないですし、どうしてもComposeだけで実装できない場合は既存のViewと共存させることができるため、かなり使いやすい仕組みになっていると思いますよ。

We are hiring!

ミラティブでは『好きでつながり、自分の物語(ナラティブ)が生まれる居場所』を実現するエンジニアを募集中です!

www.mirrativ.co.jp

speakerdeck.com

mirrativ.notion.site