みなさんJetpack Compose
使ってますでしょうか 導入しているrepositoryも多くなってきましたね
Composeで遊んでいる中で、Buttonとつくcomponentが多くどれを使うか迷うこともあったのでまとめることにします
xmlでButtonを使っていて軽くcomposeを触り始めたくらいの人に参考になれば嬉しいです
環境
kotlin: 1.5.10
compose: 1.0.0-beta09
投稿時点では
1.0.0-beta09
のため参考にする時点では変更が入っている恐れがあります
Buttons
Button

Arguments
ごく普通のボタンです 最初からMaterial仕様なのがいいですね
argumentから以下が設定可能です
- onClick
- enabled
- interactionSource
- elevation
- shape
- border
- colors (enabled/disable等状態に応じてbackgroundなど色を変える)
- contentPadding
展開するとButtonのコードが見れます
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit
) {
val contentColor by colors.contentColor(enabled)
Surface(
modifier = modifier,
shape = shape,
color = colors.backgroundColor(enabled).value,
contentColor = contentColor.copy(alpha = 1f),
border = border,
elevation = elevation?.elevation(enabled, interactionSource)?.value ?: 0.dp,
onClick = onClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple()
) {
CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
ProvideTextStyle(
value = MaterialTheme.typography.button
) {
Row(
Modifier
.defaultMinSize(
minWidth = ButtonDefaults.MinWidth,
minHeight = ButtonDefaults.MinHeight
)
.padding(contentPadding),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
content = content
)
}
}
}
}
コードを見るとscopeがRow
のためIcon
とText
を並べてxmlでのdrawableStart
的な挙動をさせることができます
ButtonDefaults
実際すべて指定しなくてもdefaultを用意してくれています、それがButtonDefaultsです
(わざわざstyle/attrをjumpしてみなくてもよくなったので可読性上がったなと個人的には思ってます)
展開するとButtonDefaultsのコードの1部が見れます
object ButtonDefaults {
private val ButtonHorizontalPadding = 16.dp
private val ButtonVerticalPadding = 8.dp
/**
* The default content padding used by [Button]
*/
val ContentPadding = PaddingValues(
start = ButtonHorizontalPadding,
top = ButtonVerticalPadding,
end = ButtonHorizontalPadding,
bottom = ButtonVerticalPadding
)
/**
* The default min width applied for the [Button].
* Note that you can override it by applying Modifier.widthIn directly on [Button].
*/
val MinWidth = 64.dp
/**
* The default min width applied for the [Button].
* Note that you can override it by applying Modifier.heightIn directly on [Button].
*/
val MinHeight = 36.dp
/**
* The default size of the icon when used inside a [Button].
*
* @sample androidx.compose.material.samples.ButtonWithIconSample
*/
val IconSize = 18.dp
/**
* The default size of the spacing between an icon and a text when they used inside a [Button].
*
* @sample androidx.compose.material.samples.ButtonWithIconSample
*/
val IconSpacing = 8.dp
TextButton

展開するとTextButtonのコードが見れます
@Composable
fun TextButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = null,
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.textButtonColors(),
contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
content: @Composable RowScope.() -> Unit
) = Button(
onClick = onClick,
modifier = modifier,
enabled = enabled,
interactionSource = interactionSource,
elevation = elevation,
shape = shape,
border = border,
colors = colors,
contentPadding = contentPadding,
content = content
)
Buttonと違うのは、
- colorsがTextButton専用
- elevationがnull
- contentPaddingがTextButton専用
くらいでしょうか 公式ドキュメントには
Text buttons are typically used for less-pronounced actions, including those located in dialogs and cards. In cards, text buttons help maintain an emphasis on card content.
とあるので、主張はしないけれどButtonとして役割を持たせたい時に使えますね
OutlinedButton

展開するとOutlinedButtonのコードが見れます
@Composable
fun OutlinedButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = null,
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = ButtonDefaults.outlinedBorder,
colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit
) = Button(
onClick = onClick,
modifier = modifier,
enabled = enabled,
interactionSource = interactionSource,
elevation = elevation,
shape = shape,
border = border,
colors = colors,
contentPadding = contentPadding,
content = content
)
Buttonと違うのは、
- colorsがOutlinedButton専用
- borderがOutlinedButton専用
- elevationがnull
くらいでしょうか
Outlined buttons are medium-emphasis buttons. They contain actions that are important, but aren't the primary action in an app.
とあるのでButtonよりは目立たせたくないものに使いましょう
IconButton
文字通りIconに使われるButtonということですね
ただその場合ClickableにしたIconとは何が違うのでしょうか
コード及びdocを見てみると
展開するとIconButtonのコードが見れます
@Composable
fun IconButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable () -> Unit
) {
Box(
modifier = modifier
.clickable(
onClick = onClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = RippleRadius)
)
.then(IconButtonSizeModifier),
contentAlignment = Alignment.Center
) {
val contentAlpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
CompositionLocalProvider(LocalContentAlpha provides contentAlpha, content = content)
}
}
docにはさらに詳細な記述があります
IconButton is a clickable icon, used to represent actions. An IconButton has an overall minimum touch target size of 48 x 48dp, to meet accessibility guidelines. content is centered inside the IconButton.
This component is typically used inside an App Bar for the navigation icon / actions. See App Bar documentation for samples of this.
content should typically be an Icon, using an icon from androidx.compose.material.icons.Icons. If using a custom icon, note that the typical size for the internal icon is 24 x 24 dp.
今回はButton
を使ってはいませんね
size
が48dp
で主にAppbar(Toolbarなど)
, Navigation
に使うアイコンということですね
Box
など使いかぶせてサイズ自体を変えることはできますが中のIconButton自体を変える想定はしていないように見えます、rippleもちょうどアイコン分設定されています (private val RippleRadius = 24.dp
)
そのためカスタムして使うというよりはこのユースケース限定で使うという方がよさそうですね
IconToggleButton
文字通りIconButtonにToggleが加わったということですね
argumentにchecked, onCheckChange
があるのが特徴です
展開するとIconButtonのコードが見れます
@Composable
fun IconToggleButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable () -> Unit
) {
Box(
modifier = modifier.toggleable(
value = checked,
onValueChange = onCheckedChange,
enabled = enabled,
role = Role.Checkbox,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = RippleRadius)
).then(IconButtonSizeModifier),
contentAlignment = Alignment.Center
) {
val contentAlpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled
CompositionLocalProvider(LocalContentAlpha provides contentAlpha, content = content)
}
}
docをみると
An IconButton with two states, for icons that can be toggled 'on' and 'off', such as a bookmark icon, or a navigation icon that opens a drawer.
とあるので状態(ON/OFF)に応じてIconButtonを変更したい場合に大きく役立ちます
いいねボタンなどがいい例ですね
公式にあるSnippetがとてもわかりやすいです
import androidx.compose.animation.animateColorAsState
import androidx.compose.material.Icon
import androidx.compose.material.IconToggleButton
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
var checked by remember { mutableStateOf(false) }
IconToggleButton(checked = checked, onCheckedChange = { checked = it }) {
val tint by animateColorAsState(if (checked) Color(0xFFEC407A) else Color(0xFFB0BEC5))
Icon(Icons.Filled.Favorite, contentDescription = "Localized description", tint = tint)
}
FloatingActionButton

みなさんご存知FABです 特にxml時代から変わったところもありませんね
Button
とは違いenabled
はなく、またcolors
もbackground/content
のcolorをそれぞれ指定する形に変わっています
elevation
は専用のものがdefaultで指定されています
content
部分にはText
も使えますが、大体Icon
を指定することになるでしょう
ちなみにsize
のdefaultは56dp
です
展開するとFloatingActionButtonのコードが見れます
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun FloatingActionButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
backgroundColor: Color = MaterialTheme.colors.secondary,
contentColor: Color = contentColorFor(backgroundColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
content: @Composable () -> Unit
) {
Surface(
modifier = modifier,
shape = shape,
color = backgroundColor,
contentColor = contentColor,
elevation = elevation.elevation(interactionSource).value,
onClick = onClick,
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple()
) {
CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
ProvideTextStyle(MaterialTheme.typography.button) {
Box(
modifier = Modifier
.defaultMinSize(minWidth = FabSize, minHeight = FabSize),
contentAlignment = Alignment.Center
) { content() }
}
}
}
}
ExtendedFloatingActionButton

普通のFABとは違い
-
minWidth/Height
は48dp
(FABは56dp
) - 横長も想定 (Extended)
-
Text
も想定されている -
Icon
はnullableのためText
だけも可能
のためIconやTextを使ってメインのアクションをさせたいときに使いましょう
展開するとExtendedFloatingActionButtonのコードが見れます
@Composable
fun ExtendedFloatingActionButton(
text: @Composable () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
icon: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
backgroundColor: Color = MaterialTheme.colors.secondary,
contentColor: Color = contentColorFor(backgroundColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation()
) {
FloatingActionButton(
modifier = modifier.sizeIn(
minWidth = ExtendedFabSize,
minHeight = ExtendedFabSize
),
onClick = onClick,
interactionSource = interactionSource,
shape = shape,
backgroundColor = backgroundColor,
contentColor = contentColor,
elevation = elevation
) {
Box(
modifier = Modifier.padding(
start = ExtendedFabTextPadding,
end = ExtendedFabTextPadding
),
contentAlignment = Alignment.Center
) {
if (icon == null) {
text()
} else {
Row(verticalAlignment = Alignment.CenterVertically) {
icon()
Spacer(Modifier.width(ExtendedFabIconPadding))
text()
}
}
}
}
}
RadioButton

おなじみRadioButtonです
コードを見てみるとdotとcanvasでシンプルに構成されているのが面白いですね
展開するとExtendedFloatingActionButtonのコードが見れます
@Composable
fun RadioButton(
selected: Boolean,
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: RadioButtonColors = RadioButtonDefaults.colors()
) {
val dotRadius by animateDpAsState(
targetValue = if (selected) RadioButtonDotSize / 2 else 0.dp,
animationSpec = tween(durationMillis = RadioAnimationDuration)
)
val radioColor by colors.radioColor(enabled, selected)
val selectableModifier =
if (onClick != null) {
Modifier.selectable(
selected = selected,
onClick = onClick,
enabled = enabled,
role = Role.RadioButton,
interactionSource = interactionSource,
indication = rememberRipple(
bounded = false,
radius = RadioButtonRippleRadius
)
)
} else {
Modifier
}
Canvas(
modifier
.then(selectableModifier)
.wrapContentSize(Alignment.Center)
.padding(RadioButtonPadding)
.requiredSize(RadioButtonSize)
) {
drawRadio(radioColor, dotRadius)
}
}
公式にあるこのsnippetが面白いのですが、Composeではこのようにselected
とonClick
にてstate
をわざわざsetすることがほとんどです
これはComposeが生まれた思想の中に、これまでのAndroidのView自体が中にstateを持ってしまっていてそれによってViewを変えていることが多く(Spinner/CheckBoxなど)、現状のMVVMはじめAndroidの設計と噛み合わないところも出てきているため今後はこうなっていくのが一般的なようですね
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material.RadioButton
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
// We have two radio buttons and only one can be selected
var state by remember { mutableStateOf(true) }
// Note that Modifier.selectableGroup() is essential to ensure correct accessibility behavior
Row(Modifier.selectableGroup()) {
RadioButton(
selected = state,
onClick = { state = true }
)
RadioButton(
selected = !state,
onClick = { state = false }
)
}
なぜこのような思想にしているのか、これまでのAndroid ViewとJetPack Composeは何が違うのかについては
この動画がとても面白いので見ることをお勧めします
まとめ
今回はJetPack ComposeにあるButtonの違いをコード交えながら見てきました
個人的にはIconButtonが印象的です
要所要所で使い分けて良いCompose Lifeを送りましょう