[go: nahoru, domu]

blob: 17963d4142aabc9793d122c67455445b5c32a4af [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material3
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.tokens.RadioButtonTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
/**
* Material Design radio button.
*
* Radio buttons allow users to select one option from a set.
* @sample androidx.compose.material3.samples.RadioButtonSample
*
* [RadioButton]s can be combined together with [Text] in the desired layout (e.g. [Column] or
* [Row]) to achieve radio group-like behaviour, where the entire layout is selectable:
* @sample androidx.compose.material3.samples.RadioGroupSample
*
* @param selected whether this radio button is selected or not
* @param onClick callback to be invoked when the RadioButton is clicked. If null, then this
* RadioButton will not handle input events, and only act as a visual indicator of [selected] state
* @param modifier Modifier to be applied to the radio button
* @param enabled Controls the enabled state of the [RadioButton]. When `false`, this button will
* not be selectable and appears disabled
* @param interactionSource the [MutableInteractionSource] representing the stream of
* [Interaction]s for this RadioButton. You can create and pass in your own remembered
* [MutableInteractionSource] if you want to observe [Interaction]s and customize the
* appearance / behavior of this RadioButton in different [Interaction]s.
* @param colors [RadioButtonColors] that will be used to resolve the color used for this
* RadioButton in different states. See [RadioButtonDefaults.colors].
*/
@ExperimentalMaterial3Api
@Composable
fun RadioButton(
selected: Boolean,
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: RadioButtonColors = RadioButtonDefaults.colors()
) {
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.minimumTouchTargetSize()
} 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)
}
}
}
/**
* Represents the color used by a [RadioButton] in different states.
*
* See [RadioButtonDefaults.colors] for the default implementation that follows Material
* specifications.
*/
@Stable
interface RadioButtonColors {
/**
* Represents the main color used to draw the outer and inner circles, depending on whether
* the [RadioButton] is [enabled] / [selected].
*
* @param enabled whether the [RadioButton] is enabled
* @param selected whether the [RadioButton] is selected
*/
@Composable
fun radioColor(enabled: Boolean, selected: Boolean): State<Color>
}
/**
* Defaults used in [RadioButton].
*/
object RadioButtonDefaults {
/**
* Creates a [RadioButtonColors] that will animate between the provided colors according to
* the Material specification.
*
* @param selectedColor the color to use for the RadioButton when selected and enabled.
* @param unselectedColor the color to use for the RadioButton when unselected and enabled.
* @param disabledSelectedColor the color to use for the RadioButton when disabled and selected.
* @param disabledUnselectedColor the color to use for the RadioButton when disabled and not
* selected.
* @return the resulting [RadioButtonColors] used for the RadioButton
*/
@Composable
fun colors(
selectedColor: Color = RadioButtonTokens.SelectedIconColor.toColor(),
unselectedColor: Color = RadioButtonTokens.UnselectedIconColor.toColor(),
disabledSelectedColor: Color = RadioButtonTokens.DisabledSelectedIconColor
.toColor()
.copy(alpha = RadioButtonTokens.DisabledSelectedIconOpacity),
disabledUnselectedColor: Color = RadioButtonTokens.DisabledUnselectedIconColor
.toColor()
.copy(alpha = RadioButtonTokens.DisabledUnselectedIconOpacity)
): RadioButtonColors {
return remember(
selectedColor,
unselectedColor,
disabledSelectedColor,
disabledUnselectedColor
) {
DefaultRadioButtonColors(
selectedColor,
unselectedColor,
disabledSelectedColor,
disabledUnselectedColor
)
}
}
}
/**
* Default [RadioButtonColors] implementation.
*/
@Immutable
private class DefaultRadioButtonColors(
private val selectedColor: Color,
private val unselectedColor: Color,
private val disabledSelectedColor: Color,
private val disabledUnselectedColor: Color
) : RadioButtonColors {
@Composable
override fun radioColor(enabled: Boolean, selected: Boolean): State<Color> {
val target = when {
enabled && selected -> selectedColor
enabled && !selected -> unselectedColor
!enabled && selected -> disabledSelectedColor
else -> disabledUnselectedColor
}
// If not enabled 'snap' to the disabled state, as there should be no animations between
// enabled / disabled.
return if (enabled) {
animateColorAsState(target, tween(durationMillis = RadioAnimationDuration))
} else {
rememberUpdatedState(target)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as DefaultRadioButtonColors
if (selectedColor != other.selectedColor) return false
if (unselectedColor != other.unselectedColor) return false
if (disabledSelectedColor != other.disabledSelectedColor) return false
if (disabledUnselectedColor != other.disabledUnselectedColor) return false
return true
}
override fun hashCode(): Int {
var result = selectedColor.hashCode()
result = 31 * result + unselectedColor.hashCode()
result = 31 * result + disabledSelectedColor.hashCode()
result = 31 * result + disabledUnselectedColor.hashCode()
return result
}
}
private const val RadioAnimationDuration = 100
private val RadioButtonPadding = 2.dp
private val RadioButtonDotSize = 12.dp
private val RadioStrokeWidth = 2.dp