blob: 50a52e3f374e3d77e7ab5af75a8061c26a4d7696 [file] [log] [blame]
package com.airbnb.lottie.parser;
import android.graphics.Color;
import com.airbnb.lottie.model.content.GradientColor;
import com.airbnb.lottie.parser.moshi.JsonReader;
import com.airbnb.lottie.utils.MiscUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class GradientColorParser implements com.airbnb.lottie.parser.ValueParser<GradientColor> {
/**
* The number of colors if it exists in the json or -1 if it doesn't (legacy bodymovin)
*/
private int colorPoints;
public GradientColorParser(int colorPoints) {
this.colorPoints = colorPoints;
}
/**
* Both the color stops and opacity stops are in the same array.
* There are {@link #colorPoints} colors sequentially as:
* [
* ...,
* position,
* red,
* green,
* blue,
* ...
* ]
* <p>
* The remainder of the array is the opacity stops sequentially as:
* [
* ...,
* position,
* opacity,
* ...
* ]
*/
@Override
public GradientColor parse(JsonReader reader, float scale)
throws IOException {
List<Float> array = new ArrayList<>();
// The array was started by Keyframe because it thought that this may be an array of keyframes
// but peek returned a number so it considered it a static array of numbers.
boolean isArray = reader.peek() == JsonReader.Token.BEGIN_ARRAY;
if (isArray) {
reader.beginArray();
}
while (reader.hasNext()) {
array.add((float) reader.nextDouble());
}
if (array.size() == 4 && array.get(0) == 1f) {
// If a gradient color only contains one color at position 1, add a second stop with the same
// color at position 0. Android's LinearGradient shader requires at least two colors.
// https://github.com/airbnb/lottie-android/issues/1967
array.set(0, 0f);
array.add(1f);
array.add(array.get(1));
array.add(array.get(2));
array.add(array.get(3));
colorPoints = 2;
}
if (isArray) {
reader.endArray();
}
if (colorPoints == -1) {
colorPoints = array.size() / 4;
}
float[] positions = new float[colorPoints];
int[] colors = new int[colorPoints];
int r = 0;
int g = 0;
for (int i = 0; i < colorPoints * 4; i++) {
int colorIndex = i / 4;
double value = array.get(i);
switch (i % 4) {
case 0:
// Positions should monotonically increase. If they don't, it can cause rendering problems on some phones.
// https://github.com/airbnb/lottie-android/issues/1675
if (colorIndex > 0 && positions[colorIndex - 1] >= (float) value) {
positions[colorIndex] = (float) value + 0.01f;
} else {
positions[colorIndex] = (float) value;
}
break;
case 1:
r = (int) (value * 255);
break;
case 2:
g = (int) (value * 255);
break;
case 3:
int b = (int) (value * 255);
colors[colorIndex] = Color.argb(255, r, g, b);
break;
}
}
GradientColor gradientColor = new GradientColor(positions, colors);
gradientColor = addOpacityStopsToGradientIfNeeded(gradientColor, array);
return gradientColor;
}
/**
* This cheats a little bit.
* Opacity stops can be at arbitrary intervals independent of color stops.
* This uses the existing color stops and modifies the opacity at each existing color stop
* based on what the opacity would be.
* <p>
* This should be a good approximation is nearly all cases. However, if there are many more
* opacity stops than color stops, information will be lost.
*/
private GradientColor addOpacityStopsToGradientIfNeeded(GradientColor gradientColor, List<Float> array) {
int startIndex = colorPoints * 4;
if (array.size() <= startIndex) {
return gradientColor;
}
// When there are opacity stops, we create a merged list of color stops and opacity stops.
// For a given color stop, we linearly interpolate the opacity for the two opacity stops around it.
// For a given opacity stop, we linearly interpolate the color for the two color stops around it.
float[] colorStopPositions = gradientColor.getPositions();
int[] colorStopColors = gradientColor.getColors();
int opacityStops = (array.size() - startIndex) / 2;
float[] opacityStopPositions = new float[opacityStops];
float[] opacityStopOpacities = new float[opacityStops];
for (int i = startIndex, j = 0; i < array.size(); i++) {
if (i % 2 == 0) {
opacityStopPositions[j] = array.get(i);
} else {
opacityStopOpacities[j] = array.get(i);
j++;
}
}
// Pre-SKIA (Oreo) devices render artifacts when there is two stops in the same position.
// As a result, we have to de-dupe the merge color and opacity stop positions.
float[] newPositions = mergeUniqueElements(gradientColor.getPositions(), opacityStopPositions);
int newColorPoints = newPositions.length;
int[] newColors = new int[newColorPoints];
for (int i = 0; i < newColorPoints; i++) {
float position = newPositions[i];
int colorStopIndex = Arrays.binarySearch(colorStopPositions, position);
int opacityIndex = Arrays.binarySearch(opacityStopPositions, position);
if (colorStopIndex < 0 || opacityIndex > 0) {
// This is a stop derived from an opacity stop.
if (opacityIndex < 0) {
// The formula here is derived from the return value for binarySearch. When an item isn't found, it returns -insertionPoint - 1.
opacityIndex = -(opacityIndex + 1);
}
newColors[i] = getColorInBetweenColorStops(position, opacityStopOpacities[opacityIndex], colorStopPositions, colorStopColors);
} else {
// This os a step derived from a color stop.
newColors[i] = getColorInBetweenOpacityStops(position, colorStopColors[colorStopIndex], opacityStopPositions, opacityStopOpacities);
}
}
return new GradientColor(newPositions, newColors);
}
private int getColorInBetweenColorStops(float position, float opacity, float[] colorStopPositions, int[] colorStopColors) {
if (colorStopColors.length < 2 || position == colorStopPositions[0]) {
return colorStopColors[0];
}
for (int i = 1; i < colorStopPositions.length; i++) {
float colorStopPosition = colorStopPositions[i];
if (colorStopPosition < position && i != colorStopPositions.length - 1) {
continue;
}
// We found the position in which position is between i - 1 and i.
float distanceBetweenColors = colorStopPositions[i] - colorStopPositions[i - 1];
float distanceToLowerColor = position - colorStopPositions[i - 1];
float percentage = distanceToLowerColor / distanceBetweenColors;
int upperColor = colorStopColors[i];
int lowerColor = colorStopColors[i - 1];
int a = (int) (opacity * 255);
int r = MiscUtils.lerp(Color.red(lowerColor), Color.red(upperColor), percentage);
int g = MiscUtils.lerp(Color.green(lowerColor), Color.green(upperColor), percentage);
int b = MiscUtils.lerp(Color.blue(lowerColor), Color.blue(upperColor), percentage);
return Color.argb(a, r, g, b);
}
throw new IllegalArgumentException("Unreachable code.");
}
private int getColorInBetweenOpacityStops(float position, int color, float[] opacityStopPositions, float[] opacityStopOpacities) {
if (opacityStopOpacities.length < 2 || position <= opacityStopPositions[0]) {
int a = (int) (opacityStopOpacities[0] * 255);
int r = Color.red(color);
int g = Color.green(color);
int b = Color.blue(color);
return Color.argb(a, r, g, b);
}
for (int i = 1; i < opacityStopPositions.length; i++) {
float opacityStopPosition = opacityStopPositions[i];
if (opacityStopPosition < position && i != opacityStopPositions.length - 1) {
continue;
}
final int a;
if (opacityStopPosition <= position) {
a = (int) (opacityStopOpacities[i] * 255);
} else {
// We found the position in which position in between i - 1 and i.
float distanceBetweenOpacities = opacityStopPositions[i] - opacityStopPositions[i - 1];
float distanceToLowerOpacity = position - opacityStopPositions[i - 1];
float percentage = distanceToLowerOpacity / distanceBetweenOpacities;
a = (int) (MiscUtils.lerp(opacityStopOpacities[i - 1], opacityStopOpacities[i], percentage) * 255);
}
int r = Color.red(color);
int g = Color.green(color);
int b = Color.blue(color);
return Color.argb(a, r, g, b);
}
throw new IllegalArgumentException("Unreachable code.");
}
/**
* Takes two sorted float arrays and merges their elements while removing duplicates.
*/
protected static float[] mergeUniqueElements(float[] arrayA, float[] arrayB) {
if (arrayA.length == 0) {
return arrayB;
} else if (arrayB.length == 0) {
return arrayA;
}
int aIndex = 0;
int bIndex = 0;
int numDuplicates = 0;
// This will be the merged list but may be longer than what is needed if there are duplicates.
// If there are, the 0 elements at the end need to be truncated.
float[] mergedNotTruncated = new float[arrayA.length + arrayB.length];
for (int i = 0; i < mergedNotTruncated.length; i++) {
final float a = aIndex < arrayA.length ? arrayA[aIndex] : Float.NaN;
final float b = bIndex < arrayB.length ? arrayB[bIndex] : Float.NaN;
if (Float.isNaN(b) || a < b) {
mergedNotTruncated[i] = a;
aIndex++;
} else if (Float.isNaN(a) || b < a) {
mergedNotTruncated[i] = b;
bIndex++;
} else {
mergedNotTruncated[i] = a;
aIndex++;
bIndex++;
numDuplicates++;
}
}
if (numDuplicates == 0) {
return mergedNotTruncated;
}
return Arrays.copyOf(mergedNotTruncated, mergedNotTruncated.length - numDuplicates);
}
}