From 66cb3c03b89ea854972b8478ae77e4a4bb02300c Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Fri, 26 Aug 2022 20:19:47 +0700 Subject: [PATCH] Bump Compose M3 to 1.0.0-beta01 (#7867) (cherry picked from commit 655fa25b511a423bc17f1f88282150e335df0cd9) # Conflicts: # app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt --- .../kanade/presentation/components/AppBar.kt | 20 +- .../kanade/presentation/components/Button.kt | 296 ++++++++++- .../eu/kanade/presentation/components/Chip.kt | 468 +++++++++++++++++- .../presentation/components/IconButton.kt | 91 +++- .../library/components/LibraryToolbar.kt | 7 - .../manga/components/MangaAppBar.kt | 27 +- .../manga/components/NamespaceTags.kt | 21 +- .../eu/kanade/presentation/util/Elevation.kt | 125 +++++ gradle/compose.versions.toml | 2 +- 9 files changed, 995 insertions(+), 62 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/util/Elevation.kt diff --git a/app/src/main/java/eu/kanade/presentation/components/AppBar.kt b/app/src/main/java/eu/kanade/presentation/components/AppBar.kt index e36cde7c6..566607000 100644 --- a/app/src/main/java/eu/kanade/presentation/components/AppBar.kt +++ b/app/src/main/java/eu/kanade/presentation/components/AppBar.kt @@ -2,7 +2,8 @@ package eu.kanade.presentation.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.statusBars import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Close @@ -15,6 +16,7 @@ import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -22,12 +24,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.R @Composable @@ -97,14 +98,10 @@ fun AppBar( scrollBehavior: TopAppBarScrollBehavior? = null, ) { - val scrollFraction = if (isActionMode) 1f else scrollBehavior?.state?.overlappedFraction ?: 0f - val backgroundColor by TopAppBarDefaults.smallTopAppBarColors().containerColor(scrollFraction) - Column( - modifier = modifier.drawBehind { drawRect(backgroundColor) }, + modifier = modifier, ) { SmallTopAppBar( - modifier = Modifier.statusBarsPadding(), navigationIcon = { if (isActionMode) { IconButton(onClick = onCancelActionMode) { @@ -126,10 +123,11 @@ fun AppBar( }, title = titleContent, actions = actions, - // Background handled by parent + windowInsets = WindowInsets.statusBars, colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = Color.Transparent, - scrolledContainerColor = Color.Transparent, + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + elevation = if (isActionMode) 3.dp else 0.dp, + ), ), scrollBehavior = scrollBehavior, ) diff --git a/app/src/main/java/eu/kanade/presentation/components/Button.kt b/app/src/main/java/eu/kanade/presentation/components/Button.kt index e8edba9e6..2f261f9c4 100644 --- a/app/src/main/java/eu/kanade/presentation/components/Button.kt +++ b/app/src/main/java/eu/kanade/presentation/components/Button.kt @@ -1,26 +1,45 @@ package eu.kanade.presentation.components +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.VectorConverter 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 import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction 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.ButtonColors -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ButtonElevation +import androidx.compose.material3.Button +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.ElevatedButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.TextButton 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 import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import eu.kanade.presentation.util.animateElevation +import androidx.compose.material3.ButtonDefaults as M3ButtonDefaults @Composable fun TextButton( @@ -30,10 +49,15 @@ fun TextButton( enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = null, - shape: Shape = ButtonDefaults.textShape, + shape: Shape = M3ButtonDefaults.textShape, border: BorderStroke? = null, - colors: ButtonColors = ButtonDefaults.textButtonColors(), - contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding, + 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, content: @Composable RowScope.() -> Unit, ) = Button( @@ -58,10 +82,10 @@ fun Button( enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), - shape: Shape = ButtonDefaults.textShape, + shape: Shape = M3ButtonDefaults.textShape, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), - contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + contentPadding: PaddingValues = M3ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit, ) { val containerColor = colors.containerColor(enabled).value @@ -86,8 +110,8 @@ fun Button( ProvideTextStyle(value = MaterialTheme.typography.labelLarge) { Row( Modifier.defaultMinSize( - minWidth = ButtonDefaults.MinWidth, - minHeight = ButtonDefaults.MinHeight, + minWidth = M3ButtonDefaults.MinWidth, + minHeight = M3ButtonDefaults.MinHeight, ) .padding(contentPadding), horizontalArrangement = Arrangement.Center, @@ -98,3 +122,255 @@ fun Button( } } } + +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 { + 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 { + return animateElevation(enabled = enabled, interactionSource = interactionSource) + } + + @Composable + private fun animateElevation( + enabled: Boolean, + interactionSource: InteractionSource, + ): State { + val interactions = remember { mutableStateListOf() } + 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 { + 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 { + 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 + } +} diff --git a/app/src/main/java/eu/kanade/presentation/components/Chip.kt b/app/src/main/java/eu/kanade/presentation/components/Chip.kt index e4b0903d2..da7989e97 100644 --- a/app/src/main/java/eu/kanade/presentation/components/Chip.kt +++ b/app/src/main/java/eu/kanade/presentation/components/Chip.kt @@ -1,7 +1,15 @@ package eu.kanade.presentation.components +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.VectorConverter import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.FocusInteraction +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.Interaction +import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -9,25 +17,33 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material3.ChipBorder -import androidx.compose.material3.ChipColors -import androidx.compose.material3.ChipElevation +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.InputChipDefaults import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SuggestionChipDefaults +import androidx.compose.material3.SuggestionChip import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import eu.kanade.presentation.util.animateElevation +import androidx.compose.material3.SuggestionChipDefaults as SuggestionChipDefaultsM3 @ExperimentalMaterial3Api @Composable @@ -54,7 +70,7 @@ fun SuggestionChip( containerColor = colors.containerColor(enabled).value, tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp, shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp, - minHeight = SuggestionChipDefaults.Height, + minHeight = SuggestionChipDefaultsM3.Height, paddingValues = SuggestionChipPadding, shape = shape, border = border?.borderStroke(enabled)?.value, @@ -90,7 +106,7 @@ fun SuggestionChip( containerColor = colors.containerColor(enabled).value, tonalElevation = elevation?.tonalElevation(enabled, interactionSource)?.value ?: 0.dp, shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp, - minHeight = SuggestionChipDefaults.Height, + minHeight = SuggestionChipDefaultsM3.Height, paddingValues = SuggestionChipPadding, shape = shape, border = border?.borderStroke(enabled)?.value, @@ -236,6 +252,446 @@ private fun ChipContent( } } +/** + * Contains the baseline values used by [SuggestionChip]. + */ +@ExperimentalMaterial3Api +object SuggestionChipDefaults { + + /** + * Creates a [ChipColors] that represents the default container, label, and icon colors used in + * a flat [SuggestionChip]. + * + * @param containerColor the container color of this chip when enabled + * @param labelColor the label color of this chip when enabled + * @param iconContentColor the color of this chip's icon when enabled + * @param disabledContainerColor the container color of this chip when not enabled + * @param disabledLabelColor the label color of this chip when not enabled + * @param disabledIconContentColor the color of this chip's icon when not enabled + */ + @Composable + fun suggestionChipColors( + containerColor: Color = Color.Transparent, + labelColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + iconContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + disabledContainerColor: Color = Color.Transparent, + disabledLabelColor: Color = MaterialTheme.colorScheme.onSurface + .copy(alpha = 0.38f), + disabledIconContentColor: Color = MaterialTheme.colorScheme.onSurface + .copy(alpha = 0.38f), + ): ChipColors = ChipColors( + containerColor = containerColor, + labelColor = labelColor, + leadingIconContentColor = iconContentColor, + trailingIconContentColor = Color.Unspecified, + disabledContainerColor = disabledContainerColor, + disabledLabelColor = disabledLabelColor, + disabledLeadingIconContentColor = disabledIconContentColor, + disabledTrailingIconContentColor = Color.Unspecified, + ) + + /** + * Creates a [ChipElevation] that will animate between the provided values according to the + * Material specification for a flat [SuggestionChip]. + * + * @param defaultElevation the elevation used when the chip is has no other + * [Interaction]s + * @param pressedElevation the elevation used when the chip is pressed + * @param focusedElevation the elevation used when the chip is focused + * @param hoveredElevation the elevation used when the chip is hovered + * @param draggedElevation the elevation used when the chip is dragged + * @param disabledElevation the elevation used when the chip is not enabled + */ + @Composable + fun suggestionChipElevation( + defaultElevation: Dp = 0.0.dp, + pressedElevation: Dp = defaultElevation, + focusedElevation: Dp = defaultElevation, + hoveredElevation: Dp = defaultElevation, + draggedElevation: Dp = 8.0.dp, + disabledElevation: Dp = defaultElevation, + ): ChipElevation = ChipElevation( + defaultElevation = defaultElevation, + pressedElevation = pressedElevation, + focusedElevation = focusedElevation, + hoveredElevation = hoveredElevation, + draggedElevation = draggedElevation, + disabledElevation = disabledElevation, + ) + + /** + * Creates a [ChipBorder] that represents the default border used in a flat [SuggestionChip]. + * + * @param borderColor the border color of this chip when enabled + * @param disabledBorderColor the border color of this chip when not enabled + * @param borderWidth the border stroke width of this chip + */ + @Composable + fun suggestionChipBorder( + borderColor: Color = MaterialTheme.colorScheme.outline, + disabledBorderColor: Color = MaterialTheme.colorScheme.onSurface + .copy(alpha = 0.12f), + borderWidth: Dp = 1.0.dp, + ): ChipBorder = ChipBorder( + borderColor = borderColor, + disabledBorderColor = disabledBorderColor, + borderWidth = borderWidth, + ) + + /** + * Creates a [ChipColors] that represents the default container, label, and icon colors used in + * an elevated [SuggestionChip]. + * + * @param containerColor the container color of this chip when enabled + * @param labelColor the label color of this chip when enabled + * @param iconContentColor the color of this chip's icon when enabled + * @param disabledContainerColor the container color of this chip when not enabled + * @param disabledLabelColor the label color of this chip when not enabled + * @param disabledIconContentColor the color of this chip's icon when not enabled + */ + @Composable + fun elevatedSuggestionChipColors( + containerColor: Color = MaterialTheme.colorScheme.surface, + labelColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + iconContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface + .copy(alpha = 0.12f), + disabledLabelColor: Color = MaterialTheme.colorScheme.onSurface + .copy(alpha = 0.38f), + disabledIconContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + ): ChipColors = ChipColors( + containerColor = containerColor, + labelColor = labelColor, + leadingIconContentColor = iconContentColor, + trailingIconContentColor = Color.Unspecified, + disabledContainerColor = disabledContainerColor, + disabledLabelColor = disabledLabelColor, + disabledLeadingIconContentColor = disabledIconContentColor, + disabledTrailingIconContentColor = Color.Unspecified, + ) + + /** + * Creates a [ChipElevation] that will animate between the provided values according to the + * Material specification for an elevated [SuggestionChip]. + * + * @param defaultElevation the elevation used when the chip is has no other + * [Interaction]s + * @param pressedElevation the elevation used when the chip is pressed + * @param focusedElevation the elevation used when the chip is focused + * @param hoveredElevation the elevation used when the chip is hovered + * @param draggedElevation the elevation used when the chip is dragged + * @param disabledElevation the elevation used when the chip is not enabled + */ + @Composable + fun elevatedSuggestionChipElevation( + defaultElevation: Dp = 1.0.dp, + pressedElevation: Dp = 1.0.dp, + focusedElevation: Dp = 1.0.dp, + hoveredElevation: Dp = 3.0.dp, + draggedElevation: Dp = 8.0.dp, + disabledElevation: Dp = 0.0.dp, + ): ChipElevation = ChipElevation( + defaultElevation = defaultElevation, + pressedElevation = pressedElevation, + focusedElevation = focusedElevation, + hoveredElevation = hoveredElevation, + draggedElevation = draggedElevation, + disabledElevation = disabledElevation, + ) +} + +/** + * Represents the container and content colors used in a clickable chip in different states. + * + * See [AssistChipDefaults], [InputChipDefaults], and [SuggestionChipDefaults] for the default + * colors used in the various Chip configurations. + */ +@ExperimentalMaterial3Api +@Immutable +class ChipColors internal constructor( + private val containerColor: Color, + private val labelColor: Color, + private val leadingIconContentColor: Color, + private val trailingIconContentColor: Color, + private val disabledContainerColor: Color, + private val disabledLabelColor: Color, + private val disabledLeadingIconContentColor: Color, + private val disabledTrailingIconContentColor: Color, +) { + /** + * Represents the container color for this chip, depending on [enabled]. + * + * @param enabled whether the chip is enabled + */ + @Composable + internal fun containerColor(enabled: Boolean): State { + return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor) + } + + /** + * Represents the label color for this chip, depending on [enabled]. + * + * @param enabled whether the chip is enabled + */ + @Composable + internal fun labelColor(enabled: Boolean): State { + return rememberUpdatedState(if (enabled) labelColor else disabledLabelColor) + } + + /** + * Represents the leading icon's content color for this chip, depending on [enabled]. + * + * @param enabled whether the chip is enabled + */ + @Composable + internal fun leadingIconContentColor(enabled: Boolean): State { + return rememberUpdatedState( + if (enabled) leadingIconContentColor else disabledLeadingIconContentColor, + ) + } + + /** + * Represents the trailing icon's content color for this chip, depending on [enabled]. + * + * @param enabled whether the chip is enabled + */ + @Composable + internal fun trailingIconContentColor(enabled: Boolean): State { + return rememberUpdatedState( + if (enabled) trailingIconContentColor else disabledTrailingIconContentColor, + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other !is ChipColors) return false + + if (containerColor != other.containerColor) return false + if (labelColor != other.labelColor) return false + if (leadingIconContentColor != other.leadingIconContentColor) return false + if (trailingIconContentColor != other.trailingIconContentColor) return false + if (disabledContainerColor != other.disabledContainerColor) return false + if (disabledLabelColor != other.disabledLabelColor) return false + if (disabledLeadingIconContentColor != other.disabledLeadingIconContentColor) return false + if (disabledTrailingIconContentColor != other.disabledTrailingIconContentColor) return false + + return true + } + + override fun hashCode(): Int { + var result = containerColor.hashCode() + result = 31 * result + labelColor.hashCode() + result = 31 * result + leadingIconContentColor.hashCode() + result = 31 * result + trailingIconContentColor.hashCode() + result = 31 * result + disabledContainerColor.hashCode() + result = 31 * result + disabledLabelColor.hashCode() + result = 31 * result + disabledLeadingIconContentColor.hashCode() + result = 31 * result + disabledTrailingIconContentColor.hashCode() + + return result + } +} + +/** + * Represents the border stroke used in a chip in different states. + */ +@ExperimentalMaterial3Api +@Immutable +class ChipBorder internal constructor( + private val borderColor: Color, + private val disabledBorderColor: Color, + private val borderWidth: Dp, +) { + /** + * Represents the [BorderStroke] for this chip, depending on [enabled]. + * + * @param enabled whether the chip is enabled + */ + @Composable + internal fun borderStroke(enabled: Boolean): State { + return rememberUpdatedState( + BorderStroke(borderWidth, if (enabled) borderColor else disabledBorderColor), + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other !is ChipBorder) return false + + if (borderColor != other.borderColor) return false + if (disabledBorderColor != other.disabledBorderColor) return false + if (borderWidth != other.borderWidth) return false + + return true + } + + override fun hashCode(): Int { + var result = borderColor.hashCode() + result = 31 * result + disabledBorderColor.hashCode() + result = 31 * result + borderWidth.hashCode() + + return result + } +} + +/** + * Represents the elevation for a chip in different states. + */ +@ExperimentalMaterial3Api +@Immutable +class ChipElevation internal constructor( + private val defaultElevation: Dp, + private val pressedElevation: Dp, + private val focusedElevation: Dp, + private val hoveredElevation: Dp, + private val draggedElevation: Dp, + private val disabledElevation: Dp, +) { + /** + * Represents the tonal elevation used in a chip, 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 chip. + * + * @param enabled whether the chip is enabled + * @param interactionSource the [InteractionSource] for this chip + */ + @Composable + internal fun tonalElevation( + enabled: Boolean, + interactionSource: InteractionSource, + ): State { + return animateElevation(enabled = enabled, interactionSource = interactionSource) + } + + /** + * Represents the shadow elevation used in a chip, 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 chip to give it higher emphasis. + * + * See [tonalElevation] which controls the elevation with a color shift to the surface. + * + * @param enabled whether the chip is enabled + * @param interactionSource the [InteractionSource] for this chip + */ + @Composable + internal fun shadowElevation( + enabled: Boolean, + interactionSource: InteractionSource, + ): State { + return animateElevation(enabled = enabled, interactionSource = interactionSource) + } + + @Composable + private fun animateElevation( + enabled: Boolean, + interactionSource: InteractionSource, + ): State { + val interactions = remember { mutableStateListOf() } + 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) + } + is DragInteraction.Start -> { + interactions.add(interaction) + } + is DragInteraction.Stop -> { + interactions.remove(interaction.start) + } + is DragInteraction.Cancel -> { + interactions.remove(interaction.start) + } + } + } + } + + val interaction = interactions.lastOrNull() + + val target = if (!enabled) { + disabledElevation + } else { + when (interaction) { + is PressInteraction.Press -> pressedElevation + is HoverInteraction.Enter -> hoveredElevation + is FocusInteraction.Focus -> focusedElevation + is DragInteraction.Start -> draggedElevation + 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() + draggedElevation -> DragInteraction.Start() + 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 ChipElevation) 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 + } +} + /** * The padding between the elements in the chip. */ diff --git a/app/src/main/java/eu/kanade/presentation/components/IconButton.kt b/app/src/main/java/eu/kanade/presentation/components/IconButton.kt index 618da2b5c..3cfbc4403 100644 --- a/app/src/main/java/eu/kanade/presentation/components/IconButton.kt +++ b/app/src/main/java/eu/kanade/presentation/components/IconButton.kt @@ -23,15 +23,20 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon -import androidx.compose.material3.IconButtonColors -import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.OutlinedIconButton import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +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.semantics.Role import androidx.compose.ui.unit.dp import eu.kanade.presentation.util.minimumTouchTargetSize @@ -100,6 +105,88 @@ fun IconButton( } } +object IconButtonDefaults { + /** + * Creates a [IconButtonColors] that represents the default colors used in a [IconButton]. + * + * @param containerColor the container color of this icon button when enabled. + * @param contentColor the content color of this icon button when enabled. + * @param disabledContainerColor the container color of this icon button when not enabled. + * @param disabledContentColor the content color of this icon button when not enabled. + */ + @Composable + fun iconButtonColors( + containerColor: Color = Color.Transparent, + contentColor: Color = LocalContentColor.current, + disabledContainerColor: Color = Color.Transparent, + disabledContentColor: Color = contentColor.copy(alpha = 0.38f), + ): IconButtonColors = + IconButtonColors( + containerColor = containerColor, + contentColor = contentColor, + disabledContainerColor = disabledContainerColor, + disabledContentColor = disabledContentColor, + ) +} + object IconButtonTokens { val StateLayerSize = 40.0.dp } + +/** + * Represents the container and content colors used in an icon button in different states. + * + * - See [IconButtonDefaults.filledIconButtonColors] and + * [IconButtonDefaults.filledTonalIconButtonColors] for the default colors used in a + * [FilledIconButton]. + * - See [IconButtonDefaults.outlinedIconButtonColors] for the default colors used in an + * [OutlinedIconButton]. + */ +@Immutable +class IconButtonColors internal constructor( + private val containerColor: Color, + private val contentColor: Color, + private val disabledContainerColor: Color, + private val disabledContentColor: Color, +) { + /** + * Represents the container color for this icon button, depending on [enabled]. + * + * @param enabled whether the icon button is enabled + */ + @Composable + internal fun containerColor(enabled: Boolean): State { + return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor) + } + + /** + * Represents the content color for this icon button, depending on [enabled]. + * + * @param enabled whether the icon button is enabled + */ + @Composable + internal fun contentColor(enabled: Boolean): State { + return rememberUpdatedState(if (enabled) contentColor else disabledContentColor) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || other !is IconButtonColors) 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 + } +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt index 44fbfcf66..64400aef8 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt @@ -19,7 +19,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable @@ -30,7 +29,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor @@ -179,12 +177,7 @@ fun LibrarySelectionToolbar( onClickSelectAll: () -> Unit, onClickInvertSelection: () -> Unit, ) { - val backgroundColor by TopAppBarDefaults.smallTopAppBarColors().containerColor(1f) AppBar( - modifier = Modifier - .drawBehind { - drawRect(backgroundColor.copy(alpha = 1f)) - }, titleContent = { Text(text = "${state.selection.size}") }, actions = { IconButton(onClick = onClickSelectAll) { diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaAppBar.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaAppBar.kt index 500cc5538..b566fed67 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaAppBar.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaAppBar.kt @@ -3,10 +3,7 @@ package eu.kanade.presentation.manga.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.statusBars import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Close @@ -18,19 +15,19 @@ import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import eu.kanade.presentation.components.DownloadedOnlyModeBanner import eu.kanade.presentation.components.DropdownMenu import eu.kanade.presentation.components.IncognitoModeBanner @@ -64,16 +61,11 @@ fun MangaAppBar( onSelectAll: () -> Unit, onInvertSelection: () -> Unit, ) { - val isActionMode = actionModeCounter > 0 - val backgroundAlpha = if (isActionMode) 1f else backgroundAlphaProvider() - val backgroundColor by TopAppBarDefaults.smallTopAppBarColors().containerColor(1f) Column( - modifier = modifier.drawBehind { - drawRect(backgroundColor.copy(alpha = backgroundAlpha)) - }, + modifier = modifier, ) { + val isActionMode = actionModeCounter > 0 SmallTopAppBar( - modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)), title = { Text( text = if (isActionMode) actionModeCounter.toString() else title, @@ -247,10 +239,11 @@ fun MangaAppBar( } } }, - // Background handled by parent + windowInsets = WindowInsets.statusBars, colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = Color.Transparent, - scrolledContainerColor = Color.Transparent, + containerColor = MaterialTheme.colorScheme + .surfaceColorAtElevation(3.dp) + .copy(alpha = if (isActionMode) 1f else backgroundAlphaProvider()), ), ) diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/NamespaceTags.kt b/app/src/main/java/eu/kanade/presentation/manga/components/NamespaceTags.kt index 4137ebb4d..b9fe4cac2 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/NamespaceTags.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/NamespaceTags.kt @@ -5,11 +5,9 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ChipBorder import androidx.compose.material3.LocalMinimumTouchTargetEnforcement import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SuggestionChip -import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -22,7 +20,9 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.accompanist.flowlayout.FlowRow +import eu.kanade.presentation.components.ChipBorder import eu.kanade.presentation.components.SuggestionChip +import eu.kanade.presentation.components.SuggestionChipDefaults import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.online.all.EHentai @@ -32,6 +32,8 @@ import exh.metadata.metadata.base.RaisedTag import exh.source.EH_SOURCE_ID import exh.source.EXH_SOURCE_ID import exh.util.SourceTagsUtil +import androidx.compose.material3.ChipBorder as ChipBorderM3 +import androidx.compose.material3.SuggestionChipDefaults as SuggestionChipDefaultsM3 @Immutable data class DisplayTag( @@ -106,14 +108,16 @@ fun NamespaceTags( crossAxisSpacing = 8.dp, ) { tags.forEach { (_, text, search, border) -> + val borderDp = border?.let { with(LocalDensity.current) { it.toDp() } } TagsChip( text = text, onClick = { onClick(search) }, onLongClick = { onLongClick(search) }, - border = border?.let { - with(LocalDensity.current) { - SuggestionChipDefaults.suggestionChipBorder(borderWidth = it.toDp()) - } + border = borderDp?.let { + SuggestionChipDefaults.suggestionChipBorder(borderWidth = it) + }, + borderM3 = borderDp?.let { + SuggestionChipDefaultsM3.suggestionChipBorder(borderWidth = it) }, ) } @@ -129,6 +133,7 @@ fun TagsChip( onClick: (() -> Unit)?, onLongClick: (() -> Unit)?, border: ChipBorder? = null, + borderM3: ChipBorderM3? = null, ) { CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) { if (onClick != null) { @@ -147,8 +152,8 @@ fun TagsChip( SuggestionChip( onClick = onClick, label = { Text(text = text, style = MaterialTheme.typography.bodySmall) }, - border = border, - colors = SuggestionChipDefaults.suggestionChipColors( + border = borderM3, + colors = SuggestionChipDefaultsM3.suggestionChipColors( containerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), labelColor = MaterialTheme.colorScheme.onSurface, ), diff --git a/app/src/main/java/eu/kanade/presentation/util/Elevation.kt b/app/src/main/java/eu/kanade/presentation/util/Elevation.kt new file mode 100644 index 000000000..bece50fc7 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/util/Elevation.kt @@ -0,0 +1,125 @@ +/* + * 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. + */ + +/** + * Straight copy from Compose M3 for Button fork + */ + +package eu.kanade.presentation.util + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.TweenSpec +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.FocusInteraction +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.Interaction +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.ui.unit.Dp + +/** + * Animates the [Dp] value of [this] between [from] and [to] [Interaction]s, to [target]. The + * [AnimationSpec] used depends on the values for [from] and [to], see + * [ElevationDefaults.incomingAnimationSpecForInteraction] and + * [ElevationDefaults.outgoingAnimationSpecForInteraction] for more details. + * + * @param target the [Dp] target elevation for this component, corresponding to the elevation + * desired for the [to] state. + * @param from the previous [Interaction] that was used to calculate elevation. `null` if there + * was no previous [Interaction], such as when the component is in its default state. + * @param to the [Interaction] that this component is moving to, such as [PressInteraction.Press] + * when this component is being pressed. `null` if this component is moving back to its default + * state. + */ +internal suspend fun Animatable.animateElevation( + target: Dp, + from: Interaction? = null, + to: Interaction? = null, +) { + val spec = when { + // Moving to a new state + to != null -> ElevationDefaults.incomingAnimationSpecForInteraction(to) + // Moving to default, from a previous state + from != null -> ElevationDefaults.outgoingAnimationSpecForInteraction(from) + // Loading the initial state, or moving back to the baseline state from a disabled / + // unknown state, so just snap to the final value. + else -> null + } + if (spec != null) animateTo(target, spec) else snapTo(target) +} + +/** + * Contains default [AnimationSpec]s used for animating elevation between different [Interaction]s. + * + * Typically you should use [animateElevation] instead, which uses these [AnimationSpec]s + * internally. [animateElevation] in turn is used by the defaults for cards and buttons. + * + * @see animateElevation + */ +private object ElevationDefaults { + /** + * Returns the [AnimationSpec]s used when animating elevation to [interaction], either from a + * previous [Interaction], or from the default state. If [interaction] is unknown, then + * returns `null`. + * + * @param interaction the [Interaction] that is being animated to + */ + fun incomingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec? { + return when (interaction) { + is PressInteraction.Press -> DefaultIncomingSpec + is DragInteraction.Start -> DefaultIncomingSpec + is HoverInteraction.Enter -> DefaultIncomingSpec + is FocusInteraction.Focus -> DefaultIncomingSpec + else -> null + } + } + + /** + * Returns the [AnimationSpec]s used when animating elevation away from [interaction], to the + * default state. If [interaction] is unknown, then returns `null`. + * + * @param interaction the [Interaction] that is being animated away from + */ + fun outgoingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec? { + return when (interaction) { + is PressInteraction.Press -> DefaultOutgoingSpec + is DragInteraction.Start -> DefaultOutgoingSpec + is HoverInteraction.Enter -> HoveredOutgoingSpec + is FocusInteraction.Focus -> DefaultOutgoingSpec + else -> null + } + } +} + +private val OutgoingSpecEasing: Easing = CubicBezierEasing(0.40f, 0.00f, 0.60f, 1.00f) + +private val DefaultIncomingSpec = TweenSpec( + durationMillis = 120, + easing = FastOutSlowInEasing, +) + +private val DefaultOutgoingSpec = TweenSpec( + durationMillis = 150, + easing = OutgoingSpecEasing, +) + +private val HoveredOutgoingSpec = TweenSpec( + durationMillis = 120, + easing = OutgoingSpecEasing, +) diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 2030ce7ec..1d13c7da0 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -2,7 +2,7 @@ compiler = "1.3.0-rc02" compose = "1.2.1" accompanist = "0.25.1" -material3 = "1.0.0-alpha16" +material3 = "1.0.0-beta01" [libraries] activity = "androidx.activity:activity-compose:1.6.0-beta01"