0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Material 3 Expressive実装例 : Split button

Last updated at Posted at 2025-06-11

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 のスタイルが用意されています。

実装

SplitButtonLayout

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

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

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(...)
        ...
    }
}

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

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

// 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 },
            ...
        ) { ... }
    }
)

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

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

// 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 と同様
)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?