1. はじめに
Jetpack Compose の基本の Codelab では、Text
のようなコンポーザブルと、画面上にアイテムを配置してその中の要素の配置を設定できる Column
や Row
(配置はそれぞれ縦、横)などの柔軟なレイアウト コンポーザブルを使用して、Compose でシンプルな UI を作成する方法を学びました。一方、アイテムを縦や横に並べないようにする場合は、Box
を使用すると、アイテムを他のアイテムの手前や背後に配置できます。
こうした標準的なレイアウト コンポーネントを使用して、次のような UI を作成できます。
@Composable
fun PhotographerProfile(photographer: Photographer) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(...)
Column {
Text(photographer.name)
Text(photographer.lastSeenOnline, ...)
}
}
}
Compose の再利用性とコンポーザビリティにより、必要な各種のパーツを適切な抽象度で組み合わせ、新しいコンポーズ可能な関数にすることで、独自のコンポーザブルを作成できます。
この Codelab では、Compose における最上位の UI 抽象化であるマテリアル デザインと、測定して画面上に要素を配置できる Layout
のような低レベルのコンポーザブルの使用方法について説明します。
マテリアル デザインに基づいた UI を作成する場合、この Codelab で示すように、Compose に組み込まれているマテリアル コンポーネントを使用できます。マテリアル デザインを使用しない場合、またはマテリアル デザインの仕様にないものを作成する場合の、カスタム レイアウトの作成方法についても学びます。
学習内容
この Codelab では、以下について学びます。
- マテリアル コンポーネントのコンポーザブルの使用方法
- 修飾子の意義とレイアウトでの使用方法
- カスタム レイアウトの作成方法
- intrinsic が必要となる状況
前提条件
- ラムダを含む Kotlin 構文の使用経験。
- Compose の基本に関する知識。
必要なもの
2. 新しい Compose プロジェクトを開始する
新しい Compose プロジェクトを開始するには、Android Studio Bumblebee を開き、[Start a new Android Studio project] を選択します。次の画面が表示されます。
上の画面が表示されない場合は、[File] > [New] > [New Project] にアクセスします。
新しいプロジェクトを作成するため、利用可能なテンプレートの中から [Empty Compose Activity] を選択します。
[Next] をクリックし、通常どおりにプロジェクトを設定します。API レベル 21(API Compose がサポートする最小レベル)以上の minimumSdkVersion を選択してください。
[Empty Compose Activity] テンプレートを選択すると、プロジェクトに次のコードが生成されます。
- プロジェクトは、すでに Compose を使用するように設定されています。
AndroidManifest.xml
ファイルが作成されます。app/build.gradle
(またはbuild.gradle (Module: YourApplicationName.app)
)ファイルで Compose の依存関係がインポートされ、Android Studio でbuildFeatures { compose true }
フラグを使用して Compose を扱えるようになります。
android {
...
kotlinOptions {
jvmTarget = '1.8'
useIR = true
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
}
dependencies {
...
implementation "androidx.compose.ui:ui:$compose_version"
implementation 'androidx.activity:activity-compose:1.4.0'
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc01"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$compose_version"
...
}
Codelab の解答
この Codelab の解答コードは GitHub から入手できます。
$ git clone https://github.com/googlecodelabs/android-compose-codelabs
または、リポジトリを ZIP ファイルとしてダウンロードすることもできます。
解答コードは LayoutsCodelab
プロジェクトにあります。自分のペースで順を追って Codelab の学習を進め、必要と思われるときに解答を確認することをおすすめします。Codelab を進める過程で、プロジェクトに追加する必要があるコード スニペットを示します。
3. 修飾子
修飾子を使用すると、コンポーザブルを装飾できます。動作や外観の変更、ユーザー補助ラベルなどの情報の追加、ユーザー入力の処理、さらにはクリック、スクロール、ドラッグやズームが可能なハイレベルのインタラクションの追加を行えます。修飾子は標準の Kotlin オブジェクトです。変数に代入し、再利用できます。複数の修飾子を交互に連鎖させて構成することもできます。
「はじめに」のセクションで見たプロフィール レイアウトを実装してみましょう。
MainActivity.kt
を開き、次のコードを追加します。
@Composable
fun PhotographerCard() {
Column {
Text("Alfred Sisley", fontWeight = FontWeight.Bold)
// LocalContentAlpha is defining opacity level of its children
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("3 minutes ago", style = MaterialTheme.typography.body2)
}
}
}
@Preview
@Composable
fun PhotographerCardPreview() {
LayoutsCodelabTheme {
PhotographerCard()
}
}
プレビュー:
次に、画像を読み込んでいる間、プレースホルダを表示させるとします。そのためには、円形とプレースホルダの色を指定する Surface
を使用します。大きさを指定するには size
修飾子を使用します。
@Composable
fun PhotographerCard() {
Row {
Surface(
modifier = Modifier.size(50.dp),
shape = CircleShape,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
) {
// Image goes here
}
Column {
Text("Alfred Sisley", fontWeight = FontWeight.Bold)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("3 minutes ago", style = MaterialTheme.typography.body2)
}
}
}
}
改善すべき点がいくつかあります。
- プレースホルダとテキストをある程度離す
- テキストを垂直方向に中央揃えする
1 番については、テキストを含む Column
に Modifier.padding
を使用することで、コンポーザブルの start
にスペースを追加して画像とテキストを離すことができます。2 番については、レイアウトによっては、そのレイアウトとレイアウト特性にしか適用されない修飾子があります。たとえば Row
のコンポーザブルは、weight
や align
など、そこで意味をなす特定の修飾子に(Row のコンテンツの RowScope
レシーバから)アクセスできます。このスコープ設定が型安全性をもたらすため、別のレイアウトでは意味をなさない修飾子を誤って使用することがなくなります。たとえば weight
は Box
では意味をなさないため、コンパイル時エラーとして防止されます。
@Composable
fun PhotographerCard() {
Row {
Surface(
modifier = Modifier.size(50.dp),
shape = CircleShape,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
) {
// Image goes here
}
Column(
modifier = Modifier
.padding(start = 8.dp)
.align(Alignment.CenterVertically)
) {
Text("Alfred Sisley", fontWeight = FontWeight.Bold)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("3 minutes ago", style = MaterialTheme.typography.body2)
}
}
}
}
プレビュー:
ほとんどのコンポーザブルは、オプションの修飾子パラメータを受け入れることで柔軟性が高くなっているおり、呼び出し元による変更が可能です。独自のコンポーザブルを作成する場合、パラメータとして修飾子を使用し、デフォルトで Modifier
に設定して(つまり何もしない空の修飾子)、関数のルート コンポーザブルに適用することを検討してください。この場合、次のようになります。
@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
Row(modifier) { ... }
}
修飾子の順序の重要性
コードで、工場出荷時の拡張機能を使用して複数の修飾子を交互に連鎖させることができる点に注目してくだい(つまり Modifier.padding(start = 8.dp).align(Alignment.CenterVertically)
)。
修飾子を連鎖させるときは、順序が重要であることに注意してください。1 つの引数に連結されるため、順序が最終結果に影響します。
写真家のプロフィールをクリックできるようにし、パディングを持たせる場合は、次のようにします。
@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
Row(modifier
.padding(16.dp)
.clickable(onClick = { /* Ignoring onClick */ })
) {
...
}
}
インタラクティブ プレビューを使用するか、エミュレータで実行します。
エリア全体がクリックできるわけではないことに注目してください。これは、padding
修飾子が clickable
修飾子より前に適用されたためです。padding
修飾子を clickable
修飾子より後に適用すると、パディングはクリック可能エリア内に含まれます。
@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
Row(modifier
.clickable(onClick = { /* Ignoring onClick */ })
.padding(16.dp)
) {
...
}
}
インタラクティブ プレビューを使用するか、エミュレータで実行します。
創造力を発揮しましょう。修飾子を使用すると、非常に柔軟な方法でコンポーザブルを変更できます。たとえば、外側にスペースを追加し、コンポーザブルの背景色を変更して、Row
の角を丸くする場合、次のようなコードを使用します。
@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
Row(modifier
.padding(8.dp)
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colors.surface)
.clickable(onClick = { /* Ignoring onClick */ })
.padding(16.dp)
) {
...
}
}
インタラクティブ プレビューを使用するか、エミュレータで実行します。
修飾子が機能する仕組みについては、この Codelab で後ほど詳しく説明します。
4. スロット API
Compose には、UI を作成するために使用できるハイレベルのマテリアル コンポーネント コンポーザブルが用意されています。UI を作成するためのビルディング ブロックであるため、画面に表示するものについての情報を提供する必要はあります。
スロット API は、コンポーザブル(このユースケースでは利用可能なマテリアル コンポーネント コンポーザブル)の上にカスタマイズのレイヤを適用するために Compose で導入されたパターンです。
次の例で確認しましょう。
マテリアル ボタンについて考えてみると、ボタンの外観や何を含むべきかについてのガイドラインがありますが、これはシンプルな API に置き換えることができます。
Button(text = "Button")
しかし、想定を超えてコンポーネントをカスタマイズしたい場合もよくあります。カスタマイズできる個々の要素ごとにパラメータを追加することもできますが、すぐに手に負えなくなります。
Button(
text = "Button",
icon: Icon? = myIcon,
textStyle = TextStyle(...),
spacingBetweenIconAndText = 4.dp,
...
)
そこで、想定外の方法でコンポーネントをカスタマイズするために複数のパラメータを追加するのではなく、スロットを追加しました。スロットは UI に空のスペースを残し、デベロッパーが自由に使用できるようにします。
たとえばボタンの場合、アイコンとテキストを含む行を挿入できるよう、ボタンの内側を残しておけます。
Button {
Row {
MyImage()
Spacer(4.dp)
Text("Button")
}
}
これを実現するために、子のコンポーザブル ラムダ(content: @Composable () -> Unit
)を受け取るボタンの API が用意されています。これによりボタン内で出力される独自のコンポーザブルを定義できます。
@Composable
fun Button(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
...
content: @Composable () -> Unit
)
content
と名付けたこのラムダが、最後のパラメータとなっています。これにより、後置ラムダ構文を使用して、構造化された方法でボタンにコンテンツを挿入できます。
Compose では、トップ アプリバーなどのさらに複雑なコンポーネントでスロットを多用します。
タイトル以外にもさまざまなものをカスタマイズできます。
使用例を次に示します。
TopAppBar(
title = {
Text(text = "Page title", maxLines = 2)
},
navigationIcon = {
Icon(myNavIcon)
}
)
独自のコンポーザブルを作成する場合、スロット API パターンを使用することで、再利用しやすくできます。
次のセクションでは、利用可能なさまざまなマテリアル コンポーネント コンポーザブルと、Android アプリを作成するときに使用する方法について説明します。
5. マテリアル コンポーネント
Compose にはマテリアル コンポーネント コンポーザブルが組み込まれており、アプリの作成に使用できます。最もハイレベルのコンポーザブルは Scaffold
です。
Scaffold
Scaffold
を使用すると、マテリアル デザインの基本的なレイアウト構造で UI を実装できます。Scaffold には、TopAppBar、BottomAppBar、FloatingActionButton、Drawer など、最も一般的なトップレベルのマテリアル コンポーネント向けのスロットが用意されています。Scaffold
を使用すると、こうしたコンポーネントを正しく配置し、連携させることができます。
生成された Android Studio テンプレートに基づき、Scaffold
を使用するようにサンプルコードを変更します。MainActivity.kt
を開きます。Greeting
と GreetingPreview
のコンポーザブルは使用しないため自由に削除してください。
LayoutsCodelab
という新しいコンポーザブルを作成します。Codelab の全体を通してこれを変更していきます。
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.codelab.layouts.ui.LayoutsCodelabTheme
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LayoutsCodelabTheme {
LayoutsCodelab()
}
}
}
}
@Composable
fun LayoutsCodelab() {
Text(text = "Hi there!")
}
@Preview
@Composable
fun LayoutsCodelabPreview() {
LayoutsCodelabTheme {
LayoutsCodelab()
}
}
@Preview
のアノテーションを付ける必要がある Compose プレビュー関数では、LayoutsCodelab
が次のように表示されます。
典型的なマテリアル デザイン構造になるよう、例に Scaffold
コンポーザブルを追加してみましょう。Scaffold API
のパラメータは、@Composable (InnerPadding) -> Unit
型の本文コンテンツを除き、すべてオプションです。ラムダはパラメータとしてパディングを受け取ります。これは、画面上でアイテムを適切に制約するために、コンテンツのルート コンポーザブルに適用する必要のあるパディングです。簡単に始めるために、他のマテリアル コンポーネントなしで Scaffold
を追加してみましょう。
@Composable
fun LayoutsCodelab() {
Scaffold { innerPadding ->
Text(text = "Hi there!", modifier = Modifier.padding(innerPadding))
}
}
プレビュー:
Column
を画面のメイン コンテンツにする場合、Column
に修飾子を適用する必要があります。
@Composable
fun LayoutsCodelab() {
Scaffold { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
Text(text = "Hi there!")
Text(text = "Thanks for going through the Layouts codelab")
}
}
}
プレビュー:
コードを再利用しやすくし、テストしやすくするには、コードを小さなチャンクに構造化する必要があります。そのため、画面のコンテンツを使用して別のコンポーズ可能な関数を作成してみましょう。
@Composable
fun LayoutsCodelab() {
Scaffold { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text(text = "Hi there!")
Text(text = "Thanks for going through the Layouts codelab")
}
}
一般的に、Android アプリのトップ アプリバーには、現在の画面、ナビゲーション、アクションに関する情報が表示されます。それでは例に追加してみましょう。
TopAppBar
Scaffold
には、@Composable () -> Unit
型の topBar
パラメータによるトップ アプリバーのスロットがあります。つまり、このスロットは自由にコンポーザブルで埋めることができます。たとえば単に h3
スタイルのテキストを入れる場合、次のように、提供されたスロットに Text
を使用できます。
@Composable
fun LayoutsCodelab() {
Scaffold(
topBar = {
Text(
text = "LayoutsCodelab",
style = MaterialTheme.typography.h3
)
}
) { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}
プレビュー:
ただし、ほとんどのマテリアル コンポーネントと同様に、Compose にはタイトル、ナビゲーション アイコン、アクションのスロットを持つ TopAppBar
コンポーザブルがあります。また、各コンポーネントで使用する色など、一部のデフォルト設定がマテリアル仕様の推奨事項に合わせて調整されています。
スロット API のパターンに沿って、TopAppBar
の title
スロットに画面タイトルの Text
を入れます。
@Composable
fun LayoutsCodelab() {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "LayoutsCodelab")
}
)
}
) { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}
プレビュー:
トップ アプリバーには通常、なんらかのアクション アイテムがあります。この例では、何かを学んだと思ったときにタップできるお気に入りボタンを追加します。Compose には、たとえば閉じるアイコン、お気に入りアイコン、メニューアイコンに使用できる、事前定義されたマテリアル アイコンも用意されています。
トップ アプリバーのアクション アイテム用のスロットは、内部で Row
を使用する actions
パラメータであるため、複数のアクションが横並びに配置されます。事前定義されたアイコンのいずれかを使用するには、IconButton
コンポーザブルと、その内部に Icon
を使用します。
@Composable
fun LayoutsCodelab() {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "LayoutsCodelab")
},
actions = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
}
)
}
) { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}
プレビュー:
通常、アクションはなんらかの形でアプリの状態を変更します。状態については、Jetpack Compose の基本の Codelab で状態管理の基本を詳しく学ぶことができます。
修飾子の配置
コンポーザブルを再利用しやすくするために、新しいコンポーザブルを作成するときは常に、デフォルトが Modifier
である modifier
パラメータを使用することをおすすめします。BodyContent
コンポーザブルはすでに修飾子をパラメータとして受け取っています。BodyContent
にパディングをさらに追加する場合は、どこに padding
修飾子を配置すればよいでしょうか。
2 通り考えられます。
BodyContent
の呼び出しすべてに追加のパディングが適用されるように、コンポーザブル内にある唯一の直接の子に修飾子を適用する。
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(8.dp)) {
Text(text = "Hi there!")
Text(text = "Thanks for going through the Layouts codelab")
}
}
- コンポーザブルを呼び出すとき、必要な場合にのみパディングをさらに追加する修飾子を適用する。
@Composable
fun LayoutsCodelab() {
Scaffold(...) { innerPadding ->
BodyContent(Modifier.padding(innerPadding).padding(8.dp))
}
}
どこで行うかは、コンポーザブルの種類とユースケースによって異なります。修飾子がコンポーザブルに固有のものである場合は内側に配置し、そうでない場合は外側に配置します。今回、パディングは BodyContent
を呼び出すたびに強制するものではなく、ケースバイケースで適用する必要があるため、2 つ目の方法を採用します。
修飾子は、前の修飾子関数において次の修飾子関数を呼び出すことで、連鎖させることができます。利用可能な連鎖メソッドがない場合は .then()
を使用できます。この例では modifier
(小文字)で始めます。つまり、パラメータとして渡された連鎖の上に連鎖を作成します。
他のアイコン
前述のアイコンとは別に、プロジェクトに新しい依存関係を追加することで、マテリアル アイコンの完全なリストを使用できます。こうしたアイコンを試す場合は、app/build.gradle
(または build.gradle (Module: app)
)ファイルを開き、ui-material-icons-extended
依存関係をインポートします。
dependencies {
...
implementation "androidx.compose.material:material-icons-extended:$compose_version"
}
TopAppBar
のアイコンは自由に変更して構いません。
追加の課題
Scaffold
と TopAppBar
は、マテリアル ルックのアプリを作成するために使用できるコンポーザブルの一部にすぎません。BottomNavigation
や BottomDrawer
など、他のマテリアル コンポーネントでも同じことができます。演習として、これまでと同様にこれらの API を使用して Scaffold
スロットを埋めてみましょう。
6. リストの利用
アイテムのリストを表示することは、アプリでよくあるパターンです。Jetpack Compose では、Column
コンポーザブルと Row
コンポーザブルを使用してこのパターンを簡単に実装できますが、現在表示されているアイテムのみを作成して配置する遅延リストも用意されています。
Column
コンポーザブルを使用し、アイテム 100 個の縦方向リストを作成して練習してみましょう。
@Composable
fun SimpleList() {
Column {
repeat(100) {
Text("Item #$it")
}
}
}
Column
はデフォルトでスクロールを処理しないため、一部のアイテムが画面外にはみ出し、表示されません。verticalScroll
修飾子を追加して、Column
内をスクロールできるようにします。
@Composable
fun SimpleList() {
// We save the scrolling position with this state that can also
// be used to programmatically scroll the list
val scrollState = rememberScrollState()
Column(Modifier.verticalScroll(scrollState)) {
repeat(100) {
Text("Item #$it")
}
}
}
遅延リスト
Column
は、画面に表示されないものも含め、すべてのリストアイテムをレンダリングします。リストサイズが大きくなったとき、これはパフォーマンスの問題となります。この問題を防ぐには、LazyColumn
を使用します。画面に表示されているアイテムのみがレンダリングされ、パフォーマンスが向上し、scroll
修飾子が必要なくなります。
LazyColumn
には、リストのコンテンツを記述する DSL があります。リストサイズとして数字を受け取ることができる items
を使用します。配列とリストもサポートされています(詳細についてはリストのドキュメントのセクションをご覧ください)。
@Composable
fun LazyList() {
// We save the scrolling position with this state that can also
// be used to programmatically scroll the list
val scrollState = rememberLazyListState()
LazyColumn(state = scrollState) {
items(100) {
Text("Item #$it")
}
}
}
画像の表示
先ほど PhotographCard
で確認したように、Image
はビットマップやベクター画像を表示するために使用できるコンポーザブルです。画像をリモートで取得する場合、アプリはアセットをダウンロードし、ビットマップにデコードして、最終的に Image
内にレンダリングする必要があるため、プロセスのステップが多くなります。
こうしたステップを簡略化するために、タスクを効率的に行うコンポーザブルが用意されている Coil ライブラリを使用します。
プロジェクトの build.gradle
ファイルに Coil の依存関係を追加します。
// build.gradle
implementation 'io.coil-kt:coil-compose:1.4.0'
リモートで画像を取得するため、マニフェスト ファイルに INTERNET
権限を追加します。
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />
ここで、画像とその横にアイテムのインデックスを表示するアイテム コンポーザブルを作成します。
@Composable
fun ImageListItem(index: Int) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = rememberImagePainter(
data = "https://developer.android.com/images/brand/Android_Robot.png"
),
contentDescription = "Android Logo",
modifier = Modifier.size(50.dp)
)
Spacer(Modifier.width(10.dp))
Text("Item #$index", style = MaterialTheme.typography.subtitle1)
}
}
次に、リストの Text
コンポーザブルをこの ImageListItem
と入れ替えます。
@Composable
fun ImageList() {
// We save the scrolling position with this state
val scrollState = rememberLazyListState()
LazyColumn(state = scrollState) {
items(100) {
ImageListItem(it)
}
}
}
リストのスクロール
リストのスクロール位置を手動で制御してみましょう。リストの最上部と最下部までスムーズにスクロールできるように、ボタンを 2 つ追加します。スクロール中にリストのレンダリングがブロックされないよう、スクロール API は suspend 関数になっています。そこで、コルーチン内で呼び出す必要があります。そのためには、rememberCoroutineScope
関数を使用して CoroutineScope
を作成し、ボタンイベント ハンドラからコルーチンを作成します。この CoroutineScope
は、コールサイトのライフサイクルに従います。コンポーザブルのライフサイクル、コルーチン、副作用について詳しくは、こちらのガイドをご覧ください。
val listSize = 100
// We save the scrolling position with this state
val scrollState = rememberLazyListState()
// We save the coroutine scope where our animated scroll will be executed
val coroutineScope = rememberCoroutineScope()
最後に、スクロールを制御するボタンを追加します。
Row {
Button(onClick = {
coroutineScope.launch {
// 0 is the first item index
scrollState.animateScrollToItem(0)
}
}) {
Text("Scroll to the top")
}
Button(onClick = {
coroutineScope.launch {
// listSize - 1 is the last index of the list
scrollState.animateScrollToItem(listSize - 1)
}
}) {
Text("Scroll to the end")
}
}
このセクションのコードの全文
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import coil.compose.rememberImagePainter
import kotlinx.coroutines.launch
@Composable
fun ImageListItem(index: Int) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = rememberImagePainter(
data = "https://developer.android.com/images/brand/Android_Robot.png"
),
contentDescription = "Android Logo",
modifier = Modifier.size(50.dp)
)
Spacer(Modifier.width(10.dp))
Text("Item #$index", style = MaterialTheme.typography.subtitle1)
}
}
@Composable
fun ScrollingList() {
val listSize = 100
// We save the scrolling position with this state
val scrollState = rememberLazyListState()
// We save the coroutine scope where our animated scroll will be executed
val coroutineScope = rememberCoroutineScope()
Column {
Row {
Button(onClick = {
coroutineScope.launch {
// 0 is the first item index
scrollState.animateScrollToItem(0)
}
}) {
Text("Scroll to the top")
}
Button(onClick = {
coroutineScope.launch {
// listSize - 1 is the last index of the list
scrollState.animateScrollToItem(listSize - 1)
}
}) {
Text("Scroll to the end")
}
}
LazyColumn(state = scrollState) {
items(listSize) {
ImageListItem(it)
}
}
}
}
7. カスタム レイアウトを作成する
Compose は、Column
、Row
、Box
などの組み込みコンポーザブルを組み合わせることで、カスタム レイアウトに十分な小さいチャンクとして、コンポーザブルの再利用性を高めます。
ただし、手動で測定して子を配置する必要がある、アプリに固有なものを作成する必要が生じることがあります。その場合、Layout
コンポーザブルを使用できます。実際、Column
や Row
のような上位のレイアウトはすべて、このコンポーザブルで作成します。
カスタム レイアウトの作成方法に入る前に、Compose におけるレイアウトの原則について詳しく知っておく必要があります。
Compose におけるレイアウトの原則
コンポーズ可能な関数の中には、呼び出されたときに UI の一部を出力し、それを画面にレンダリングされる UI ツリーに追加するものもあります。各出力(要素)には親が 1 つあり、場合によっては多くの子があります。また、親要素内の場所((x, y) 位置)とサイズ(width
、height
)もあります。
要素は、満たす必要がある制約を使用して、自身を測定するよう求められます。制約により、要素の width
と height
の最小値と最大値が制限されます。要素に子要素がある場合、親の要素は、自身のサイズを判断しやすくするために、子のそれぞれを測定できます。要素が自身のサイズを報告すると、子要素を自身に対し相対的に配置できるようになります。これについてはカスタム レイアウトを作成する際に詳しく説明します。
注: Compose UI では、マルチパス測定は許可されていません。つまり、さまざまな測定構成を試すために、レイアウト要素でその子要素を複数回測定することはできません。シングルパス測定はパフォーマンスに優れており、Compose は深い UI ツリーを効率的に処理できます。レイアウト要素が子を 2 回測定し、その子が自身の子のいずれかを 2 回測定した場合など、UI 全体をレイアウトしようとすると多くの作業が必要になるため、アプリのパフォーマンスを良好に保つことが難しくなります。しかし、子の測定 1 回でわかることに加えて、追加の情報が本当に必要な場合もあります。そうした場合に対応する方法については後ほど説明します。
レイアウト修飾子の使用
layout
修飾子を使用して、要素の測定と配置の方法を手動で制御します。通常、カスタム layout
修飾子の共通構造は次のとおりです。
fun Modifier.customLayoutModifier(...) = Modifier.layout { measurable, constraints ->
...
})
layout
修飾子を使用する場合、ラムダ パラメータを 2 つ取得します。
measurable
: 測定し配置する子constraints
: 子の幅と高さの最小値と最大値
画面に Text
を表示し、上端からテキスト先頭行のベースラインまでの距離を制御するとします。そのためには、layout
修飾子を使用して画面上に手動でコンポーザブルを配置する必要があります。望ましい動作を次の画像に示します。上端から最初のベースラインまでの距離は 24.dp
です。
まず、firstBaselineToTop
修飾子を作成しましょう。
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = this.then(
layout { measurable, constraints ->
...
}
)
コンポーザブルの測定から行います。コンポーザブルにおけるレイアウトの原則のセクションで述べたように、子の測定は 1 回しかできません。
measurable.measure(constraints)
を呼び出してコンポーザブルを測定します。measure(constraints)
を呼び出すときは、constraints
ラムダ パラメータで利用可能なコンポーザブルの所与の制約を渡すことも、独自に作成することもできます。Measurable
の measure()
呼び出しの結果は Placeable
であり、後で行うように placeRelative(x, y)
を呼び出して配置できます。
このユースケースでは、測定をそれ以上制約することなく、単に所与の制約を使用します。
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = this.then(
layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
...
}
)
コンポーザブルを測定したので、サイズを計算する必要があります。また、コンテンツを配置するために使用するラムダも受け入れる layout(width, height)
メソッドを呼び出して、サイズを指定する必要があります。
この場合、対象のコンポーザブルの幅は測定したコンポーザブルの width
になり、高さは、希望の上端からベースラインまでの高さと最初のベースラインの差を、コンポーザブルの height
に足したものになります。
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = this.then(
layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
// Check the composable has a first baseline
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
// Height of the composable with padding - first baseline
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
val height = placeable.height + placeableY
layout(placeable.width, height) {
...
}
}
)
これで、placeable.placeRelative(x, y)
を呼び出して画面上にコンポーザブルを配置できるようになりました。placeRelative
を呼び出さない場合、コンポーザブルは表示されません。placeRelative
は、現在の layoutDirection
に基づいて placeable の位置を自動的に調整します。
この場合、テキストの y
位置は、上パディングと最初のベースライン位置の差に相当します。
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = this.then(
layout { measurable, constraints ->
...
// Height of the composable with padding - first baseline
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
val height = placeable.height + placeableY
layout(placeable.width, height) {
// Where the composable gets placed
placeable.placeRelative(0, placeableY)
}
}
)
これが期待どおりに機能することを確認するには、上の図のように、Text
でこの修飾子を使用します。
@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
LayoutsCodelabTheme {
Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
}
}
@Preview
@Composable
fun TextWithNormalPaddingPreview() {
LayoutsCodelabTheme {
Text("Hi there!", Modifier.padding(top = 32.dp))
}
}
プレビュー:
Layout コンポーザブルの使用
単一のコンポーザブルを測定して画面に配置する方法を制御するのではなく、コンポーザブルのグループに対して同じことを行う必要が生じることもあります。その場合、Layout
コンポーザブルを使用して、レイアウトの子要素を測定し配置する方法を手動で制御できます。通常、Layout
を使用するコンポーザブルの共通構造は次のとおりです。
@Composable
fun CustomLayout(
modifier: Modifier = Modifier,
// custom layout attributes
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}
CustomLayout
に最低限必要なパラメータは、Layout
に渡される modifier
と content
です。Layout
の後置ラムダ(MeasurePolicy
型)では、layout
修飾子の場合と同じラムダ パラメータが得られます。
Layout
の動作を確認するために、まずは Layout
を使用して非常に基本的な Column
を実装し、API を理解しましょう。後ほど、さらに複雑なものを作成して Layout
コンポーザブルの柔軟性を紹介します。
基本的な Column の実装
Column
のカスタム実装では、アイテムを縦方向に配置します。また、簡略化するために、レイアウトが親の中で可能な限り多くのスペースを占有しています。
MyOwnColumn
という新しいコンポーザブルを作成し、Layout
コンポーザブルの共通構造を追加します。
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}
前のように、まずは 1 回しか測定できない子を測定します。レイアウト修飾子の仕組みと同様に、measurables
ラムダ パラメータで、measurable.measure(constraints)
を呼び出して測定できる content
をすべて取得します。
このユースケースでは、子ビューをさらに制約することはありません。子ビューを測定するときは、後で画面上に正しく配置できるように、各行の width
と最大 height
を記録しておく必要もあります。
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each child
measurable.measure(constraints)
}
}
}
これで、測定した子のリストがロジック内にできました。画面上に配置する前に、今回の Column
のサイズを計算する必要があります。親と同じ大きさにするため、サイズは親から渡された制約となります。layout(width, height)
メソッドを呼び出して、独自の Column
のサイズを指定します。これにより、子を配置するために使用するラムダも得られます。
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Measure children - code in the previous code snippet
...
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Place children
}
}
}
最後に、placeable.placeRelative(x, y)
を呼び出して画面上に子を配置します。子を縦方向に配置するために、子を配置した y
座標を記録しておきます。MyOwnColumn
のコードは最終的に次のようになります。
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each child
measurable.measure(constraints)
}
// Track the y co-ord we have placed children up to
var yPosition = 0
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Place children in the parent layout
placeables.forEach { placeable ->
// Position item on the screen
placeable.placeRelative(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
MyOwnColumn の動作
BodyContent
コンポーザブルの中で MyOwnColumn
を使用して画面上で確認してみましょう。BodyContent の内容を次のように置き換えます。
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
MyOwnColumn(modifier.padding(8.dp)) {
Text("MyOwnColumn")
Text("places items")
Text("vertically.")
Text("We've done it by hand!")
}
}
プレビュー:
8. 複雑なカスタム レイアウト
Layout
の基本を理解したところで、API の柔軟性を紹介するために、さらに複雑な例を作成しましょう。次の図の中ほどにある、カスタムの Material Study Owl の千鳥格子を作成します。
Owl の千鳥格子はアイテムを縦方向に配置し、n
行で一度に 1 列を埋めます。Columns
の Row
でこれを行うことはできません。レイアウトを千鳥状にできないためです。Rows
の Column
では、縦方向に表示できるようにデータを準備すれば可能です。
しかしカスタム レイアウトでは、千鳥格子内のすべてのアイテムの高さを制約することもできます。そこで、レイアウトをさらに制御し、カスタム レイアウトを作成する方法を学ぶために、自身で子を測定し配置しましょう。
格子をさまざまな方向で再利用できるようにする場合、画面に表示する行数をパラメータとして受け取ります。この情報はレイアウトが呼び出されたときに得られるため、パラメータとして渡します。
@Composable
fun StaggeredGrid(
modifier: Modifier = Modifier,
rows: Int = 3,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}
前のように、まずは子を測定します。ご存じのように、子は 1 回しか測定できません。
このユースケースでは、子ビューをさらに制約することはありません。子ビューを測定するときは、各行の width
と最大 height
を記録しておく必要もあります。
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Keep track of the width of each row
val rowWidths = IntArray(rows) { 0 }
// Keep track of the max height of each row
val rowHeights = IntArray(rows) { 0 }
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.mapIndexed { index, measurable ->
// Measure each child
val placeable = measurable.measure(constraints)
// Track the width and max height of each row
val row = index % rows
rowWidths[row] += placeable.width
rowHeights[row] = Math.max(rowHeights[row], placeable.height)
placeable
}
...
}
これで、測定した子のリストがロジック内にできました。画面上に配置する前に、格子のサイズを計算する必要があります(全 width
と全 height
)。また、各行の最大高は既知であるため、各行の要素を配置する Y 位置を計算できます。Y 位置は rowY
変数に保存します。
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
...
// Grid's width is the widest row
val width = rowWidths.maxOrNull()
?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) ?: constraints.minWidth
// Grid's height is the sum of the tallest element of each row
// coerced to the height constraints
val height = rowHeights.sumOf { it }
.coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
// Y of each row, based on the height accumulation of previous rows
val rowY = IntArray(rows) { 0 }
for (i in 1 until rows) {
rowY[i] = rowY[i-1] + rowHeights[i-1]
}
...
}
最後に、placeable.placeRelative(x, y)
を呼び出して画面上に子を配置します。このユースケースでは、各行の X 座標も rowX
に記録しておきます。
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
...
// Set the size of the parent layout
layout(width, height) {
// x cord we have placed up to, per row
val rowX = IntArray(rows) { 0 }
placeables.forEachIndexed { index, placeable ->
val row = index % rows
placeable.placeRelative(
x = rowX[row],
y = rowY[row]
)
rowX[row] += placeable.width
}
}
}
カスタムの StaggeredGrid の使用例
子を測定し配置する方法がわかっているカスタムの格子レイアウトができたので、アプリで使用しましょう。格子内の Owl のチップをシミュレートするために、同様のことを行うコンポーザブルを簡単に作成できます。
@Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
Card(
modifier = modifier,
border = BorderStroke(color = Color.Black, width = Dp.Hairline),
shape = RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier.size(16.dp, 16.dp)
.background(color = MaterialTheme.colors.secondary)
)
Spacer(Modifier.width(4.dp))
Text(text = text)
}
}
}
@Preview
@Composable
fun ChipPreview() {
LayoutsCodelabTheme {
Chip(text = "Hi there")
}
}
プレビュー:
今度は、BodyContent
に表示できるトピックのリストを作成し、StaggeredGrid
に表示しましょう。
val topics = listOf(
"Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
"Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
"Religion", "Social sciences", "Technology", "TV", "Writing"
)
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
StaggeredGrid(modifier = modifier) {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
}
@Preview
@Composable
fun LayoutsCodelabPreview() {
LayoutsCodelabTheme {
BodyContent()
}
}
プレビュー:
格子の行数を変更しても期待どおりに動作します。
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
StaggeredGrid(modifier = modifier, rows = 5) {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
}
プレビュー:
行数によってはトピックが画面外にはみ出すため、StaggeredGrid
をスクロール可能な Row
でラップして、StaggeredGrid
ではなく修飾子を渡すと、BodyContent
をスクロール可能にできます。
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Row(modifier = modifier.horizontalScroll(rememberScrollState())) {
StaggeredGrid {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
}
}
インタラクティブ プレビュー ボタン()を使用するか、Android Studio の実行ボタンをタップしてデバイスでアプリを実行すると、コンテンツを横方向にスクロールできることが確認できます。
9. レイアウト修飾子の仕組み
修飾子の基本、カスタム コンポーザブルの作成方法、子を手動で測定し配置する方法について理解したところで、修飾子の仕組みについて理解を深めましょう。
まとめると、修飾子を使用することで、コンポーザブルの動作をカスタマイズできます。複数の修飾子を互いに連鎖させることで組み合わせることができます。修飾子の種類は複数ありますが、UI コンポーネントを測定し配置する方法を変更できることから、このセクションでは LayoutModifier
に着目します。
コンポーザブルは自身のコンテンツを扱い、そのコンテンツは、コンポーザブルの作成者が明示的な API を公開しない限り、親によって検査されたり、操作されたりしません。同様に、コンポーザブルの修飾子は、変更内容を同じ不透明な方法で装飾します(修飾子がカプセル化されます)。
修飾子の分析
Modifier
と LayoutModifier
は公開インターフェースであるため、独自の修飾子を作成できます。前に使用した Modifier.padding
の実装を分析し、修飾子について理解を深めましょう。
padding
は、LayoutModifier
インターフェースを実装するクラスでバックアップされる関数であり、measure
メソッドをオーバーライドします。PaddingModifier
は equals()
を実装する通常のクラスであるため、修飾子は再コンポジション間で比較できます。
例として、適用される要素のサイズと制約が padding
によって変更されるソースコードを示します。
// How to create a modifier
@Stable
fun Modifier.padding(all: Dp) =
this.then(
PaddingModifier(start = all, top = all, end = all, bottom = all, rtlAware = true)
)
// Implementation detail
private class PaddingModifier(
val start: Dp = 0.dp,
val top: Dp = 0.dp,
val end: Dp = 0.dp,
val bottom: Dp = 0.dp,
val rtlAware: Boolean,
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val horizontal = start.roundToPx() + end.roundToPx()
val vertical = top.roundToPx() + bottom.roundToPx()
val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
val width = constraints.constrainWidth(placeable.width + horizontal)
val height = constraints.constrainHeight(placeable.height + vertical)
return layout(width, height) {
if (rtlAware) {
placeable.placeRelative(start.roundToPx(), top.roundToPx())
} else {
placeable.place(start.roundToPx(), top.roundToPx())
}
}
}
}
要素の新しい width
は、子の width
と、要素の幅の制約に強制された左右のパディング値の和です。height
は、子の height
と、要素の高さの制約に強制された上下のパディング値の和です。
順序の重要性
最初のセクションで確認したように、修飾子を連鎖させるときは順序が重要です。これは、変更するコンポーザブルに対して、早いものから順に適用されるためです。つまり、左側の修飾子の測定とレイアウトが右側の修飾子に影響します。コンポーザブルの最終的なサイズは、パラメータとして渡されるすべての修飾子に左右されます。
まず、修飾子は左から右に制約を更新し、その後、右から左にサイズを戻します。次の例で確認しましょう。
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.background(color = Color.LightGray)
.size(200.dp)
.padding(16.dp)
.horizontalScroll(rememberScrollState())
) {
StaggeredGrid {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
}
}
このように適用された修飾子のプレビューは次のようになります。
まず、修飾子が UI に与える影響を確認するために背景を変更し、次に width
と height
が 200.dp
になるようにサイズを制約して、最後にテキストと周囲の間が空くようにパディングを適用します。
制約は左から右への連鎖を通じて反映されるため、測定される Row
のコンテンツの制約は、最小と最大の width
と height
について、(200-16-16)=168
dp です。つまり StaggeredGrid
のサイズはちょうど 168x168
dp になります。そのため、modifySize
の連鎖が右から左に行われた後で、スクロール可能な Row
の最終的なサイズは 200x200
dp になります。
修飾子の順序を変更し、最初にパディングを適用してからサイズを適用した場合は、異なる UI が得られます。
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.background(color = Color.LightGray, shape = RectangleShape)
.padding(16.dp)
.size(200.dp)
.horizontalScroll(rememberScrollState())
) {
StaggeredGrid {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
}
}
プレビュー:
この場合、スクロール可能な Row
と padding
の元の制約が size
制約に強制されて子が測定されます。そのため StaggeredGrid
は、最小と最大の width
と height
について、200
dp に制約されます。StaggeredGrid
サイズは 200x200
dp です。サイズが右から左に変更されると、padding
修飾子がサイズを (200+16+16)x(200+16+16)=232x232
に増加させ、これが Row
の最終的なサイズにもなります。
レイアウト方向
LayoutDirection
アンビエントを使用してコンポーザブルのレイアウト方向を変更できます。
コンポーザブルを手動で画面に配置する場合、layoutDirection
は、layout
修飾子または Layout
コンポーザブルの LayoutScope
の一部になります。layoutDirection
を使用するときは、placeRelative
メソッドとは異なり、右から左へのコンテキストで位置を自動的にミラーリングしないため、place
を使用してコンポーザブルを配置してください。
このセクションのコードの全文
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.codelab.layouts.ui.LayoutsCodelabTheme
import kotlin.math.max
val topics = listOf(
"Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
"Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
"Religion", "Social sciences", "Technology", "TV", "Writing"
)
@Composable
fun LayoutsCodelab() {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "LayoutsCodelab")
},
actions = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
}
)
}
) { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Row(modifier = modifier
.background(color = Color.LightGray)
.padding(16.dp)
.size(200.dp)
.horizontalScroll(rememberScrollState()),
content = {
StaggeredGrid {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
})
}
@Composable
fun StaggeredGrid(
modifier: Modifier = Modifier,
rows: Int = 3,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Keep track of the width of each row
val rowWidths = IntArray(rows) { 0 }
// Keep track of the max height of each row
val rowHeights = IntArray(rows) { 0 }
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.mapIndexed { index, measurable ->
// Measure each child
val placeable = measurable.measure(constraints)
// Track the width and max height of each row
val row = index % rows
rowWidths[row] += placeable.width
rowHeights[row] = Math.max(rowHeights[row], placeable.height)
placeable
}
// Grid's width is the widest row
val width = rowWidths.maxOrNull()
?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) ?: constraints.minWidth
// Grid's height is the sum of the tallest element of each row
// coerced to the height constraints
val height = rowHeights.sumOf { it }
.coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
// Y of each row, based on the height accumulation of previous rows
val rowY = IntArray(rows) { 0 }
for (i in 1 until rows) {
rowY[i] = rowY[i - 1] + rowHeights[i - 1]
}
// Set the size of the parent layout
layout(width, height) {
// x co-ord we have placed up to, per row
val rowX = IntArray(rows) { 0 }
placeables.forEachIndexed { index, placeable ->
val row = index % rows
placeable.placeRelative(
x = rowX[row],
y = rowY[row]
)
rowX[row] += placeable.width
}
}
}
}
@Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
Card(
modifier = modifier,
border = BorderStroke(color = Color.Black, width = Dp.Hairline),
shape = RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(16.dp, 16.dp)
.background(color = MaterialTheme.colors.secondary)
)
Spacer(Modifier.width(4.dp))
Text(text = text)
}
}
}
@Preview
@Composable
fun ChipPreview() {
LayoutsCodelabTheme {
Chip(text = "Hi there")
}
}
@Preview
@Composable
fun LayoutsCodelabPreview() {
LayoutsCodelabTheme {
LayoutsCodelab()
}
}
10. 制約レイアウト
ConstraintLayout
を使用すると、コンポーザブルを画面上の他の要素と相対的に配置できます。Row
、Column
、Box
を複数使用することに代わる手段です。ConstraintLayout は、配置要件がより複雑な、大規模なレイアウトを実装する場合に便利です。
Compose の制約レイアウトの依存関係は、プロジェクトの build.gradle
ファイルに記載されています。
// build.gradle
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc01"
Compose の ConstraintLayout
は DSL で機能します。
- 参照は
createRefs()
またはcreateRef()
を使用して作成されます。ConstraintLayout
内の各コンポーザブルは、関連付けられた参照を持つ必要があります。 - 制約は、参照をパラメータとして受け取る
constrainAs
修飾子を使用して指定します。この修飾子により、本文のラムダで制約を指定できます。 - 制約は、
linkTo
またはその他の便利なメソッドを使用して指定します。 parent
は、ConstraintLayout
コンポーザブル自体に対する制約を指定するために使用できる、既存の参照です。
簡単な例を見てみましょう。
@Composable
fun ConstraintLayoutContent() {
ConstraintLayout {
// Create references for the composables to constrain
val (button, text) = createRefs()
Button(
onClick = { /* Do something */ },
// Assign reference "button" to the Button composable
// and constrain it to the top of the ConstraintLayout
modifier = Modifier.constrainAs(button) {
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text("Button")
}
// Assign reference "text" to the Text composable
// and constrain it to the bottom of the Button composable
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 16.dp)
})
}
}
@Preview
@Composable
fun ConstraintLayoutContentPreview() {
LayoutsCodelabTheme {
ConstraintLayoutContent()
}
}
このコードは、Button
の頂部を親に対しマージン 16.dp
に制約し、Text
も Button
の底部に対しマージン 16.dp
に制約します。
テキストを横方向に中央揃えする場合は、centerHorizontallyTo
関数を使用して、Text
の start
と end
を parent
の両端に設定します。
@Composable
fun ConstraintLayoutContent() {
ConstraintLayout {
... // Same as before
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 16.dp)
// Centers Text horizontally in the ConstraintLayout
centerHorizontallyTo(parent)
})
}
}
プレビュー:
ConstraintLayout
のサイズは、そのコンテンツをラップできる範囲で可能な限り小さくなります。そのため、Text
が親ではなく Button
を中心にしているように見えます。他のサイズ設定動作が必要な場合は、Compose の他のレイアウトと同様に、サイズ設定修飾子(fillMaxSize
、size
など)を ConstraintLayout
コンポーザブルに適用する必要があります。
ヘルパー
DSL は、ガイドライン、バリア、チェーンの作成もサポートしています。次に例を示します。
@Composable
fun ConstraintLayoutContent() {
ConstraintLayout {
// Creates references for the three composables
// in the ConstraintLayout's body
val (button1, button2, text) = createRefs()
Button(
onClick = { /* Do something */ },
modifier = Modifier.constrainAs(button1) {
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text("Button 1")
}
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button1.bottom, margin = 16.dp)
centerAround(button1.end)
})
val barrier = createEndBarrier(button1, text)
Button(
onClick = { /* Do something */ },
modifier = Modifier.constrainAs(button2) {
top.linkTo(parent.top, margin = 16.dp)
start.linkTo(barrier)
}
) {
Text("Button 2")
}
}
}
プレビュー:
以下の点に注意してください。
- バリア(および他のすべてのヘルパー)は、
ConstraintLayout
の本文で作成できますが、constrainAs
内では作成できません。 linkTo
は、レイアウトの端の場合と同様に、ガイドラインとバリアで制約するために使用できます。
サイズのカスタマイズ
デフォルトで、ConstraintLayout
の子は、コンテンツをラップするために必要なサイズを選択できます。つまり、たとえばテキストが長すぎる場合、Text は画面の外にはみ出てしまいます。
@Composable
fun LargeConstraintLayout() {
ConstraintLayout {
val text = createRef()
val guideline = createGuidelineFromStart(fraction = 0.5f)
Text(
"This is a very very very very very very very long text",
Modifier.constrainAs(text) {
linkTo(start = guideline, end = parent.end)
}
)
}
}
@Preview
@Composable
fun LargeConstraintLayoutPreview() {
LayoutsCodelabTheme {
LargeConstraintLayout()
}
}
もちろん、空いているスペースにテキストを改行します。そのためには、テキストの width
の動作を変更します。
@Composable
fun LargeConstraintLayout() {
ConstraintLayout {
val text = createRef()
val guideline = createGuidelineFromStart(0.5f)
Text(
"This is a very very very very very very very long text",
Modifier.constrainAs(text) {
linkTo(guideline, parent.end)
width = Dimension.preferredWrapContent
}
)
}
}
プレビュー:
利用可能な Dimension
の動作は次のとおりです。
preferredWrapContent
- レイアウトは、そのサイズでの制約に従ったラップ コンテンツになります。wrapContent
- レイアウトは、制約で許可されない場合でもラップ コンテンツになります。fillToConstraints
- レイアウトは、そのサイズでの制約で定義されたスペースを埋めるように拡大します。preferredValue
- レイアウトは、そのサイズでの制約に従った固定 dp 値になります。value
- レイアウトは、そのサイズでの制約に関係なく固定 dp 値になります。
また、特定の Dimension
を強制することもできます。
width = Dimension.preferredWrapContent.atLeast(100.dp)
API の分離
ここまでの例では、制約をインラインで指定し、適用対象のコンポーザブルの修飾子を使用していました。しかし、適用対象のレイアウトから制約を分離しておくことが重要な場合があります。よくある例としては、画面構成に基づいて制約を簡単に変更する場合や、2 つの制約セット間でアニメーション化する場合などが挙げられます。
このような場合は、別の方法で ConstraintLayout
を使用できます。
ConstraintSet
を、ConstraintLayout
のパラメータとして渡します。ConstraintSet
で作成した参照を、layoutId
修飾子を使用してコンポーザブルに割り当てます。
この API の形を前述した最初の ConstraintLayout
の例に適用し、画面の幅について最適化すると、次のようになります。
@Composable
fun DecoupledConstraintLayout() {
BoxWithConstraints {
val constraints = if (maxWidth < maxHeight) {
decoupledConstraints(margin = 16.dp) // Portrait constraints
} else {
decoupledConstraints(margin = 32.dp) // Landscape constraints
}
ConstraintLayout(constraints) {
Button(
onClick = { /* Do something */ },
modifier = Modifier.layoutId("button")
) {
Text("Button")
}
Text("Text", Modifier.layoutId("text"))
}
}
}
private fun decoupledConstraints(margin: Dp): ConstraintSet {
return ConstraintSet {
val button = createRefFor("button")
val text = createRefFor("text")
constrain(button) {
top.linkTo(parent.top, margin= margin)
}
constrain(text) {
top.linkTo(button.bottom, margin)
}
}
}
11. Intrinsic
Compose には、子を 1 回しか測定できないというルールがあります。子を 2 回測定した場合、ランタイム例外がスローされます。しかし、測定する前に子の情報が必要になる場合もあります。
Intrinsic を使用すると、実際に測定する前に子をクエリできます。
コンポーザブルに対して、次のように intrinsicWidth
または intrinsicHeight
を要求できます。
(min|max)IntrinsicWidth
: ある高さの場合に、コンテンツを正しく描画できる最小 / 最大の幅。(min|max)IntrinsicHeight
: ある幅の場合に、コンテンツを正しく描画できる最小 / 最大の高さ。
たとえば、Text
の minIntrinsicHeight
を無限大の width
で要求した場合、テキストが 1 行で描画されているかのように、Text
の height
が返されます。
Intrinsic の動作
次のように、2 つのテキストを分割線で区切って画面上に表示するコンポーザブルを作成するとします。
これを実現するには、1 つの Row
に Text
を 2 つ入れて可能な限り広がるようにし、中央に Divider
を置きます。Divider の高さは最も高い Text
と同じにして、幅は狭くします(width = 1.dp
)。
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
Row(modifier = modifier) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp)
.wrapContentWidth(Alignment.Start),
text = text1
)
Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
Text(
modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
.wrapContentWidth(Alignment.End),
text = text2
)
}
}
@Preview
@Composable
fun TwoTextsPreview() {
LayoutsCodelabTheme {
Surface {
TwoTexts(text1 = "Hi", text2 = "there")
}
}
}
これをプレビューすると、想定とは異なり、分割線が画面全体に拡大されます。
これは、Row
がそれぞれの子を個別に測定し、Text
の高さを Divider
の制約に使用できないためです。Divider
が指定の高さで空きスペースを埋めるように設定する必要があります。そのためには、height(IntrinsicSize.Min)
修飾子を使用します。
height(IntrinsicSize.Min)
は、子の高さが Intrinsic の最小の高さと同じになるように強制します。この修飾子は再帰的であるため、Row
とその子の minIntrinsicHeight
をクエリします。
これを次のようにコードに適用すると、想定どおりに動作します。
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
Row(modifier = modifier.height(IntrinsicSize.Min)) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp)
.wrapContentWidth(Alignment.Start),
text = text1
)
Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
Text(
modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
.wrapContentWidth(Alignment.End),
text = text2
)
}
}
@Preview
@Composable
fun TwoTextsPreview() {
LayoutsCodelabTheme {
Surface {
TwoTexts(text1 = "Hi", text2 = "there")
}
}
}
プレビュー:
Row の minIntrinsicHeight
は、その子の最大 minIntrinsicHeight
になります。Divider の minIntrinsicHeight
は、制約がない場合はスペースを占有しないため 0 になります。Text の minIntrinsicHeight
は、特定の width
が指定されたテキストの minIntrinsicHeight になります。そのため、Row の height
制約が、Text
の最大 minIntrinsicHeight
になります。Divider
は自身の height
を、Row で指定された height
制約まで拡大します。
DIY
カスタム レイアウトを作成するときはいつでも、MeasurePolicy
インターフェースの (min|max)Intrinsic(Width|Height)
で intrinsic を計算する方法を変更できます。ただしほとんどの場合、デフォルトで十分です。
また、Modifier インターフェースの Density.(min|max)Intrinsic(Width|Height)Of
メソッドをオーバーライドする修飾子で intrinsic を変更できます。これもデフォルト値で間に合います。
12. 完了
これで、この Codelab は終了です。
Codelab の解答
この Codelab の解答コードは GitHub から入手できます。
$ git clone https://github.com/googlecodelabs/android-compose-codelabs
または、リポジトリを ZIP ファイルとしてダウンロードすることもできます。
次のステップ
Compose パスウェイに関する他の Codelab をご確認ください。
参考資料
サンプルアプリ
- カスタム レイアウトを作成する Owl
- グラフと表を示す Rally
- カスタム レイアウトの Jetsnack