1 /*
2  * Copyright (C) 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 
17 package com.android.internal.util;
18 
19 import android.annotation.ColorInt;
20 import android.annotation.FloatRange;
21 import android.annotation.IntRange;
22 import android.annotation.NonNull;
23 import android.app.Notification;
24 import android.content.Context;
25 import android.content.res.ColorStateList;
26 import android.content.res.Resources;
27 import android.graphics.Bitmap;
28 import android.graphics.Color;
29 import android.graphics.drawable.AnimationDrawable;
30 import android.graphics.drawable.BitmapDrawable;
31 import android.graphics.drawable.Drawable;
32 import android.graphics.drawable.Icon;
33 import android.graphics.drawable.VectorDrawable;
34 import android.text.SpannableStringBuilder;
35 import android.text.Spanned;
36 import android.text.style.CharacterStyle;
37 import android.text.style.ForegroundColorSpan;
38 import android.text.style.TextAppearanceSpan;
39 import android.util.Log;
40 import android.util.Pair;
41 
42 import java.util.Arrays;
43 import java.util.WeakHashMap;
44 
45 /**
46  * Helper class to process legacy (Holo) notifications to make them look like material notifications.
47  *
48  * @hide
49  */
50 public class NotificationColorUtil {
51 
52     private static final String TAG = "NotificationColorUtil";
53     private static final boolean DEBUG = false;
54 
55     private static final Object sLock = new Object();
56     private static NotificationColorUtil sInstance;
57 
58     private final ImageUtils mImageUtils = new ImageUtils();
59     private final WeakHashMap<Bitmap, Pair<Boolean, Integer>> mGrayscaleBitmapCache =
60             new WeakHashMap<Bitmap, Pair<Boolean, Integer>>();
61 
62     private final int mGrayscaleIconMaxSize; // @dimen/notification_large_icon_width (64dp)
63 
getInstance(Context context)64     public static NotificationColorUtil getInstance(Context context) {
65         synchronized (sLock) {
66             if (sInstance == null) {
67                 sInstance = new NotificationColorUtil(context);
68             }
69             return sInstance;
70         }
71     }
72 
NotificationColorUtil(Context context)73     private NotificationColorUtil(Context context) {
74         mGrayscaleIconMaxSize = context.getResources().getDimensionPixelSize(
75                 com.android.internal.R.dimen.notification_large_icon_width);
76     }
77 
78     /**
79      * Checks whether a Bitmap is a small grayscale icon.
80      * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
81      *
82      * @param bitmap The bitmap to test.
83      * @return True if the bitmap is grayscale; false if it is color or too large to examine.
84      */
isGrayscaleIcon(Bitmap bitmap)85     public boolean isGrayscaleIcon(Bitmap bitmap) {
86         // quick test: reject large bitmaps
87         if (bitmap.getWidth() > mGrayscaleIconMaxSize
88                 || bitmap.getHeight() > mGrayscaleIconMaxSize) {
89             return false;
90         }
91 
92         synchronized (sLock) {
93             Pair<Boolean, Integer> cached = mGrayscaleBitmapCache.get(bitmap);
94             if (cached != null) {
95                 if (cached.second == bitmap.getGenerationId()) {
96                     return cached.first;
97                 }
98             }
99         }
100         boolean result;
101         int generationId;
102         synchronized (mImageUtils) {
103             result = mImageUtils.isGrayscale(bitmap);
104 
105             // generationId and the check whether the Bitmap is grayscale can't be read atomically
106             // here. However, since the thread is in the process of posting the notification, we can
107             // assume that it doesn't modify the bitmap while we are checking the pixels.
108             generationId = bitmap.getGenerationId();
109         }
110         synchronized (sLock) {
111             mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId));
112         }
113         return result;
114     }
115 
116     /**
117      * Checks whether a Drawable is a small grayscale icon.
118      * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
119      *
120      * @param d The drawable to test.
121      * @return True if the bitmap is grayscale; false if it is color or too large to examine.
122      */
isGrayscaleIcon(Drawable d)123     public boolean isGrayscaleIcon(Drawable d) {
124         if (d == null) {
125             return false;
126         } else if (d instanceof BitmapDrawable) {
127             BitmapDrawable bd = (BitmapDrawable) d;
128             return bd.getBitmap() != null && isGrayscaleIcon(bd.getBitmap());
129         } else if (d instanceof AnimationDrawable) {
130             AnimationDrawable ad = (AnimationDrawable) d;
131             int count = ad.getNumberOfFrames();
132             return count > 0 && isGrayscaleIcon(ad.getFrame(0));
133         } else if (d instanceof VectorDrawable) {
134             // We just assume you're doing the right thing if using vectors
135             return true;
136         } else {
137             return false;
138         }
139     }
140 
isGrayscaleIcon(Context context, Icon icon)141     public boolean isGrayscaleIcon(Context context, Icon icon) {
142         if (icon == null) {
143             return false;
144         }
145         switch (icon.getType()) {
146             case Icon.TYPE_BITMAP:
147                 return isGrayscaleIcon(icon.getBitmap());
148             case Icon.TYPE_RESOURCE:
149                 return isGrayscaleIcon(context, icon.getResId());
150             default:
151                 return false;
152         }
153     }
154 
155     /**
156      * Checks whether a drawable with a resoure id is a small grayscale icon.
157      * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
158      *
159      * @param context The context to load the drawable from.
160      * @return True if the bitmap is grayscale; false if it is color or too large to examine.
161      */
isGrayscaleIcon(Context context, int drawableResId)162     public boolean isGrayscaleIcon(Context context, int drawableResId) {
163         if (drawableResId != 0) {
164             try {
165                 return isGrayscaleIcon(context.getDrawable(drawableResId));
166             } catch (Resources.NotFoundException ex) {
167                 Log.e(TAG, "Drawable not found: " + drawableResId);
168                 return false;
169             }
170         } else {
171             return false;
172         }
173     }
174 
175     /**
176      * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on
177      * the text.
178      *
179      * @param charSequence The text to process.
180      * @return The color inverted text.
181      */
invertCharSequenceColors(CharSequence charSequence)182     public CharSequence invertCharSequenceColors(CharSequence charSequence) {
183         if (charSequence instanceof Spanned) {
184             Spanned ss = (Spanned) charSequence;
185             Object[] spans = ss.getSpans(0, ss.length(), Object.class);
186             SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
187             for (Object span : spans) {
188                 Object resultSpan = span;
189                 if (resultSpan instanceof CharacterStyle) {
190                     resultSpan = ((CharacterStyle) span).getUnderlying();
191                 }
192                 if (resultSpan instanceof TextAppearanceSpan) {
193                     TextAppearanceSpan processedSpan = processTextAppearanceSpan(
194                             (TextAppearanceSpan) span);
195                     if (processedSpan != resultSpan) {
196                         resultSpan = processedSpan;
197                     } else {
198                         // we need to still take the orgininal for wrapped spans
199                         resultSpan = span;
200                     }
201                 } else if (resultSpan instanceof ForegroundColorSpan) {
202                     ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan;
203                     int foregroundColor = originalSpan.getForegroundColor();
204                     resultSpan = new ForegroundColorSpan(processColor(foregroundColor));
205                 } else {
206                     resultSpan = span;
207                 }
208                 builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
209                         ss.getSpanFlags(span));
210             }
211             return builder;
212         }
213         return charSequence;
214     }
215 
processTextAppearanceSpan(TextAppearanceSpan span)216     private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) {
217         ColorStateList colorStateList = span.getTextColor();
218         if (colorStateList != null) {
219             int[] colors = colorStateList.getColors();
220             boolean changed = false;
221             for (int i = 0; i < colors.length; i++) {
222                 if (ImageUtils.isGrayscale(colors[i])) {
223 
224                     // Allocate a new array so we don't change the colors in the old color state
225                     // list.
226                     if (!changed) {
227                         colors = Arrays.copyOf(colors, colors.length);
228                     }
229                     colors[i] = processColor(colors[i]);
230                     changed = true;
231                 }
232             }
233             if (changed) {
234                 return new TextAppearanceSpan(
235                         span.getFamily(), span.getTextStyle(), span.getTextSize(),
236                         new ColorStateList(colorStateList.getStates(), colors),
237                         span.getLinkTextColor());
238             }
239         }
240         return span;
241     }
242 
processColor(int color)243     private int processColor(int color) {
244         return Color.argb(Color.alpha(color),
245                 255 - Color.red(color),
246                 255 - Color.green(color),
247                 255 - Color.blue(color));
248     }
249 
250     /**
251      * Finds a suitable color such that there's enough contrast.
252      *
253      * @param color the color to start searching from.
254      * @param other the color to ensure contrast against. Assumed to be lighter than {@param color}
255      * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
256      * @param minRatio the minimum contrast ratio required.
257      * @return a color with the same hue as {@param color}, potentially darkened to meet the
258      *          contrast ratio.
259      */
findContrastColor(int color, int other, boolean findFg, double minRatio)260     public static int findContrastColor(int color, int other, boolean findFg, double minRatio) {
261         int fg = findFg ? color : other;
262         int bg = findFg ? other : color;
263         if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
264             return color;
265         }
266 
267         double[] lab = new double[3];
268         ColorUtilsFromCompat.colorToLAB(findFg ? fg : bg, lab);
269 
270         double low = 0, high = lab[0];
271         final double a = lab[1], b = lab[2];
272         for (int i = 0; i < 15 && high - low > 0.00001; i++) {
273             final double l = (low + high) / 2;
274             if (findFg) {
275                 fg = ColorUtilsFromCompat.LABToColor(l, a, b);
276             } else {
277                 bg = ColorUtilsFromCompat.LABToColor(l, a, b);
278             }
279             if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
280                 low = l;
281             } else {
282                 high = l;
283             }
284         }
285         return ColorUtilsFromCompat.LABToColor(low, a, b);
286     }
287 
288     /**
289      * Finds a suitable alpha such that there's enough contrast.
290      *
291      * @param color the color to start searching from.
292      * @param backgroundColor the color to ensure contrast against.
293      * @param minRatio the minimum contrast ratio required.
294      * @return the same color as {@param color} with potentially modified alpha to meet contrast
295      */
findAlphaToMeetContrast(int color, int backgroundColor, double minRatio)296     public static int findAlphaToMeetContrast(int color, int backgroundColor, double minRatio) {
297         int fg = color;
298         int bg = backgroundColor;
299         if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
300             return color;
301         }
302         int startAlpha = Color.alpha(color);
303         int r = Color.red(color);
304         int g = Color.green(color);
305         int b = Color.blue(color);
306 
307         int low = startAlpha, high = 255;
308         for (int i = 0; i < 15 && high - low > 0; i++) {
309             final int alpha = (low + high) / 2;
310             fg = Color.argb(alpha, r, g, b);
311             if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
312                 high = alpha;
313             } else {
314                 low = alpha;
315             }
316         }
317         return Color.argb(high, r, g, b);
318     }
319 
320     /**
321      * Finds a suitable color such that there's enough contrast.
322      *
323      * @param color the color to start searching from.
324      * @param other the color to ensure contrast against. Assumed to be darker than {@param color}
325      * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
326      * @param minRatio the minimum contrast ratio required.
327      * @return a color with the same hue as {@param color}, potentially darkened to meet the
328      *          contrast ratio.
329      */
findContrastColorAgainstDark(int color, int other, boolean findFg, double minRatio)330     public static int findContrastColorAgainstDark(int color, int other, boolean findFg,
331             double minRatio) {
332         int fg = findFg ? color : other;
333         int bg = findFg ? other : color;
334         if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
335             return color;
336         }
337 
338         float[] hsl = new float[3];
339         ColorUtilsFromCompat.colorToHSL(findFg ? fg : bg, hsl);
340 
341         float low = hsl[2], high = 1;
342         for (int i = 0; i < 15 && high - low > 0.00001; i++) {
343             final float l = (low + high) / 2;
344             hsl[2] = l;
345             if (findFg) {
346                 fg = ColorUtilsFromCompat.HSLToColor(hsl);
347             } else {
348                 bg = ColorUtilsFromCompat.HSLToColor(hsl);
349             }
350             if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
351                 high = l;
352             } else {
353                 low = l;
354             }
355         }
356         return findFg ? fg : bg;
357     }
358 
ensureTextContrastOnBlack(int color)359     public static int ensureTextContrastOnBlack(int color) {
360         return findContrastColorAgainstDark(color, Color.BLACK, true /* fg */, 12);
361     }
362 
363     /**
364      * Finds a text color with sufficient contrast over bg that has the same hue as the original
365      * color, assuming it is for large text.
366      */
ensureLargeTextContrast(int color, int bg)367     public static int ensureLargeTextContrast(int color, int bg) {
368         return findContrastColor(color, bg, true, 3);
369     }
370 
371     /**
372      * Finds a text color with sufficient contrast over bg that has the same hue as the original
373      * color.
374      */
ensureTextContrast(int color, int bg)375     private static int ensureTextContrast(int color, int bg) {
376         return findContrastColor(color, bg, true, 4.5);
377     }
378 
379     /** Finds a background color for a text view with given text color and hint text color, that
380      * has the same hue as the original color.
381      */
ensureTextBackgroundColor(int color, int textColor, int hintColor)382     public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) {
383         color = findContrastColor(color, hintColor, false, 3.0);
384         return findContrastColor(color, textColor, false, 4.5);
385     }
386 
contrastChange(int colorOld, int colorNew, int bg)387     private static String contrastChange(int colorOld, int colorNew, int bg) {
388         return String.format("from %.2f:1 to %.2f:1",
389                 ColorUtilsFromCompat.calculateContrast(colorOld, bg),
390                 ColorUtilsFromCompat.calculateContrast(colorNew, bg));
391     }
392 
393     /**
394      * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT}
395      */
resolveColor(Context context, int color)396     public static int resolveColor(Context context, int color) {
397         if (color == Notification.COLOR_DEFAULT) {
398             return context.getColor(com.android.internal.R.color.notification_icon_default_color);
399         }
400         return color;
401     }
402 
403     /**
404      * Resolves a Notification's color such that it has enough contrast to be used as the
405      * color for the Notification's action and header text.
406      *
407      * @param notificationColor the color of the notification or {@link Notification#COLOR_DEFAULT}
408      * @param backgroundColor the background color to ensure the contrast against.
409      * @return a color of the same hue with enough contrast against the backgrounds.
410      */
resolveContrastColor(Context context, int notificationColor, int backgroundColor)411     public static int resolveContrastColor(Context context, int notificationColor,
412             int backgroundColor) {
413         final int resolvedColor = resolveColor(context, notificationColor);
414 
415         final int actionBg = context.getColor(
416                 com.android.internal.R.color.notification_action_list);
417 
418         int color = resolvedColor;
419         color = NotificationColorUtil.ensureLargeTextContrast(color, actionBg);
420         color = NotificationColorUtil.ensureTextContrast(color, backgroundColor);
421 
422         if (color != resolvedColor) {
423             if (DEBUG){
424                 Log.w(TAG, String.format(
425                         "Enhanced contrast of notification for %s %s (over action)"
426                                 + " and %s (over background) by changing #%s to %s",
427                         context.getPackageName(),
428                         NotificationColorUtil.contrastChange(resolvedColor, color, actionBg),
429                         NotificationColorUtil.contrastChange(resolvedColor, color, backgroundColor),
430                         Integer.toHexString(resolvedColor), Integer.toHexString(color)));
431             }
432         }
433         return color;
434     }
435 
436     /**
437      * Change a color by a specified value
438      * @param baseColor the base color to lighten
439      * @param amount the amount to lighten the color from 0 to 100. This corresponds to the L
440      *               increase in the LAB color space. A negative value will darken the color and
441      *               a positive will lighten it.
442      * @return the changed color
443      */
changeColorLightness(int baseColor, int amount)444     public static int changeColorLightness(int baseColor, int amount) {
445         final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
446         ColorUtilsFromCompat.colorToLAB(baseColor, result);
447         result[0] = Math.max(Math.min(100, result[0] + amount), 0);
448         return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
449     }
450 
resolveAmbientColor(Context context, int notificationColor)451     public static int resolveAmbientColor(Context context, int notificationColor) {
452         final int resolvedColor = resolveColor(context, notificationColor);
453 
454         int color = resolvedColor;
455         color = NotificationColorUtil.ensureTextContrastOnBlack(color);
456 
457         if (color != resolvedColor) {
458             if (DEBUG){
459                 Log.w(TAG, String.format(
460                         "Ambient contrast of notification for %s is %s (over black)"
461                                 + " by changing #%s to #%s",
462                         context.getPackageName(),
463                         NotificationColorUtil.contrastChange(resolvedColor, color, Color.BLACK),
464                         Integer.toHexString(resolvedColor), Integer.toHexString(color)));
465             }
466         }
467         return color;
468     }
469 
resolvePrimaryColor(Context context, int backgroundColor)470     public static int resolvePrimaryColor(Context context, int backgroundColor) {
471         boolean useDark = shouldUseDark(backgroundColor);
472         if (useDark) {
473             return context.getColor(
474                     com.android.internal.R.color.notification_primary_text_color_light);
475         } else {
476             return context.getColor(
477                     com.android.internal.R.color.notification_primary_text_color_dark);
478         }
479     }
480 
resolveSecondaryColor(Context context, int backgroundColor)481     public static int resolveSecondaryColor(Context context, int backgroundColor) {
482         boolean useDark = shouldUseDark(backgroundColor);
483         if (useDark) {
484             return context.getColor(
485                     com.android.internal.R.color.notification_secondary_text_color_light);
486         } else {
487             return context.getColor(
488                     com.android.internal.R.color.notification_secondary_text_color_dark);
489         }
490     }
491 
resolveActionBarColor(Context context, int backgroundColor)492     public static int resolveActionBarColor(Context context, int backgroundColor) {
493         if (backgroundColor == Notification.COLOR_DEFAULT) {
494             return context.getColor(com.android.internal.R.color.notification_action_list);
495         }
496         return getShiftedColor(backgroundColor, 7);
497     }
498 
499     /**
500      * Get a color that stays in the same tint, but darkens or lightens it by a certain
501      * amount.
502      * This also looks at the lightness of the provided color and shifts it appropriately.
503      *
504      * @param color the base color to use
505      * @param amount the amount from 1 to 100 how much to modify the color
506      * @return the now color that was modified
507      */
getShiftedColor(int color, int amount)508     public static int getShiftedColor(int color, int amount) {
509         final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
510         ColorUtilsFromCompat.colorToLAB(color, result);
511         if (result[0] >= 4) {
512             result[0] = Math.max(0, result[0] - amount);
513         } else {
514             result[0] = Math.min(100, result[0] + amount);
515         }
516         return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
517     }
518 
shouldUseDark(int backgroundColor)519     private static boolean shouldUseDark(int backgroundColor) {
520         boolean useDark = backgroundColor == Notification.COLOR_DEFAULT;
521         if (!useDark) {
522             useDark = ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5;
523         }
524         return useDark;
525     }
526 
calculateLuminance(int backgroundColor)527     public static double calculateLuminance(int backgroundColor) {
528         return ColorUtilsFromCompat.calculateLuminance(backgroundColor);
529     }
530 
531 
calculateContrast(int foregroundColor, int backgroundColor)532     public static double calculateContrast(int foregroundColor, int backgroundColor) {
533         return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor);
534     }
535 
satisfiesTextContrast(int backgroundColor, int foregroundColor)536     public static boolean satisfiesTextContrast(int backgroundColor, int foregroundColor) {
537         return NotificationColorUtil.calculateContrast(foregroundColor, backgroundColor) >= 4.5;
538     }
539 
540     /**
541      * Composite two potentially translucent colors over each other and returns the result.
542      */
compositeColors(int foreground, int background)543     public static int compositeColors(int foreground, int background) {
544         return ColorUtilsFromCompat.compositeColors(foreground, background);
545     }
546 
isColorLight(int backgroundColor)547     public static boolean isColorLight(int backgroundColor) {
548         return calculateLuminance(backgroundColor) > 0.5f;
549     }
550 
551     /**
552      * Framework copy of functions needed from android.support.v4.graphics.ColorUtils.
553      */
554     private static class ColorUtilsFromCompat {
555         private static final double XYZ_WHITE_REFERENCE_X = 95.047;
556         private static final double XYZ_WHITE_REFERENCE_Y = 100;
557         private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
558         private static final double XYZ_EPSILON = 0.008856;
559         private static final double XYZ_KAPPA = 903.3;
560 
561         private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
562         private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
563 
564         private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
565 
ColorUtilsFromCompat()566         private ColorUtilsFromCompat() {}
567 
568         /**
569          * Composite two potentially translucent colors over each other and returns the result.
570          */
compositeColors(@olorInt int foreground, @ColorInt int background)571         public static int compositeColors(@ColorInt int foreground, @ColorInt int background) {
572             int bgAlpha = Color.alpha(background);
573             int fgAlpha = Color.alpha(foreground);
574             int a = compositeAlpha(fgAlpha, bgAlpha);
575 
576             int r = compositeComponent(Color.red(foreground), fgAlpha,
577                     Color.red(background), bgAlpha, a);
578             int g = compositeComponent(Color.green(foreground), fgAlpha,
579                     Color.green(background), bgAlpha, a);
580             int b = compositeComponent(Color.blue(foreground), fgAlpha,
581                     Color.blue(background), bgAlpha, a);
582 
583             return Color.argb(a, r, g, b);
584         }
585 
compositeAlpha(int foregroundAlpha, int backgroundAlpha)586         private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
587             return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
588         }
589 
compositeComponent(int fgC, int fgA, int bgC, int bgA, int a)590         private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
591             if (a == 0) return 0;
592             return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
593         }
594 
595         /**
596          * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}.
597          * <p>Defined as the Y component in the XYZ representation of {@code color}.</p>
598          */
599         @FloatRange(from = 0.0, to = 1.0)
calculateLuminance(@olorInt int color)600         public static double calculateLuminance(@ColorInt int color) {
601             final double[] result = getTempDouble3Array();
602             colorToXYZ(color, result);
603             // Luminance is the Y component
604             return result[1] / 100;
605         }
606 
607         /**
608          * Returns the contrast ratio between {@code foreground} and {@code background}.
609          * {@code background} must be opaque.
610          * <p>
611          * Formula defined
612          * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
613          */
calculateContrast(@olorInt int foreground, @ColorInt int background)614         public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) {
615             if (Color.alpha(background) != 255) {
616                 Log.wtf(TAG, "background can not be translucent: #"
617                         + Integer.toHexString(background));
618             }
619             if (Color.alpha(foreground) < 255) {
620                 // If the foreground is translucent, composite the foreground over the background
621                 foreground = compositeColors(foreground, background);
622             }
623 
624             final double luminance1 = calculateLuminance(foreground) + 0.05;
625             final double luminance2 = calculateLuminance(background) + 0.05;
626 
627             // Now return the lighter luminance divided by the darker luminance
628             return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
629         }
630 
631         /**
632          * Convert the ARGB color to its CIE Lab representative components.
633          *
634          * @param color  the ARGB color to convert. The alpha component is ignored
635          * @param outLab 3-element array which holds the resulting LAB components
636          */
colorToLAB(@olorInt int color, @NonNull double[] outLab)637         public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) {
638             RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab);
639         }
640 
641         /**
642          * Convert RGB components to its CIE Lab representative components.
643          *
644          * <ul>
645          * <li>outLab[0] is L [0 ...100)</li>
646          * <li>outLab[1] is a [-128...127)</li>
647          * <li>outLab[2] is b [-128...127)</li>
648          * </ul>
649          *
650          * @param r      red component value [0..255]
651          * @param g      green component value [0..255]
652          * @param b      blue component value [0..255]
653          * @param outLab 3-element array which holds the resulting LAB components
654          */
RGBToLAB(@ntRangefrom = 0x0, to = 0xFF) int r, @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, @NonNull double[] outLab)655         public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r,
656                 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
657                 @NonNull double[] outLab) {
658             // First we convert RGB to XYZ
659             RGBToXYZ(r, g, b, outLab);
660             // outLab now contains XYZ
661             XYZToLAB(outLab[0], outLab[1], outLab[2], outLab);
662             // outLab now contains LAB representation
663         }
664 
665         /**
666          * Convert the ARGB color to it's CIE XYZ representative components.
667          *
668          * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
669          * 2° Standard Observer (1931).</p>
670          *
671          * <ul>
672          * <li>outXyz[0] is X [0 ...95.047)</li>
673          * <li>outXyz[1] is Y [0...100)</li>
674          * <li>outXyz[2] is Z [0...108.883)</li>
675          * </ul>
676          *
677          * @param color  the ARGB color to convert. The alpha component is ignored
678          * @param outXyz 3-element array which holds the resulting LAB components
679          */
colorToXYZ(@olorInt int color, @NonNull double[] outXyz)680         public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
681             RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
682         }
683 
684         /**
685          * Convert RGB components to it's CIE XYZ representative components.
686          *
687          * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
688          * 2° Standard Observer (1931).</p>
689          *
690          * <ul>
691          * <li>outXyz[0] is X [0 ...95.047)</li>
692          * <li>outXyz[1] is Y [0...100)</li>
693          * <li>outXyz[2] is Z [0...108.883)</li>
694          * </ul>
695          *
696          * @param r      red component value [0..255]
697          * @param g      green component value [0..255]
698          * @param b      blue component value [0..255]
699          * @param outXyz 3-element array which holds the resulting XYZ components
700          */
RGBToXYZ(@ntRangefrom = 0x0, to = 0xFF) int r, @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, @NonNull double[] outXyz)701         public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r,
702                 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
703                 @NonNull double[] outXyz) {
704             if (outXyz.length != 3) {
705                 throw new IllegalArgumentException("outXyz must have a length of 3.");
706             }
707 
708             double sr = r / 255.0;
709             sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
710             double sg = g / 255.0;
711             sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
712             double sb = b / 255.0;
713             sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
714 
715             outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
716             outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
717             outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
718         }
719 
720         /**
721          * Converts a color from CIE XYZ to CIE Lab representation.
722          *
723          * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
724          * 2° Standard Observer (1931).</p>
725          *
726          * <ul>
727          * <li>outLab[0] is L [0 ...100)</li>
728          * <li>outLab[1] is a [-128...127)</li>
729          * <li>outLab[2] is b [-128...127)</li>
730          * </ul>
731          *
732          * @param x      X component value [0...95.047)
733          * @param y      Y component value [0...100)
734          * @param z      Z component value [0...108.883)
735          * @param outLab 3-element array which holds the resulting Lab components
736          */
737         public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
738                 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
739                 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
740                 @NonNull double[] outLab) {
741             if (outLab.length != 3) {
742                 throw new IllegalArgumentException("outLab must have a length of 3.");
743             }
744             x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
745             y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
746             z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
747             outLab[0] = Math.max(0, 116 * y - 16);
748             outLab[1] = 500 * (x - y);
749             outLab[2] = 200 * (y - z);
750         }
751 
752         /**
753          * Converts a color from CIE Lab to CIE XYZ representation.
754          *
755          * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
756          * 2° Standard Observer (1931).</p>
757          *
758          * <ul>
759          * <li>outXyz[0] is X [0 ...95.047)</li>
760          * <li>outXyz[1] is Y [0...100)</li>
761          * <li>outXyz[2] is Z [0...108.883)</li>
762          * </ul>
763          *
764          * @param l      L component value [0...100)
765          * @param a      A component value [-128...127)
766          * @param b      B component value [-128...127)
767          * @param outXyz 3-element array which holds the resulting XYZ components
768          */
769         public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l,
770                 @FloatRange(from = -128, to = 127) final double a,
771                 @FloatRange(from = -128, to = 127) final double b,
772                 @NonNull double[] outXyz) {
773             final double fy = (l + 16) / 116;
774             final double fx = a / 500 + fy;
775             final double fz = fy - b / 200;
776 
777             double tmp = Math.pow(fx, 3);
778             final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA;
779             final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA;
780 
781             tmp = Math.pow(fz, 3);
782             final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA;
783 
784             outXyz[0] = xr * XYZ_WHITE_REFERENCE_X;
785             outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y;
786             outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z;
787         }
788 
789         /**
790          * Converts a color from CIE XYZ to its RGB representation.
791          *
792          * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
793          * 2° Standard Observer (1931).</p>
794          *
795          * @param x X component value [0...95.047)
796          * @param y Y component value [0...100)
797          * @param z Z component value [0...108.883)
798          * @return int containing the RGB representation
799          */
800         @ColorInt
XYZToColor(@loatRangefrom = 0f, to = XYZ_WHITE_REFERENCE_X) double x, @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z)801         public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
802                 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
803                 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) {
804             double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100;
805             double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100;
806             double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100;
807 
808             r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
809             g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
810             b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;
811 
812             return Color.rgb(
813                     constrain((int) Math.round(r * 255), 0, 255),
814                     constrain((int) Math.round(g * 255), 0, 255),
815                     constrain((int) Math.round(b * 255), 0, 255));
816         }
817 
818         /**
819          * Converts a color from CIE Lab to its RGB representation.
820          *
821          * @param l L component value [0...100]
822          * @param a A component value [-128...127]
823          * @param b B component value [-128...127]
824          * @return int containing the RGB representation
825          */
826         @ColorInt
LABToColor(@loatRangefrom = 0f, to = 100) final double l, @FloatRange(from = -128, to = 127) final double a, @FloatRange(from = -128, to = 127) final double b)827         public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l,
828                 @FloatRange(from = -128, to = 127) final double a,
829                 @FloatRange(from = -128, to = 127) final double b) {
830             final double[] result = getTempDouble3Array();
831             LABToXYZ(l, a, b, result);
832             return XYZToColor(result[0], result[1], result[2]);
833         }
834 
constrain(int amount, int low, int high)835         private static int constrain(int amount, int low, int high) {
836             return amount < low ? low : (amount > high ? high : amount);
837         }
838 
constrain(float amount, float low, float high)839         private static float constrain(float amount, float low, float high) {
840             return amount < low ? low : (amount > high ? high : amount);
841         }
842 
pivotXyzComponent(double component)843         private static double pivotXyzComponent(double component) {
844             return component > XYZ_EPSILON
845                     ? Math.pow(component, 1 / 3.0)
846                     : (XYZ_KAPPA * component + 16) / 116;
847         }
848 
getTempDouble3Array()849         public static double[] getTempDouble3Array() {
850             double[] result = TEMP_ARRAY.get();
851             if (result == null) {
852                 result = new double[3];
853                 TEMP_ARRAY.set(result);
854             }
855             return result;
856         }
857 
858         /**
859          * Convert HSL (hue-saturation-lightness) components to a RGB color.
860          * <ul>
861          * <li>hsl[0] is Hue [0 .. 360)</li>
862          * <li>hsl[1] is Saturation [0...1]</li>
863          * <li>hsl[2] is Lightness [0...1]</li>
864          * </ul>
865          * If hsv values are out of range, they are pinned.
866          *
867          * @param hsl 3-element array which holds the input HSL components
868          * @return the resulting RGB color
869          */
870         @ColorInt
HSLToColor(@onNull float[] hsl)871         public static int HSLToColor(@NonNull float[] hsl) {
872             final float h = hsl[0];
873             final float s = hsl[1];
874             final float l = hsl[2];
875 
876             final float c = (1f - Math.abs(2 * l - 1f)) * s;
877             final float m = l - 0.5f * c;
878             final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
879 
880             final int hueSegment = (int) h / 60;
881 
882             int r = 0, g = 0, b = 0;
883 
884             switch (hueSegment) {
885                 case 0:
886                     r = Math.round(255 * (c + m));
887                     g = Math.round(255 * (x + m));
888                     b = Math.round(255 * m);
889                     break;
890                 case 1:
891                     r = Math.round(255 * (x + m));
892                     g = Math.round(255 * (c + m));
893                     b = Math.round(255 * m);
894                     break;
895                 case 2:
896                     r = Math.round(255 * m);
897                     g = Math.round(255 * (c + m));
898                     b = Math.round(255 * (x + m));
899                     break;
900                 case 3:
901                     r = Math.round(255 * m);
902                     g = Math.round(255 * (x + m));
903                     b = Math.round(255 * (c + m));
904                     break;
905                 case 4:
906                     r = Math.round(255 * (x + m));
907                     g = Math.round(255 * m);
908                     b = Math.round(255 * (c + m));
909                     break;
910                 case 5:
911                 case 6:
912                     r = Math.round(255 * (c + m));
913                     g = Math.round(255 * m);
914                     b = Math.round(255 * (x + m));
915                     break;
916             }
917 
918             r = constrain(r, 0, 255);
919             g = constrain(g, 0, 255);
920             b = constrain(b, 0, 255);
921 
922             return Color.rgb(r, g, b);
923         }
924 
925         /**
926          * Convert the ARGB color to its HSL (hue-saturation-lightness) components.
927          * <ul>
928          * <li>outHsl[0] is Hue [0 .. 360)</li>
929          * <li>outHsl[1] is Saturation [0...1]</li>
930          * <li>outHsl[2] is Lightness [0...1]</li>
931          * </ul>
932          *
933          * @param color  the ARGB color to convert. The alpha component is ignored
934          * @param outHsl 3-element array which holds the resulting HSL components
935          */
colorToHSL(@olorInt int color, @NonNull float[] outHsl)936         public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) {
937             RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl);
938         }
939 
940         /**
941          * Convert RGB components to HSL (hue-saturation-lightness).
942          * <ul>
943          * <li>outHsl[0] is Hue [0 .. 360)</li>
944          * <li>outHsl[1] is Saturation [0...1]</li>
945          * <li>outHsl[2] is Lightness [0...1]</li>
946          * </ul>
947          *
948          * @param r      red component value [0..255]
949          * @param g      green component value [0..255]
950          * @param b      blue component value [0..255]
951          * @param outHsl 3-element array which holds the resulting HSL components
952          */
RGBToHSL(@ntRangefrom = 0x0, to = 0xFF) int r, @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, @NonNull float[] outHsl)953         public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r,
954                 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
955                 @NonNull float[] outHsl) {
956             final float rf = r / 255f;
957             final float gf = g / 255f;
958             final float bf = b / 255f;
959 
960             final float max = Math.max(rf, Math.max(gf, bf));
961             final float min = Math.min(rf, Math.min(gf, bf));
962             final float deltaMaxMin = max - min;
963 
964             float h, s;
965             float l = (max + min) / 2f;
966 
967             if (max == min) {
968                 // Monochromatic
969                 h = s = 0f;
970             } else {
971                 if (max == rf) {
972                     h = ((gf - bf) / deltaMaxMin) % 6f;
973                 } else if (max == gf) {
974                     h = ((bf - rf) / deltaMaxMin) + 2f;
975                 } else {
976                     h = ((rf - gf) / deltaMaxMin) + 4f;
977                 }
978 
979                 s = deltaMaxMin / (1f - Math.abs(2f * l - 1f));
980             }
981 
982             h = (h * 60f) % 360f;
983             if (h < 0) {
984                 h += 360f;
985             }
986 
987             outHsl[0] = constrain(h, 0f, 360f);
988             outHsl[1] = constrain(s, 0f, 1f);
989             outHsl[2] = constrain(l, 0f, 1f);
990         }
991 
992     }
993 }
994