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