ソースを参照

Bump Compose M3 to 1.0.0-beta01 (#7867)

Ivan Iskandar 2 年 前
コミット
655fa25b51

+ 9 - 11
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,
         )

+ 286 - 10
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<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
+    }
+}

+ 89 - 2
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<Color> {
+        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<Color> {
+        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
+    }
+}

+ 0 - 8
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) {

+ 10 - 17
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()),
             ),
         )
 

+ 125 - 0
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<Dp, *>.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<Dp>? {
+        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<Dp>? {
+        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<Dp>(
+    durationMillis = 120,
+    easing = FastOutSlowInEasing,
+)
+
+private val DefaultOutgoingSpec = TweenSpec<Dp>(
+    durationMillis = 150,
+    easing = OutgoingSpecEasing,
+)
+
+private val HoveredOutgoingSpec = TweenSpec<Dp>(
+    durationMillis = 120,
+    easing = OutgoingSpecEasing,
+)

+ 1 - 1
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"