Adds Elevation state animation and applies to Buttons.
Relnote: Adds Elevation for animating elevation state changes and applies it to all Buttons with elevation.
Test: To be added in follow-up. Tested locally with mouse.
Change-Id: I3fed2eb1e17af63e2d7d8ad5f23b4eec7bc2706c
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Elevation.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Elevation.kt
new file mode 100644
index 0000000..b104e20
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Elevation.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+import androidx.compose.animation.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
+)