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