blob: 577fa1758f5e9326e9b21407685e94d9144b8fc5 [file] [log] [blame]
Jorim Jaggi5c2d8462014-03-21 17:37:00 +01001/*
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
17package com.android.internal.util;
18
Adrian Roos4ff3b122016-02-01 12:26:13 -080019import android.annotation.ColorInt;
20import android.annotation.FloatRange;
21import android.annotation.IntRange;
22import android.annotation.NonNull;
23import android.app.Notification;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010024import android.content.Context;
25import android.content.res.ColorStateList;
26import android.content.res.Resources;
27import android.graphics.Bitmap;
28import android.graphics.Color;
29import android.graphics.drawable.AnimationDrawable;
30import android.graphics.drawable.BitmapDrawable;
31import android.graphics.drawable.Drawable;
Dan Sandlerd63f9322015-05-06 15:18:49 -040032import android.graphics.drawable.Icon;
Dan Sandler26e81cf2014-05-06 10:01:27 -040033import android.graphics.drawable.VectorDrawable;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010034import android.text.SpannableStringBuilder;
35import android.text.Spanned;
Selim Cinek87c31532017-08-18 18:53:44 -070036import android.text.style.BackgroundColorSpan;
Selim Cinek7b9605b2017-01-19 17:36:00 -080037import android.text.style.CharacterStyle;
38import android.text.style.ForegroundColorSpan;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010039import android.text.style.TextAppearanceSpan;
40import android.util.Log;
41import android.util.Pair;
42
43import java.util.Arrays;
44import java.util.WeakHashMap;
45
46/**
Alan Viverette830960c2014-06-06 15:48:55 -070047 * Helper class to process legacy (Holo) notifications to make them look like material notifications.
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010048 *
49 * @hide
50 */
Dan Sandler26e81cf2014-05-06 10:01:27 -040051public class NotificationColorUtil {
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010052
Dan Sandler26e81cf2014-05-06 10:01:27 -040053 private static final String TAG = "NotificationColorUtil";
Adrian Roos4ff3b122016-02-01 12:26:13 -080054 private static final boolean DEBUG = false;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010055
56 private static final Object sLock = new Object();
Dan Sandler26e81cf2014-05-06 10:01:27 -040057 private static NotificationColorUtil sInstance;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010058
59 private final ImageUtils mImageUtils = new ImageUtils();
60 private final WeakHashMap<Bitmap, Pair<Boolean, Integer>> mGrayscaleBitmapCache =
61 new WeakHashMap<Bitmap, Pair<Boolean, Integer>>();
62
Dan Sandler05c362d2014-09-03 00:16:27 +020063 private final int mGrayscaleIconMaxSize; // @dimen/notification_large_icon_width (64dp)
64
65 public static NotificationColorUtil getInstance(Context context) {
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010066 synchronized (sLock) {
67 if (sInstance == null) {
Dan Sandler05c362d2014-09-03 00:16:27 +020068 sInstance = new NotificationColorUtil(context);
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010069 }
70 return sInstance;
71 }
72 }
73
Dan Sandler05c362d2014-09-03 00:16:27 +020074 private NotificationColorUtil(Context context) {
75 mGrayscaleIconMaxSize = context.getResources().getDimensionPixelSize(
76 com.android.internal.R.dimen.notification_large_icon_width);
77 }
78
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010079 /**
Dan Sandler05c362d2014-09-03 00:16:27 +020080 * Checks whether a Bitmap is a small grayscale icon.
81 * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010082 *
83 * @param bitmap The bitmap to test.
Dan Sandler05c362d2014-09-03 00:16:27 +020084 * @return True if the bitmap is grayscale; false if it is color or too large to examine.
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010085 */
Dan Sandler05c362d2014-09-03 00:16:27 +020086 public boolean isGrayscaleIcon(Bitmap bitmap) {
87 // quick test: reject large bitmaps
88 if (bitmap.getWidth() > mGrayscaleIconMaxSize
89 || bitmap.getHeight() > mGrayscaleIconMaxSize) {
90 return false;
91 }
92
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010093 synchronized (sLock) {
94 Pair<Boolean, Integer> cached = mGrayscaleBitmapCache.get(bitmap);
95 if (cached != null) {
96 if (cached.second == bitmap.getGenerationId()) {
97 return cached.first;
98 }
99 }
100 }
101 boolean result;
102 int generationId;
103 synchronized (mImageUtils) {
104 result = mImageUtils.isGrayscale(bitmap);
105
106 // generationId and the check whether the Bitmap is grayscale can't be read atomically
107 // here. However, since the thread is in the process of posting the notification, we can
108 // assume that it doesn't modify the bitmap while we are checking the pixels.
109 generationId = bitmap.getGenerationId();
110 }
111 synchronized (sLock) {
112 mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId));
113 }
114 return result;
115 }
116
117 /**
Dan Sandler05c362d2014-09-03 00:16:27 +0200118 * Checks whether a Drawable is a small grayscale icon.
119 * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100120 *
121 * @param d The drawable to test.
Dan Sandler05c362d2014-09-03 00:16:27 +0200122 * @return True if the bitmap is grayscale; false if it is color or too large to examine.
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100123 */
Dan Sandler05c362d2014-09-03 00:16:27 +0200124 public boolean isGrayscaleIcon(Drawable d) {
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100125 if (d == null) {
126 return false;
127 } else if (d instanceof BitmapDrawable) {
128 BitmapDrawable bd = (BitmapDrawable) d;
Dan Sandler05c362d2014-09-03 00:16:27 +0200129 return bd.getBitmap() != null && isGrayscaleIcon(bd.getBitmap());
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100130 } else if (d instanceof AnimationDrawable) {
131 AnimationDrawable ad = (AnimationDrawable) d;
132 int count = ad.getNumberOfFrames();
Dan Sandler05c362d2014-09-03 00:16:27 +0200133 return count > 0 && isGrayscaleIcon(ad.getFrame(0));
Dan Sandler26e81cf2014-05-06 10:01:27 -0400134 } else if (d instanceof VectorDrawable) {
135 // We just assume you're doing the right thing if using vectors
136 return true;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100137 } else {
138 return false;
139 }
140 }
141
Dan Sandlerd63f9322015-05-06 15:18:49 -0400142 public boolean isGrayscaleIcon(Context context, Icon icon) {
143 if (icon == null) {
144 return false;
145 }
146 switch (icon.getType()) {
147 case Icon.TYPE_BITMAP:
148 return isGrayscaleIcon(icon.getBitmap());
149 case Icon.TYPE_RESOURCE:
150 return isGrayscaleIcon(context, icon.getResId());
151 default:
152 return false;
153 }
154 }
155
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100156 /**
Dan Sandler05c362d2014-09-03 00:16:27 +0200157 * Checks whether a drawable with a resoure id is a small grayscale icon.
158 * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100159 *
160 * @param context The context to load the drawable from.
Dan Sandler05c362d2014-09-03 00:16:27 +0200161 * @return True if the bitmap is grayscale; false if it is color or too large to examine.
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100162 */
Dan Sandler05c362d2014-09-03 00:16:27 +0200163 public boolean isGrayscaleIcon(Context context, int drawableResId) {
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100164 if (drawableResId != 0) {
165 try {
Dan Sandler05c362d2014-09-03 00:16:27 +0200166 return isGrayscaleIcon(context.getDrawable(drawableResId));
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100167 } catch (Resources.NotFoundException ex) {
168 Log.e(TAG, "Drawable not found: " + drawableResId);
169 return false;
170 }
171 } else {
172 return false;
173 }
174 }
175
176 /**
177 * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on
178 * the text.
179 *
180 * @param charSequence The text to process.
181 * @return The color inverted text.
182 */
183 public CharSequence invertCharSequenceColors(CharSequence charSequence) {
184 if (charSequence instanceof Spanned) {
185 Spanned ss = (Spanned) charSequence;
186 Object[] spans = ss.getSpans(0, ss.length(), Object.class);
187 SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
188 for (Object span : spans) {
189 Object resultSpan = span;
Selim Cinek7b9605b2017-01-19 17:36:00 -0800190 if (resultSpan instanceof CharacterStyle) {
191 resultSpan = ((CharacterStyle) span).getUnderlying();
192 }
193 if (resultSpan instanceof TextAppearanceSpan) {
194 TextAppearanceSpan processedSpan = processTextAppearanceSpan(
195 (TextAppearanceSpan) span);
196 if (processedSpan != resultSpan) {
197 resultSpan = processedSpan;
198 } else {
199 // we need to still take the orgininal for wrapped spans
200 resultSpan = span;
201 }
202 } else if (resultSpan instanceof ForegroundColorSpan) {
203 ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan;
204 int foregroundColor = originalSpan.getForegroundColor();
205 resultSpan = new ForegroundColorSpan(processColor(foregroundColor));
206 } else {
207 resultSpan = span;
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100208 }
209 builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
210 ss.getSpanFlags(span));
211 }
212 return builder;
213 }
214 return charSequence;
215 }
216
217 private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) {
218 ColorStateList colorStateList = span.getTextColor();
219 if (colorStateList != null) {
220 int[] colors = colorStateList.getColors();
221 boolean changed = false;
222 for (int i = 0; i < colors.length; i++) {
223 if (ImageUtils.isGrayscale(colors[i])) {
224
225 // Allocate a new array so we don't change the colors in the old color state
226 // list.
227 if (!changed) {
228 colors = Arrays.copyOf(colors, colors.length);
229 }
230 colors[i] = processColor(colors[i]);
231 changed = true;
232 }
233 }
234 if (changed) {
235 return new TextAppearanceSpan(
236 span.getFamily(), span.getTextStyle(), span.getTextSize(),
237 new ColorStateList(colorStateList.getStates(), colors),
238 span.getLinkTextColor());
239 }
240 }
241 return span;
242 }
243
Selim Cinek87c31532017-08-18 18:53:44 -0700244 /**
245 * Clears all color spans of a text
246 * @param charSequence the input text
247 * @return the same text but without color spans
248 */
249 public static CharSequence clearColorSpans(CharSequence charSequence) {
250 if (charSequence instanceof Spanned) {
251 Spanned ss = (Spanned) charSequence;
252 Object[] spans = ss.getSpans(0, ss.length(), Object.class);
253 SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
254 for (Object span : spans) {
255 Object resultSpan = span;
256 if (resultSpan instanceof CharacterStyle) {
257 resultSpan = ((CharacterStyle) span).getUnderlying();
258 }
259 if (resultSpan instanceof TextAppearanceSpan) {
260 TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan;
261 if (originalSpan.getTextColor() != null) {
262 resultSpan = new TextAppearanceSpan(
263 originalSpan.getFamily(),
264 originalSpan.getTextStyle(),
265 originalSpan.getTextSize(),
266 null,
267 originalSpan.getLinkTextColor());
268 }
269 } else if (resultSpan instanceof ForegroundColorSpan
270 || (resultSpan instanceof BackgroundColorSpan)) {
271 continue;
272 } else {
273 resultSpan = span;
274 }
275 builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
276 ss.getSpanFlags(span));
277 }
278 return builder;
279 }
280 return charSequence;
281 }
282
Jorim Jaggi5c2d8462014-03-21 17:37:00 +0100283 private int processColor(int color) {
284 return Color.argb(Color.alpha(color),
285 255 - Color.red(color),
286 255 - Color.green(color),
287 255 - Color.blue(color));
288 }
Adrian Roos4ff3b122016-02-01 12:26:13 -0800289
290 /**
291 * Finds a suitable color such that there's enough contrast.
292 *
293 * @param color the color to start searching from.
294 * @param other the color to ensure contrast against. Assumed to be lighter than {@param color}
295 * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
296 * @param minRatio the minimum contrast ratio required.
297 * @return a color with the same hue as {@param color}, potentially darkened to meet the
298 * contrast ratio.
299 */
Selim Cinek5fb73f82017-04-20 16:55:38 -0700300 public static int findContrastColor(int color, int other, boolean findFg, double minRatio) {
Adrian Roos4ff3b122016-02-01 12:26:13 -0800301 int fg = findFg ? color : other;
302 int bg = findFg ? other : color;
303 if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
304 return color;
305 }
306
307 double[] lab = new double[3];
308 ColorUtilsFromCompat.colorToLAB(findFg ? fg : bg, lab);
309
310 double low = 0, high = lab[0];
311 final double a = lab[1], b = lab[2];
312 for (int i = 0; i < 15 && high - low > 0.00001; i++) {
313 final double l = (low + high) / 2;
314 if (findFg) {
315 fg = ColorUtilsFromCompat.LABToColor(l, a, b);
316 } else {
317 bg = ColorUtilsFromCompat.LABToColor(l, a, b);
318 }
319 if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
320 low = l;
321 } else {
322 high = l;
323 }
324 }
325 return ColorUtilsFromCompat.LABToColor(low, a, b);
326 }
327
328 /**
Selim Cinekac5f0272017-05-02 16:05:41 -0700329 * Finds a suitable alpha such that there's enough contrast.
330 *
331 * @param color the color to start searching from.
332 * @param backgroundColor the color to ensure contrast against.
333 * @param minRatio the minimum contrast ratio required.
334 * @return the same color as {@param color} with potentially modified alpha to meet contrast
335 */
336 public static int findAlphaToMeetContrast(int color, int backgroundColor, double minRatio) {
337 int fg = color;
338 int bg = backgroundColor;
339 if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
340 return color;
341 }
342 int startAlpha = Color.alpha(color);
343 int r = Color.red(color);
344 int g = Color.green(color);
345 int b = Color.blue(color);
346
347 int low = startAlpha, high = 255;
348 for (int i = 0; i < 15 && high - low > 0; i++) {
349 final int alpha = (low + high) / 2;
350 fg = Color.argb(alpha, r, g, b);
351 if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
352 high = alpha;
353 } else {
354 low = alpha;
355 }
356 }
357 return Color.argb(high, r, g, b);
358 }
359
360 /**
Adrian Roosf9d13f62016-11-08 15:42:20 -0800361 * Finds a suitable color such that there's enough contrast.
362 *
363 * @param color the color to start searching from.
364 * @param other the color to ensure contrast against. Assumed to be darker than {@param color}
365 * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
366 * @param minRatio the minimum contrast ratio required.
367 * @return a color with the same hue as {@param color}, potentially darkened to meet the
368 * contrast ratio.
369 */
370 public static int findContrastColorAgainstDark(int color, int other, boolean findFg,
371 double minRatio) {
372 int fg = findFg ? color : other;
373 int bg = findFg ? other : color;
374 if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
375 return color;
376 }
377
Adrian Roos99d2e642017-02-09 14:58:44 +0100378 float[] hsl = new float[3];
379 ColorUtilsFromCompat.colorToHSL(findFg ? fg : bg, hsl);
Adrian Roosf9d13f62016-11-08 15:42:20 -0800380
Adrian Roos99d2e642017-02-09 14:58:44 +0100381 float low = hsl[2], high = 1;
Adrian Roosf9d13f62016-11-08 15:42:20 -0800382 for (int i = 0; i < 15 && high - low > 0.00001; i++) {
Adrian Roos99d2e642017-02-09 14:58:44 +0100383 final float l = (low + high) / 2;
384 hsl[2] = l;
Adrian Roosf9d13f62016-11-08 15:42:20 -0800385 if (findFg) {
Adrian Roos99d2e642017-02-09 14:58:44 +0100386 fg = ColorUtilsFromCompat.HSLToColor(hsl);
Adrian Roosf9d13f62016-11-08 15:42:20 -0800387 } else {
Adrian Roos99d2e642017-02-09 14:58:44 +0100388 bg = ColorUtilsFromCompat.HSLToColor(hsl);
Adrian Roosf9d13f62016-11-08 15:42:20 -0800389 }
390 if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
391 high = l;
392 } else {
393 low = l;
394 }
395 }
Adrian Roos99d2e642017-02-09 14:58:44 +0100396 return findFg ? fg : bg;
Adrian Roosf9d13f62016-11-08 15:42:20 -0800397 }
398
Adrian Roos487374f2017-01-11 15:48:14 -0800399 public static int ensureTextContrastOnBlack(int color) {
400 return findContrastColorAgainstDark(color, Color.BLACK, true /* fg */, 12);
401 }
402
Anthony Chenad4d1582017-04-10 16:07:58 -0700403 /**
404 * Finds a large text color with sufficient contrast over bg that has the same or darker hue as
405 * the original color, depending on the value of {@code isBgDarker}.
406 *
407 * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}.
Adrian Roos4ff3b122016-02-01 12:26:13 -0800408 */
Anthony Chenad4d1582017-04-10 16:07:58 -0700409 public static int ensureLargeTextContrast(int color, int bg, boolean isBgDarker) {
410 return isBgDarker
411 ? findContrastColorAgainstDark(color, bg, true, 3)
412 : findContrastColor(color, bg, true, 3);
Adrian Roos4ff3b122016-02-01 12:26:13 -0800413 }
414
415 /**
Anthony Chenad4d1582017-04-10 16:07:58 -0700416 * Finds a text color with sufficient contrast over bg that has the same or darker hue as the
417 * original color, depending on the value of {@code isBgDarker}.
418 *
419 * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}.
Adrian Roos4ff3b122016-02-01 12:26:13 -0800420 */
Anthony Chenad4d1582017-04-10 16:07:58 -0700421 private static int ensureTextContrast(int color, int bg, boolean isBgDarker) {
422 return isBgDarker
423 ? findContrastColorAgainstDark(color, bg, true, 4.5)
424 : findContrastColor(color, bg, true, 4.5);
Adrian Roos4ff3b122016-02-01 12:26:13 -0800425 }
426
427 /** Finds a background color for a text view with given text color and hint text color, that
428 * has the same hue as the original color.
429 */
430 public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) {
431 color = findContrastColor(color, hintColor, false, 3.0);
432 return findContrastColor(color, textColor, false, 4.5);
433 }
434
435 private static String contrastChange(int colorOld, int colorNew, int bg) {
436 return String.format("from %.2f:1 to %.2f:1",
437 ColorUtilsFromCompat.calculateContrast(colorOld, bg),
438 ColorUtilsFromCompat.calculateContrast(colorNew, bg));
439 }
440
441 /**
442 * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT}
443 */
444 public static int resolveColor(Context context, int color) {
445 if (color == Notification.COLOR_DEFAULT) {
Selim Cinekc7f5a822018-03-20 19:32:06 -0700446 return context.getColor(com.android.internal.R.color.notification_default_color_light);
Adrian Roos4ff3b122016-02-01 12:26:13 -0800447 }
448 return color;
449 }
450
451 /**
452 * Resolves a Notification's color such that it has enough contrast to be used as the
Anthony Chenad4d1582017-04-10 16:07:58 -0700453 * color for the Notification's action and header text on a background that is lighter than
454 * {@code notificationColor}.
455 *
456 * @see {@link #resolveContrastColor(Context, int, boolean)}
457 */
458 public static int resolveContrastColor(Context context, int notificationColor,
459 int backgroundColor) {
460 return NotificationColorUtil.resolveContrastColor(context, notificationColor,
461 backgroundColor, false /* isDark */);
462 }
463
464 /**
465 * Resolves a Notification's color such that it has enough contrast to be used as the
Adrian Roos4ff3b122016-02-01 12:26:13 -0800466 * color for the Notification's action and header text.
467 *
468 * @param notificationColor the color of the notification or {@link Notification#COLOR_DEFAULT}
Selim Cinekac5f0272017-05-02 16:05:41 -0700469 * @param backgroundColor the background color to ensure the contrast against.
Anthony Chenad4d1582017-04-10 16:07:58 -0700470 * @param isDark whether or not the {@code notificationColor} will be placed on a background
471 * that is darker than the color itself
Adrian Roos4ff3b122016-02-01 12:26:13 -0800472 * @return a color of the same hue with enough contrast against the backgrounds.
473 */
Selim Cinekac5f0272017-05-02 16:05:41 -0700474 public static int resolveContrastColor(Context context, int notificationColor,
Anthony Chenad4d1582017-04-10 16:07:58 -0700475 int backgroundColor, boolean isDark) {
Adrian Roos4ff3b122016-02-01 12:26:13 -0800476 final int resolvedColor = resolveColor(context, notificationColor);
477
Adrian Roos4ff3b122016-02-01 12:26:13 -0800478 int color = resolvedColor;
Anthony Chenad4d1582017-04-10 16:07:58 -0700479 color = NotificationColorUtil.ensureTextContrast(color, backgroundColor, isDark);
Adrian Roos4ff3b122016-02-01 12:26:13 -0800480
481 if (color != resolvedColor) {
482 if (DEBUG){
483 Log.w(TAG, String.format(
Selim Cinekc7f5a822018-03-20 19:32:06 -0700484 "Enhanced contrast of notification for %s"
Adrian Roos4ff3b122016-02-01 12:26:13 -0800485 + " and %s (over background) by changing #%s to %s",
486 context.getPackageName(),
Selim Cinekac5f0272017-05-02 16:05:41 -0700487 NotificationColorUtil.contrastChange(resolvedColor, color, backgroundColor),
Adrian Roos4ff3b122016-02-01 12:26:13 -0800488 Integer.toHexString(resolvedColor), Integer.toHexString(color)));
489 }
490 }
491 return color;
492 }
493
494 /**
Selim Cinek5fb73f82017-04-20 16:55:38 -0700495 * Change a color by a specified value
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700496 * @param baseColor the base color to lighten
497 * @param amount the amount to lighten the color from 0 to 100. This corresponds to the L
Selim Cinek5fb73f82017-04-20 16:55:38 -0700498 * increase in the LAB color space. A negative value will darken the color and
499 * a positive will lighten it.
500 * @return the changed color
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700501 */
Selim Cinek5fb73f82017-04-20 16:55:38 -0700502 public static int changeColorLightness(int baseColor, int amount) {
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700503 final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
504 ColorUtilsFromCompat.colorToLAB(baseColor, result);
Selim Cinek5fb73f82017-04-20 16:55:38 -0700505 result[0] = Math.max(Math.min(100, result[0] + amount), 0);
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700506 return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
507 }
508
Adrian Roos487374f2017-01-11 15:48:14 -0800509 public static int resolveAmbientColor(Context context, int notificationColor) {
510 final int resolvedColor = resolveColor(context, notificationColor);
511
512 int color = resolvedColor;
513 color = NotificationColorUtil.ensureTextContrastOnBlack(color);
514
515 if (color != resolvedColor) {
516 if (DEBUG){
517 Log.w(TAG, String.format(
518 "Ambient contrast of notification for %s is %s (over black)"
519 + " by changing #%s to #%s",
520 context.getPackageName(),
521 NotificationColorUtil.contrastChange(resolvedColor, color, Color.BLACK),
522 Integer.toHexString(resolvedColor), Integer.toHexString(color)));
523 }
524 }
525 return color;
526 }
527
Selim Cinek7b9605b2017-01-19 17:36:00 -0800528 public static int resolvePrimaryColor(Context context, int backgroundColor) {
529 boolean useDark = shouldUseDark(backgroundColor);
530 if (useDark) {
531 return context.getColor(
532 com.android.internal.R.color.notification_primary_text_color_light);
533 } else {
534 return context.getColor(
535 com.android.internal.R.color.notification_primary_text_color_dark);
536 }
537 }
538
539 public static int resolveSecondaryColor(Context context, int backgroundColor) {
540 boolean useDark = shouldUseDark(backgroundColor);
541 if (useDark) {
542 return context.getColor(
543 com.android.internal.R.color.notification_secondary_text_color_light);
544 } else {
545 return context.getColor(
546 com.android.internal.R.color.notification_secondary_text_color_dark);
547 }
548 }
549
Selim Cinekc7f5a822018-03-20 19:32:06 -0700550 public static int resolveDefaultColor(Context context, int backgroundColor) {
551 boolean useDark = shouldUseDark(backgroundColor);
552 if (useDark) {
553 return context.getColor(
554 com.android.internal.R.color.notification_default_color_light);
555 } else {
556 return context.getColor(
557 com.android.internal.R.color.notification_default_color_dark);
558 }
559 }
560
Selim Cinek875ba9b2017-02-13 16:20:17 -0800561 public static int resolveActionBarColor(Context context, int backgroundColor) {
562 if (backgroundColor == Notification.COLOR_DEFAULT) {
563 return context.getColor(com.android.internal.R.color.notification_action_list);
564 }
Selim Cinek622c64a2017-04-17 17:10:05 -0700565 return getShiftedColor(backgroundColor, 7);
566 }
567
568 /**
569 * Get a color that stays in the same tint, but darkens or lightens it by a certain
570 * amount.
571 * This also looks at the lightness of the provided color and shifts it appropriately.
572 *
573 * @param color the base color to use
574 * @param amount the amount from 1 to 100 how much to modify the color
575 * @return the now color that was modified
576 */
577 public static int getShiftedColor(int color, int amount) {
Selim Cinek7b9605b2017-01-19 17:36:00 -0800578 final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
Selim Cinek622c64a2017-04-17 17:10:05 -0700579 ColorUtilsFromCompat.colorToLAB(color, result);
580 if (result[0] >= 4) {
581 result[0] = Math.max(0, result[0] - amount);
Selim Cinek7b9605b2017-01-19 17:36:00 -0800582 } else {
Selim Cinek622c64a2017-04-17 17:10:05 -0700583 result[0] = Math.min(100, result[0] + amount);
Selim Cinek7b9605b2017-01-19 17:36:00 -0800584 }
585 return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
586 }
587
588 private static boolean shouldUseDark(int backgroundColor) {
589 boolean useDark = backgroundColor == Notification.COLOR_DEFAULT;
590 if (!useDark) {
591 useDark = ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5;
592 }
593 return useDark;
594 }
595
Selim Cinek5fb73f82017-04-20 16:55:38 -0700596 public static double calculateLuminance(int backgroundColor) {
597 return ColorUtilsFromCompat.calculateLuminance(backgroundColor);
598 }
599
600
601 public static double calculateContrast(int foregroundColor, int backgroundColor) {
602 return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor);
603 }
604
Selim Cinek389edcd2017-05-11 19:16:44 -0700605 public static boolean satisfiesTextContrast(int backgroundColor, int foregroundColor) {
Selim Cinek4c807912017-06-23 17:22:38 -0700606 return NotificationColorUtil.calculateContrast(foregroundColor, backgroundColor) >= 4.5;
Selim Cinek389edcd2017-05-11 19:16:44 -0700607 }
608
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700609 /**
Selim Cinekac5f0272017-05-02 16:05:41 -0700610 * Composite two potentially translucent colors over each other and returns the result.
611 */
612 public static int compositeColors(int foreground, int background) {
613 return ColorUtilsFromCompat.compositeColors(foreground, background);
614 }
615
Selim Cinek389edcd2017-05-11 19:16:44 -0700616 public static boolean isColorLight(int backgroundColor) {
617 return calculateLuminance(backgroundColor) > 0.5f;
618 }
619
Selim Cinekac5f0272017-05-02 16:05:41 -0700620 /**
Adrian Roos4ff3b122016-02-01 12:26:13 -0800621 * Framework copy of functions needed from android.support.v4.graphics.ColorUtils.
622 */
623 private static class ColorUtilsFromCompat {
624 private static final double XYZ_WHITE_REFERENCE_X = 95.047;
625 private static final double XYZ_WHITE_REFERENCE_Y = 100;
626 private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
627 private static final double XYZ_EPSILON = 0.008856;
628 private static final double XYZ_KAPPA = 903.3;
629
630 private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
631 private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
632
633 private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
634
635 private ColorUtilsFromCompat() {}
636
637 /**
638 * Composite two potentially translucent colors over each other and returns the result.
639 */
640 public static int compositeColors(@ColorInt int foreground, @ColorInt int background) {
641 int bgAlpha = Color.alpha(background);
642 int fgAlpha = Color.alpha(foreground);
643 int a = compositeAlpha(fgAlpha, bgAlpha);
644
645 int r = compositeComponent(Color.red(foreground), fgAlpha,
646 Color.red(background), bgAlpha, a);
647 int g = compositeComponent(Color.green(foreground), fgAlpha,
648 Color.green(background), bgAlpha, a);
649 int b = compositeComponent(Color.blue(foreground), fgAlpha,
650 Color.blue(background), bgAlpha, a);
651
652 return Color.argb(a, r, g, b);
653 }
654
655 private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
656 return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
657 }
658
659 private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
660 if (a == 0) return 0;
661 return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
662 }
663
664 /**
665 * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}.
666 * <p>Defined as the Y component in the XYZ representation of {@code color}.</p>
667 */
668 @FloatRange(from = 0.0, to = 1.0)
669 public static double calculateLuminance(@ColorInt int color) {
670 final double[] result = getTempDouble3Array();
671 colorToXYZ(color, result);
672 // Luminance is the Y component
673 return result[1] / 100;
674 }
675
676 /**
677 * Returns the contrast ratio between {@code foreground} and {@code background}.
678 * {@code background} must be opaque.
679 * <p>
680 * Formula defined
681 * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
682 */
683 public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) {
684 if (Color.alpha(background) != 255) {
Selim Cinek4c807912017-06-23 17:22:38 -0700685 Log.wtf(TAG, "background can not be translucent: #"
Adrian Roos4ff3b122016-02-01 12:26:13 -0800686 + Integer.toHexString(background));
687 }
688 if (Color.alpha(foreground) < 255) {
689 // If the foreground is translucent, composite the foreground over the background
690 foreground = compositeColors(foreground, background);
691 }
692
693 final double luminance1 = calculateLuminance(foreground) + 0.05;
694 final double luminance2 = calculateLuminance(background) + 0.05;
695
696 // Now return the lighter luminance divided by the darker luminance
697 return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
698 }
699
700 /**
701 * Convert the ARGB color to its CIE Lab representative components.
702 *
703 * @param color the ARGB color to convert. The alpha component is ignored
704 * @param outLab 3-element array which holds the resulting LAB components
705 */
706 public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) {
707 RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab);
708 }
709
710 /**
711 * Convert RGB components to its CIE Lab representative components.
712 *
713 * <ul>
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700714 * <li>outLab[0] is L [0 ...100)</li>
Adrian Roos4ff3b122016-02-01 12:26:13 -0800715 * <li>outLab[1] is a [-128...127)</li>
716 * <li>outLab[2] is b [-128...127)</li>
717 * </ul>
718 *
719 * @param r red component value [0..255]
720 * @param g green component value [0..255]
721 * @param b blue component value [0..255]
722 * @param outLab 3-element array which holds the resulting LAB components
723 */
724 public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r,
725 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
726 @NonNull double[] outLab) {
727 // First we convert RGB to XYZ
728 RGBToXYZ(r, g, b, outLab);
729 // outLab now contains XYZ
730 XYZToLAB(outLab[0], outLab[1], outLab[2], outLab);
731 // outLab now contains LAB representation
732 }
733
734 /**
735 * Convert the ARGB color to it's CIE XYZ representative components.
736 *
737 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
738 * 2° Standard Observer (1931).</p>
739 *
740 * <ul>
741 * <li>outXyz[0] is X [0 ...95.047)</li>
742 * <li>outXyz[1] is Y [0...100)</li>
743 * <li>outXyz[2] is Z [0...108.883)</li>
744 * </ul>
745 *
746 * @param color the ARGB color to convert. The alpha component is ignored
747 * @param outXyz 3-element array which holds the resulting LAB components
748 */
749 public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
750 RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
751 }
752
753 /**
754 * Convert RGB components to it's CIE XYZ representative components.
755 *
756 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
757 * 2° Standard Observer (1931).</p>
758 *
759 * <ul>
760 * <li>outXyz[0] is X [0 ...95.047)</li>
761 * <li>outXyz[1] is Y [0...100)</li>
762 * <li>outXyz[2] is Z [0...108.883)</li>
763 * </ul>
764 *
765 * @param r red component value [0..255]
766 * @param g green component value [0..255]
767 * @param b blue component value [0..255]
768 * @param outXyz 3-element array which holds the resulting XYZ components
769 */
770 public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r,
771 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
772 @NonNull double[] outXyz) {
773 if (outXyz.length != 3) {
774 throw new IllegalArgumentException("outXyz must have a length of 3.");
775 }
776
777 double sr = r / 255.0;
778 sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
779 double sg = g / 255.0;
780 sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
781 double sb = b / 255.0;
782 sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
783
784 outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
785 outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
786 outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
787 }
788
789 /**
790 * Converts a color from CIE XYZ to CIE Lab 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 * <ul>
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700796 * <li>outLab[0] is L [0 ...100)</li>
Adrian Roos4ff3b122016-02-01 12:26:13 -0800797 * <li>outLab[1] is a [-128...127)</li>
798 * <li>outLab[2] is b [-128...127)</li>
799 * </ul>
800 *
801 * @param x X component value [0...95.047)
802 * @param y Y component value [0...100)
803 * @param z Z component value [0...108.883)
804 * @param outLab 3-element array which holds the resulting Lab components
805 */
806 public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
807 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
808 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
809 @NonNull double[] outLab) {
810 if (outLab.length != 3) {
811 throw new IllegalArgumentException("outLab must have a length of 3.");
812 }
813 x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
814 y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
815 z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
816 outLab[0] = Math.max(0, 116 * y - 16);
817 outLab[1] = 500 * (x - y);
818 outLab[2] = 200 * (y - z);
819 }
820
821 /**
822 * Converts a color from CIE Lab to CIE XYZ representation.
823 *
824 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
825 * 2° Standard Observer (1931).</p>
826 *
827 * <ul>
828 * <li>outXyz[0] is X [0 ...95.047)</li>
829 * <li>outXyz[1] is Y [0...100)</li>
830 * <li>outXyz[2] is Z [0...108.883)</li>
831 * </ul>
832 *
833 * @param l L component value [0...100)
834 * @param a A component value [-128...127)
835 * @param b B component value [-128...127)
836 * @param outXyz 3-element array which holds the resulting XYZ components
837 */
838 public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l,
839 @FloatRange(from = -128, to = 127) final double a,
840 @FloatRange(from = -128, to = 127) final double b,
841 @NonNull double[] outXyz) {
842 final double fy = (l + 16) / 116;
843 final double fx = a / 500 + fy;
844 final double fz = fy - b / 200;
845
846 double tmp = Math.pow(fx, 3);
847 final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA;
848 final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA;
849
850 tmp = Math.pow(fz, 3);
851 final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA;
852
853 outXyz[0] = xr * XYZ_WHITE_REFERENCE_X;
854 outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y;
855 outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z;
856 }
857
858 /**
859 * Converts a color from CIE XYZ to its RGB representation.
860 *
861 * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
862 * 2° Standard Observer (1931).</p>
863 *
864 * @param x X component value [0...95.047)
865 * @param y Y component value [0...100)
866 * @param z Z component value [0...108.883)
867 * @return int containing the RGB representation
868 */
869 @ColorInt
870 public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
871 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
872 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) {
873 double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100;
874 double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100;
875 double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100;
876
877 r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
878 g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
879 b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;
880
881 return Color.rgb(
882 constrain((int) Math.round(r * 255), 0, 255),
883 constrain((int) Math.round(g * 255), 0, 255),
884 constrain((int) Math.round(b * 255), 0, 255));
885 }
886
887 /**
888 * Converts a color from CIE Lab to its RGB representation.
889 *
890 * @param l L component value [0...100]
891 * @param a A component value [-128...127]
892 * @param b B component value [-128...127]
893 * @return int containing the RGB representation
894 */
895 @ColorInt
896 public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l,
897 @FloatRange(from = -128, to = 127) final double a,
898 @FloatRange(from = -128, to = 127) final double b) {
899 final double[] result = getTempDouble3Array();
900 LABToXYZ(l, a, b, result);
901 return XYZToColor(result[0], result[1], result[2]);
902 }
903
904 private static int constrain(int amount, int low, int high) {
905 return amount < low ? low : (amount > high ? high : amount);
906 }
907
Adrian Roos99d2e642017-02-09 14:58:44 +0100908 private static float constrain(float amount, float low, float high) {
909 return amount < low ? low : (amount > high ? high : amount);
910 }
911
Adrian Roos4ff3b122016-02-01 12:26:13 -0800912 private static double pivotXyzComponent(double component) {
913 return component > XYZ_EPSILON
914 ? Math.pow(component, 1 / 3.0)
915 : (XYZ_KAPPA * component + 16) / 116;
916 }
917
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700918 public static double[] getTempDouble3Array() {
Adrian Roos4ff3b122016-02-01 12:26:13 -0800919 double[] result = TEMP_ARRAY.get();
920 if (result == null) {
921 result = new double[3];
922 TEMP_ARRAY.set(result);
923 }
924 return result;
925 }
926
Adrian Roos99d2e642017-02-09 14:58:44 +0100927 /**
928 * Convert HSL (hue-saturation-lightness) components to a RGB color.
929 * <ul>
930 * <li>hsl[0] is Hue [0 .. 360)</li>
931 * <li>hsl[1] is Saturation [0...1]</li>
932 * <li>hsl[2] is Lightness [0...1]</li>
933 * </ul>
934 * If hsv values are out of range, they are pinned.
935 *
936 * @param hsl 3-element array which holds the input HSL components
937 * @return the resulting RGB color
938 */
939 @ColorInt
940 public static int HSLToColor(@NonNull float[] hsl) {
941 final float h = hsl[0];
942 final float s = hsl[1];
943 final float l = hsl[2];
944
945 final float c = (1f - Math.abs(2 * l - 1f)) * s;
946 final float m = l - 0.5f * c;
947 final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
948
949 final int hueSegment = (int) h / 60;
950
951 int r = 0, g = 0, b = 0;
952
953 switch (hueSegment) {
954 case 0:
955 r = Math.round(255 * (c + m));
956 g = Math.round(255 * (x + m));
957 b = Math.round(255 * m);
958 break;
959 case 1:
960 r = Math.round(255 * (x + m));
961 g = Math.round(255 * (c + m));
962 b = Math.round(255 * m);
963 break;
964 case 2:
965 r = Math.round(255 * m);
966 g = Math.round(255 * (c + m));
967 b = Math.round(255 * (x + m));
968 break;
969 case 3:
970 r = Math.round(255 * m);
971 g = Math.round(255 * (x + m));
972 b = Math.round(255 * (c + m));
973 break;
974 case 4:
975 r = Math.round(255 * (x + m));
976 g = Math.round(255 * m);
977 b = Math.round(255 * (c + m));
978 break;
979 case 5:
980 case 6:
981 r = Math.round(255 * (c + m));
982 g = Math.round(255 * m);
983 b = Math.round(255 * (x + m));
984 break;
985 }
986
987 r = constrain(r, 0, 255);
988 g = constrain(g, 0, 255);
989 b = constrain(b, 0, 255);
990
991 return Color.rgb(r, g, b);
992 }
993
994 /**
995 * Convert the ARGB color to its HSL (hue-saturation-lightness) components.
996 * <ul>
997 * <li>outHsl[0] is Hue [0 .. 360)</li>
998 * <li>outHsl[1] is Saturation [0...1]</li>
999 * <li>outHsl[2] is Lightness [0...1]</li>
1000 * </ul>
1001 *
1002 * @param color the ARGB color to convert. The alpha component is ignored
1003 * @param outHsl 3-element array which holds the resulting HSL components
1004 */
1005 public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) {
1006 RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl);
1007 }
1008
1009 /**
1010 * Convert RGB components to HSL (hue-saturation-lightness).
1011 * <ul>
1012 * <li>outHsl[0] is Hue [0 .. 360)</li>
1013 * <li>outHsl[1] is Saturation [0...1]</li>
1014 * <li>outHsl[2] is Lightness [0...1]</li>
1015 * </ul>
1016 *
1017 * @param r red component value [0..255]
1018 * @param g green component value [0..255]
1019 * @param b blue component value [0..255]
1020 * @param outHsl 3-element array which holds the resulting HSL components
1021 */
1022 public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r,
1023 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
1024 @NonNull float[] outHsl) {
1025 final float rf = r / 255f;
1026 final float gf = g / 255f;
1027 final float bf = b / 255f;
1028
1029 final float max = Math.max(rf, Math.max(gf, bf));
1030 final float min = Math.min(rf, Math.min(gf, bf));
1031 final float deltaMaxMin = max - min;
1032
1033 float h, s;
1034 float l = (max + min) / 2f;
1035
1036 if (max == min) {
1037 // Monochromatic
1038 h = s = 0f;
1039 } else {
1040 if (max == rf) {
1041 h = ((gf - bf) / deltaMaxMin) % 6f;
1042 } else if (max == gf) {
1043 h = ((bf - rf) / deltaMaxMin) + 2f;
1044 } else {
1045 h = ((rf - gf) / deltaMaxMin) + 4f;
1046 }
1047
1048 s = deltaMaxMin / (1f - Math.abs(2f * l - 1f));
1049 }
1050
1051 h = (h * 60f) % 360f;
1052 if (h < 0) {
1053 h += 360f;
1054 }
1055
1056 outHsl[0] = constrain(h, 0f, 360f);
1057 outHsl[1] = constrain(s, 0f, 1f);
1058 outHsl[2] = constrain(l, 0f, 1f);
1059 }
1060
Adrian Roos4ff3b122016-02-01 12:26:13 -08001061 }
Jorim Jaggi5c2d8462014-03-21 17:37:00 +01001062}