Make ColorUtils.calculateMinimumAlpha more accurate
We were previously only calculating the value to error value of 10.
Since we will only ever do a maximum number of 8 recursions, it's worth
calculating the value to a precision of 1.
Also expanded the ColorUtils API with more annotations.
BUG: 22691297
Change-Id: I7f2fbe169629f4ec53debd3dfa6748606f8e9944
diff --git a/v4/java/android/support/v4/graphics/ColorUtils.java b/v4/java/android/support/v4/graphics/ColorUtils.java
index aac809b..4d9d9b2 100644
--- a/v4/java/android/support/v4/graphics/ColorUtils.java
+++ b/v4/java/android/support/v4/graphics/ColorUtils.java
@@ -17,6 +17,10 @@
package android.support.v4.graphics;
import android.graphics.Color;
+import android.support.annotation.ColorInt;
+import android.support.annotation.FloatRange;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
/**
* A set of color-related utility methods, building upon those available in {@code Color}.
@@ -24,14 +28,14 @@
public class ColorUtils {
private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
- private static final int MIN_ALPHA_SEARCH_PRECISION = 10;
+ private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
private ColorUtils() {}
/**
* Composite two potentially translucent colors over each other and returns the result.
*/
- public static int compositeColors(int foreground, int background) {
+ public static int compositeColors(@ColorInt int foreground, @ColorInt int background) {
int bgAlpha = Color.alpha(background);
int fgAlpha = Color.alpha(foreground);
int a = compositeAlpha(fgAlpha, bgAlpha);
@@ -57,10 +61,13 @@
/**
* Returns the luminance of a color.
- *
- * Formula defined here: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
+ * <p>
+ * Formula defined
+ * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef">here</a>.
+ * </p>
*/
- public static double calculateLuminance(int color) {
+ @FloatRange(from = 0.0, to = 1.0)
+ public static double calculateLuminance(@ColorInt int color) {
double red = Color.red(color) / 255d;
red = red < 0.03928 ? red / 12.92 : Math.pow((red + 0.055) / 1.055, 2.4);
@@ -80,9 +87,10 @@
* Formula defined
* <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
*/
- public static double calculateContrast(int foreground, int background) {
+ public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) {
if (Color.alpha(background) != 255) {
- throw new IllegalArgumentException("background can not be translucent");
+ throw new IllegalArgumentException("background can not be translucent: #"
+ + Integer.toHexString(background));
}
if (Color.alpha(foreground) < 255) {
// If the foreground is translucent, composite the foreground over the background
@@ -106,10 +114,11 @@
* @param minContrastRatio the minimum contrast ratio.
* @return the alpha value in the range 0-255, or -1 if no value could be calculated.
*/
- public static int calculateMinimumAlpha(int foreground, int background,
+ public static int calculateMinimumAlpha(@ColorInt int foreground, @ColorInt int background,
float minContrastRatio) {
if (Color.alpha(background) != 255) {
- throw new IllegalArgumentException("background can not be translucent");
+ throw new IllegalArgumentException("background can not be translucent: #"
+ + Integer.toHexString(background));
}
// First lets check that a fully opaque foreground has sufficient contrast
@@ -158,7 +167,9 @@
* @param b blue component value [0..255]
* @param hsl 3 element array which holds the resulting HSL components.
*/
- public static void RGBToHSL(int r, int g, int b, float[] hsl) {
+ public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r,
+ @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
+ @NonNull float[] hsl) {
final float rf = r / 255f;
final float gf = g / 255f;
final float bf = b / 255f;
@@ -206,7 +217,7 @@
* @param color the ARGB color to convert. The alpha component is ignored.
* @param hsl 3 element array which holds the resulting HSL components.
*/
- public static void colorToHSL(int color, float[] hsl) {
+ public static void colorToHSL(@ColorInt int color, @NonNull float[] hsl) {
RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl);
}
@@ -222,7 +233,8 @@
* @param hsl 3 element array which holds the input HSL components.
* @return the resulting RGB color
*/
- public static int HSLToColor(float[] hsl) {
+ @ColorInt
+ public static int HSLToColor(@NonNull float[] hsl) {
final float h = hsl[0];
final float s = hsl[1];
final float l = hsl[2];
@@ -279,7 +291,9 @@
/**
* Set the alpha component of {@code color} to be {@code alpha}.
*/
- public static int setAlphaComponent(int color, int alpha) {
+ @ColorInt
+ public static int setAlphaComponent(@ColorInt int color,
+ @IntRange(from = 0x0, to = 0xFF) int alpha) {
if (alpha < 0 || alpha > 255) {
throw new IllegalArgumentException("alpha must be between 0 and 255.");
}
diff --git a/v4/tests/java/android/support/v4/graphics/ColorUtilsTest.java b/v4/tests/java/android/support/v4/graphics/ColorUtilsTest.java
index 855cdba..56cb6fb 100644
--- a/v4/tests/java/android/support/v4/graphics/ColorUtilsTest.java
+++ b/v4/tests/java/android/support/v4/graphics/ColorUtilsTest.java
@@ -17,9 +17,9 @@
package android.support.v4.graphics;
import android.graphics.Color;
-import android.support.v4.graphics.ColorUtils;
import android.test.AndroidTestCase;
+import java.lang.Integer;
import java.util.ArrayList;
/**
@@ -31,21 +31,31 @@
private static final float ALLOWED_OFFSET_HUE = 360 * 0.005f;
private static final float ALLOWED_OFFSET_SATURATION = 0.005f;
private static final float ALLOWED_OFFSET_LIGHTNESS = 0.005f;
+ private static final float ALLOWED_OFFSET_MIN_ALPHA = 0.01f;
private static final int ALLOWED_OFFSET_RGB_COMPONENT = 2;
private static final ArrayList<TestEntry> sEntryList = new ArrayList<>();
static {
- sEntryList.add(new TestEntry(Color.BLACK).setHsl(0f, 0f, 0f));
- sEntryList.add(new TestEntry(Color.WHITE).setHsl(0f, 0f, 1f));
- sEntryList.add(new TestEntry(Color.BLUE).setHsl(240f, 1f, 0.5f));
- sEntryList.add(new TestEntry(Color.GREEN).setHsl(120f, 1f, 0.5f));
- sEntryList.add(new TestEntry(Color.RED).setHsl(0f, 1f, 0.5f));
- sEntryList.add(new TestEntry(Color.CYAN).setHsl(180f, 1f, 0.5f));
- sEntryList.add(new TestEntry(0x2196F3).setHsl(207f, 0.9f, 0.54f));
- sEntryList.add(new TestEntry(0xD1C4E9).setHsl(261f, 0.46f, 0.84f));
- sEntryList.add(new TestEntry(0x311B92).setHsl(251.09f, 0.687f, 0.339f));
+ sEntryList.add(new TestEntry(Color.BLACK).setHsl(0f, 0f, 0f)
+ .setWhiteMinAlpha30(0.35f).setWhiteMinAlpha45(0.46f));
+ sEntryList.add(new TestEntry(Color.WHITE).setHsl(0f, 0f, 1f)
+ .setBlackMinAlpha30(0.42f).setBlackMinAlpha45(0.54f));
+ sEntryList.add(new TestEntry(Color.BLUE).setHsl(240f, 1f, 0.5f)
+ .setWhiteMinAlpha30(0.55f).setWhiteMinAlpha45(0.71f));
+ sEntryList.add(new TestEntry(Color.GREEN).setHsl(120f, 1f, 0.5f)
+ .setBlackMinAlpha30(0.43f).setBlackMinAlpha45(0.55f));
+ sEntryList.add(new TestEntry(Color.RED).setHsl(0f, 1f, 0.5f)
+ .setWhiteMinAlpha30(0.84f).setBlackMinAlpha30(0.55f).setBlackMinAlpha45(0.78f));
+ sEntryList.add(new TestEntry(Color.CYAN).setHsl(180f, 1f, 0.5f)
+ .setBlackMinAlpha30(0.43f).setBlackMinAlpha45(0.55f));
+ sEntryList.add(new TestEntry(0xFF2196F3).setHsl(207f, 0.9f, 0.54f)
+ .setBlackMinAlpha30(0.52f).setWhiteMinAlpha30(0.97f).setBlackMinAlpha45(0.7f));
+ sEntryList.add(new TestEntry(0xFFD1C4E9).setHsl(261f, 0.46f, 0.84f)
+ .setBlackMinAlpha30(0.45f).setBlackMinAlpha45(0.58f));
+ sEntryList.add(new TestEntry(0xFF311B92).setHsl(251.09f, 0.687f, 0.339f)
+ .setWhiteMinAlpha30(0.39f).setWhiteMinAlpha45(0.54f));
}
public void testToHSL() {
@@ -72,6 +82,28 @@
}
}
+ public void testMinAlphas() {
+ for (TestEntry entry : sEntryList) {
+ testMinAlpha("Black title", entry.rgb, entry.blackMinAlpha30,
+ ColorUtils.calculateMinimumAlpha(Color.BLACK, entry.rgb, 3.0f));
+ testMinAlpha("Black body", entry.rgb, entry.blackMinAlpha45,
+ ColorUtils.calculateMinimumAlpha(Color.BLACK, entry.rgb, 4.5f));
+ testMinAlpha("White title", entry.rgb, entry.whiteMinAlpha30,
+ ColorUtils.calculateMinimumAlpha(Color.WHITE, entry.rgb, 3.0f));
+ testMinAlpha("White body", entry.rgb, entry.whiteMinAlpha45,
+ ColorUtils.calculateMinimumAlpha(Color.WHITE, entry.rgb, 4.5f));
+ }
+ }
+
+ private static void testMinAlpha(String title, int color, float expected, int actual) {
+ final String message = title + " text within error for #" + Integer.toHexString(color);
+ if (expected < 0) {
+ assertEquals(message, actual, -1);
+ } else {
+ assertClose(message, expected, actual / 255f, ALLOWED_OFFSET_MIN_ALPHA);
+ }
+ }
+
private static void assertClose(String message, float expected, float actual,
float allowedOffset) {
StringBuilder sb = new StringBuilder(message);
@@ -114,6 +146,10 @@
private static class TestEntry {
final int rgb;
final float[] hsl = new float[3];
+ float blackMinAlpha45 = -1;
+ float blackMinAlpha30 = -1;
+ float whiteMinAlpha45 = -1;
+ float whiteMinAlpha30 = -1;
TestEntry(int rgb) {
this.rgb = rgb;
@@ -125,5 +161,25 @@
hsl[2] = l;
return this;
}
+
+ TestEntry setBlackMinAlpha30(float minAlpha) {
+ blackMinAlpha30 = minAlpha;
+ return this;
+ }
+
+ TestEntry setBlackMinAlpha45(float minAlpha) {
+ blackMinAlpha45 = minAlpha;
+ return this;
+ }
+
+ TestEntry setWhiteMinAlpha30(float minAlpha) {
+ whiteMinAlpha30 = minAlpha;
+ return this;
+ }
+
+ TestEntry setWhiteMinAlpha45(float minAlpha) {
+ whiteMinAlpha45 = minAlpha;
+ return this;
+ }
}
}
\ No newline at end of file