1 /*
2  * Copyright 2023 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.ui.graphics.IntervalTree
21 import androidx.compose.ui.graphics.Path
22 import androidx.compose.ui.graphics.PathIterator
23 import androidx.compose.ui.graphics.PathSegment
24 import androidx.compose.ui.graphics.computeHorizontalBounds
25 import androidx.compose.ui.graphics.evaluateY
26 import androidx.compose.ui.graphics.findFirstRoot
27 
28 /**
29  * An easing function for an arbitrary [Path].
30  *
31  * The [Path] must begin at `(0, 0)` and end at `(1, 1)`. The x-coordinate along the [Path] is the
32  * input value and the output is the y coordinate of the line at that point. This means that the
33  * Path must conform to a function `y = f(x)`.
34  *
35  * The [Path] must be continuous along the x axis. The [Path] should also be monotonically
36  * increasing along the x axis. If the [Path] is not monotonic and there are multiple y values for a
37  * given x, the chosen y value is implementation dependent and may vary.
38  *
39  * The [Path] must not contain any [Path.close] command as it would force the path to restart from
40  * the beginning.
41  *
42  * This is equivalent to the Android `PathInterpolator`.
43  *
44  * [CubicBezierEasing] should be used if a single bezier curve is required as it performs fewer
45  * allocations. [PathEasing] should be used when creating an arbitrary path.
46  *
47  * Note: a [PathEasing] instance can be used from any thread, but not concurrently.
48  *
49  * @sample androidx.compose.animation.core.samples.PathEasingSample
50  * @param path The [Path] to use to make the curve representing the easing curve.
51  */
52 @Immutable
53 public class PathEasing(private val path: Path) : Easing {
54     private lateinit var intervals: IntervalTree<PathSegment>
55 
transformnull56     override fun transform(fraction: Float): Float {
57         if (fraction <= 0.0f) {
58             return 0.0f
59         } else if (fraction >= 1.0f) {
60             return 1.0f
61         }
62 
63         if (!::intervals.isInitialized) {
64             initializeEasing()
65         }
66 
67         val result = intervals.findFirstOverlap(fraction)
68         val segment =
69             checkPreconditionNotNull(result.data) {
70                 "The easing path is invalid. Make sure it is continuous on the x axis."
71             }
72 
73         val t = findFirstRoot(segment, fraction)
74         checkPrecondition(!t.isNaN()) {
75             "The easing path is invalid. Make sure it does not contain NaN/Infinity values."
76         }
77 
78         return evaluateY(segment, t)
79     }
80 
initializeEasingnull81     private fun initializeEasing() {
82         val roots = FloatArray(5)
83 
84         // Using an interval tree is a bit heavy handed but since we are dealing with
85         // easing curves, we don't expect many segments, and therefore few allocations.
86         // The interval tree allows us to quickly query for the correct segment inside
87         // the transform() function.
88         val segmentIntervals =
89             IntervalTree<PathSegment>().apply {
90                 // A path easing curve is defined in the domain 0..1, use an error
91                 // appropriate for this domain (the default is 0.25). Conic segments
92                 // should be unlikely in path easing curves, but just in case...
93                 val iterator = path.iterator(PathIterator.ConicEvaluation.AsQuadratics, 2e-4f)
94                 while (iterator.hasNext()) {
95                     val segment = iterator.next()
96                     requirePrecondition(segment.type != PathSegment.Type.Close) {
97                         "The path cannot contain a close() command."
98                     }
99                     if (
100                         segment.type != PathSegment.Type.Move &&
101                             segment.type != PathSegment.Type.Done
102                     ) {
103                         val bounds = computeHorizontalBounds(segment, roots)
104                         addInterval(bounds.first, bounds.second, segment)
105                     }
106                 }
107             }
108 
109         requirePrecondition(0.0f in segmentIntervals && 1.0f in segmentIntervals) {
110             "The easing path must start at 0.0f and end at 1.0f."
111         }
112 
113         intervals = segmentIntervals
114     }
115 }
116