mihon/app/src/main/java/eu/kanade/presentation/components/Button.kt

387 lines
15 KiB
Kotlin
Raw Normal View History

2022-06-25 11:03:48 -04:00
package eu.kanade.presentation.components
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
2022-06-25 11:03:48 -04:00
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.HoverInteraction
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.InteractionSource
2022-06-25 11:03:48 -04:00
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
2022-06-25 11:03:48 -04:00
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.ElevatedButton
2022-06-25 11:03:48 -04:00
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.TextButton
2022-06-25 11:03:48 -04:00
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateListOf
2022-06-25 11:03:48 -04:00
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
2022-06-25 11:03:48 -04:00
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
2022-06-25 11:03:48 -04:00
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
2022-06-25 11:03:48 -04:00
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.util.animateElevation
import androidx.compose.material3.ButtonDefaults as M3ButtonDefaults
2022-06-25 11:03:48 -04:00
/**
* TextButton with additional onLongClick functionality.
*
* @see androidx.compose.material3.TextButton
*/
2022-06-25 11:03:48 -04:00
@Composable
fun TextButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
onLongClick: (() -> Unit)? = null,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = null,
shape: Shape = M3ButtonDefaults.textShape,
2022-06-25 11:03:48 -04:00
border: BorderStroke? = null,
colors: ButtonColors = ButtonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.primary,
disabledContainerColor = Color.Transparent,
disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
),
contentPadding: PaddingValues = M3ButtonDefaults.TextButtonContentPadding,
2022-06-25 11:03:48 -04:00
content: @Composable RowScope.() -> Unit,
) =
Button(
onClick = onClick,
modifier = modifier,
onLongClick = onLongClick,
enabled = enabled,
interactionSource = interactionSource,
elevation = elevation,
shape = shape,
border = border,
colors = colors,
contentPadding = contentPadding,
content = content,
)
/**
* Button with additional onLongClick functionality.
*
* @see androidx.compose.material3.TextButton
*/
2022-06-25 11:03:48 -04:00
@Composable
fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
onLongClick: (() -> Unit)? = null,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
shape: Shape = M3ButtonDefaults.textShape,
2022-06-25 11:03:48 -04:00
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = M3ButtonDefaults.ContentPadding,
2022-06-25 11:03:48 -04:00
content: @Composable RowScope.() -> Unit,
) {
val containerColor = colors.containerColor(enabled).value
val contentColor = colors.contentColor(enabled).value
val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp
val tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp
Surface(
onClick = onClick,
modifier = modifier,
onLongClick = onLongClick,
shape = shape,
color = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
shadowElevation = shadowElevation,
border = border,
interactionSource = interactionSource,
enabled = enabled,
) {
CompositionLocalProvider(LocalContentColor provides contentColor) {
ProvideTextStyle(value = MaterialTheme.typography.labelLarge) {
Row(
Modifier.defaultMinSize(
minWidth = M3ButtonDefaults.MinWidth,
minHeight = M3ButtonDefaults.MinHeight,
2022-06-25 11:03:48 -04:00
)
.padding(contentPadding),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
content = content,
)
}
}
}
}
object ButtonDefaults {
/**
* Creates a [ButtonColors] that represents the default container and content colors used in a
* [Button].
*
* @param containerColor the container color of this [Button] when enabled.
* @param contentColor the content color of this [Button] when enabled.
* @param disabledContainerColor the container color of this [Button] when not enabled.
* @param disabledContentColor the content color of this [Button] when not enabled.
*/
@Composable
fun buttonColors(
containerColor: Color = MaterialTheme.colorScheme.primary,
contentColor: Color = MaterialTheme.colorScheme.onPrimary,
disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
): ButtonColors = ButtonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor,
)
/**
* Creates a [ButtonElevation] that will animate between the provided values according to the
* Material specification for a [Button].
*
* @param defaultElevation the elevation used when the [Button] is enabled, and has no other
* [Interaction]s.
* @param pressedElevation the elevation used when this [Button] is enabled and pressed.
* @param focusedElevation the elevation used when the [Button] is enabled and focused.
* @param hoveredElevation the elevation used when the [Button] is enabled and hovered.
* @param disabledElevation the elevation used when the [Button] is not enabled.
*/
@Composable
fun buttonElevation(
defaultElevation: Dp = 0.dp,
pressedElevation: Dp = 0.dp,
focusedElevation: Dp = 0.dp,
hoveredElevation: Dp = 1.dp,
disabledElevation: Dp = 0.dp,
): ButtonElevation = ButtonElevation(
defaultElevation = defaultElevation,
pressedElevation = pressedElevation,
focusedElevation = focusedElevation,
hoveredElevation = hoveredElevation,
disabledElevation = disabledElevation,
)
}
/**
* Represents the elevation for a button in different states.
*
* - See [M3ButtonDefaults.buttonElevation] for the default elevation used in a [Button].
* - See [M3ButtonDefaults.elevatedButtonElevation] for the default elevation used in a
* [ElevatedButton].
*/
@Stable
class ButtonElevation internal constructor(
private val defaultElevation: Dp,
private val pressedElevation: Dp,
private val focusedElevation: Dp,
private val hoveredElevation: Dp,
private val disabledElevation: Dp,
) {
/**
* Represents the tonal elevation used in a button, depending on its [enabled] state and
* [interactionSource]. This should typically be the same value as the [shadowElevation].
*
* Tonal elevation is used to apply a color shift to the surface to give the it higher emphasis.
* When surface's color is [ColorScheme.surface], a higher elevation will result in a darker
* color in light theme and lighter color in dark theme.
*
* See [shadowElevation] which controls the elevation of the shadow drawn around the button.
*
* @param enabled whether the button is enabled
* @param interactionSource the [InteractionSource] for this button
*/
@Composable
internal fun tonalElevation(enabled: Boolean, interactionSource: InteractionSource): State<Dp> {
return animateElevation(enabled = enabled, interactionSource = interactionSource)
}
/**
* Represents the shadow elevation used in a button, depending on its [enabled] state and
* [interactionSource]. This should typically be the same value as the [tonalElevation].
*
* Shadow elevation is used to apply a shadow around the button to give it higher emphasis.
*
* See [tonalElevation] which controls the elevation with a color shift to the surface.
*
* @param enabled whether the button is enabled
* @param interactionSource the [InteractionSource] for this button
*/
@Composable
internal fun shadowElevation(
enabled: Boolean,
interactionSource: InteractionSource,
): State<Dp> {
return animateElevation(enabled = enabled, interactionSource = interactionSource)
}
@Composable
private fun animateElevation(
enabled: Boolean,
interactionSource: InteractionSource,
): State<Dp> {
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is HoverInteraction.Enter -> {
interactions.add(interaction)
}
is HoverInteraction.Exit -> {
interactions.remove(interaction.enter)
}
is FocusInteraction.Focus -> {
interactions.add(interaction)
}
is FocusInteraction.Unfocus -> {
interactions.remove(interaction.focus)
}
is PressInteraction.Press -> {
interactions.add(interaction)
}
is PressInteraction.Release -> {
interactions.remove(interaction.press)
}
is PressInteraction.Cancel -> {
interactions.remove(interaction.press)
}
}
}
}
val interaction = interactions.lastOrNull()
val target =
if (!enabled) {
disabledElevation
} else {
when (interaction) {
is PressInteraction.Press -> pressedElevation
is HoverInteraction.Enter -> hoveredElevation
is FocusInteraction.Focus -> focusedElevation
else -> defaultElevation
}
}
val animatable = remember { Animatable(target, Dp.VectorConverter) }
if (!enabled) {
// No transition when moving to a disabled state
LaunchedEffect(target) { animatable.snapTo(target) }
} else {
LaunchedEffect(target) {
val lastInteraction = when (animatable.targetValue) {
pressedElevation -> PressInteraction.Press(Offset.Zero)
hoveredElevation -> HoverInteraction.Enter()
focusedElevation -> FocusInteraction.Focus()
else -> null
}
animatable.animateElevation(
from = lastInteraction,
to = interaction,
target = target,
)
}
}
return animatable.asState()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || other !is ButtonElevation) return false
if (defaultElevation != other.defaultElevation) return false
if (pressedElevation != other.pressedElevation) return false
if (focusedElevation != other.focusedElevation) return false
if (hoveredElevation != other.hoveredElevation) return false
if (disabledElevation != other.disabledElevation) return false
return true
}
override fun hashCode(): Int {
var result = defaultElevation.hashCode()
result = 31 * result + pressedElevation.hashCode()
result = 31 * result + focusedElevation.hashCode()
result = 31 * result + hoveredElevation.hashCode()
result = 31 * result + disabledElevation.hashCode()
return result
}
}
/**
* Represents the container and content colors used in a button in different states.
*
* - See [M3ButtonDefaults.buttonColors] for the default colors used in a [Button].
* - See [M3ButtonDefaults.elevatedButtonColors] for the default colors used in a [ElevatedButton].
* - See [M3ButtonDefaults.textButtonColors] for the default colors used in a [TextButton].
*/
@Immutable
class ButtonColors internal constructor(
private val containerColor: Color,
private val contentColor: Color,
private val disabledContainerColor: Color,
private val disabledContentColor: Color,
) {
/**
* Represents the container color for this button, depending on [enabled].
*
* @param enabled whether the button is enabled
*/
@Composable
internal fun containerColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor)
}
/**
* Represents the content color for this button, depending on [enabled].
*
* @param enabled whether the button is enabled
*/
@Composable
internal fun contentColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(if (enabled) contentColor else disabledContentColor)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || other !is ButtonColors) return false
if (containerColor != other.containerColor) return false
if (contentColor != other.contentColor) return false
if (disabledContainerColor != other.disabledContainerColor) return false
if (disabledContentColor != other.disabledContentColor) return false
return true
}
override fun hashCode(): Int {
var result = containerColor.hashCode()
result = 31 * result + contentColor.hashCode()
result = 31 * result + disabledContainerColor.hashCode()
result = 31 * result + disabledContentColor.hashCode()
return result
}
}