• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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