| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.graphics.cts; |
| |
| import static androidx.test.InstrumentationRegistry.getInstrumentation; |
| |
| import static com.google.common.truth.Truth.assertWithMessage; |
| |
| import android.R; |
| import android.content.Context; |
| import android.graphics.Color; |
| import android.graphics.cts.utils.Cam; |
| import android.util.Pair; |
| |
| |
| import androidx.annotation.ColorInt; |
| import androidx.core.graphics.ColorUtils; |
| import androidx.test.filters.SmallTest; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import org.junit.Assert; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| |
| |
| @SmallTest |
| @RunWith(AndroidJUnit4.class) |
| public class SystemPaletteTest { |
| |
| // Hue goes from 0 to 360 |
| private static final int MAX_HUE_DISTANCE = 15; |
| |
| @Test |
| public void testShades0and1000() { |
| final Context context = getInstrumentation().getTargetContext(); |
| final int[] leftmostColors = new int[]{ |
| R.color.system_neutral1_0, R.color.system_neutral2_0, R.color.system_accent1_0, |
| R.color.system_accent2_0, R.color.system_accent3_0 |
| }; |
| final int[] rightmostColors = new int[]{ |
| R.color.system_neutral1_1000, R.color.system_neutral2_1000, |
| R.color.system_accent1_1000, R.color.system_accent2_1000, |
| R.color.system_accent3_1000 |
| }; |
| for (int i = 0; i < leftmostColors.length; i++) { |
| assertColor(context.getColor(leftmostColors[0]), Color.WHITE); |
| } |
| for (int i = 0; i < rightmostColors.length; i++) { |
| assertColor(context.getColor(rightmostColors[0]), Color.BLACK); |
| } |
| } |
| |
| @Test |
| public void testAllColorsBelongToSameFamily() { |
| final Context context = getInstrumentation().getTargetContext(); |
| List<int[]> allPalettes = Arrays.asList(getAllAccent1Colors(context), |
| getAllAccent2Colors(context), getAllAccent3Colors(context), |
| getAllNeutral1Colors(context), getAllNeutral2Colors(context)); |
| |
| final float[] tones = {100, 99, 95, 90, 80, 70, 60, 49, 40, 30, 20, 10, 0}; |
| for (int[] palette : allPalettes) { |
| // Determine the median hue of the palette. Each color in the palette colors will have |
| // its hue measured against the median hue. If the difference is too large, the test |
| // fails. |
| List<Float> hues = new ArrayList<>(); |
| for (int i = 0; i < palette.length - 1; i++) { |
| // Avoid measuring hue of colors above 90 or below 10 in tone. |
| // |
| // Converting from HCT to sRGB from display quantizes colors - i.e. not every |
| // HCT color can be expressed in sRGB. As colors approach the extreme tones, white |
| // at 100 and black at 0, hues begin overlapping overlay - made up example: hues |
| // 110 to 128 at tone 95, when mapped to sRGB for display, all end up being measured |
| // as hue 114. |
| final float tone = tones[i]; |
| if (tone <= 10.0 || tone > 90.0) { |
| continue; |
| } |
| final Cam cam = Cam.fromInt(palette[i]); |
| hues.add(cam.getHue()); |
| } |
| Collections.sort(hues); |
| final float medianHue = hues.get(hues.size() / 2); |
| |
| // Measure the hue of each color in the palette against the median hue. |
| for (int i = 0; i < palette.length - 1; i++) { |
| final float tone = tones[i]; |
| // Skip testing hue of extreme tones, due to overlap due to quantization that occurs |
| // when converting from HCT to sRGB for display. |
| if (tone <= 10.0 || tone > 90.0) { |
| continue; |
| } |
| final Cam cam = Cam.fromInt(palette[i]); |
| final float hue = cam.getHue(); |
| final boolean hueWithinTolerance = deltaHueWithinTolerance(hue, medianHue); |
| assertWithMessage("Color " + toHctString(cam) |
| + " has different hue compared to median hue " + Math.round(medianHue) |
| + " of palette: " + Arrays.toString(palette)) |
| .that(hueWithinTolerance).isTrue(); |
| } |
| } |
| } |
| |
| private static String toHctString(Cam cam) { |
| final double[] labColor = new double[3]; |
| ColorUtils.colorToLAB(cam.viewedInSrgb(), labColor); |
| return "H" + Math.round(cam.getHue()) + " C" + Math.round(cam.getChroma()) + " T" |
| + Math.round(labColor[0]); |
| } |
| |
| /** |
| * Compare if color A and B have similar hue, in gCAM space. |
| * |
| * @param colorA Color 1 |
| * @param colorB Color 2 |
| * @return True when colors have similar hue. |
| */ |
| private boolean deltaHueWithinTolerance(float hueA, float hueB) { |
| |
| float hue1 = Math.max(hueA, hueB); |
| float hue2 = Math.min(hueA, hueB); |
| |
| float diffDegrees = 180.0f - Math.abs(Math.abs(hue1 - hue2) - 180.0f); |
| return diffDegrees < MAX_HUE_DISTANCE; |
| } |
| |
| @Test |
| public void testColorsMatchExpectedLuminance() { |
| final Context context = getInstrumentation().getTargetContext(); |
| List<int[]> allPalettes = Arrays.asList(getAllAccent1Colors(context), |
| getAllAccent2Colors(context), getAllAccent3Colors(context), |
| getAllNeutral1Colors(context), getAllNeutral2Colors(context)); |
| |
| final double[] labColor = new double[3]; |
| final double[] expectedL = {100, 99, 95, 90, 80, 70, 60, 49, 40, 30, 20, 10, 0}; |
| |
| for (int[] palette : allPalettes) { |
| for (int i = 0; i < palette.length; i++) { |
| ColorUtils.colorToLAB(palette[i], labColor); |
| |
| // Colors in the same palette should vary mostly in L, decreasing lightness as we |
| // move across the palette. |
| assertWithMessage("Color " + Integer.toHexString((palette[i])) |
| + " at index " + i + " should have L " + expectedL[i] + " in LAB space.") |
| .that(labColor[0]).isWithin(3).of(expectedL[i]); |
| } |
| } |
| } |
| |
| @Test |
| public void testContrastRatio() { |
| final Context context = getInstrumentation().getTargetContext(); |
| |
| final List<Pair<Integer, Integer>> atLeast4dot5 = Arrays.asList(new Pair<>(0, 500), |
| new Pair<>(50, 600), new Pair<>(100, 600), new Pair<>(200, 700), |
| new Pair<>(300, 800), new Pair<>(400, 900), new Pair<>(500, 1000)); |
| final List<Pair<Integer, Integer>> atLeast3dot0 = Arrays.asList(new Pair<>(0, 400), |
| new Pair<>(50, 500), new Pair<>(100, 500), new Pair<>(200, 600), |
| new Pair<>(300, 700), new Pair<>(400, 800), new Pair<>(500, 900), |
| new Pair<>(600, 1000)); |
| |
| List<int[]> allPalettes = Arrays.asList(getAllAccent1Colors(context), |
| getAllAccent2Colors(context), getAllAccent3Colors(context), |
| getAllNeutral1Colors(context), getAllNeutral2Colors(context)); |
| |
| for (int[] palette : allPalettes) { |
| for (Pair<Integer, Integer> shades : atLeast4dot5) { |
| final int background = palette[shadeToArrayIndex(shades.first)]; |
| final int foreground = palette[shadeToArrayIndex(shades.second)]; |
| final double contrast = ColorUtils.calculateContrast(foreground, background); |
| assertWithMessage("Shade " + shades.first + " (#" + Integer.toHexString(background) |
| + ") should have at least 4.5 contrast ratio against " + shades.second |
| + " (#" + Integer.toHexString(foreground) + ")").that(contrast) |
| .isGreaterThan(4.5); |
| } |
| |
| for (Pair<Integer, Integer> shades : atLeast3dot0) { |
| final int background = palette[shadeToArrayIndex(shades.first)]; |
| final int foreground = palette[shadeToArrayIndex(shades.second)]; |
| final double contrast = ColorUtils.calculateContrast(foreground, background); |
| assertWithMessage("Shade " + shades.first + " (#" + Integer.toHexString(background) |
| + ") should have at least 3.0 contrast ratio against " + shades.second |
| + " (#" + Integer.toHexString(foreground) + ")").that(contrast) |
| .isGreaterThan(3); |
| } |
| } |
| } |
| |
| /** |
| * Convert the Material shade to an array position. |
| * |
| * @param shade Shade from 0 to 1000. |
| * @return index in array |
| * @see #getAllAccent1Colors(Context) (Context) |
| * @see #getAllNeutral1Colors(Context) |
| */ |
| private int shadeToArrayIndex(int shade) { |
| if (shade == 0) { |
| return 0; |
| } else if (shade == 10) { |
| return 1; |
| } else if (shade == 50) { |
| return 2; |
| } else { |
| return shade / 100 + 2; |
| } |
| } |
| |
| private void assertColor(@ColorInt int observed, @ColorInt int expected) { |
| Assert.assertEquals("Color = " + Integer.toHexString(observed) + ", " |
| + Integer.toHexString(expected) + " expected", expected, observed); |
| } |
| |
| private int[] getAllAccent1Colors(Context context) { |
| final int[] colors = new int[13]; |
| colors[0] = context.getColor(R.color.system_accent1_0); |
| colors[1] = context.getColor(R.color.system_accent1_10); |
| colors[2] = context.getColor(R.color.system_accent1_50); |
| colors[3] = context.getColor(R.color.system_accent1_100); |
| colors[4] = context.getColor(R.color.system_accent1_200); |
| colors[5] = context.getColor(R.color.system_accent1_300); |
| colors[6] = context.getColor(R.color.system_accent1_400); |
| colors[7] = context.getColor(R.color.system_accent1_500); |
| colors[8] = context.getColor(R.color.system_accent1_600); |
| colors[9] = context.getColor(R.color.system_accent1_700); |
| colors[10] = context.getColor(R.color.system_accent1_800); |
| colors[11] = context.getColor(R.color.system_accent1_900); |
| colors[12] = context.getColor(R.color.system_accent1_1000); |
| return colors; |
| } |
| |
| private int[] getAllAccent2Colors(Context context) { |
| final int[] colors = new int[13]; |
| colors[0] = context.getColor(R.color.system_accent2_0); |
| colors[1] = context.getColor(R.color.system_accent2_10); |
| colors[2] = context.getColor(R.color.system_accent2_50); |
| colors[3] = context.getColor(R.color.system_accent2_100); |
| colors[4] = context.getColor(R.color.system_accent2_200); |
| colors[5] = context.getColor(R.color.system_accent2_300); |
| colors[6] = context.getColor(R.color.system_accent2_400); |
| colors[7] = context.getColor(R.color.system_accent2_500); |
| colors[8] = context.getColor(R.color.system_accent2_600); |
| colors[9] = context.getColor(R.color.system_accent2_700); |
| colors[10] = context.getColor(R.color.system_accent2_800); |
| colors[11] = context.getColor(R.color.system_accent2_900); |
| colors[12] = context.getColor(R.color.system_accent2_1000); |
| return colors; |
| } |
| |
| private int[] getAllAccent3Colors(Context context) { |
| final int[] colors = new int[13]; |
| colors[0] = context.getColor(R.color.system_accent3_0); |
| colors[1] = context.getColor(R.color.system_accent3_10); |
| colors[2] = context.getColor(R.color.system_accent3_50); |
| colors[3] = context.getColor(R.color.system_accent3_100); |
| colors[4] = context.getColor(R.color.system_accent3_200); |
| colors[5] = context.getColor(R.color.system_accent3_300); |
| colors[6] = context.getColor(R.color.system_accent3_400); |
| colors[7] = context.getColor(R.color.system_accent3_500); |
| colors[8] = context.getColor(R.color.system_accent3_600); |
| colors[9] = context.getColor(R.color.system_accent3_700); |
| colors[10] = context.getColor(R.color.system_accent3_800); |
| colors[11] = context.getColor(R.color.system_accent3_900); |
| colors[12] = context.getColor(R.color.system_accent3_1000); |
| return colors; |
| } |
| |
| private int[] getAllNeutral1Colors(Context context) { |
| final int[] colors = new int[13]; |
| colors[0] = context.getColor(R.color.system_neutral1_0); |
| colors[1] = context.getColor(R.color.system_neutral1_10); |
| colors[2] = context.getColor(R.color.system_neutral1_50); |
| colors[3] = context.getColor(R.color.system_neutral1_100); |
| colors[4] = context.getColor(R.color.system_neutral1_200); |
| colors[5] = context.getColor(R.color.system_neutral1_300); |
| colors[6] = context.getColor(R.color.system_neutral1_400); |
| colors[7] = context.getColor(R.color.system_neutral1_500); |
| colors[8] = context.getColor(R.color.system_neutral1_600); |
| colors[9] = context.getColor(R.color.system_neutral1_700); |
| colors[10] = context.getColor(R.color.system_neutral1_800); |
| colors[11] = context.getColor(R.color.system_neutral1_900); |
| colors[12] = context.getColor(R.color.system_neutral1_1000); |
| return colors; |
| } |
| |
| private int[] getAllNeutral2Colors(Context context) { |
| final int[] colors = new int[13]; |
| colors[0] = context.getColor(R.color.system_neutral2_0); |
| colors[1] = context.getColor(R.color.system_neutral2_10); |
| colors[2] = context.getColor(R.color.system_neutral2_50); |
| colors[3] = context.getColor(R.color.system_neutral2_100); |
| colors[4] = context.getColor(R.color.system_neutral2_200); |
| colors[5] = context.getColor(R.color.system_neutral2_300); |
| colors[6] = context.getColor(R.color.system_neutral2_400); |
| colors[7] = context.getColor(R.color.system_neutral2_500); |
| colors[8] = context.getColor(R.color.system_neutral2_600); |
| colors[9] = context.getColor(R.color.system_neutral2_700); |
| colors[10] = context.getColor(R.color.system_neutral2_800); |
| colors[11] = context.getColor(R.color.system_neutral2_900); |
| colors[12] = context.getColor(R.color.system_neutral2_1000); |
| return colors; |
| } |
| } |