1 /*
2  * Copyright 2018 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 package androidx.core.animation;
17 
18 import android.graphics.Path;
19 import android.graphics.PointF;
20 
21 import org.jspecify.annotations.NonNull;
22 
23 import java.util.ArrayList;
24 import java.util.List;
25 
26 /**
27  * PathKeyframes relies on approximating the Path as a series of line segments.
28  * The line segments are recursively divided until there is less than 1/2 pixel error
29  * between the lines and the curve. Each point of the line segment is converted
30  * to a Keyframe and a linear interpolation between Keyframes creates a good approximation
31  * of the curve.
32  * <p>
33  * PathKeyframes is optimized to reduce the number of objects created when there are
34  * many keyframes for a curve.
35  * </p>
36  * <p>
37  * Typically, the returned type is a PointF, but the individual components can be extracted
38  * as either an IntKeyframes or FloatKeyframes.
39  * </p>
40  */
41 class PathKeyframes implements Keyframes<PointF> {
42     private static final int FRACTION_OFFSET = 0;
43     private static final int X_OFFSET = 1;
44     private static final int Y_OFFSET = 2;
45     private static final int NUM_COMPONENTS = 3;
46     private static final ArrayList<Keyframe<PointF>> EMPTY_KEYFRAMES = new ArrayList<>();
47 
48     private PointF mTempPointF = new PointF();
49     private final float[] mKeyframeData;
50 
PathKeyframes(Path path)51     PathKeyframes(Path path) {
52         this(path, 0.5f);
53     }
54 
PathKeyframes(Path path, float error)55     PathKeyframes(Path path, float error) {
56         if (path == null || path.isEmpty()) {
57             throw new IllegalArgumentException("The path must not be null or empty");
58         }
59         mKeyframeData = PathUtils.createKeyFrameData(path, error);
60     }
61 
62     @Override
getKeyframes()63     public List<Keyframe<PointF>> getKeyframes() {
64         return EMPTY_KEYFRAMES;
65     }
66 
67     @Override
getValue(float fraction)68     public PointF getValue(float fraction) {
69         int numPoints = mKeyframeData.length / 3;
70         if (fraction < 0) {
71             return interpolateInRange(fraction, 0, 1);
72         } else if (fraction > 1) {
73             return interpolateInRange(fraction, numPoints - 2, numPoints - 1);
74         } else if (fraction == 0) {
75             return pointForIndex(0);
76         } else if (fraction == 1) {
77             return pointForIndex(numPoints - 1);
78         } else {
79             // Binary search for the correct section
80             int low = 0;
81             int high = numPoints - 1;
82 
83             while (low <= high) {
84                 int mid = (low + high) / 2;
85                 float midFraction = mKeyframeData[(mid * NUM_COMPONENTS) + FRACTION_OFFSET];
86 
87                 if (fraction < midFraction) {
88                     high = mid - 1;
89                 } else if (fraction > midFraction) {
90                     low = mid + 1;
91                 } else {
92                     return pointForIndex(mid);
93                 }
94             }
95 
96             // now high is below the fraction and low is above the fraction
97             return interpolateInRange(fraction, high, low);
98         }
99     }
100 
interpolateInRange(float fraction, int startIndex, int endIndex)101     private PointF interpolateInRange(float fraction, int startIndex, int endIndex) {
102         int startBase = (startIndex * NUM_COMPONENTS);
103         int endBase = (endIndex * NUM_COMPONENTS);
104 
105         float startFraction = mKeyframeData[startBase + FRACTION_OFFSET];
106         float endFraction = mKeyframeData[endBase + FRACTION_OFFSET];
107 
108         float intervalFraction = (fraction - startFraction) / (endFraction - startFraction);
109 
110         float startX = mKeyframeData[startBase + X_OFFSET];
111         float endX = mKeyframeData[endBase + X_OFFSET];
112         float startY = mKeyframeData[startBase + Y_OFFSET];
113         float endY = mKeyframeData[endBase + Y_OFFSET];
114 
115         float x = interpolate(intervalFraction, startX, endX);
116         float y = interpolate(intervalFraction, startY, endY);
117 
118         mTempPointF.set(x, y);
119         return mTempPointF;
120     }
121 
122     @Override
setEvaluator(TypeEvaluator evaluator)123     public void setEvaluator(TypeEvaluator evaluator) {
124     }
125 
126     @Override
getType()127     public Class<PointF> getType() {
128         return PointF.class;
129     }
130 
131     @Override
clone()132     public @NonNull Keyframes clone() {
133         Keyframes clone = null;
134         try {
135             clone = (Keyframes) super.clone();
136         } catch (CloneNotSupportedException e) { }
137         return clone;
138     }
139 
pointForIndex(int index)140     private PointF pointForIndex(int index) {
141         int base = (index * NUM_COMPONENTS);
142         int xOffset = base + X_OFFSET;
143         int yOffset = base + Y_OFFSET;
144         mTempPointF.set(mKeyframeData[xOffset], mKeyframeData[yOffset]);
145         return mTempPointF;
146     }
147 
interpolate(float fraction, float startValue, float endValue)148     private static float interpolate(float fraction, float startValue, float endValue) {
149         float diff = endValue - startValue;
150         return startValue + (diff * fraction);
151     }
152 
153     /**
154      * Returns a FloatKeyframes for the X component of the Path.
155      * @return a FloatKeyframes for the X component of the Path.
156      */
createXFloatKeyframes()157     public FloatKeyframes createXFloatKeyframes() {
158         return new FloatKeyframesBase() {
159             @Override
160             public float getFloatValue(float fraction) {
161                 PointF pointF = (PointF) PathKeyframes.this.getValue(fraction);
162                 return pointF.x;
163             }
164         };
165     }
166 
167     /**
168      * Returns a FloatKeyframes for the Y component of the Path.
169      * @return a FloatKeyframes for the Y component of the Path.
170      */
171     public FloatKeyframes createYFloatKeyframes() {
172         return new FloatKeyframesBase() {
173             @Override
174             public float getFloatValue(float fraction) {
175                 PointF pointF = (PointF) PathKeyframes.this.getValue(fraction);
176                 return pointF.y;
177             }
178         };
179     }
180 
181     /**
182      * Returns an IntKeyframes for the X component of the Path.
183      * @return an IntKeyframes for the X component of the Path.
184      */
185     public IntKeyframes createXIntKeyframes() {
186         return new IntKeyframesBase() {
187             @Override
188             public int getIntValue(float fraction) {
189                 PointF pointF = (PointF) PathKeyframes.this.getValue(fraction);
190                 return Math.round(pointF.x);
191             }
192         };
193     }
194 
195     /**
196      * Returns an IntKeyframeSet for the Y component of the Path.
197      * @return an IntKeyframeSet for the Y component of the Path.
198      */
199     public IntKeyframes createYIntKeyframes() {
200         return new IntKeyframesBase() {
201             @Override
202             public int getIntValue(float fraction) {
203                 PointF pointF = (PointF) PathKeyframes.this.getValue(fraction);
204                 return Math.round(pointF.y);
205             }
206         };
207     }
208 
209     private abstract static class SimpleKeyframes<T> implements Keyframes<T> {
210         private final ArrayList<Keyframe<T>> mEmptyFrames = new ArrayList<>();
211         @Override
212         public void setEvaluator(TypeEvaluator<T> evaluator) {
213         }
214 
215         @Override
216         public List<Keyframe<T>> getKeyframes() {
217             return mEmptyFrames;
218         }
219 
220         @Override
221         @SuppressWarnings({"unchecked", "CatchAndPrintStackTrace"})
222         public @NonNull Keyframes<T> clone() {
223             Keyframes<T> clone = null;
224             try {
225                 clone = (Keyframes<T>) super.clone();
226             } catch (CloneNotSupportedException e) {
227                 e.printStackTrace();
228             }
229             return clone;
230         }
231     }
232 
233     abstract static class IntKeyframesBase extends SimpleKeyframes<Integer>
234             implements IntKeyframes {
235         @Override
236         public Class<Integer> getType() {
237             return Integer.class;
238         }
239 
240         @Override
241         public Integer getValue(float fraction) {
242             return getIntValue(fraction);
243         }
244     }
245 
246     abstract static class FloatKeyframesBase extends SimpleKeyframes<Float>
247             implements FloatKeyframes {
248         @Override
249         public Class<Float> getType() {
250             return Float.class;
251         }
252 
253         @Override
254         public Float getValue(float fraction) {
255             return getFloatValue(fraction);
256         }
257     }
258 }
259