1 /*
<lambda>null2 * Copyright 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.animation.core
18
19 import androidx.compose.runtime.Immutable
20 import androidx.compose.runtime.Stable
21 import androidx.compose.ui.graphics.computeCubicVerticalBounds
22 import androidx.compose.ui.graphics.evaluateCubic
23 import androidx.compose.ui.graphics.findFirstCubicRoot
24 import androidx.compose.ui.util.fastCoerceIn
25 import kotlin.math.max
26
27 /**
28 * Easing is a way to adjust an animation’s fraction. Easing allows transitioning elements to speed
29 * up and slow down, rather than moving at a constant rate.
30 *
31 * Fraction is a value between 0 and 1.0 indicating our current point in the animation where 0
32 * represents the start and 1.0 represents the end.
33 *
34 * An [Easing] must map fraction=0.0 to 0.0 and fraction=1.0 to 1.0.
35 */
36 @Stable
37 public fun interface Easing {
38 public fun transform(fraction: Float): Float
39 }
40
41 /**
42 * Elements that begin and end at rest use this standard easing. They speed up quickly and slow down
43 * gradually, in order to emphasize the end of the transition.
44 *
45 * Standard easing puts subtle attention at the end of an animation, by giving more time to
46 * deceleration than acceleration. It is the most common form of easing.
47 *
48 * This is equivalent to the Android `FastOutSlowInInterpolator`
49 */
50 public val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
51
52 /**
53 * Incoming elements are animated using deceleration easing, which starts a transition at peak
54 * velocity (the fastest point of an element’s movement) and ends at rest.
55 *
56 * This is equivalent to the Android `LinearOutSlowInInterpolator`
57 */
58 public val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
59
60 /**
61 * Elements exiting a screen use acceleration easing, where they start at rest and end at peak
62 * velocity.
63 *
64 * This is equivalent to the Android `FastOutLinearInInterpolator`
65 */
66 public val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
67
68 /**
69 * It returns fraction unmodified. This is useful as a default value for cases where a [Easing] is
70 * required but no actual easing is desired.
71 */
fractionnull72 public val LinearEasing: Easing = Easing { fraction -> fraction }
73
74 // This is equal to 1f.ulp or 1f.nextUp() - 1f, but neither ulp nor nextUp() are part of all KMP
75 // targets, only JVM and native
76 private const val OneUlpAt1 = 1.1920929e-7f
77
78 /**
79 * A cubic polynomial easing.
80 *
81 * The [CubicBezierEasing] class implements third-order Bézier curves.
82 *
83 * This is equivalent to the Android `PathInterpolator` when a single cubic Bézier curve is
84 * specified.
85 *
86 * Note: [CubicBezierEasing] instances are stateless and can be used concurrently from multiple
87 * threads.
88 *
89 * Rather than creating a new instance, consider using one of the common cubic [Easing]s:
90 *
91 * @param a The x coordinate of the first control point. The line through the point (0, 0) and the
92 * first control point is tangent to the easing at the point (0, 0).
93 * @param b The y coordinate of the first control point. The line through the point (0, 0) and the
94 * first control point is tangent to the easing at the point (0, 0).
95 * @param c The x coordinate of the second control point. The line through the point (1, 1) and the
96 * second control point is tangent to the easing at the point (1, 1).
97 * @param d The y coordinate of the second control point. The line through the point (1, 1) and the
98 * second control point is tangent to the easing at the point (1, 1).
99 * @see FastOutSlowInEasing
100 * @see LinearOutSlowInEasing
101 * @see FastOutLinearInEasing
102 */
103 @Immutable
104 public class CubicBezierEasing(
105 private val a: Float,
106 private val b: Float,
107 private val c: Float,
108 private val d: Float
109 ) : Easing {
110 private val min: Float
111 private val max: Float
112
113 init {
<lambda>null114 requirePrecondition(!a.isNaN() && !b.isNaN() && !c.isNaN() && !d.isNaN()) {
115 "Parameters to CubicBezierEasing cannot be NaN. Actual parameters are: $a, $b, $c, $d."
116 }
117 val roots = FloatArray(5)
118 val extrema = computeCubicVerticalBounds(0.0f, b, d, 1.0f, roots, 0)
119 min = extrema.first
120 max = extrema.second
121 }
122
123 /**
124 * Transforms the specified [fraction] in the range 0..1 by this cubic Bézier curve. To solve
125 * the curve, [fraction] is used as the x coordinate along the curve, and the corresponding y
126 * coordinate on the curve is returned. If no solution exists, this method throws an
127 * [IllegalArgumentException].
128 *
129 * @throws IllegalArgumentException If the cubic Bézier curve cannot be solved
130 */
transformnull131 override fun transform(fraction: Float): Float {
132 return if (fraction > 0f && fraction < 1f) {
133 // We translate the coordinates by the fraction when calling findFirstCubicRoot,
134 // but we need to make sure the translation can be done at 1.0f so we take at
135 // least 1 ulp at 1.0f
136 val f = max(fraction, OneUlpAt1)
137 val t =
138 findFirstCubicRoot(
139 0.0f - f,
140 a - f,
141 c - f,
142 1.0f - f,
143 )
144
145 // No root, the cubic curve has no solution
146 if (t.isNaN()) {
147 throwNoSolution(fraction)
148 }
149
150 // Don't clamp the values since the curve might be used to over- or under-shoot
151 // The test above that checks if fraction is in ]0..1[ will ensure we start and
152 // end at 0 and 1 respectively
153 evaluateCubic(b, d, t).fastCoerceIn(min, max)
154 } else {
155 fraction
156 }
157 }
158
throwNoSolutionnull159 private fun throwNoSolution(fraction: Float) {
160 throw IllegalArgumentException(
161 "The cubic curve with parameters ($a, $b, $c, $d) has no solution at $fraction"
162 )
163 }
164
equalsnull165 override fun equals(other: Any?): Boolean {
166 return other is CubicBezierEasing &&
167 a == other.a &&
168 b == other.b &&
169 c == other.c &&
170 d == other.d
171 }
172
hashCodenull173 override fun hashCode(): Int {
174 return ((a.hashCode() * 31 + b.hashCode()) * 31 + c.hashCode()) * 31 + d.hashCode()
175 }
176
toStringnull177 override fun toString(): String = "CubicBezierEasing(a=$a, b=$b, c=$c, d=$d)"
178 }
179