Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Composeでカスタムレイアウトを組むときの気持ち
Search
mikan
June 21, 2023
Technology
500
0
Share
Composeでカスタムレイアウトを組むときの気持ち
YUMEMI.grow Mobile #4の発表資料
mikan
June 21, 2023
More Decks by mikan
See All by mikan
Navigation3でViewModelにデータを渡す方法
mikanichinose
0
680
「脳に収まるコードの書き方」を読んで学んだこと
mikanichinose
1
200
RepositoryのSSoT化
mikanichinose
0
87
Kotlin Multiplatform 始めました
mikanichinose
1
140
Web APIをなぜつくるのか
mikanichinose
0
3.8k
イベントをどう管理するか
mikanichinose
3
400
ライブラリでしかお目にかかれない珍しい実装
mikanichinose
2
500
Strong Skipping Mode によってrecompositionはどう変わったのか
mikanichinose
0
390
Modeling UiEvent
mikanichinose
0
130
Other Decks in Technology
See All in Technology
バイブコーディング、仕様駆動、その先へ - 「不確実性に対する検査‧適応のサイクル」を設計する
littlehands
1
530
How to learn AWS Well-Architected with AWS BuilderCards: Security Edition
coosuke
PRO
0
150
クラウドネイティブ DB はいかにして制約を 克服したか? 〜進化歴史から紐解く、スケーラブルアーキテクチャ設計指針〜
hacomono
PRO
6
1k
Every Conversation Counts
kawaguti
PRO
0
240
ECSのTerraformモジュールにコントリビュートした話
harukasakihara
0
220
サンプリングは「作る」のか「使う」のか? 分散トレースのコストと運用を両立する実践的戦略 / Why you need the tail sampling and why you don't want it
ymotongpoo
4
190
(きっとたぶん)人材育成や教育のような何かの話
sejima
0
750
Claude Code で使える DuckDB Skills を試してみた / DuckDB Skills and Claude Code
masahirokawahara
1
590
GCASアップデート(202603-202605)
techniczna
0
200
Redmine次期バージョン7.0の注目新機能解説 — UI/UX強化と連携強化を中心に
vividtone
1
150
開発サイクルのボーダーレス化に伴う組織変革から学んだこと / Organizational Transformation Amid the Borderless Development Cycle
mii3king
0
170
AI-Assisted Contributions and Maintainer Load - PyCon US 2026
pauloxnet
1
160
Featured
See All Featured
Scaling GitHub
holman
464
140k
Believing is Seeing
oripsolob
1
120
Noah Learner - AI + Me: how we built a GSC Bulk Export data pipeline
techseoconnect
PRO
0
180
Why You Should Never Use an ORM
jnunemaker
PRO
61
9.8k
My Coaching Mixtape
mlcsv
0
130
How People are Using Generative and Agentic AI to Supercharge Their Products, Projects, Services and Value Streams Today
helenjbeal
1
180
Designing Dashboards & Data Visualisations in Web Apps
destraynor
231
54k
How Software Deployment tools have changed in the past 20 years
geshan
0
33k
The Art of Programming - Codeland 2020
erikaheidi
57
14k
What the history of the web can teach us about the future of AI
inesmontani
PRO
1
560
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
52
5.9k
We Have a Design System, Now What?
morganepeng
55
8.1k
Transcript
Compose でカスタムレイアウト を組むときの気持ち YUMEMI.grow Mobile #4 一瀬喜弘(@mikanIchinose)
自己紹介 object Mikan { val name = " 一瀬喜弘" val
company = "karabiner.tech" val hoby = listOf( " 漫画", " アニメ", " ゲーム", " 折り紙", "OSS 開発・コントリビュート", ) }
今日発表すること 🙏 Google I/O, WWDC に関連しない 📐 Layout API ❌
measurable, placeable, constraints ⭕ UI 要素のポジションを決める計算をするときの思考 ↓ ソースコード
題材: TopAppBar
要件 タイトルを中央寄せ タイトルが長すぎて領域に収まらなければ3 点リーダーで省略 左にボタンが1 つ、右にボタンが複数ある非対称なレイアウト
長いタイトルの中央寄せの挙動が Flutter と異なる Flutter: AppBar package:flutter/material.dart Compose: CenterAlignedTopAppBar androidx.compose.material3:material3
長いタイトルの中央寄せの挙動が Flutter と異なる Flutter: AppBar package:flutter/material.dart Compose: CenterAlignedTopAppBar androidx.compose.material3:material3
タイトルのレイアウトを細かく 制御する必要がある
タイトル ( 中央にある UI 要素 ) が満すべき要件 タイトルが短いときは中央寄せ タイトルが長くなって隣接するアイコンに接し始めたら場所に応じて右寄せまたは左寄せ タイトルがさらに長くなってもう片方のアイコンにも接し始めたら省略開始
ひとまず中央寄せなレイアウト を組んでみよう
インターフェース CenterAlignedTopAppBar を参考にします @Composable fun MyCenterAlignedTopAppBar( title: @Composable () ->
Unit, modifier: Modifier = Modifier, navigationIcon: @Composable () -> Unit = {}, actions: @Composable () -> Unit = {} ): Unit
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 -> // ...
つづき ) { 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)) // ...
つづき 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)) // ...
つづき // 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 ) } )
placeable によるレイアウト : ナビゲーションアイコン y x (0, 0) (?, ?)
navigationIconPlaceable.placeRelative( x = 0, // 左詰め y = (height - navigationIconPlaceable.height) / 2 // 中央 )
placeable によるレイアウト : ナビゲーションアイコン y x (0, 0) (0, ?)
navigationIconPlaceable.placeRelative( x = 0, // 左詰め y = (height - navigationIconPlaceable.height) / 2 // 中央 )
placeable によるレイアウト : ナビゲーションアイコン navigationIconPlaceable.placeRelative( x = 0, // 左詰め
y = (height - navigationIconPlaceable.height) / 2 // 中央 )
placeable によるレイアウト : ナビゲーションアイコン navigationIconPlaceable.height height navigationIconPlaceable.placeRelative( x = 0,
// 左詰め y = (height - navigationIconPlaceable.height) / 2 // 中央 )
placeable によるレイアウト : ナビゲーションアイコン navigationIconPlaceable.height height navigationIconPlaceable.placeRelative( x = 0,
// 左詰め y = (height - navigationIconPlaceable.height) / 2 // 中央 )
placeable によるレイアウト : ナビゲーションアイコン (0, 0) (0, (height - navigationIconPlaceable.height)
/ 2) navigationIconPlaceable.placeRelative( x = 0, // 左詰め y = (height - navigationIconPlaceable.height) / 2 // 中央 )
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 )
placeable によるレイアウト : アクション x = (constraints.maxWidth - actionsPlaceable.width) y
= (height - actionsPlaceable.height) / 2 actionsPlaceable.placeRelative( x = (constraints.maxWidth - actionsPlaceable.width), y = (height - actionsPlaceable.height) / 2 )
中くらいの長さにおける右寄せ、左寄せを考慮する // .. 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 ) // ... }
左寄せが必要なシチュエーション if ((constraints.maxWidth / 2) < ((titlePlaceable.width / 2) +
navigationIconPlaceable.width)) { val titleX = navigationIconPlaceable.width // ...
左寄せが必要なシチュエーション navigationIconPlaceable.width val titleX = if ((constraints.maxWidth / 2) <
((titlePlaceable.width / 2) + navigationIconPlaceable.width)) { // ...
右寄せが必要なシチュエーション } else if ((constraints.maxWidth / 2) < ((titlePlaceable.width /
2) + actionsPlaceable.width)) { val titleX = // ... constraints.maxWidth - actionsPlaceable.width - titlePlaceable.width // ...
右寄せが必要なシチュエーション constraints.maxWidth - actionsPlaceable.width - titlePlaceable.width val titleX = //
... } else if ((constraints.maxWidth / 2) < ((titlePlaceable.width / 2) + actionsPlaceable.width)) { // ...
完成!! Full source https://gist.github.com/mikanIchinose/551303ea02bd457dfbbde92896384d65
まとめ カスタムレイアウトを組むときは要素の始点をどこにどうやって持っていくかを座標系を意識して考える 実際はもっと時間かかってますし、試行錯誤しまくってますw