PathInterpolatorCompat.java revision 1f1b5540916924b36bc81cc3edb4f02245116dde
1/* 2 * Copyright (C) 2017 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 */ 16package androidx.vectordrawable.graphics.drawable; 17 18import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 19 20import static java.lang.Math.abs; 21import static java.lang.Math.min; 22 23import android.content.Context; 24import android.content.res.Resources; 25import android.content.res.TypedArray; 26import android.graphics.Path; 27import android.graphics.PathMeasure; 28import android.util.AttributeSet; 29import android.view.InflateException; 30import android.view.animation.Interpolator; 31 32import androidx.annotation.RestrictTo; 33import androidx.core.content.res.TypedArrayUtils; 34import androidx.core.graphics.PathParser; 35 36import org.xmlpull.v1.XmlPullParser; 37 38/** 39 * An interpolator that can traverse a Path that extends from <code>Point</code> 40 * <code>(0, 0)</code> to <code>(1, 1)</code>. The x coordinate along the <code>Path</code> 41 * is the input value and the output is the y coordinate of the line at that point. 42 * This means that the Path must conform to a function <code>y = f(x)</code>. 43 * 44 * <p>The <code>Path</code> must not have gaps in the x direction and must not 45 * loop back on itself such that there can be two points sharing the same x coordinate. 46 * It is alright to have a disjoint line in the vertical direction:</p> 47 * <p><blockquote><pre> 48 * Path path = new Path(); 49 * path.lineTo(0.25f, 0.25f); 50 * path.moveTo(0.25f, 0.5f); 51 * path.lineTo(1f, 1f); 52 * </pre></blockquote></p> 53 * @hide 54 */ 55@RestrictTo(LIBRARY_GROUP) 56public class PathInterpolatorCompat implements Interpolator { 57 58 // This governs how accurate the approximation of the Path is. 59 private static final float PRECISION = 0.002f; 60 public static final int MAX_NUM_POINTS = 3000; 61 public static final double EPSILON = 0.00001; 62 63 private float[] mX; // x coordinates in the line 64 65 private float[] mY; // y coordinates in the line 66 67 public PathInterpolatorCompat(Context context, AttributeSet attrs, XmlPullParser parser) { 68 this(context.getResources(), context.getTheme(), attrs, parser); 69 } 70 71 public PathInterpolatorCompat(Resources res, Resources.Theme theme, AttributeSet attrs, 72 XmlPullParser parser) { 73 TypedArray a = TypedArrayUtils.obtainAttributes(res, theme, 74 attrs, AndroidResources.STYLEABLE_PATH_INTERPOLATOR); 75 parseInterpolatorFromTypeArray(a, parser); 76 a.recycle(); 77 } 78 79 private void parseInterpolatorFromTypeArray(TypedArray a, XmlPullParser parser) { 80 // If there is pathData defined in the xml file, then the controls points 81 // will be all coming from pathData. 82 if (TypedArrayUtils.hasAttribute(parser, "pathData")) { 83 String pathData = TypedArrayUtils.getNamedString(a, parser, "pathData", 84 AndroidResources.STYLEABLE_PATH_INTERPOLATOR_PATH_DATA); 85 Path path = PathParser.createPathFromPathData(pathData); 86 if (path == null) { 87 throw new InflateException("The path is null, which is created" 88 + " from " + pathData); 89 } 90 initPath(path); 91 } else { 92 if (!TypedArrayUtils.hasAttribute(parser, "controlX1")) { 93 throw new InflateException("pathInterpolator requires the controlX1 attribute"); 94 } else if (!TypedArrayUtils.hasAttribute(parser, "controlY1")) { 95 throw new InflateException("pathInterpolator requires the controlY1 attribute"); 96 } 97 float x1 = TypedArrayUtils.getNamedFloat(a, parser, "controlX1", 98 AndroidResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_1, 0); 99 float y1 = TypedArrayUtils.getNamedFloat(a, parser, "controlY1", 100 AndroidResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_1, 0); 101 102 boolean hasX2 = TypedArrayUtils.hasAttribute(parser, "controlX2"); 103 boolean hasY2 = TypedArrayUtils.hasAttribute(parser, "controlY2"); 104 105 if (hasX2 != hasY2) { 106 throw new InflateException("pathInterpolator requires both controlX2 and" 107 + " controlY2 for cubic Beziers."); 108 } 109 110 if (!hasX2) { 111 initQuad(x1, y1); 112 } else { 113 float x2 = TypedArrayUtils.getNamedFloat(a, parser, "controlX2", 114 AndroidResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_X_2, 0); 115 float y2 = TypedArrayUtils.getNamedFloat(a, parser, "controlY2", 116 AndroidResources.STYLEABLE_PATH_INTERPOLATOR_CONTROL_Y_2, 0); 117 initCubic(x1, y1, x2, y2); 118 } 119 } 120 } 121 122 private void initQuad(float controlX, float controlY) { 123 Path path = new Path(); 124 path.moveTo(0, 0); 125 path.quadTo(controlX, controlY, 1f, 1f); 126 initPath(path); 127 } 128 129 private void initCubic(float x1, float y1, float x2, float y2) { 130 Path path = new Path(); 131 path.moveTo(0, 0); 132 path.cubicTo(x1, y1, x2, y2, 1f, 1f); 133 initPath(path); 134 } 135 136 private void initPath(Path path) { 137 final PathMeasure pathMeasure = new PathMeasure(path, false /* forceClosed */); 138 139 final float pathLength = pathMeasure.getLength(); 140 final int numPoints = min(MAX_NUM_POINTS, (int) (pathLength / PRECISION) + 1); 141 142 if (numPoints <= 0) { 143 throw new IllegalArgumentException("The Path has a invalid length " + pathLength); 144 } 145 146 mX = new float[numPoints]; 147 mY = new float[numPoints]; 148 149 final float[] position = new float[2]; 150 for (int i = 0; i < numPoints; ++i) { 151 final float distance = (i * pathLength) / (numPoints - 1); 152 pathMeasure.getPosTan(distance, position, null /* tangent */); 153 154 mX[i] = position[0]; 155 mY[i] = position[1]; 156 } 157 158 if (abs(mX[0]) > EPSILON || abs(mY[0]) > EPSILON || abs(mX[numPoints - 1] - 1) > EPSILON 159 || abs(mY[numPoints - 1] - 1) > EPSILON) { 160 throw new IllegalArgumentException("The Path must start at (0,0) and end at (1,1)" 161 + " start: " + mX[0] + "," + mY[0] + " end:" + mX[numPoints - 1] + "," 162 + mY[numPoints - 1]); 163 164 } 165 166 float prevX = 0; 167 int componentIndex = 0; 168 for (int i = 0; i < numPoints; i++) { 169 float x = mX[componentIndex++]; 170 if (x < prevX) { 171 throw new IllegalArgumentException("The Path cannot loop back on itself, x :" + x); 172 } 173 mX[i] = x; 174 prevX = x; 175 } 176 177 if (pathMeasure.nextContour()) { 178 throw new IllegalArgumentException("The Path should be continuous," 179 + " can't have 2+ contours"); 180 } 181 } 182 183 /** 184 * Using the line in the Path in this interpolator that can be described as 185 * <code>y = f(x)</code>, finds the y coordinate of the line given <code>t</code> 186 * as the x coordinate. Values less than 0 will always return 0 and values greater 187 * than 1 will always return 1. 188 * 189 * @param t Treated as the x coordinate along the line. 190 * @return The y coordinate of the Path along the line where x = <code>t</code>. 191 * @see Interpolator#getInterpolation(float) 192 */ 193 @Override 194 public float getInterpolation(float t) { 195 if (t <= 0) { 196 return 0; 197 } else if (t >= 1) { 198 return 1; 199 } 200 // Do a binary search for the correct x to interpolate between. 201 int startIndex = 0; 202 int endIndex = mX.length - 1; 203 204 while (endIndex - startIndex > 1) { 205 int midIndex = (startIndex + endIndex) / 2; 206 if (t < mX[midIndex]) { 207 endIndex = midIndex; 208 } else { 209 startIndex = midIndex; 210 } 211 } 212 213 float xRange = mX[endIndex] - mX[startIndex]; 214 if (xRange == 0) { 215 return mY[startIndex]; 216 } 217 218 float tInRange = t - mX[startIndex]; 219 float fraction = tInRange / xRange; 220 221 float startY = mY[startIndex]; 222 float endY = mY[endIndex]; 223 return startY + (fraction * (endY - startY)); 224 } 225} 226