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