diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/GradientColorParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/GradientColorParser.java index 0dbfb5b76..50a52e3f3 100644 --- a/lottie/src/main/java/com/airbnb/lottie/parser/GradientColorParser.java +++ b/lottie/src/main/java/com/airbnb/lottie/parser/GradientColorParser.java @@ -142,14 +142,12 @@ private GradientColor addOpacityStopsToGradientIfNeeded(GradientColor gradientCo } } - int newColorPoints = colorPoints + opacityStops; - float[] newPositions = new float[newColorPoints]; + // 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]; - System.arraycopy(gradientColor.getPositions(), 0, newPositions, 0, colorPoints); - System.arraycopy(opacityStopPositions, 0, newPositions, colorPoints, opacityStops); - Arrays.sort(newPositions); - for (int i = 0; i < newColorPoints; i++) { float position = newPositions[i]; int colorStopIndex = Arrays.binarySearch(colorStopPositions, position); @@ -223,4 +221,46 @@ private int getColorInBetweenOpacityStops(float position, int color, float[] opa } 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); + } } \ No newline at end of file diff --git a/lottie/src/test/java/com/airbnb/lottie/parser/GradientColorParserTest.java b/lottie/src/test/java/com/airbnb/lottie/parser/GradientColorParserTest.java new file mode 100644 index 000000000..79682e167 --- /dev/null +++ b/lottie/src/test/java/com/airbnb/lottie/parser/GradientColorParserTest.java @@ -0,0 +1,32 @@ +package com.airbnb.lottie.parser; + +import static org.junit.Assert.assertArrayEquals; + +import org.junit.Test; + +public class GradientColorParserTest { + + @Test public void testNoDistinctShort() { + assertMerged(new float[]{1}, new float[]{2}, new float[]{1, 2}); + } + + @Test public void testNoDistinct() { + assertMerged(new float[]{1, 2, 3}, new float[]{4, 5, 6}, new float[]{1, 2, 3, 4, 5, 6}); + } + + @Test public void testWithDistinct() { + assertMerged(new float[]{1, 2, 3, 5}, new float[]{4, 5, 6}, new float[]{1, 2, 3, 4, 5, 6}); + } + + @Test public void testWithDistinctInterleavingValues() { + assertMerged(new float[]{2, 4, 5, 6, 8, 10}, new float[]{1, 3, 4, 5, 7, 9}, new float[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); + } + + @Test public void testIdentical() { + assertMerged(new float[]{2, 3}, new float[]{2, 3}, new float[]{2, 3}); + } + + private void assertMerged(float[] arrayA, float[] arrayB, float[] merged) { + assertArrayEquals(merged, GradientColorParser.mergeUniqueElements(arrayA, arrayB), 0f); + } +} \ No newline at end of file