1 /* 2 * Copyright 2015 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 17 package android.support.v4.graphics; 18 19 import android.graphics.Color; 20 21 /** 22 * A set of color-related utility methods, building upon those available in {@code Color}. 23 */ 24 public class ColorUtils { 25 26 private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10; 27 private static final int MIN_ALPHA_SEARCH_PRECISION = 10; 28 ColorUtils()29 private ColorUtils() {} 30 31 /** 32 * Composite two potentially translucent colors over each other and returns the result. 33 */ compositeColors(int foreground, int background)34 public static int compositeColors(int foreground, int background) { 35 int bgAlpha = Color.alpha(background); 36 int fgAlpha = Color.alpha(foreground); 37 int a = compositeAlpha(fgAlpha, bgAlpha); 38 39 int r = compositeComponent(Color.red(foreground), fgAlpha, 40 Color.red(background), bgAlpha, a); 41 int g = compositeComponent(Color.green(foreground), fgAlpha, 42 Color.green(background), bgAlpha, a); 43 int b = compositeComponent(Color.blue(foreground), fgAlpha, 44 Color.blue(background), bgAlpha, a); 45 46 return Color.argb(a, r, g, b); 47 } 48 compositeAlpha(int foregroundAlpha, int backgroundAlpha)49 private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) { 50 return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF); 51 } 52 compositeComponent(int fgC, int fgA, int bgC, int bgA, int a)53 private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) { 54 if (a == 0) return 0; 55 return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF); 56 } 57 58 /** 59 * Returns the luminance of a color. 60 * 61 * Formula defined here: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef 62 */ calculateLuminance(int color)63 public static double calculateLuminance(int color) { 64 double red = Color.red(color) / 255d; 65 red = red < 0.03928 ? red / 12.92 : Math.pow((red + 0.055) / 1.055, 2.4); 66 67 double green = Color.green(color) / 255d; 68 green = green < 0.03928 ? green / 12.92 : Math.pow((green + 0.055) / 1.055, 2.4); 69 70 double blue = Color.blue(color) / 255d; 71 blue = blue < 0.03928 ? blue / 12.92 : Math.pow((blue + 0.055) / 1.055, 2.4); 72 73 return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue); 74 } 75 76 /** 77 * Returns the contrast ratio between {@code foreground} and {@code background}. 78 * {@code background} must be opaque. 79 * <p> 80 * Formula defined 81 * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>. 82 */ 83 public static double calculateContrast(int foreground, int background) { 84 if (Color.alpha(background) != 255) { 85 throw new IllegalArgumentException("background can not be translucent"); 86 } 87 if (Color.alpha(foreground) < 255) { 88 // If the foreground is translucent, composite the foreground over the background 89 foreground = compositeColors(foreground, background); 90 } 91 92 final double luminance1 = calculateLuminance(foreground) + 0.05; 93 final double luminance2 = calculateLuminance(background) + 0.05; 94 95 // Now return the lighter luminance divided by the darker luminance 96 return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2); 97 } 98 99 /** 100 * Calculates the minimum alpha value which can be applied to {@code foreground} so that would 101 * have a contrast value of at least {@code minContrastRatio} when compared to 102 * {@code background}. 103 * 104 * @param foreground the foreground color. 105 * @param background the background color. Should be opaque. 106 * @param minContrastRatio the minimum contrast ratio. 107 * @return the alpha value in the range 0-255, or -1 if no value could be calculated. 108 */ 109 public static int calculateMinimumAlpha(int foreground, int background, 110 float minContrastRatio) { 111 if (Color.alpha(background) != 255) { 112 throw new IllegalArgumentException("background can not be translucent"); 113 } 114 115 // First lets check that a fully opaque foreground has sufficient contrast 116 int testForeground = setAlphaComponent(foreground, 255); 117 double testRatio = calculateContrast(testForeground, background); 118 if (testRatio < minContrastRatio) { 119 // Fully opaque foreground does not have sufficient contrast, return error 120 return -1; 121 } 122 123 // Binary search to find a value with the minimum value which provides sufficient contrast 124 int numIterations = 0; 125 int minAlpha = 0; 126 int maxAlpha = 255; 127 128 while (numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS && 129 (maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION) { 130 final int testAlpha = (minAlpha + maxAlpha) / 2; 131 132 testForeground = setAlphaComponent(foreground, testAlpha); 133 testRatio = calculateContrast(testForeground, background); 134 135 if (testRatio < minContrastRatio) { 136 minAlpha = testAlpha; 137 } else { 138 maxAlpha = testAlpha; 139 } 140 141 numIterations++; 142 } 143 144 // Conservatively return the max of the range of possible alphas, which is known to pass. 145 return maxAlpha; 146 } 147 148 /** 149 * Convert RGB components to HSL (hue-saturation-lightness). 150 * <ul> 151 * <li>hsl[0] is Hue [0 .. 360)</li> 152 * <li>hsl[1] is Saturation [0...1]</li> 153 * <li>hsl[2] is Lightness [0...1]</li> 154 * </ul> 155 * 156 * @param r red component value [0..255] 157 * @param g green component value [0..255] 158 * @param b blue component value [0..255] 159 * @param hsl 3 element array which holds the resulting HSL components. 160 */ 161 public static void RGBToHSL(int r, int g, int b, float[] hsl) { 162 final float rf = r / 255f; 163 final float gf = g / 255f; 164 final float bf = b / 255f; 165 166 final float max = Math.max(rf, Math.max(gf, bf)); 167 final float min = Math.min(rf, Math.min(gf, bf)); 168 final float deltaMaxMin = max - min; 169 170 float h, s; 171 float l = (max + min) / 2f; 172 173 if (max == min) { 174 // Monochromatic 175 h = s = 0f; 176 } else { 177 if (max == rf) { 178 h = ((gf - bf) / deltaMaxMin) % 6f; 179 } else if (max == gf) { 180 h = ((bf - rf) / deltaMaxMin) + 2f; 181 } else { 182 h = ((rf - gf) / deltaMaxMin) + 4f; 183 } 184 185 s = deltaMaxMin / (1f - Math.abs(2f * l - 1f)); 186 } 187 188 h = (h * 60f) % 360f; 189 if (h < 0) { 190 h += 360f; 191 } 192 193 hsl[0] = constrain(h, 0f, 360f); 194 hsl[1] = constrain(s, 0f, 1f); 195 hsl[2] = constrain(l, 0f, 1f); 196 } 197 198 /** 199 * Convert the ARGB color to its HSL (hue-saturation-lightness) components. 200 * <ul> 201 * <li>hsl[0] is Hue [0 .. 360)</li> 202 * <li>hsl[1] is Saturation [0...1]</li> 203 * <li>hsl[2] is Lightness [0...1]</li> 204 * </ul> 205 * 206 * @param color the ARGB color to convert. The alpha component is ignored. 207 * @param hsl 3 element array which holds the resulting HSL components. 208 */ 209 public static void colorToHSL(int color, float[] hsl) { 210 RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl); 211 } 212 213 /** 214 * Convert HSL (hue-saturation-lightness) components to a RGB color. 215 * <ul> 216 * <li>hsl[0] is Hue [0 .. 360)</li> 217 * <li>hsl[1] is Saturation [0...1]</li> 218 * <li>hsl[2] is Lightness [0...1]</li> 219 * </ul> 220 * If hsv values are out of range, they are pinned. 221 * 222 * @param hsl 3 element array which holds the input HSL components. 223 * @return the resulting RGB color 224 */ 225 public static int HSLToColor(float[] hsl) { 226 final float h = hsl[0]; 227 final float s = hsl[1]; 228 final float l = hsl[2]; 229 230 final float c = (1f - Math.abs(2 * l - 1f)) * s; 231 final float m = l - 0.5f * c; 232 final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f)); 233 234 final int hueSegment = (int) h / 60; 235 236 int r = 0, g = 0, b = 0; 237 238 switch (hueSegment) { 239 case 0: 240 r = Math.round(255 * (c + m)); 241 g = Math.round(255 * (x + m)); 242 b = Math.round(255 * m); 243 break; 244 case 1: 245 r = Math.round(255 * (x + m)); 246 g = Math.round(255 * (c + m)); 247 b = Math.round(255 * m); 248 break; 249 case 2: 250 r = Math.round(255 * m); 251 g = Math.round(255 * (c + m)); 252 b = Math.round(255 * (x + m)); 253 break; 254 case 3: 255 r = Math.round(255 * m); 256 g = Math.round(255 * (x + m)); 257 b = Math.round(255 * (c + m)); 258 break; 259 case 4: 260 r = Math.round(255 * (x + m)); 261 g = Math.round(255 * m); 262 b = Math.round(255 * (c + m)); 263 break; 264 case 5: 265 case 6: 266 r = Math.round(255 * (c + m)); 267 g = Math.round(255 * m); 268 b = Math.round(255 * (x + m)); 269 break; 270 } 271 272 r = constrain(r, 0, 255); 273 g = constrain(g, 0, 255); 274 b = constrain(b, 0, 255); 275 276 return Color.rgb(r, g, b); 277 } 278 279 /** 280 * Set the alpha component of {@code color} to be {@code alpha}. 281 */ 282 public static int setAlphaComponent(int color, int alpha) { 283 if (alpha < 0 || alpha > 255) { 284 throw new IllegalArgumentException("alpha must be between 0 and 255."); 285 } 286 return (color & 0x00ffffff) | (alpha << 24); 287 } 288 289 private static float constrain(float amount, float low, float high) { 290 return amount < low ? low : (amount > high ? high : amount); 291 } 292 293 private static int constrain(int amount, int low, int high) { 294 return amount < low ? low : (amount > high ? high : amount); 295 } 296 297 } 298