LoginSignup
18
13

More than 1 year has passed since last update.

Jetpack ComposeのButton達を比較してみよう

Posted at

みなさんJetpack Compose使ってますでしょうか 導入しているrepositoryも多くなってきましたね
Composeで遊んでいる中で、Buttonとつくcomponentが多くどれを使うか迷うこともあったのでまとめることにします
xmlでButtonを使っていて軽くcomposeを触り始めたくらいの人に参考になれば嬉しいです

環境

kotlin: 1.5.10
compose: 1.0.0-beta09

:warning: 投稿時点では1.0.0-beta09のため参考にする時点では変更が入っている恐れがあります:warning:

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のためIconTextを並べて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を使ってはいませんね
size48dpで主に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はなく、またcolorsbackground/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/Height48dp (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ではこのようにselectedonClickにて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を送りましょう

Ref

18
13
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
18
13