blog

DeNAのエンジニアが考えていることや、担当しているサービスについて情報発信しています

2023.09.20 イベントレポート

JetpackCompose 脳内プレビュー大会の問題紹介&解答解説

by tsutomu hayakawa

#droidkaigi #mobile #android #jetpackcompose #brain-previews

こんにちは、エンジニアリング室の tsutomu hayakawa です!

2023年9月14日(木)から16日(土)までオンライン・オフラインのハイブリッド形式で開催されたDroidKaigi 2023にて、DeNA はゴールドスポンサーとして協賛しました!
また、DeNA は DroidKaigi 2023 の「スポンサーブース」に出展し、ブース企画として「SWETチームの取り組み紹介」「Pococha実機体験会」「JetpackCompose 脳内プレビュー大会」を実施しました!!

Booth

DeNA Boothの様子

当日はたくさんの方々にご参加いただき、大盛況となりました。「JetpackCompose 脳内プレビュー大会」で集めた解答用紙は、なんと280枚にものぼりました!
このブログでは、「JetpackCompose 脳内プレビュー大会」の問題とその答え、さらに解説を行います。

※ 問題文においてimport部分の記載は省略する形で記載しております。

Question1の問題と解説

Question1の問題とPreview結果は以下のとおりです。

fun Question1() {
    Column(
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxSize()
    ) {
        Text(
            text = "DeNA",
            style = LocalTextStyle.current.merge(
                TextStyle(
                    fontSize = 100.sp,
                    drawStyle = Stroke(width = 4f, join = StrokeJoin.Round)
                )
            )
        )
    }
}

Question1

Question1のPreview結果

解説

この問題は、Textに対してLocalTextStyle.current.mergeを利用して、カスタムスタイルを適応しています。 カスタムのスタイルは文字のサイズ、アウトライン、角丸設定を指定しています。

Question2の問題と解説

Question2の問題とPreview結果は以下のとおりです。

@Composable
fun Question() {
    Column(
        modifier = Modifier
            .fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Jetpack Compose",
            modifier = Modifier
                .offset(x = 20.dp)
                .border(1.dp, Color.Black)
                .padding(20.dp),
            style = MaterialTheme.typography.titleMedium,
        )
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .offset(y = (-20).dp),
            horizontalArrangement = Arrangement.Center
        ) {
            Box(
                modifier = Modifier
                    .size(100.dp)
                    .clip(RectangleShape)
                    .border(1.dp, Color.Black)
            )
            Box(
                modifier = Modifier
                    .size(100.dp)
                    .padding(20.dp)
            ) {
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .rotate(degrees = 45f)
                        .clip(RectangleShape)
                        .border(1.dp, Color.Black)
                )
            }
        }
    }
}

Question2

Question2のPreview結果

解説

このコードは、ColumnRowを組み合わせて要素を配置し、TextBoxを描画します。各要素にはさまざまな修飾子が適用され、位置や形状、境界線、スタイルなどが調整されており描画を予想する必要があります。

上部のTextは、offset後にborderによる描画が行われます。
下部の□の描画はBoxが複数個並んでいます。最初のBoxは正方形の形状を持ち、borderが追加されています。
2つ目のBoxは、正方形の形状を持ち、余白が設定されています。内部にさらにBoxが含まれており、45度回転し、borderが追加されていますので小さい正方形の描画になります。

Question3の問題と解説

Question3の問題とPreview結果は以下のとおりです。

@Composable
fun Question1() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Row(
            modifier = Modifier
                .border(1.dp, Color.Black)
                .fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Text(text = "One")
            Text(text = "Two")
        }
        Row(
            modifier = Modifier
                .weight(1f)
                .border(1.dp, Color.Black)
                .fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceEvenly,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(text = "Three")
            Text(text = "Four")
        }
        Row(
            modifier = Modifier
                .border(1.dp, Color.Black)
                .fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceAround
        ) {
            Text(text = "Five")
            Text(text = "Six")
        }
    }
}

Question3

Question3のPreview結果

解説

この問題はModifierweightArrangementAlignmentがどのようにレイアウトされるのかがキーポイントになります。 weight(1f)で指定した場合、親のレイアウトをもとに目一杯のサイズで描画されます。 Arrangement.SpaceBetweenは両端へ、Arrangement.SpaceEvenlyは等間隔に、Arrangement.SpaceAroundは要素の両端のスペースを均等に配置する。

Arrangementについて

Alignment.Centerを指定することで真ん中にComponentが配置されます。

Question4の問題と解説

Question4の問題は以下の通りです。

@Composable
fun Question() {
    Column(
        modifier = Modifier
            .fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Box(
            modifier = Modifier
                .padding(32.dp)
                .size(200.dp)
                .border(8.dp, Color.Red, CircleShape)
                .background(Color.Black)
                .padding(16.dp)
                .border(8.dp, Color.Blue, CircleShape)
                .background(Color.Gray)
        )

        Box(
            modifier = Modifier
                .size(200.dp)
                .border(8.dp, Color.Red, CircleShape)
                .padding(32.dp)
                .background(Color.Black)
                .border(8.dp, Color.Blue, CircleShape)
                .padding(16.dp)
                .background(Color.Gray)
        )
    }
}

Question4

Question4のPreview結果

解説

Modifierの適用順序の違いによる視覚的な変化を考える問題です。2つのBoxコンポーネントを用意し、それぞれに同じスタイルを異なる順序で適用しています。

1つ目のBox

  1. 32dpのpadding
  2. 200dpのサイズ
  3. 赤い円形のborder
  4. 黒背景
  5. 16dpのpadding
  6. 青い円形のborder
  7. 灰色背景

2つ目のBox

  1. 200dpのサイズ
  2. 赤い円形のborder
  3. 32dpのpadding
  4. 黒背景
  5. 青い円形のborder
  6. 16dpのpadding
  7. 灰色背景

反省点

解答の際に色の指定がむずかしかったですね。

Question5の問題と解説

Question5の問題とPreview結果は以下のとおりです。

fun Question() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.CenterVertically),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Box(
            modifier = Modifier
                .size(100.dp)
                .clip(CircleShape)
                .background(Color.Gray, CircleShape)
                .clickable {  }
        )

        Box(
            modifier = Modifier
                .size(100.dp)
                .clickable {  }
                .clip(CircleShape)
                .background(Color.Gray, CircleShape)
        )
    }
}

Question5

Question5のPreview結果

※ Ripple Effectの表示範囲をわかりやすくするため、プレビュー画像には赤点線で範囲を見えるようにしてあります。

解説

Modifierの順番と同じ趣旨でRipple Effectがどう出るかを意識する必要がある問題です。 この問題では回答時にクリック時のRippleEffectがどの範囲に出るかを意識してみてください。 clickableclipより先に呼ばれているか後に呼ばれているかでRippe Effectの表示範囲変わってくる点に注意する必要があります。

1つ目のBox

  1. 100dpのサイズ
  2. CircleShapeによる円形指定
  3. 灰色円形背景
  4. clickable処理

2つ目のBox

  1. 100dpのサイズ
  2. clickable処理
  3. CircleShapeによる円形指定
  4. 灰色円形背景

反省点

実機でアプリを動かして解説させていただきましたが、RippleEffectが見えにくいOS依存の問題らしい?ものがあり、見えにくかった点がありました。

Question6の問題と解説

Question6の問題とPreview結果は以下のとおりです。

@Composable
fun Question() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(10.dp),
        verticalArrangement = Arrangement.spacedBy(15.dp)
    ) {
        massageWrapper("テキスト".repeat(3), "21:00")
        massageWrapper("テキスト".repeat(6), "21:03")
    }
}

@Composable
fun massageWrapper(
    body: String,
    time: String
) {
    Row(modifier = Modifier.fillMaxWidth()) {
        Box(
            modifier = Modifier
                .size(30.dp)
                .border(1.dp, Color.Black, CircleShape),
        )
        CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
            Row {
                Text(
                    modifier = Modifier.align(Alignment.Bottom),
                    text = time,
                    style = MaterialTheme.typography.bodySmall
                )
                Box(
                    modifier = Modifier
                        .padding(start = 5.dp, end = 10.dp)
                        .clip(RoundedCornerShape(10.dp))
                        .background(Color.LightGray)
                        .padding(10.dp),
                ) {
                    Text(text = body, textAlign = TextAlign.End)
                }
            }
        }
    }
}

Question6

Question6のPreview結果

解説

よくある、チャットの画面を再現する問題です。

Rowを利用し、左から順に配置をしてしまうと、中央のメッセージTextが長いときに時間Textがメッセージに幅を専有されて消えてしまいます。 そこで、中央のメッセージTextと+時間Textの部分のみをLayoutDirectionを変えて時間Text -> 中央のメッセージTextの順で右から配置します。 これによって時間を優先的に表示することができ、テキストが長い場合でも時間がしっかりと表示されます。
directionが変わっているRowではpaddingstartendが逆になることも注意です。

Question7の問題と解説

Question7の問題とPreview結果は以下のとおりです。

@Composable
fun Question2() {
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Row(
            horizontalArrangement = Arrangement.Center
        ) {
            Box(
                modifier = Modifier
                    .size(70.dp)
                    .background(Color.Black)
            )
            Spacer(
                modifier = Modifier
                    .width(100.dp)
            )
            Box(
                modifier = Modifier
                    .size(70.dp)
                    .background(Color.Black)
            )
        }
        Box(
            modifier = Modifier
                .width(100.dp)
                .height(50.dp)
                .background(Color.Black)
        )
        Box(
            modifier = Modifier
                .width(200.dp)
                .height(100.dp)
                .background(Color.Black)
        )
        Row(
            modifier = Modifier
                .fillMaxWidth(),
            horizontalArrangement = Arrangement.Center
        ) {
            Box(
                modifier = Modifier
                    .size(50.dp)
                    .background(Color.Black)
            )
            Spacer(
                modifier = Modifier
                    .width(100.dp)
            )
            Box(
                modifier = Modifier
                    .size(50.dp)
                    .background(Color.Black)
            )
        }
    }
}

Question7

Question7のPreview結果

解説

BoxSpacerがどのようにレイアウトされるかがキーポイントになります。 widthheightdpを元に、それぞれ何を描画しているかをあてる必要があります。 BoxSpacerがどれぐらいのサイズになるのかを正確に当てるのが難しいポイント。

Question8の問題と解説

Question8の問題とPreview結果は以下のとおりです。

@Composable
fun Question3() {
    val squareCount = 8
    val radius = 300f
    Canvas(modifier = Modifier.fillMaxSize()) {
        for (i in 0 until squareCount) {
            val cubeSize = (20f + 10 * i)
            val radian = i * (360f / squareCount) / 180f * Math.PI
            drawRect(
                topLeft = Offset(
                    x = (size.width / 2f) + cos(radian).toFloat() * radius - (cubeSize / 2f),
                    y = (size.height / 2f) + -sin(radian).toFloat() * radius - (cubeSize / 2f)
                ),
                color = Color.Black,
                size = Size(width = cubeSize, height = cubeSize),
            )
        }
    }
}

Question8

Question8のPreview結果

解説

Canvasで三角関数をx軸とy軸に用いて円環状に正方形を配置する問題。正方形は徐々に大きくなっていくので、それぞれがどのような順番で配置されるのか意識する必要があります。
下記の式で角度(ラジアン)を求めていきます。今回は8個の Cube を作るので、 360°÷8=45° ずつCubeを配置すれば良いことがわかります。sin, cosではラジアンを渡す必要があるので、(角度÷180)×πで変換します。45°のラジアンは (45÷180)×π=π/4 となり、これを index ごとにずらして配置していきます。

val radian = i * (360f / squareCount) / 180f * Math.PI

8個のCubeが配置されるはずなので、π/4, π/2, 3π/4, π, 5π/4, 3π/2, 7π/4, 2πがラジアンとなることがわかります。 求めた角度をもとにcubeを配置していきます。以下の式でCubeを配置していきます。

topLeft = Offset(
    x = (size.width / 2f) + cos(radian).toFloat() * radius - (cubeSize / 2f),
    y = (size.height / 2f) + -sin(radian).toFloat() * radius - (cubeSize / 2f)
),
  • (size.width(height) / 2f):画面中央までずらすために画面の半分のサイズを足します。
  • cos(radian)-sin(radian):三角関数を用いて円環状に配置します。以下のような表があると便利かもしれません。
    • xはcos、yは-sinであることに注意です。
    • sin-なので、反時計回りに配置されます。
  • radius:三角関数に半径をかけて、円環を大きくします。
  • (cubeSize / 2f):CanvasのCubeの座標は矩形の左上を基点にしているので、これをCubeの真ん中にくるようにしています。
角度 30° 45° 60° 90° 120° 135° 150° 180° 210° 225° 240° 270° 300° 315° 330° 360°
ラジアン 0 π/6 π/4 π/3 π/2 2π/3 3π/4 5π/6 π 7π/6 5π/4 4π/3 3π/2 5π/3 7π/4 11π/6
sin 0 1/2 1/√2 √3/2 1 √3/2 1/√2 1/2 0 -1/2 -1/√2 -√3/2 -1 -√3/2 -1/√2 -1/2 0
cos 1 √3/2 1/√2 1/2 0 -1/2 -1/√2 -√3/2 -1 -√3/2 -1/√2 -1/2 0 1/2 1/√2 √3/2 1

「JetpackCompose 脳内プレビュー大会」の問題解説は以上になります。 DroidKaigi 2023 のブースでたくさんの方々に問題の挑戦していただけたようで、非常に嬉しく思います! 多くの方々の参加と体験を提供できたブースイベントは、今後も続けていきたいと思っています。 コメントやフィードバックもお待ちしております。最後まで読んでいただき、本当にありがとうございました!!

最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。

recruit

DeNAでは、失敗を恐れず常に挑戦し続けるエンジニアを募集しています。