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.