Material 3 Expressive実装例 : Split button

Material 3 Expressive の実装例の紹介シリーズです。
今回は新たに追加されたコンポーネントである Split button を紹介します。
(`androidx.compose.material3:material3:1.4.0-alpha15` 時点での内容になります。)

## Split button とは

ボタンのアクションに関連するオプションを提供するコンポーネントです。
Button にあわせて Extra small、Small、Medium、Large、Extra large の 5 つのサイズと、Elevated、Filled、Tonal、Outlined のスタイルが用意されています。

https://m3.material.io/components/split-button/overview

## 実装

### `SplitButtonLayout`

`SplitButtonLayout` を使用し、Button 系は `SplitButtonDefaults` にある `SplitButtonDefaults.LeadingButton` と `SplitButtonDefaults.TrailingButton` を使います。
`TrailingButton` のアイコンの回転や Menu の表示は実装する必要があります。

::: note info
`TrailingButton` のアイコンは自由に設定できる形にはなっていますが、展開と折りたたみのアイコンを常に使用し、選択時に回転するようにガイドラインに記載があります。
https://m3.material.io/components/split-button/guidelines#b535fb7d-62ea-478b-a815-34567f22c5ae
:::

```kotlin
Box {
    var checked by remember { mutableStateOf(false) }
    SplitButtonLayout(
        leadingButton = {
            SplitButtonDefaults.LeadingButton(
                onClick = { },
            ) {
                Icon(
                    painter = painterResource(R.drawable.ic_edit),
                    modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
                    contentDescription = null,
                )
                Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                Text("Label")
            }
        },
        trailingButton = {
            SplitButtonDefaults.TrailingButton(
                checked = checked,
                onCheckedChange = { checked = it },
                modifier = Modifier.semantics {
                    stateDescription = if (checked) "Expanded" else "Collapsed"
                    contentDescription = "Toggle Button"
                },
            ) {
                val rotation: Float by
                animateFloatAsState(
                    targetValue = if (checked) 180f else 0f,
                    label = "Trailing Icon Rotation"
                )
                Icon(
                    painter = painterResource(R.drawable.ic_keyboard_arrow_down),
                    modifier = Modifier
                        .size(SplitButtonDefaults.TrailingIconSize)
                        .graphicsLayer {
                            this.rotationZ = rotation
                        },
                    contentDescription = null
                )
            }
        }
    )

    DropdownMenu(
        expanded = checked, 
        onDismissRequest = { checked = false }
    ) {
        DropdownMenuItem(...)
        ...
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/c5c8c7a2-4d40-46a8-8fbd-532bd8d2f61f.gif" width="300px" />



### Elevated、Filled、Tonal、Outlined のスタイル

`SplitButtonDefaults` にそれぞれのスタイルが標準で用意されています。

```kotlin
// Elevated
SplitButtonLayout(
    leadingButton = {
        SplitButtonDefaults.ElevatedLeadingButton(
            onClick = { },
        ) { ... }
    },
    trailingButton = {
        SplitButtonDefaults.ElevatedTrailingButton(
            checked = checked,
            onCheckedChange = { checked = it },
            ...
        ) { ... }
    }
)

// Filled
SplitButtonLayout(
    leadingButton = {
        SplitButtonDefaults.LeadingButton(
            onClick = { },
        ) { ... }
    },
    trailingButton = {
        SplitButtonDefaults.TrailingButton(
            checked = checked,
            onCheckedChange = { checked = it },
            ...
        ) { ... }
    }
)

// Tonal
SplitButtonLayout(
    leadingButton = {
        SplitButtonDefaults.TonalLeadingButton(
            onClick = { },
        ) { ... }
    },
    trailingButton = {
        SplitButtonDefaults.TonalTrailingButton(
            checked = checked,
            onCheckedChange = { checked = it },
            ...
        ) { ... }
    }
)

// Outlined
SplitButtonLayout(
    leadingButton = {
        SplitButtonDefaults.OutlinedLeadingButton(
            onClick = { },
        ) { ... }
    },
    trailingButton = {
        SplitButtonDefaults.OutlinedTrailingButton(
            checked = checked,
            onCheckedChange = { checked = it },
            ...
        ) { ... }
    }
)
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/23392898-9211-4414-bda1-926cce092773.png" width ="300px" />

### Extra small、Small、Medium、Large、Extra large のサイズ

デフォルトでない Small 以外の Button の高さが Button 同様に Split button でも標準で用意されており、アイコンのサイズや Padding 等が高さによって自動で適切な値になるように実装する形になります。

```kotlin
// Extra small
val buttonHeight = SplitButtonDefaults.ExtraSmallContainerHeight
SplitButtonLayout(
    leadingButton = {
        SplitButtonDefaults.LeadingButton(
            onClick = { },
            modifier = Modifier.heightIn(buttonHeight),
            shapes = SplitButtonDefaults.leadingButtonShapesFor(buttonHeight),
            contentPadding = SplitButtonDefaults.leadingButtonContentPaddingFor(buttonHeight),
        ) {
            Icon(
                painter = painterResource(R.drawable.ic_edit),
                modifier = Modifier
                    .size(SplitButtonDefaults.leadingButtonIconSizeFor(buttonHeight)),
                contentDescription = null,
            )
            Spacer(Modifier.size(ButtonDefaults.iconSpacingFor(buttonHeight)))
            Text(
                text = "Label",
                style = ButtonDefaults.textStyleFor(buttonHeight)
            )
        }
    },
    trailingButton = {
        SplitButtonDefaults.TrailingButton(
            checked = checked,
            onCheckedChange = { checked = it },
            modifier = Modifier
                .heightIn(buttonHeight),
            shapes = SplitButtonDefaults.trailingButtonShapesFor(buttonHeight),
            contentPadding = SplitButtonDefaults.trailingButtonContentPaddingFor(buttonHeight),
        ) {
            val rotation: Float by
            animateFloatAsState(
                targetValue = if (checked) 180f else 0f,
                label = "Trailing Icon Rotation"
            )
            Icon(
                painter = painterResource(R.drawable.ic_keyboard_arrow_down),
                modifier = Modifier
                    .size(SplitButtonDefaults.trailingButtonIconSizeFor(buttonHeight))
                    .graphicsLayer {
                        this.rotationZ = rotation
                    },
                contentDescription = null
            )
        }
    }
)

// Small(Default)
SplitButtonLayout(
    leadingButton = {
        SplitButtonDefaults.LeadingButton(
            onClick = { },
        ) {
            Icon(
                painter = painterResource(R.drawable.ic_edit),
                modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
                contentDescription = null,
            )
            Spacer(Modifier.size(ButtonDefaults.IconSpacing))
            Text("Label")
        }
    },
    trailingButton = {
        SplitButtonDefaults.TrailingButton(
            checked = checked,
            onCheckedChange = { checked = it },
            ...
        ) {
            val rotation: Float by
            animateFloatAsState(
                targetValue = if (checked) 180f else 0f,
                label = "Trailing Icon Rotation"
            )

            Icon(
                painter = painterResource(R.drawable.ic_keyboard_arrow_down),
                modifier = Modifier
                    .size(SplitButtonDefaults.TrailingIconSize)
                    .graphicsLayer {
                        this.rotationZ = rotation
                    },
                contentDescription = null
            )
        }
    }
)

// Medium
val buttonHeight = SplitButtonDefaults.MediumContainerHeight
SplitButtonLayout(
    ... // Extra small と同様
)

// Large
val buttonHeight = SplitButtonDefaults.LargeContainerHeight
SplitButtonLayout(
    ... // Extra small と同様
)

// Extra large
val buttonHeight = SplitButtonDefaults.ExtraLargeContainerHeight
SplitButtonLayout(
    ... // Extra small と同様
)
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/64cb1e80-f1b6-42a8-89a2-6e283a631a16.png" width="300px" />