Material 3 Expressive実装例 : Toolbar

Material 3 Expressive の実装例の紹介シリーズです。
今回は大きく変更された Toolbar を紹介します。
(`androidx.compose.material3:material3:1.4.0-alpha15` 時点での内容になります。)

## Material 3 Expressive での変更点
- Docked toolbar と Floating toolbar の 2 種類
  - Material 3 Expressive では Bottom app bar よりも Docked toolbar を使うことが推奨される
- スタイルが Standard と Vibrant の 2 パターン
- Floating toolbar には Horizontal と Vertical の向きのパターンがある


https://m3.material.io/components/toolbars/overview


## 実装

### Docked toolbar

Docked toolbar という名前ではなく現時点では `FlexibleBottomAppBar` を使った実装になります。

```kotlin
FlexibleBottomAppBar(
    horizontalArrangement = BottomAppBarDefaults.FlexibleFixedHorizontalArrangement,
    content = {
        IconButton(onClick = { }) {
            Icon(...)
        }
        IconButton(onClick = { }) {
            Icon(...)
        }
        FilledIconButton(
            onClick = { },
            modifier = Modifier.size(IconButtonDefaults.smallContainerSize()),
            shape = IconButtonDefaults.smallSquareShape
        ) {
            Icon(
                painterResource(R.drawable.ic_add),
                contentDescription = "Add",
                modifier = Modifier.size(IconButtonDefaults.smallIconSize)
            )
        }
        IconButton(onClick = { }) {
            Icon(...)
        }
        IconButton(onClick = { /* doSomething() */ }) {
            Icon(...)
        }
    }
)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/d80a796d-6228-47dc-9879-2bd2b9b17c23.png" width="300px" />

また、`horizontalArrangement` を `BottomAppBarDefaults.FlexibleFixedHorizontalArrangement` / `BottomAppBarDefaults.FlexibleHorizontalArrangement` に切り替えることで大画面でのアイテムの並び方が変わります。

|FlexibleFixedHorizontalArrangement|FlexibleHorizontalArrangement|
|:--:|:--:|
|![Screenshot_20250610_180804.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/edcf9fe4-9d1b-48f6-8f13-90d55b2f364c.png) | ![Screenshot_20250610_180834.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/4b46c41f-fe9a-4656-84d3-0d4f16123853.png)|

### Floating toolbar

Floating toolbar の実装には `HorizontalFloatingToolbar` を使います。`VerticalFloatingToolbar` に置き換えるだけで Vertical toolbar に切り替えが可能です。

```kotlin
// Horizontal
HorizontalFloatingToolbar(
    modifier = Modifier
        .align(Alignment.BottomCenter)
        .offset(y = -FloatingToolbarDefaults.ScreenOffset),
    expanded = expanded,
    leadingContent = {
        IconButton(onClick = { }) {
            Icon(...)
        }
        IconButton(onClick = { }) {
            Icon(...)
        }
    },
    trailingContent = {
        IconButton(onClick = { }) {
            Icon(...)
        }
        IconButton(onClick = { }) {
            Icon(...)
        }
    },
    content = {
        FilledIconButton(
            onClick = { },
            modifier = Modifier.size(
                IconButtonDefaults.smallContainerSize(
                    IconButtonDefaults.IconButtonWidthOption.Wide
                )
            ),
        ) {
            Icon(
                painterResource(R.drawable.ic_add),
                contentDescription = "Add",
                modifier = Modifier.size(IconButtonDefaults.smallIconSize)
            )
        }
    }
)

// Vertical
VerticalFloatingToolbar(
    modifier = Modifier
        .align(Alignment.CenterEnd)
        .offset(x = -FloatingToolbarDefaults.ScreenOffset),
    expanded = expanded,
    content = {
        IconButton(onClick = { }) {
            Icon(...)
        }
        IconButton(onClick = { }) {
            Icon(...)
        }
        ...
    }
)
```

|Horizontal|Vertical|
|:--:|:--:|
| ![Screenshot_20250610_182046.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/ff3daf7c-afa2-4a91-8e05-bbc1d3820478.png)| ![Screenshot_20250610_182714.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/5b5a193c-065a-415a-a96d-b080b19d6894.png)|


### Vibrant のスタイル

Vibrant のスタイルは `FloatingToolbarDefaults.vibrantFloatingToolbarColors()` が標準で用意されています。

```kotlin
HorizontalFloatingToolbar(
    modifier = Modifier
        .align(Alignment.BottomCenter)
        .offset(y = -FloatingToolbarDefaults.ScreenOffset),
    colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors(),
    leadingContent = {
        ...
    },
    trailingContent = {
        ...
    },
    content = {
        ...
    }
)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/42eb0c5f-a0e6-496f-8ae1-b1fcd6273b81.png" width="300px" />


### スクロールとの連動

`Modifier#floatingToolbarVerticalNestedScroll` を使うことでスクロールに合わせて Floating toolbar を折りたたむことができます。

```kotlin
var expanded by rememberSaveable { mutableStateOf(true) }
Box(
    modifier = Modifier.floatingToolbarVerticalNestedScroll(
        expanded = expanded,
        onExpand = { expanded = true },
        onCollapse = { expanded = false },
    ),
) {
    LazyColumn { ... }

    HorizontalFloatingToolbar(
        modifier = Modifier
            .align(Alignment.BottomCenter)
            .offset(y = -FloatingToolbarDefaults.ScreenOffset),
        expanded = expanded,
        leadingContent = {
            IconButton(onClick = { }) {
                Icon(...)
            }
            ...
        },
        trailingContent = {
            IconButton(onClick = { }) {
                Icon(...)
            }
            ...
        },
        content = {
            FilledIconButton(
                onClick = { },
                modifier = Modifier.size(
                    IconButtonDefaults.smallContainerSize(
                        IconButtonDefaults.IconButtonWidthOption.Wide
                    )
                ),
            ) {
                Icon(
                    painterResource(R.drawable.ic_add),
                    contentDescription = "Add",
                    modifier = Modifier.size(IconButtonDefaults.smallIconSize)
                )
            }
        }
    )
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/87976bde-ff74-44a1-b62c-c92859e8d853.gif" width="300px" />


別パターンで `FloatingToolbarDefaults.exitAlwaysScrollBehavior` を使うことでスクロールに連動して Floating toolbar を画面外へアニメーションさせることも可能となっています。

```kotlin
val exitAlwaysScrollBehavior = FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = Bottom)
Box(
    modifier = Modifier.nestedScroll(exitAlwaysScrollBehavior),
) {
    LazyColumn { ... }

    HorizontalFloatingToolbar(
        modifier = Modifier
            .align(Alignment.BottomCenter)
            .offset(y = -FloatingToolbarDefaults.ScreenOffset),
        expanded = true,
        scrollBehavior = exitAlwaysScrollBehavior,
        leadingContent = {
            IconButton(onClick = { }) {
                Icon(...)
            }
            ...
        },
        trailingContent = {
            IconButton(onClick = { }) {
                Icon(...)
            }
            ...
        },
        content = {
            FilledIconButton(
                onClick = { },
                modifier = Modifier.size(
                    IconButtonDefaults.smallContainerSize(
                        IconButtonDefaults.IconButtonWidthOption.Wide
                    )
                ),
            ) {
                Icon(
                    painterResource(R.drawable.ic_add),
                    contentDescription = "Add",
                    modifier = Modifier.size(IconButtonDefaults.smallIconSize)
                )
            }
        }
    )
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/6f4a2d10-30ec-4827-a4d8-f7f9b5d3b8d7.gif" width="300px" />



### FAB との組み合わせ

Floating toolbar には FAB との連携が標準で用意されており、Standard と Vibrant それぞれのスタイルがあります。
Standard には `FloatingToolbarDefaults.StandardFloatingActionButton` を、Vibrant には `FloatingToolbarDefaults.VibrantFloatingActionButton` を使用します。

```kotlin
// Standard
HorizontalFloatingToolbar(
    expanded = true,
    floatingActionButton = {
        FloatingToolbarDefaults.StandardFloatingActionButton(
            onClick = { },
        ) {
            Icon(
                painterResource(R.drawable.ic_add),
                contentDescription = "Add"
            )
        }
    },
    content = {
        IconButton(onClick = { }) {
            Icon(...)
        }
        ...
    }
)

// Vibrant
HorizontalFloatingToolbar(
    expanded = true,
    colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors(),
    floatingActionButton = {
        FloatingToolbarDefaults.VibrantFloatingActionButton(
            onClick = { },
        ) {
            Icon(
                painterResource(R.drawable.ic_add),
                contentDescription = "Add"
            )
        }
    },
    content = {
        IconButton(onClick = { }) {
            Icon(...)
        }
        ...
    }
)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/b95bfffb-213c-42a0-a12b-c1df8f4ffa41.png" width="300px" />

FAB との組み合わせにおいてもコンテンツのスクロールと連携する機能が標準で用意されています。
先ほどの `FloatingToolbarDefaults.exitAlwaysScrollBehavior` は FAB がある状態でも機能します。

```kotlin
val exitAlwaysScrollBehavior = FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = Bottom)
Box(
    modifier = Modifier.nestedScroll(exitAlwaysScrollBehavior),
) {
    LazyColumn { ... }

    HorizontalFloatingToolbar(
        modifier = Modifier
            .align(Alignment.BottomCenter)
            .offset(y = -FloatingToolbarDefaults.ScreenOffset),
        expanded = true,
        scrollBehavior = exitAlwaysScrollBehavior,
        floatingActionButton = {
            FloatingToolbarDefaults.StandardFloatingActionButton(
                onClick = { },
            ) {
                Icon(
                    painterResource(R.drawable.ic_add),
                    contentDescription = "Add"
                )
            }
        },
        content = {
            IconButton(onClick = { }) {
                Icon(...)
            }
            ...
        }
    )
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/c735b668-0700-4fdd-8e02-7c40e2d8213b.gif" width="300px" />




スクロールに合わせて FAB に Floating toolbar が折りたたまれるという別パターンの場合は `floatingToolbarVerticalNestedScroll` を使用して次のような実装になります。

```kotlin
var expanded by rememberSaveable { mutableStateOf(true) }
Box(
    modifier = Modifier.floatingToolbarVerticalNestedScroll(
        expanded = expanded,
        onExpand = { expanded = true },
        onCollapse = { expanded = false },
    ),
) {
    LazyColumn { ... }

    HorizontalFloatingToolbar(
        modifier = Modifier
            .align(Alignment.BottomEnd)
            .offset(x = -FloatingToolbarDefaults.ScreenOffset, y = -FloatingToolbarDefaults.ScreenOffset),
        expanded = expanded,
        floatingActionButton = {
            FloatingToolbarDefaults.StandardFloatingActionButton(
                onClick = { },
            ) {
                Icon(
                    painterResource(R.drawable.ic_add),
                    contentDescription = "Add"
                )
            }
        },
        content = {
            IconButton(onClick = { }) {
                Icon(...)
            }
            ...
        }
    )
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/d63d4c5c-01c3-4358-9cf2-ffc88c4cb55b.gif" width="300px" />