1 /* 2 * Copyright (C) 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.PathMeasure; 20 import android.os.Build; 21 22 import androidx.annotation.RequiresApi; 23 24 import java.util.ArrayList; 25 import java.util.List; 26 27 class PathUtils { 28 private static final int NUM_COMPONENTS = 3; 29 private static final int MAX_NUM_POINTS = 100; 30 private static final float EPSILON = 0.0001f; 31 PathUtils()32 private PathUtils() {} 33 createKeyFrameData(Path path, float precision)34 static float[] createKeyFrameData(Path path, float precision) { 35 if (Build.VERSION.SDK_INT >= 26) { 36 return Api26Impl.approximate(path, precision); 37 } else { 38 // Measure the total length the whole path. 39 final PathMeasure measureForTotalLength = new PathMeasure(path, false); 40 float totalLength = 0; 41 // The sum of the previous contour plus the current one. Using the sum here b/c we want 42 // to directly subtract from it later. 43 ArrayList<Float> contourLengths = new ArrayList<>(); 44 contourLengths.add(0f); 45 do { 46 final float pathLength = measureForTotalLength.getLength(); 47 totalLength += pathLength; 48 contourLengths.add(totalLength); 49 50 } while (measureForTotalLength.nextContour()); 51 52 // Now determine how many sample points we need, and the step for next sample. 53 final PathMeasure pathMeasure = new PathMeasure(path, false); 54 55 final int numPoints = Math.min(MAX_NUM_POINTS, (int) (totalLength / precision) + 1); 56 57 ArrayList<Float> results = new ArrayList<>(numPoints * NUM_COMPONENTS); 58 59 final float[] position = new float[2]; 60 61 int contourIndex = 0; 62 float step = totalLength / (numPoints - 1 - contourLengths.size()); 63 float currentDistance = 0; 64 65 float[] lastTangent = new float[2]; 66 float[] tangent = new float[2]; 67 boolean lastTwoPointsOnALine = false; 68 69 // For each sample point, determine whether we need to move on to next contour. 70 // After we find the right contour, then sample it using the current distance value 71 // minus the previously sampled contours' total length. 72 for (int i = 0; i < numPoints; ++i) { 73 pathMeasure.getPosTan(currentDistance - contourLengths.get(contourIndex), 74 position, tangent); 75 76 int lastIndex = results.size() - 1; 77 if (i > 0 && twoPointsOnTheSameLinePath(tangent, lastTangent, 78 position[0], position[1], results.get(lastIndex - 1), 79 results.get(lastIndex))) { 80 // If the current point and the last two points have the same tangent, they are 81 // on the same line. Instead of adding new points, modify the last point entries 82 if (lastTwoPointsOnALine) { 83 // Modify the entries for the last point added. 84 results.set(lastIndex - 2, (currentDistance / totalLength)); 85 results.set(lastIndex - 1, position[0]); 86 results.set(lastIndex, position[1]); 87 88 } else { 89 lastTwoPointsOnALine = true; 90 addDataEntry(results, currentDistance / totalLength, 91 position[0], position[1]); 92 } 93 } else { 94 int skippedPoints = i - results.size() / 3; 95 if (skippedPoints > 0 && lastTwoPointsOnALine) { 96 float fineGrainedDistance = totalLength * results.get(results.size() - 3); 97 float samplePoints = Math.min(skippedPoints, 4); 98 float smallStep = step / samplePoints; 99 100 while (fineGrainedDistance + smallStep < currentDistance) { 101 fineGrainedDistance += smallStep; 102 pathMeasure.getPosTan( 103 fineGrainedDistance - contourLengths.get(contourIndex), 104 position, tangent); 105 106 addDataEntry(results, fineGrainedDistance / totalLength, 107 position[0], position[1]); 108 } 109 } else { 110 addDataEntry(results, currentDistance / totalLength, 111 position[0], position[1]); 112 } 113 lastTwoPointsOnALine = false; 114 } 115 116 currentDistance += step; 117 118 if ((contourIndex + 1) < contourLengths.size() 119 && currentDistance > contourLengths.get(contourIndex + 1)) { 120 121 float currentContourSum = contourLengths.get(contourIndex + 1); 122 // Add the point that defines the end of the contour, if it's not already added 123 pathMeasure.getPosTan( 124 currentContourSum - contourLengths.get(contourIndex), 125 position, tangent); 126 addDataEntry(results, currentContourSum / totalLength, 127 position[0], position[1]); 128 129 contourIndex++; 130 pathMeasure.nextContour(); 131 } 132 133 lastTangent[0] = tangent[0]; 134 lastTangent[1] = tangent[1]; 135 136 if (currentDistance > totalLength) { 137 break; 138 } 139 } 140 141 float[] optimizedResults = new float[results.size()]; 142 for (int i = 0; i < results.size(); i++) { 143 optimizedResults[i] = results.get(i); 144 } 145 return optimizedResults; 146 } 147 } 148 twoPointsOnTheSameLinePath(float[] tan1, float[] tan2, float x1, float y1, float x2, float y2)149 private static boolean twoPointsOnTheSameLinePath(float[] tan1, float[] tan2, 150 float x1, float y1, float x2, float y2) { 151 if (Math.abs(tan1[0] - tan2[0]) > EPSILON || Math.abs(tan1[1] - tan2[1]) > EPSILON) { 152 return false; 153 } 154 float deltaX = x1 - x2; 155 float deltaY = y1 - y2; 156 // If deltaY / deltaX = tan1[1] / tan1[0], that means the two points are on the same line as 157 // the path. 158 return Math.abs(deltaX * tan1[1] - deltaY * tan1[0]) < EPSILON; 159 } 160 addDataEntry(List<Float> data, float fraction, float x, float y)161 private static void addDataEntry(List<Float> data, float fraction, float x, float y) { 162 data.add(fraction); 163 data.add(x); 164 data.add(y); 165 } 166 167 @RequiresApi(26) 168 static class Api26Impl { Api26Impl()169 private Api26Impl() { 170 // This class is not instantiable. 171 } 172 approximate(Path path, float acceptableError)173 static float[] approximate(Path path, float acceptableError) { 174 return path.approximate(acceptableError); 175 } 176 } 177 } 178