Merge changes I53e2fc68,Id24344ce into androidx-main
* changes:
Sort color stops when reading a SweepGradient.
Interpolate colors linearly to align with interpolation done by the shader.
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/color.proto b/wear/protolayout/protolayout-proto/src/main/proto/color.proto
index 1addb4d..ae81664 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/color.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/color.proto
@@ -55,13 +55,10 @@
// The offset is the relative position of the color, beginning with 0 from the
// start angle and ending with 1.0 at the end angle, spanning clockwise.
//
- // There must be at least 2 colors.
+ // There must be at least 2 colors and at most 10 colors.
//
// If offset values are not set, the colors are evenly distributed in the
// gradient.
- //
- // If the offset values are not monotonic, the drawing may produce unexpected
- // results.
repeated ColorStop color_stops = 1;
// The start angle of the gradient relative to the element's base angle. If
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineView.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineView.java
index 89536f6..0fc61ed 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineView.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineView.java
@@ -18,10 +18,10 @@
import static java.lang.Math.min;
-import android.animation.ArgbEvaluator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
@@ -339,9 +339,7 @@
*/
private static final float CAP_COLOR_SHADER_OFFSET_SIZE = 0.25f;
- private final ArgbEvaluator argbEvaluator = new ArgbEvaluator();
-
- @NonNull List<AngularColorStop> colorStops;
+ @NonNull private final List<AngularColorStop> colorStops;
SweepGradientHelper(@NonNull ColorProto.SweepGradient sweepGradProto) {
int numColors = sweepGradProto.getColorStopsCount();
@@ -376,21 +374,47 @@
float gradAngle = gradStartAngle + offset * (gradEndAngle - gradStartAngle);
colorStops.add(new AngularColorStop(gradAngle, stop.getColor().getArgb()));
}
+
+ if (offsetsRequired) {
+ colorStops.sort((a, b) -> Float.compare(a.angle, b.angle));
+ }
}
+ /**
+ * Interpolates colors linearly. Color interpolation needs to be done accordingly to the
+ * underlying SweepGradient shader implementation so that all color transitions are smooth
+ * and static.
+ *
+ * <p>The ArgbEvaluator class applies gamma correction to colors which results in a
+ * different behavior compared to the shader's native implementation.
+ */
@ColorInt
@VisibleForTesting
int interpolateColors(
- int color1, float angle1, int color2, float angle2, float targetAngle) {
- if (angle1 == angle2) {
- return color1;
+ int startColor, float startAngle, int endColor, float endAngle, float targetAngle) {
+ if (startAngle == endAngle) {
+ return startColor;
}
- float fraction = (targetAngle - angle1) / (angle2 - angle1);
+ float fraction = (targetAngle - startAngle) / (endAngle - startAngle);
if (Float.isInfinite(fraction)) {
- return color1;
+ return startColor;
}
- // TODO(lucasmo): perform linear interpolation to match what's done in the shader.
- return (int) argbEvaluator.evaluate(fraction, color1, color2);
+
+ float startA = Color.alpha(startColor);
+ float startR = Color.red(startColor);
+ float startG = Color.green(startColor);
+ float startB = Color.blue(startColor);
+
+ float endA = Color.alpha(endColor);
+ float endR = Color.red(endColor);
+ float endG = Color.green(endColor);
+ float endB = Color.blue(endColor);
+
+ int a = (int) (startA + fraction * (endA - startA));
+ int r = (int) (startR + fraction * (endR - startR));
+ int g = (int) (startG + fraction * (endG - startG));
+ int b = (int) (startB + fraction * (endB - startB));
+ return Color.argb(a, r, g, b);
}
/**
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineViewTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineViewTest.java
index 369ce46..72db5aa 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineViewTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/WearCurvedLineViewTest.java
@@ -1,12 +1,14 @@
package androidx.wear.protolayout.renderer.inflater;
import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertThrows;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.Shader;
+
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.wear.protolayout.proto.ColorProto.ColorProp;
@@ -16,194 +18,208 @@
import androidx.wear.protolayout.proto.TypesProto.FloatProp;
import androidx.wear.protolayout.renderer.inflater.WearCurvedLineView.ArcSegment.CapPosition;
import androidx.wear.protolayout.renderer.inflater.WearCurvedLineView.SweepGradientHelper;
+
import com.google.common.truth.Expect;
+
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class WearCurvedLineViewTest {
- @Rule public final Expect expect = Expect.create();
+ @Rule public final Expect expect = Expect.create();
- @NonNull RectF testBounds = new RectF(0f, 10f, 100f, 200f);
+ @NonNull RectF testBounds = new RectF(0f, 10f, 100f, 200f);
- @Test
- public void sweepGradientHelper_tooFewColors_throws() {
- SweepGradient sgProto =
- SweepGradient.newBuilder().addColorStops(colorStop(Color.RED, 0f)).build();
- assertThrows(
- IllegalArgumentException.class,
- () -> {
- SweepGradientHelper unused = new SweepGradientHelper(sgProto);
- });
- }
-
- @Test
- public void sweepGradientHelper_tooManyColors_throws() {
- int numColors = 50;
- SweepGradient.Builder sgBuilder = SweepGradient.newBuilder();
- for (int i = 0; i < numColors; i++) {
- sgBuilder.addColorStops(colorStop(Color.RED + i, (float) i / numColors));
+ @Test
+ public void sweepGradientHelper_tooFewColors_throws() {
+ SweepGradient sgProto =
+ SweepGradient.newBuilder().addColorStops(colorStop(Color.RED, 0f)).build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradientHelper unused = new SweepGradientHelper(sgProto);
+ });
}
- assertThrows(
- IllegalArgumentException.class,
- () -> {
- SweepGradientHelper unused = new SweepGradientHelper(sgBuilder.build());
- });
- }
+ @Test
+ public void sweepGradientHelper_tooManyColors_throws() {
+ int numColors = 50;
+ SweepGradient.Builder sgBuilder = SweepGradient.newBuilder();
+ for (int i = 0; i < numColors; i++) {
+ sgBuilder.addColorStops(colorStop(Color.RED + i, (float) i / numColors));
+ }
- @Test
- public void sweepGradientHelper_missingOffsets_throws() {
- SweepGradient sgProto =
- SweepGradient.newBuilder()
- .addColorStops(colorStop(Color.RED, 0f))
- .addColorStops(colorStop(Color.BLUE))
- .addColorStops(colorStop(Color.GREEN, 1f))
- .build();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradientHelper unused = new SweepGradientHelper(sgBuilder.build());
+ });
+ }
- assertThrows(
- IllegalArgumentException.class,
- () -> {
- SweepGradientHelper unused = new SweepGradientHelper(sgProto);
- });
- }
+ @Test
+ public void sweepGradientHelper_missingOffsets_throws() {
+ SweepGradient sgProto =
+ SweepGradient.newBuilder()
+ .addColorStops(colorStop(Color.RED, 0f))
+ .addColorStops(colorStop(Color.BLUE))
+ .addColorStops(colorStop(Color.GREEN, 1f))
+ .build();
- @Test
- public void sweepGradientHelper_getShader_invalidAngleSpan_throws() {
- SweepGradientHelper sgHelper = new SweepGradientHelper(basicSweepGradientProto());
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ SweepGradientHelper unused = new SweepGradientHelper(sgProto);
+ });
+ }
- float startAngle = 10f;
- float endAngle = startAngle + 400f;
+ @Test
+ public void sweepGradientHelper_getShader_invalidAngleSpan_throws() {
+ SweepGradientHelper sgHelper = new SweepGradientHelper(basicSweepGradientProto());
- assertThrows(
- IllegalArgumentException.class,
- () -> sgHelper.getShader(testBounds, startAngle, endAngle, 0f, CapPosition.NONE));
- }
+ float startAngle = 10f;
+ float endAngle = startAngle + 400f;
- @Test
- public void sweepGradientHelper_getColorAtSetOffsets() {
- // Gradient with colors [Red, Blue, Green] at offsets [0, 0.5, 1]
- SweepGradientHelper sgHelper = new SweepGradientHelper(basicSweepGradientProto());
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> sgHelper.getShader(testBounds, startAngle, endAngle, 0f, CapPosition.NONE));
+ }
- expect.that(sgHelper.getColor(0f)).isEqualTo(Color.RED);
- expect.that(sgHelper.getColor(180f)).isEqualTo(Color.BLUE);
- expect.that(sgHelper.getColor(360f)).isEqualTo(Color.GREEN);
- }
+ @Test
+ public void sweepGradientHelper_getColorAtSetOffsets() {
+ // Gradient with colors [Red, Blue, Green] at offsets [0, 0.5, 1]
+ SweepGradientHelper sgHelper = new SweepGradientHelper(basicSweepGradientProto());
- @Test
- public void sweepGradientHelper_getColor_customStartAndEndAngles() {
- float startAngle = 180f;
- float endAngle = 720f;
+ expect.that(sgHelper.getColor(0f)).isEqualTo(Color.RED);
+ expect.that(sgHelper.getColor(180f)).isEqualTo(Color.BLUE);
+ expect.that(sgHelper.getColor(360f)).isEqualTo(Color.GREEN);
+ }
- // Gradient with colors [Red, Blue, Green] at offsets [0, 0.5, 1]
- SweepGradientHelper sgHelper =
- new SweepGradientHelper(basicSweepGradientProto(startAngle, endAngle));
+ @Test
+ public void sweepGradientHelper_unsortedStops_getColorAtSetOffsets() {
+ // Gradient with colors [Red, Green, Blue] at offsets [0, 1, 0.5]
+ SweepGradient gradProto =
+ SweepGradient.newBuilder()
+ .addColorStops(colorStop(Color.RED, 0f))
+ .addColorStops(colorStop(Color.GREEN, 1f))
+ .addColorStops(colorStop(Color.BLUE, 0.5f))
+ .build();
+ SweepGradientHelper sgHelper = new SweepGradientHelper(gradProto);
- // RED before and at startAngle.
- expect.that(sgHelper.getColor(0f)).isEqualTo(Color.RED);
- expect.that(sgHelper.getColor(startAngle)).isEqualTo(Color.RED);
- // BLUE in the middle angle.
- expect.that(sgHelper.getColor((startAngle + endAngle) / 2f)).isEqualTo(Color.BLUE);
- // GREEN at the endAngle and after.
- expect.that(sgHelper.getColor(endAngle)).isEqualTo(Color.GREEN);
- expect.that(sgHelper.getColor(888f)).isEqualTo(Color.GREEN);
- }
+ expect.that(sgHelper.getColor(0f)).isEqualTo(Color.RED);
+ expect.that(sgHelper.getColor(180f)).isEqualTo(Color.BLUE);
+ expect.that(sgHelper.getColor(360f)).isEqualTo(Color.GREEN);
+ }
- @Test
- public void sweepGradientHelper_getInterpolatedColor() {
- SweepGradientHelper sgHelper = new SweepGradientHelper(basicSweepGradientProto());
+ @Test
+ public void sweepGradientHelper_getColor_customStartAndEndAngles() {
+ float startAngle = 180f;
+ float endAngle = 720f;
- float angle1 = 90f;
- expect
- .that(sgHelper.getColor(angle1))
- .isEqualTo(sgHelper.interpolateColors(Color.RED, 0f, Color.BLUE, 180f, angle1));
+ // Gradient with colors [Red, Blue, Green] at offsets [0, 0.5, 1]
+ SweepGradientHelper sgHelper =
+ new SweepGradientHelper(basicSweepGradientProto(startAngle, endAngle));
- float angle2 = 213f;
- expect
- .that(sgHelper.getColor(angle2))
- .isEqualTo(sgHelper.interpolateColors(Color.BLUE, 180f, Color.GREEN, 360f, angle2));
- }
+ // RED before and at startAngle.
+ expect.that(sgHelper.getColor(0f)).isEqualTo(Color.RED);
+ expect.that(sgHelper.getColor(startAngle)).isEqualTo(Color.RED);
+ // BLUE in the middle angle.
+ expect.that(sgHelper.getColor((startAngle + endAngle) / 2f)).isEqualTo(Color.BLUE);
+ // GREEN at the endAngle and after.
+ expect.that(sgHelper.getColor(endAngle)).isEqualTo(Color.GREEN);
+ expect.that(sgHelper.getColor(888f)).isEqualTo(Color.GREEN);
+ }
- @Test
- public void sweepGradientHelper_getInterpolatedColor_noOffsets() {
- SweepGradient sgProto =
- SweepGradient.newBuilder()
- .addColorStops(colorStop(Color.RED))
- .addColorStops(colorStop(Color.BLUE))
- .addColorStops(colorStop(Color.GREEN))
- .build();
- SweepGradientHelper sgHelper = new SweepGradientHelper(sgProto);
+ @Test
+ public void sweepGradientHelper_getInterpolatedColor() {
+ SweepGradientHelper sgHelper = new SweepGradientHelper(basicSweepGradientProto());
- float angle1 = 90f;
- expect
- .that(sgHelper.getColor(angle1))
- .isEqualTo(sgHelper.interpolateColors(Color.RED, 0f, Color.BLUE, 180f, angle1));
+ float angle1 = 90f;
+ expect.that(sgHelper.getColor(angle1))
+ .isEqualTo(sgHelper.interpolateColors(Color.RED, 0f, Color.BLUE, 180f, angle1));
- float angle2 = 213f;
- expect
- .that(sgHelper.getColor(angle2))
- .isEqualTo(sgHelper.interpolateColors(Color.BLUE, 180f, Color.GREEN, 360f, angle2));
- }
+ float angle2 = 213f;
+ expect.that(sgHelper.getColor(angle2))
+ .isEqualTo(sgHelper.interpolateColors(Color.BLUE, 180f, Color.GREEN, 360f, angle2));
+ }
- @Test
- public void sweepGradientHelper_shaderIsRotated() {
- SweepGradientHelper sgHelper = new SweepGradientHelper(basicSweepGradientProto());
- float rotationAngle = 63f;
- Matrix rotatedMatrix = new Matrix();
- rotatedMatrix.postRotate(
- rotationAngle,
- (testBounds.left + testBounds.right) / 2f,
- (testBounds.top + testBounds.bottom) / 2f);
+ @Test
+ public void sweepGradientHelper_getInterpolatedColor_noOffsets() {
+ SweepGradient sgProto =
+ SweepGradient.newBuilder()
+ .addColorStops(colorStop(Color.RED))
+ .addColorStops(colorStop(Color.BLUE))
+ .addColorStops(colorStop(Color.GREEN))
+ .build();
+ SweepGradientHelper sgHelper = new SweepGradientHelper(sgProto);
- Shader generatedShader =
- sgHelper.getShader(testBounds, 180f, 360f, rotationAngle, CapPosition.NONE);
- assertThat(generatedShader).isInstanceOf(android.graphics.SweepGradient.class);
- Matrix generatedMatrix = new Matrix();
- generatedShader.getLocalMatrix(generatedMatrix);
- assertThat(rotatedMatrix).isEqualTo(generatedMatrix);
- }
+ float angle1 = 90f;
+ expect.that(sgHelper.getColor(angle1))
+ .isEqualTo(sgHelper.interpolateColors(Color.RED, 0f, Color.BLUE, 180f, angle1));
- /** Gradient with colors [Red, Blue, Green] at offsets [0, 0.5, 1] and given angles. */
- private SweepGradient basicSweepGradientProto(float startAngle, float endAngle) {
- return SweepGradient.newBuilder()
- .addColorStops(colorStop(Color.RED, 0f))
- .addColorStops(colorStop(Color.BLUE, 0.5f))
- .addColorStops(colorStop(Color.GREEN, 1f))
- .setStartAngle(degrees(startAngle))
- .setEndAngle(degrees(endAngle))
- .build();
- }
+ float angle2 = 213f;
+ expect.that(sgHelper.getColor(angle2))
+ .isEqualTo(sgHelper.interpolateColors(Color.BLUE, 180f, Color.GREEN, 360f, angle2));
+ }
- /** Gradient with colors [Red, Blue, Green] at offsets [0, 0.5, 1]. */
- private SweepGradient basicSweepGradientProto() {
- return SweepGradient.newBuilder()
- .addColorStops(colorStop(Color.RED, 0f))
- .addColorStops(colorStop(Color.BLUE, 0.5f))
- .addColorStops(colorStop(Color.GREEN, 1f))
- .build();
- }
+ @Test
+ public void sweepGradientHelper_shaderIsRotated() {
+ SweepGradientHelper sgHelper = new SweepGradientHelper(basicSweepGradientProto());
+ float rotationAngle = 63f;
+ Matrix rotatedMatrix = new Matrix();
+ rotatedMatrix.postRotate(
+ rotationAngle,
+ (testBounds.left + testBounds.right) / 2f,
+ (testBounds.top + testBounds.bottom) / 2f);
- private ColorProp staticColor(int value) {
- return ColorProp.newBuilder().setArgb(value).build();
- }
+ Shader generatedShader =
+ sgHelper.getShader(testBounds, 180f, 360f, rotationAngle, CapPosition.NONE);
+ assertThat(generatedShader).isInstanceOf(android.graphics.SweepGradient.class);
+ Matrix generatedMatrix = new Matrix();
+ generatedShader.getLocalMatrix(generatedMatrix);
+ assertThat(rotatedMatrix).isEqualTo(generatedMatrix);
+ }
- private FloatProp staticFloat(float value) {
- return FloatProp.newBuilder().setValue(value).build();
- }
+ /** Gradient with colors [Red, Blue, Green] at offsets [0, 0.5, 1] and given angles. */
+ private SweepGradient basicSweepGradientProto(float startAngle, float endAngle) {
+ return SweepGradient.newBuilder()
+ .addColorStops(colorStop(Color.RED, 0f))
+ .addColorStops(colorStop(Color.BLUE, 0.5f))
+ .addColorStops(colorStop(Color.GREEN, 1f))
+ .setStartAngle(degrees(startAngle))
+ .setEndAngle(degrees(endAngle))
+ .build();
+ }
- private DegreesProp degrees(float value) {
- return DegreesProp.newBuilder().setValue(value).build();
- }
+ /** Gradient with colors [Red, Blue, Green] at offsets [0, 0.5, 1]. */
+ private SweepGradient basicSweepGradientProto() {
+ return SweepGradient.newBuilder()
+ .addColorStops(colorStop(Color.RED, 0f))
+ .addColorStops(colorStop(Color.BLUE, 0.5f))
+ .addColorStops(colorStop(Color.GREEN, 1f))
+ .build();
+ }
- private ColorStop colorStop(int color, float offset) {
- return ColorStop.newBuilder()
- .setColor(staticColor(color))
- .setOffset(staticFloat(offset))
- .build();
- }
+ private ColorProp staticColor(int value) {
+ return ColorProp.newBuilder().setArgb(value).build();
+ }
- private ColorStop colorStop(int color) {
- return ColorStop.newBuilder().setColor(staticColor(color)).build();
- }
+ private FloatProp staticFloat(float value) {
+ return FloatProp.newBuilder().setValue(value).build();
+ }
+
+ private DegreesProp degrees(float value) {
+ return DegreesProp.newBuilder().setValue(value).build();
+ }
+
+ private ColorStop colorStop(int color, float offset) {
+ return ColorStop.newBuilder()
+ .setColor(staticColor(color))
+ .setOffset(staticFloat(offset))
+ .build();
+ }
+
+ private ColorStop colorStop(int color) {
+ return ColorStop.newBuilder().setColor(staticColor(color)).build();
+ }
}
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ColorBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ColorBuilders.java
index 9060fc0..f08e26b 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ColorBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ColorBuilders.java
@@ -387,12 +387,10 @@
* relative position of the color, beginning with 0 from the start angle and ending with 1.0
* at the end angle, spanning clockwise.
*
- * <p>There must be at least 2 colors.
+ * <p>There must be at least 2 colors and at most 10 colors.
*
* <p>If offset values are not set, the colors are evenly distributed in the gradient.
*
- * <p>If the offset values are not monotonic, the drawing may produce unexpected results.
- *
* @since 1.3
*/
@NonNull
@@ -505,13 +503,10 @@
* the relative position of the color, beginning with 0 from the start angle and ending
* with 1.0 at the end angle, spanning clockwise.
*
- * <p>There must be at least 2 colors.
+ * <p>There must be at least 2 colors and at most 10 colors.
*
* <p>If offset values are not set, the colors are evenly distributed in the gradient.
*
- * <p>If the offset values are not monotonic, the drawing may produce unexpected
- * results.
- *
* @since 1.3
*/
@NonNull
@@ -582,8 +577,6 @@
* is the relative position of the color, beginning with 0 from the start angle and
* ending with 1.0 at the end angle, spanning clockwise.
* <p>If offsets are not set, the colors are evenly distributed in the gradient.
- * <p>If the offset values are not monotonic, the drawing may produce unexpected
- * results.
* @throws IllegalArgumentException if the number of colors is less than 2 or larger
* than 10.
* @throws IllegalArgumentException if offsets in {@code colorStops} are partially set.