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 e36cde7c6b..566607000a 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 e8edba9e64..2f261f9c41 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/IconButton.kt b/app/src/main/java/eu/kanade/presentation/components/IconButton.kt index 618da2b5c3..3cfbc44035 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 1fe17a3979..b71a6d0232 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 @@ -17,15 +17,12 @@ 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.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember 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 @@ -138,12 +135,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 35603ba846..ca2ab237cb 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 @@ -55,16 +52,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, @@ -198,10 +190,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/util/Elevation.kt b/app/src/main/java/eu/kanade/presentation/util/Elevation.kt new file mode 100644 index 0000000000..bece50fc71 --- /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 2030ce7ecd..1d13c7da05 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"