blob: f55e2921c668d0e0b9630297303992f9775bfe62 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.core.animation;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.os.Build;
import androidx.annotation.DoNotInline;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
import java.util.List;
class PathUtils {
private static final int NUM_COMPONENTS = 3;
private static final int MAX_NUM_POINTS = 100;
private static final float EPSILON = 0.0001f;
private PathUtils() {}
static float[] createKeyFrameData(Path path, float precision) {
if (Build.VERSION.SDK_INT >= 26) {
return Api26Impl.approximate(path, precision);
} else {
// Measure the total length the whole path.
final PathMeasure measureForTotalLength = new PathMeasure(path, false);
float totalLength = 0;
// The sum of the previous contour plus the current one. Using the sum here b/c we want
// to directly subtract from it later.
ArrayList<Float> contourLengths = new ArrayList<>();
contourLengths.add(0f);
do {
final float pathLength = measureForTotalLength.getLength();
totalLength += pathLength;
contourLengths.add(totalLength);
} while (measureForTotalLength.nextContour());
// Now determine how many sample points we need, and the step for next sample.
final PathMeasure pathMeasure = new PathMeasure(path, false);
final int numPoints = Math.min(MAX_NUM_POINTS, (int) (totalLength / precision) + 1);
ArrayList<Float> results = new ArrayList<>(numPoints * NUM_COMPONENTS);
final float[] position = new float[2];
int contourIndex = 0;
float step = totalLength / (numPoints - 1 - contourLengths.size());
float currentDistance = 0;
float[] lastTangent = new float[2];
float[] tangent = new float[2];
boolean lastTwoPointsOnALine = false;
// For each sample point, determine whether we need to move on to next contour.
// After we find the right contour, then sample it using the current distance value
// minus the previously sampled contours' total length.
for (int i = 0; i < numPoints; ++i) {
pathMeasure.getPosTan(currentDistance - contourLengths.get(contourIndex),
position, tangent);
int lastIndex = results.size() - 1;
if (i > 0 && twoPointsOnTheSameLinePath(tangent, lastTangent,
position[0], position[1], results.get(lastIndex - 1),
results.get(lastIndex))) {
// If the current point and the last two points have the same tangent, they are
// on the same line. Instead of adding new points, modify the last point entries
if (lastTwoPointsOnALine) {
// Modify the entries for the last point added.
results.set(lastIndex - 2, (currentDistance / totalLength));
results.set(lastIndex - 1, position[0]);
results.set(lastIndex, position[1]);
} else {
lastTwoPointsOnALine = true;
addDataEntry(results, currentDistance / totalLength,
position[0], position[1]);
}
} else {
int skippedPoints = i - results.size() / 3;
if (skippedPoints > 0 && lastTwoPointsOnALine) {
float fineGrainedDistance = totalLength * results.get(results.size() - 3);
float samplePoints = Math.min(skippedPoints, 4);
float smallStep = step / samplePoints;
while (fineGrainedDistance + smallStep < currentDistance) {
fineGrainedDistance += smallStep;
pathMeasure.getPosTan(
fineGrainedDistance - contourLengths.get(contourIndex),
position, tangent);
addDataEntry(results, fineGrainedDistance / totalLength,
position[0], position[1]);
}
} else {
addDataEntry(results, currentDistance / totalLength,
position[0], position[1]);
}
lastTwoPointsOnALine = false;
}
currentDistance += step;
if ((contourIndex + 1) < contourLengths.size()
&& currentDistance > contourLengths.get(contourIndex + 1)) {
float currentContourSum = contourLengths.get(contourIndex + 1);
// Add the point that defines the end of the contour, if it's not already added
pathMeasure.getPosTan(
currentContourSum - contourLengths.get(contourIndex),
position, tangent);
addDataEntry(results, currentContourSum / totalLength,
position[0], position[1]);
contourIndex++;
pathMeasure.nextContour();
}
lastTangent[0] = tangent[0];
lastTangent[1] = tangent[1];
if (currentDistance > totalLength) {
break;
}
}
float[] optimizedResults = new float[results.size()];
for (int i = 0; i < results.size(); i++) {
optimizedResults[i] = results.get(i);
}
return optimizedResults;
}
}
private static boolean twoPointsOnTheSameLinePath(float[] tan1, float[] tan2,
float x1, float y1, float x2, float y2) {
if (Math.abs(tan1[0] - tan2[0]) > EPSILON || Math.abs(tan1[1] - tan2[1]) > EPSILON) {
return false;
}
float deltaX = x1 - x2;
float deltaY = y1 - y2;
// If deltaY / deltaX = tan1[1] / tan1[0], that means the two points are on the same line as
// the path.
return Math.abs(deltaX * tan1[1] - deltaY * tan1[0]) < EPSILON;
}
private static void addDataEntry(List<Float> data, float fraction, float x, float y) {
data.add(fraction);
data.add(x);
data.add(y);
}
@RequiresApi(26)
static class Api26Impl {
private Api26Impl() {
// This class is not instantiable.
}
@DoNotInline
static float[] approximate(Path path, float acceptableError) {
return path.approximate(acceptableError);
}
}
}