Slide 1

Slide 1 text

Compose でカスタムレイアウト を組むときの気持ち YUMEMI.grow Mobile #4 一瀬喜弘(@mikanIchinose)

Slide 2

Slide 2 text

自己紹介 object Mikan { val name = " 一瀬喜弘" val company = "karabiner.tech" val hoby = listOf( " 漫画", " アニメ", " ゲーム", " 折り紙", "OSS 開発・コントリビュート", ) }

Slide 3

Slide 3 text

今日発表すること 🙏 Google I/O, WWDC に関連しない 📐 Layout API ❌ measurable, placeable, constraints ⭕ UI 要素のポジションを決める計算をするときの思考 ↓ ソースコード

Slide 4

Slide 4 text

題材: TopAppBar

Slide 5

Slide 5 text

要件 タイトルを中央寄せ タイトルが長すぎて領域に収まらなければ3 点リーダーで省略 左にボタンが1 つ、右にボタンが複数ある非対称なレイアウト

Slide 6

Slide 6 text

長いタイトルの中央寄せの挙動が Flutter と異なる Flutter: AppBar package:flutter/material.dart Compose: CenterAlignedTopAppBar androidx.compose.material3:material3

Slide 7

Slide 7 text

長いタイトルの中央寄せの挙動が Flutter と異なる Flutter: AppBar package:flutter/material.dart Compose: CenterAlignedTopAppBar androidx.compose.material3:material3

Slide 8

Slide 8 text

タイトルのレイアウトを細かく 制御する必要がある

Slide 9

Slide 9 text

タイトル ( 中央にある UI 要素 ) が満すべき要件 タイトルが短いときは中央寄せ タイトルが長くなって隣接するアイコンに接し始めたら場所に応じて右寄せまたは左寄せ タイトルがさらに長くなってもう片方のアイコンにも接し始めたら省略開始

Slide 10

Slide 10 text

ひとまず中央寄せなレイアウト を組んでみよう

Slide 11

Slide 11 text

インターフェース CenterAlignedTopAppBar を参考にします @Composable fun MyCenterAlignedTopAppBar( title: @Composable () -> Unit, modifier: Modifier = Modifier, navigationIcon: @Composable () -> Unit = {}, actions: @Composable () -> Unit = {} ): Unit

Slide 12

Slide 12 text

Layout Layout( modifier = modifier, content = { Box( modifier = Modifier.layoutId("navigationIcon"), ) { navigationIcon() } Box(modifier = Modifier.layoutId("title")) { title() } Box( modifier = Modifier.layoutId("actions"), ) { actions() } }, ) { measurables, constraints -> // ...

Slide 13

Slide 13 text

つづき ) { measurables, constraints -> val navigationIconPlaceable = measurables.first { it.layoutId == "navigationIcon" } .measure(constraints.copy(minWidth = 0)) val actionsPlaceable = measurables.first { it.layoutId == "actions" } .measure(constraints.copy(minWidth = 0)) // タイトルの横幅がアイコンを侵略しないようにする val maxTitleWidth = (constraints.maxWidth - navigationIconPlaceable.width - actionsPlaceable.width) .coerceAtLeast(0) // 0 未満にならないようにガードする val titlePlaceable = measurables.first { it.layoutId == "title" } .measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth)) // ...

Slide 14

Slide 14 text

つづき val maxTitleWidth = (constraints.maxWidth - navigationIconPlaceable.width - actionsPlaceable.width) .coerceAtLeast(0) // 0 未満にならないようにガードする ) { measurables, constraints -> val navigationIconPlaceable = measurables.first { it.layoutId == "navigationIcon" } .measure(constraints.copy(minWidth = 0)) val actionsPlaceable = measurables.first { it.layoutId == "actions" } .measure(constraints.copy(minWidth = 0)) // タイトルの横幅がアイコンを侵略しないようにする val titlePlaceable = measurables.first { it.layoutId == "title" } .measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth)) // ...

Slide 15

Slide 15 text

つづき // CenterAlignedTopAppBar はいい感じに高さを計算していたが、面倒なので一旦固定 val height = 64.dp.roundToPx() layout(constraints.maxWidth, height) { // 左寄せ navigationIconPlaceable.placeRelative( x = 0, y = (height - navigationIconPlaceable.height) / 2 ) // 中央寄せ titlePlaceable.placeRelative( x = (constraints.maxWidth - titlePlaceable.width) / 2, y = (height - titlePlaceable.height) / 2 ) // 右寄せ actionsPlaceable.placeRelative( x = (constraints.maxWidth - actionsPlaceable.width), y = (height - actionsPlaceable.height) / 2 ) } )

Slide 16

Slide 16 text

placeable によるレイアウト : ナビゲーションアイコン y x (0, 0) (?, ?) navigationIconPlaceable.placeRelative( x = 0, // 左詰め y = (height - navigationIconPlaceable.height) / 2 // 中央 )

Slide 17

Slide 17 text

placeable によるレイアウト : ナビゲーションアイコン y x (0, 0) (0, ?) navigationIconPlaceable.placeRelative( x = 0, // 左詰め y = (height - navigationIconPlaceable.height) / 2 // 中央 )

Slide 18

Slide 18 text

placeable によるレイアウト : ナビゲーションアイコン navigationIconPlaceable.placeRelative( x = 0, // 左詰め y = (height - navigationIconPlaceable.height) / 2 // 中央 )

Slide 19

Slide 19 text

placeable によるレイアウト : ナビゲーションアイコン navigationIconPlaceable.height height navigationIconPlaceable.placeRelative( x = 0, // 左詰め y = (height - navigationIconPlaceable.height) / 2 // 中央 )

Slide 20

Slide 20 text

placeable によるレイアウト : ナビゲーションアイコン navigationIconPlaceable.height height navigationIconPlaceable.placeRelative( x = 0, // 左詰め y = (height - navigationIconPlaceable.height) / 2 // 中央 )

Slide 21

Slide 21 text

placeable によるレイアウト : ナビゲーションアイコン (0, 0) (0, (height - navigationIconPlaceable.height) / 2) navigationIconPlaceable.placeRelative( x = 0, // 左詰め y = (height - navigationIconPlaceable.height) / 2 // 中央 )

Slide 22

Slide 22 text

placeable によるレイアウト : タイトル x = (constraints.maxWidth - titlePlaceable.width) / 2 y = (height - titlePlaceable.height) / 2 titlePlaceable.placeRelative( x = (constraints.maxWidth - titlePlaceable.width) / 2, y = (height - titlePlaceable.height) / 2 )

Slide 23

Slide 23 text

placeable によるレイアウト : アクション x = (constraints.maxWidth - actionsPlaceable.width) y = (height - actionsPlaceable.height) / 2 actionsPlaceable.placeRelative( x = (constraints.maxWidth - actionsPlaceable.width), y = (height - actionsPlaceable.height) / 2 )

Slide 24

Slide 24 text

中くらいの長さにおける右寄せ、左寄せを考慮する // .. val titleX = if ((constraints.maxWidth / 2) < ((titlePlaceable.width / 2) + navigationIconPlaceable.width)) { // 左寄せ navigationIconPlaceable.width } else if ((constraints.maxWidth / 2) < ((titlePlaceable.width / 2) + actionsPlaceable.width)) { // 右寄せ constraints.maxWidth - actionsPlaceable.width - titlePlaceable.width } else { // 中央寄せ (constraints.maxWidth - titlePlaceable.width) / 2 } // .. layout(constraints.maxWidth, height) { // .. titlePlaceable.placeRelative( x = titleX, y = (height - titlePlaceable.height) / 2 ) // ... }

Slide 25

Slide 25 text

左寄せが必要なシチュエーション if ((constraints.maxWidth / 2) < ((titlePlaceable.width / 2) + navigationIconPlaceable.width)) { val titleX = navigationIconPlaceable.width // ...

Slide 26

Slide 26 text

左寄せが必要なシチュエーション navigationIconPlaceable.width val titleX = if ((constraints.maxWidth / 2) < ((titlePlaceable.width / 2) + navigationIconPlaceable.width)) { // ...

Slide 27

Slide 27 text

右寄せが必要なシチュエーション } else if ((constraints.maxWidth / 2) < ((titlePlaceable.width / 2) + actionsPlaceable.width)) { val titleX = // ... constraints.maxWidth - actionsPlaceable.width - titlePlaceable.width // ...

Slide 28

Slide 28 text

右寄せが必要なシチュエーション constraints.maxWidth - actionsPlaceable.width - titlePlaceable.width val titleX = // ... } else if ((constraints.maxWidth / 2) < ((titlePlaceable.width / 2) + actionsPlaceable.width)) { // ...

Slide 29

Slide 29 text

完成!! Full source https://gist.github.com/mikanIchinose/551303ea02bd457dfbbde92896384d65

Slide 30

Slide 30 text

まとめ カスタムレイアウトを組むときは要素の始点をどこにどうやって持っていくかを座標系を意識して考える 実際はもっと時間かかってますし、試行錯誤しまくってますw