1/* 2 * Copyright 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.v7.graphics; 18 19import android.graphics.Color; 20 21final class ColorUtils { 22 23 private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10; 24 private static final int MIN_ALPHA_SEARCH_PRECISION = 10; 25 26 private ColorUtils() {} 27 28 /** 29 * Composite two potentially translucent colors over each other and returns the result. 30 */ 31 private static int compositeColors(int fg, int bg) { 32 final float alpha1 = Color.alpha(fg) / 255f; 33 final float alpha2 = Color.alpha(bg) / 255f; 34 35 float a = (alpha1 + alpha2) * (1f - alpha1); 36 float r = (Color.red(fg) * alpha1) + (Color.red(bg) * alpha2 * (1f - alpha1)); 37 float g = (Color.green(fg) * alpha1) + (Color.green(bg) * alpha2 * (1f - alpha1)); 38 float b = (Color.blue(fg) * alpha1) + (Color.blue(bg) * alpha2 * (1f - alpha1)); 39 40 return Color.argb((int) a, (int) r, (int) g, (int) b); 41 } 42 43 /** 44 * Returns the luminance of a color. 45 * 46 * Formula defined here: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef 47 */ 48 private static double calculateLuminance(int color) { 49 double red = Color.red(color) / 255d; 50 red = red < 0.03928 ? red / 12.92 : Math.pow((red + 0.055) / 1.055, 2.4); 51 52 double green = Color.green(color) / 255d; 53 green = green < 0.03928 ? green / 12.92 : Math.pow((green + 0.055) / 1.055, 2.4); 54 55 double blue = Color.blue(color) / 255d; 56 blue = blue < 0.03928 ? blue / 12.92 : Math.pow((blue + 0.055) / 1.055, 2.4); 57 58 return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue); 59 } 60 61 /** 62 * Returns the contrast ratio between two colors. 63 * 64 * Formula defined here: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef 65 */ 66 private static double calculateContrast(int foreground, int background) { 67 if (Color.alpha(background) != 255) { 68 throw new IllegalArgumentException("background can not be translucent"); 69 } 70 if (Color.alpha(foreground) < 255) { 71 // If the foreground is translucent, composite the foreground over the background 72 foreground = compositeColors(foreground, background); 73 } 74 75 final double luminance1 = calculateLuminance(foreground) + 0.05; 76 final double luminance2 = calculateLuminance(background) + 0.05; 77 78 // Now return the lighter luminance divided by the darker luminance 79 return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2); 80 } 81 82 /** 83 * Finds the minimum alpha value which can be applied to {@code foreground} so that is has a 84 * contrast value of at least {@code minContrastRatio} when compared to background. 85 * 86 * @return the alpha value in the range 0-255. 87 */ 88 private static int findMinimumAlpha(int foreground, int background, double minContrastRatio) { 89 if (Color.alpha(background) != 255) { 90 throw new IllegalArgumentException("background can not be translucent"); 91 } 92 93 // First lets check that a fully opaque foreground has sufficient contrast 94 int testForeground = modifyAlpha(foreground, 255); 95 double testRatio = calculateContrast(testForeground, background); 96 if (testRatio < minContrastRatio) { 97 // Fully opaque foreground does not have sufficient contrast, return error 98 return -1; 99 } 100 101 // Binary search to find a value with the minimum value which provides sufficient contrast 102 int numIterations = 0; 103 int minAlpha = 0; 104 int maxAlpha = 255; 105 106 while (numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS && 107 (maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION) { 108 final int testAlpha = (minAlpha + maxAlpha) / 2; 109 110 testForeground = modifyAlpha(foreground, testAlpha); 111 testRatio = calculateContrast(testForeground, background); 112 113 if (testRatio < minContrastRatio) { 114 minAlpha = testAlpha; 115 } else { 116 maxAlpha = testAlpha; 117 } 118 119 numIterations++; 120 } 121 122 // Conservatively return the max of the range of possible alphas, which is known to pass. 123 return maxAlpha; 124 } 125 126 static int getTextColorForBackground(int backgroundColor, int textColor, float minContrastRatio) { 127 final int minAlpha = ColorUtils 128 .findMinimumAlpha(textColor, backgroundColor, minContrastRatio); 129 130 if (minAlpha >= 0) { 131 return ColorUtils.modifyAlpha(textColor, minAlpha); 132 } 133 134 // Didn't find an opacity which provided enough contrast 135 return -1; 136 } 137 138 static void RGBtoHSL(int r, int g, int b, float[] hsl) { 139 final float rf = r / 255f; 140 final float gf = g / 255f; 141 final float bf = b / 255f; 142 143 final float max = Math.max(rf, Math.max(gf, bf)); 144 final float min = Math.min(rf, Math.min(gf, bf)); 145 final float deltaMaxMin = max - min; 146 147 float h, s; 148 float l = (max + min) / 2f; 149 150 if (max == min) { 151 // Monochromatic 152 h = s = 0f; 153 } else { 154 if (max == rf) { 155 h = ((gf - bf) / deltaMaxMin) % 6f; 156 } else if (max == gf) { 157 h = ((bf - rf) / deltaMaxMin) + 2f; 158 } else { 159 h = ((rf - gf) / deltaMaxMin) + 4f; 160 } 161 162 s = deltaMaxMin / (1f - Math.abs(2f * l - 1f)); 163 } 164 165 hsl[0] = (h * 60f) % 360f; 166 hsl[1] = s; 167 hsl[2] = l; 168 } 169 170 static int HSLtoRGB (float[] hsl) { 171 final float h = hsl[0]; 172 final float s = hsl[1]; 173 final float l = hsl[2]; 174 175 final float c = (1f - Math.abs(2 * l - 1f)) * s; 176 final float m = l - 0.5f * c; 177 final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f)); 178 179 final int hueSegment = (int) h / 60; 180 181 int r = 0, g = 0, b = 0; 182 183 switch (hueSegment) { 184 case 0: 185 r = Math.round(255 * (c + m)); 186 g = Math.round(255 * (x + m)); 187 b = Math.round(255 * m); 188 break; 189 case 1: 190 r = Math.round(255 * (x + m)); 191 g = Math.round(255 * (c + m)); 192 b = Math.round(255 * m); 193 break; 194 case 2: 195 r = Math.round(255 * m); 196 g = Math.round(255 * (c + m)); 197 b = Math.round(255 * (x + m)); 198 break; 199 case 3: 200 r = Math.round(255 * m); 201 g = Math.round(255 * (x + m)); 202 b = Math.round(255 * (c + m)); 203 break; 204 case 4: 205 r = Math.round(255 * (x + m)); 206 g = Math.round(255 * m); 207 b = Math.round(255 * (c + m)); 208 break; 209 case 5: 210 case 6: 211 r = Math.round(255 * (c + m)); 212 g = Math.round(255 * m); 213 b = Math.round(255 * (x + m)); 214 break; 215 } 216 217 r = Math.max(0, Math.min(255, r)); 218 g = Math.max(0, Math.min(255, g)); 219 b = Math.max(0, Math.min(255, b)); 220 221 return Color.rgb(r, g, b); 222 } 223 224 /** 225 * Set the alpha component of {@code color} to be {@code alpha}. 226 */ 227 static int modifyAlpha(int color, int alpha) { 228 return (color & 0x00ffffff) | (alpha << 24); 229 } 230 231} 232