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