こんにちは、エンジニアリング室の tsutomu hayakawa です!
2023年9月14日(木)から16日(土)までオンライン・オフラインのハイブリッド形式で開催されたDroidKaigi 2023にて、DeNA はゴールドスポンサーとして協賛しました!
また、DeNA は DroidKaigi 2023 の「スポンサーブース」に出展し、ブース企画として「SWETチームの取り組み紹介」「Pococha実機体験会」「JetpackCompose 脳内プレビュー大会」を実施しました!!
当日はたくさんの方々にご参加いただき、大盛況となりました。「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)
)
)
)
}
}
解説
この問題は、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)
)
}
}
}
}
解説
このコードは、Column
とRow
を組み合わせて要素を配置し、Text
やBox
を描画します。各要素にはさまざまな修飾子が適用され、位置や形状、境界線、スタイルなどが調整されており描画を予想する必要があります。
上部の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")
}
}
}
解説
この問題はModifier
のweight
、Arrangement
、Alignment
がどのようにレイアウトされるのかがキーポイントになります。
weight(1f)
で指定した場合、親のレイアウトをもとに目一杯のサイズで描画されます。
Arrangement.SpaceBetween
は両端へ、Arrangement.SpaceEvenly
は等間隔に、Arrangement.SpaceAround
は要素の両端のスペースを均等に配置する。
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)
)
}
}
解説
Modifier
の適用順序の違いによる視覚的な変化を考える問題です。2つのBox
コンポーネントを用意し、それぞれに同じスタイルを異なる順序で適用しています。
1つ目のBox
- 32dpの
padding
- 200dpのサイズ
- 赤い円形の
border
- 黒背景
- 16dpの
padding
- 青い円形の
border
- 灰色背景
2つ目のBox
- 200dpのサイズ
- 赤い円形の
border
- 32dpの
padding
- 黒背景
- 青い円形の
border
- 16dpの
padding
- 灰色背景
反省点
解答の際に色の指定がむずかしかったですね。
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)
)
}
}
※ Ripple Effectの表示範囲をわかりやすくするため、プレビュー画像には赤点線で範囲を見えるようにしてあります。
解説
Modifier
の順番と同じ趣旨でRipple Effect
がどう出るかを意識する必要がある問題です。
この問題では回答時にクリック時のRippleEffectがどの範囲に出るかを意識してみてください。
clickable
がclip
より先に呼ばれているか後に呼ばれているかでRippe Effect
の表示範囲変わってくる点に注意する必要があります。
1つ目のBox
- 100dpのサイズ
CircleShape
による円形指定- 灰色円形背景
clickable
処理
2つ目のBox
- 100dpのサイズ
clickable
処理CircleShape
による円形指定- 灰色円形背景
反省点
実機でアプリを動かして解説させていただきましたが、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)
}
}
}
}
}
解説
よくある、チャットの画面を再現する問題です。
Row
を利用し、左から順に配置をしてしまうと、中央のメッセージText
が長いときに時間Text
がメッセージに幅を専有されて消えてしまいます。
そこで、中央のメッセージText
と+時間Text
の部分のみをLayoutDirection
を変えて時間Text
-> 中央のメッセージText
の順で右から配置します。
これによって時間を優先的に表示することができ、テキストが長い場合でも時間がしっかりと表示されます。
direction
が変わっているRow
ではpadding
のstart
、end
が逆になることも注意です。
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)
)
}
}
}
解説
Box
とSpacer
がどのようにレイアウトされるかがキーポイントになります。
width
とheight
のdp
を元に、それぞれ何を描画しているかをあてる必要があります。
Box
やSpacer
がどれぐらいのサイズになるのかを正確に当てるのが難しいポイント。
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),
)
}
}
}
解説
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
は-
なので、反時計回りに配置されます。
- xは
radius
:三角関数に半径をかけて、円環を大きくします。(cubeSize / 2f)
:CanvasのCubeの座標は矩形の左上を基点にしているので、これをCubeの真ん中にくるようにしています。
角度 | 0° | 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 | 2π |
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 のブースでたくさんの方々に問題の挑戦していただけたようで、非常に嬉しく思います! 多くの方々の参加と体験を提供できたブースイベントは、今後も続けていきたいと思っています。 コメントやフィードバックもお待ちしております。最後まで読んでいただき、本当にありがとうございました!!
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。