Jetpack composeは、アプリ開発に必要な一通りのUIコンポーネントをライブラリで提供しています。
そのライブラリ中のRadioButtonについて、構成や使用方法などをまとめます。
※環境:Android Studio Hedgehog | 2023.1.1
Kotlin 1.8.10
Compose Compiler 1.4.3
UIの概要
RadioButtonはラジオボタンのボタン部分を表現するUI要素です。
ラジオボタンは2つ以上の項目から一つを選択するボタンです。常に一つだけが選択された状態になる特徴があります。
現在と別のボタンを選択すれば、旧ボタンは解除され、新ボタンが選択された状態になります。
ラジオボタンとしての動作を実現するために、RadioButtonは「2つ以上のボタン」と「ボタン間のセレクト処理」の実装が必要です。
また、RadioButtonはボタン部分のみなので、項目名(アイコン、画像なども可能)を表示するなど、ユーザに対する説明の配慮も必要です。
関数の引数
RadioButtonの状態(コンテンツ、装飾、演出など)は、引数により指定できます。
@Composable fun RadioButton( selected: Boolean, onClick: (() -> Unit)?, modifier: Modifier = Modifier, enabled: Boolean = true, colors: RadioButtonColors = RadioButtonDefaults.colors(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { ... }
引数 | 概要 | |
---|---|---|
selected | Boolean | ボタンの状態 true:選択/false:非選択 |
onClick | ( )->Unit? | クリックで実行する関数オブジェクト(ラムダ式) |
modifier | Modifier | UI全般のCompose修飾子 |
enabled | Boolean | UIの有効:true/無効:false ※有効⇒クリック可能、無効⇒クリック不可 |
colors | RadioButtonColors | 選択あり/なし(selected)、有効/無効(enabled)に合わせた色の情報 |
interactionSource | MutableInteractionSource | ユーザが行ったUI操作の観測と報告 |
関数の構成
RadioButtonは下位階層にCanvas(Composable関数)を一つ持つ構成になっています。
このCanvasの引数onDrawに指定されるラムダ式で、ボタンの図形が描画(drawCircle)されています。
onDrawはViewシステム(前UIシステム)と同じ機能を有しますが、Canvasクラスのインスタンスが引数では無く、レシーバー(this)で与えられる点が異なります。
また、RadioButtonはボタンの状態(選択/非選択)を保持していません。入力された状態(引数selected)に合わせて、ボタンの形状と配色を変えているだけです。
選択されているボタンの管理はプログラマ側で実装します(後述、セレクト処理を参照)。
@Composable fun RadioButton( ... ) { val dotRadius = animateDpAsState( targetValue = if (selected) RadioButtonDotSize / 2 else 0.dp, animationSpec = tween(durationMillis = RadioAnimationDuration) ) val radioColor = 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 = RadioButtonTokens.StateLayerSize / 2 ) ) } else { Modifier } Canvas( modifier .then( if (onClick != null) { Modifier.minimumInteractiveComponentSize() } else { Modifier } ) .then(selectableModifier) .wrapContentSize(Alignment.Center) .padding(RadioButtonPadding) .requiredSize(RadioButtonTokens.IconSize) ) { // Draw the radio button val strokeWidth = RadioStrokeWidth.toPx() drawCircle( radioColor.value, radius = (RadioButtonTokens.IconSize / 2).toPx() - strokeWidth / 2, style = Stroke(strokeWidth) ) if (dotRadius.value > 0.dp) { drawCircle(radioColor.value, dotRadius.value.toPx() - strokeWidth / 2, style = Fill) } } }
@Composable fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) = Spacer(modifier.drawBehind(onDraw))
セレクト処理
セレクト処理を実装しないと、ラジオボタンとして動作しません。
@Composable fun RadioButtonPanel(labels: Array<String>) { val _current = remember { mutableStateOf(labels.first()) } Column { labels.forEach { // itはforEachの要素、項目名 Row(verticalAlignment = Alignment.CenterVertically) { RadioButton( selected = _current.value == it, // 排他的な処理(2) onClick = { _current.value = it } // 排他的な処理(1) ) Text(text = it, modifier = Modifier.fillMaxWidth()) } } } }
セレクト処理は2つのステップから成り立ちます。
排他的な処理(1)ボタンのクリックで発生するイベントにより、onClick(ラムダ式)が実行されます。ここで、新しい状態を_currentへ保存します。これにり、関数RadioButtonPanelは再Composeの対象になります。
排他的な処理(2)関数RadioButtonPanelが再Composeされます。実行されるRadioButtonにおいて、新しい状態と自分を比較し、選択⇔非選択(selectedのfalse/true)を切り替えます。
private val labels = arrayOf("item0", "item1", "item2") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyApplicationTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { RadioButtonPanel(labels = labels) } } } }
サンプル:設定を行うActivity
RadioButtonはユーザがアプリの設定を行うような場面で多用されます。
サンプルはアプリの設定を行うActivityです。
データクラスRadioButtonDatasetを介して、ロジックからUIへ項目名を渡し、逆にUIからロジックへ選択の結果を返しています。
@Stable data class RadioButtonDataset(val labels: Array<String>) { var selectedValue: String? = null } @Composable fun RadioButtonPanel(dataset: RadioButtonDataset) { val _current = remember { mutableStateOf(dataset.selectedValue) } Column { dataset.labels.forEach { // itはforEachの要素 Row(verticalAlignment = Alignment.CenterVertically) { RadioButton( selected = _current.value == it, // 排他的な処理 onClick = { _current.value = it // 排他的な処理 dataset.selectedValue = it // ロジックへ返す } ) Text(text = it, modifier = Modifier.fillMaxWidth()) } } } }
ちなみに、UIツリーの描画処理(再Composeなど)は「onResume終了~onPause開始」の区間に行われます。
サンプルは、描画処理の前後のタイミングを利用して、onStartでボタンの初期状態を設定し、onStopで取得した選択値を処理しています。
※描画処理については「Jetpack Compose:Composeによるアプリ画面の描画」を参照
class ConfigurationActivity : ComponentActivity() { private val rbn012 = RadioButtonDataset(arrayOf("item0", "item1", "item2")) private val rbnABC = RadioButtonDataset(arrayOf("itemA", "itemB", "itemC")) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyApplicationTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { // Configの画面を定義 Column { RadioButtonPanel(dataset = rbn012) // 動作設定1 Divider(color = Color.LightGray) RadioButtonPanel(dataset = rbnABC) // 動作設定2 Divider(color = Color.LightGray) } } } } } override fun onStart() { super.onStart() // ラジオボタンの初期値を設定 rbn012.selectedValue = rbn012.labels.last() // ※ABC側は初期値なし } /* ** onResumeの実行後、UIツリーの描画処理が開始される */ override fun onStop() { super.onStop() // ラジオボタンの選択値を取得 val _value012 = rbn012.selectedValue val _index012 = rbn012.labels.indexOf(_value012) val _valueABC = rbnABC.selectedValue val _indexABC = rbnABC.labels.indexOf(_valueABC) // 選択値に応じた処理 // ~ここから~ ... // ~ここまで~ } }
関連記事: