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