blob: d037d4b42eeda26b1b337b24d5a62f25e978bd26 [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 */
Lucas Dupina291d192018-06-07 13:59:42 -070051public class ContrastColorUtil {
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010052
Lucas Dupina291d192018-06-07 13:59:42 -070053 private static final String TAG = "ContrastColorUtil";
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();
Lucas Dupina291d192018-06-07 13:59:42 -070057 private static ContrastColorUtil 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
Lucas Dupina291d192018-06-07 13:59:42 -070065 public static ContrastColorUtil getInstance(Context context) {
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010066 synchronized (sLock) {
67 if (sInstance == null) {
Lucas Dupina291d192018-06-07 13:59:42 -070068 sInstance = new ContrastColorUtil(context);
Jorim Jaggi5c2d8462014-03-21 17:37:00 +010069 }
70 return sInstance;
71 }
72 }
73
Lucas Dupina291d192018-06-07 13:59:42 -070074 private ContrastColorUtil(Context context) {
Dan Sandler05c362d2014-09-03 00:16:27 +020075 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 */
Kenny Guy14d035c2018-05-02 19:10:36 +0100421 public static int ensureTextContrast(int color, int bg, boolean isBgDarker) {
422 return ensureContrast(color, bg, isBgDarker, 4.5);
423 }
424
425 /**
426 * Finds a color with sufficient contrast over bg that has the same or darker hue as the
427 * original color, depending on the value of {@code isBgDarker}.
428 *
429 * @param color the color to start searching from
430 * @param bg the color to ensure contrast against
431 * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}
432 * @param minRatio the minimum contrast ratio required
433 */
434 public static int ensureContrast(int color, int bg, boolean isBgDarker, double minRatio) {
Anthony Chenad4d1582017-04-10 16:07:58 -0700435 return isBgDarker
Kenny Guy14d035c2018-05-02 19:10:36 +0100436 ? findContrastColorAgainstDark(color, bg, true, minRatio)
437 : findContrastColor(color, bg, true, minRatio);
Adrian Roos4ff3b122016-02-01 12:26:13 -0800438 }
439
440 /** Finds a background color for a text view with given text color and hint text color, that
441 * has the same hue as the original color.
442 */
443 public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) {
444 color = findContrastColor(color, hintColor, false, 3.0);
445 return findContrastColor(color, textColor, false, 4.5);
446 }
447
448 private static String contrastChange(int colorOld, int colorNew, int bg) {
449 return String.format("from %.2f:1 to %.2f:1",
450 ColorUtilsFromCompat.calculateContrast(colorOld, bg),
451 ColorUtilsFromCompat.calculateContrast(colorNew, bg));
452 }
453
454 /**
455 * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT}
456 */
Lucas Dupinf03e7522018-06-25 16:21:13 -0700457 public static int resolveColor(Context context, int color, boolean defaultBackgroundIsDark) {
Lucas Dupind26facc2018-09-10 18:07:30 -0700458 if (color == Notification.COLOR_DEFAULT || defaultBackgroundIsDark) {
Lucas Dupinf03e7522018-06-25 16:21:13 -0700459 int res = defaultBackgroundIsDark
460 ? com.android.internal.R.color.notification_default_color_dark
461 : com.android.internal.R.color.notification_default_color_light;
462 return context.getColor(res);
Adrian Roos4ff3b122016-02-01 12:26:13 -0800463 }
464 return color;
465 }
466
467 /**
468 * Resolves a Notification's color such that it has enough contrast to be used as the
Anthony Chenad4d1582017-04-10 16:07:58 -0700469 * color for the Notification's action and header text on a background that is lighter than
470 * {@code notificationColor}.
471 *
472 * @see {@link #resolveContrastColor(Context, int, boolean)}
473 */
474 public static int resolveContrastColor(Context context, int notificationColor,
475 int backgroundColor) {
Lucas Dupina291d192018-06-07 13:59:42 -0700476 return ContrastColorUtil.resolveContrastColor(context, notificationColor,
Anthony Chenad4d1582017-04-10 16:07:58 -0700477 backgroundColor, false /* isDark */);
478 }
479
480 /**
481 * Resolves a Notification's color such that it has enough contrast to be used as the
Adrian Roos4ff3b122016-02-01 12:26:13 -0800482 * color for the Notification's action and header text.
483 *
484 * @param notificationColor the color of the notification or {@link Notification#COLOR_DEFAULT}
Selim Cinekac5f0272017-05-02 16:05:41 -0700485 * @param backgroundColor the background color to ensure the contrast against.
Anthony Chenad4d1582017-04-10 16:07:58 -0700486 * @param isDark whether or not the {@code notificationColor} will be placed on a background
487 * that is darker than the color itself
Adrian Roos4ff3b122016-02-01 12:26:13 -0800488 * @return a color of the same hue with enough contrast against the backgrounds.
489 */
Selim Cinekac5f0272017-05-02 16:05:41 -0700490 public static int resolveContrastColor(Context context, int notificationColor,
Anthony Chenad4d1582017-04-10 16:07:58 -0700491 int backgroundColor, boolean isDark) {
Lucas Dupinf03e7522018-06-25 16:21:13 -0700492 final int resolvedColor = resolveColor(context, notificationColor, isDark);
Adrian Roos4ff3b122016-02-01 12:26:13 -0800493
Adrian Roos4ff3b122016-02-01 12:26:13 -0800494 int color = resolvedColor;
Lucas Dupina291d192018-06-07 13:59:42 -0700495 color = ContrastColorUtil.ensureTextContrast(color, backgroundColor, isDark);
Adrian Roos4ff3b122016-02-01 12:26:13 -0800496
497 if (color != resolvedColor) {
498 if (DEBUG){
499 Log.w(TAG, String.format(
Selim Cinekc7f5a822018-03-20 19:32:06 -0700500 "Enhanced contrast of notification for %s"
Adrian Roos4ff3b122016-02-01 12:26:13 -0800501 + " and %s (over background) by changing #%s to %s",
502 context.getPackageName(),
Lucas Dupina291d192018-06-07 13:59:42 -0700503 ContrastColorUtil.contrastChange(resolvedColor, color, backgroundColor),
Adrian Roos4ff3b122016-02-01 12:26:13 -0800504 Integer.toHexString(resolvedColor), Integer.toHexString(color)));
505 }
506 }
507 return color;
508 }
509
510 /**
Selim Cinek5fb73f82017-04-20 16:55:38 -0700511 * Change a color by a specified value
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700512 * @param baseColor the base color to lighten
513 * @param amount the amount to lighten the color from 0 to 100. This corresponds to the L
Selim Cinek5fb73f82017-04-20 16:55:38 -0700514 * increase in the LAB color space. A negative value will darken the color and
515 * a positive will lighten it.
516 * @return the changed color
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700517 */
Selim Cinek5fb73f82017-04-20 16:55:38 -0700518 public static int changeColorLightness(int baseColor, int amount) {
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700519 final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
520 ColorUtilsFromCompat.colorToLAB(baseColor, result);
Selim Cinek5fb73f82017-04-20 16:55:38 -0700521 result[0] = Math.max(Math.min(100, result[0] + amount), 0);
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700522 return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
523 }
524
Lucas Dupinf03e7522018-06-25 16:21:13 -0700525 public static int resolvePrimaryColor(Context context, int backgroundColor,
526 boolean defaultBackgroundIsDark) {
527 boolean useDark = shouldUseDark(backgroundColor, defaultBackgroundIsDark);
Selim Cinek7b9605b2017-01-19 17:36:00 -0800528 if (useDark) {
529 return context.getColor(
530 com.android.internal.R.color.notification_primary_text_color_light);
531 } else {
532 return context.getColor(
533 com.android.internal.R.color.notification_primary_text_color_dark);
534 }
535 }
536
Lucas Dupinf03e7522018-06-25 16:21:13 -0700537 public static int resolveSecondaryColor(Context context, int backgroundColor,
538 boolean defaultBackgroundIsDark) {
539 boolean useDark = shouldUseDark(backgroundColor, defaultBackgroundIsDark);
Selim Cinek7b9605b2017-01-19 17:36:00 -0800540 if (useDark) {
541 return context.getColor(
542 com.android.internal.R.color.notification_secondary_text_color_light);
543 } else {
544 return context.getColor(
545 com.android.internal.R.color.notification_secondary_text_color_dark);
546 }
547 }
548
Lucas Dupinf03e7522018-06-25 16:21:13 -0700549 public static int resolveDefaultColor(Context context, int backgroundColor,
550 boolean defaultBackgroundIsDark) {
551 boolean useDark = shouldUseDark(backgroundColor, defaultBackgroundIsDark);
Selim Cinekc7f5a822018-03-20 19:32:06 -0700552 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 Cinek622c64a2017-04-17 17:10:05 -0700561 /**
562 * Get a color that stays in the same tint, but darkens or lightens it by a certain
563 * amount.
564 * This also looks at the lightness of the provided color and shifts it appropriately.
565 *
566 * @param color the base color to use
567 * @param amount the amount from 1 to 100 how much to modify the color
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800568 * @return the new color that was modified
Selim Cinek622c64a2017-04-17 17:10:05 -0700569 */
570 public static int getShiftedColor(int color, int amount) {
Selim Cinek7b9605b2017-01-19 17:36:00 -0800571 final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
Selim Cinek622c64a2017-04-17 17:10:05 -0700572 ColorUtilsFromCompat.colorToLAB(color, result);
573 if (result[0] >= 4) {
574 result[0] = Math.max(0, result[0] - amount);
Selim Cinek7b9605b2017-01-19 17:36:00 -0800575 } else {
Selim Cinek622c64a2017-04-17 17:10:05 -0700576 result[0] = Math.min(100, result[0] + amount);
Selim Cinek7b9605b2017-01-19 17:36:00 -0800577 }
578 return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
579 }
580
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800581 /**
582 * Blends the provided color with white to create a muted version.
583 *
584 * @param color the color to mute
585 * @param alpha the amount from 0 to 1 to set the alpha component of the white scrim
586 * @return the new color that was modified
587 */
588 public static int getMutedColor(int color, float alpha) {
589 int whiteScrim = ColorUtilsFromCompat.setAlphaComponent(
590 Color.WHITE, (int) (255 * alpha));
591 return compositeColors(whiteScrim, color);
592 }
593
Lucas Dupinf03e7522018-06-25 16:21:13 -0700594 private static boolean shouldUseDark(int backgroundColor, boolean defaultBackgroundIsDark) {
595 if (backgroundColor == Notification.COLOR_DEFAULT) {
596 return !defaultBackgroundIsDark;
Selim Cinek7b9605b2017-01-19 17:36:00 -0800597 }
Lucas Dupinf03e7522018-06-25 16:21:13 -0700598 return ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5;
Selim Cinek7b9605b2017-01-19 17:36:00 -0800599 }
600
Selim Cinek5fb73f82017-04-20 16:55:38 -0700601 public static double calculateLuminance(int backgroundColor) {
602 return ColorUtilsFromCompat.calculateLuminance(backgroundColor);
603 }
604
605
606 public static double calculateContrast(int foregroundColor, int backgroundColor) {
607 return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor);
608 }
609
Selim Cinek389edcd2017-05-11 19:16:44 -0700610 public static boolean satisfiesTextContrast(int backgroundColor, int foregroundColor) {
Lucas Dupina291d192018-06-07 13:59:42 -0700611 return ContrastColorUtil.calculateContrast(foregroundColor, backgroundColor) >= 4.5;
Selim Cinek389edcd2017-05-11 19:16:44 -0700612 }
613
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700614 /**
Selim Cinekac5f0272017-05-02 16:05:41 -0700615 * Composite two potentially translucent colors over each other and returns the result.
616 */
617 public static int compositeColors(int foreground, int background) {
618 return ColorUtilsFromCompat.compositeColors(foreground, background);
619 }
620
Selim Cinek389edcd2017-05-11 19:16:44 -0700621 public static boolean isColorLight(int backgroundColor) {
622 return calculateLuminance(backgroundColor) > 0.5f;
623 }
624
Selim Cinekac5f0272017-05-02 16:05:41 -0700625 /**
Adrian Roos4ff3b122016-02-01 12:26:13 -0800626 * Framework copy of functions needed from android.support.v4.graphics.ColorUtils.
627 */
628 private static class ColorUtilsFromCompat {
629 private static final double XYZ_WHITE_REFERENCE_X = 95.047;
630 private static final double XYZ_WHITE_REFERENCE_Y = 100;
631 private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
632 private static final double XYZ_EPSILON = 0.008856;
633 private static final double XYZ_KAPPA = 903.3;
634
635 private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
636 private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
637
638 private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
639
640 private ColorUtilsFromCompat() {}
641
642 /**
643 * Composite two potentially translucent colors over each other and returns the result.
644 */
645 public static int compositeColors(@ColorInt int foreground, @ColorInt int background) {
646 int bgAlpha = Color.alpha(background);
647 int fgAlpha = Color.alpha(foreground);
648 int a = compositeAlpha(fgAlpha, bgAlpha);
649
650 int r = compositeComponent(Color.red(foreground), fgAlpha,
651 Color.red(background), bgAlpha, a);
652 int g = compositeComponent(Color.green(foreground), fgAlpha,
653 Color.green(background), bgAlpha, a);
654 int b = compositeComponent(Color.blue(foreground), fgAlpha,
655 Color.blue(background), bgAlpha, a);
656
657 return Color.argb(a, r, g, b);
658 }
659
660 private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
661 return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
662 }
663
664 private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
665 if (a == 0) return 0;
666 return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
667 }
668
669 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800670 * Set the alpha component of {@code color} to be {@code alpha}.
671 */
672 @ColorInt
673 public static int setAlphaComponent(@ColorInt int color,
674 @IntRange(from = 0x0, to = 0xFF) int alpha) {
675 if (alpha < 0 || alpha > 255) {
676 throw new IllegalArgumentException("alpha must be between 0 and 255.");
677 }
678 return (color & 0x00ffffff) | (alpha << 24);
679 }
680
681 /**
Adrian Roos4ff3b122016-02-01 12:26:13 -0800682 * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}.
683 * <p>Defined as the Y component in the XYZ representation of {@code color}.</p>
684 */
685 @FloatRange(from = 0.0, to = 1.0)
686 public static double calculateLuminance(@ColorInt int color) {
687 final double[] result = getTempDouble3Array();
688 colorToXYZ(color, result);
689 // Luminance is the Y component
690 return result[1] / 100;
691 }
692
693 /**
694 * Returns the contrast ratio between {@code foreground} and {@code background}.
695 * {@code background} must be opaque.
696 * <p>
697 * Formula defined
698 * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
699 */
700 public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) {
701 if (Color.alpha(background) != 255) {
Selim Cinek4c807912017-06-23 17:22:38 -0700702 Log.wtf(TAG, "background can not be translucent: #"
Adrian Roos4ff3b122016-02-01 12:26:13 -0800703 + Integer.toHexString(background));
704 }
705 if (Color.alpha(foreground) < 255) {
706 // If the foreground is translucent, composite the foreground over the background
707 foreground = compositeColors(foreground, background);
708 }
709
710 final double luminance1 = calculateLuminance(foreground) + 0.05;
711 final double luminance2 = calculateLuminance(background) + 0.05;
712
713 // Now return the lighter luminance divided by the darker luminance
714 return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
715 }
716
717 /**
718 * Convert the ARGB color to its CIE Lab representative components.
719 *
720 * @param color the ARGB color to convert. The alpha component is ignored
721 * @param outLab 3-element array which holds the resulting LAB components
722 */
723 public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) {
724 RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab);
725 }
726
727 /**
728 * Convert RGB components to its CIE Lab representative components.
729 *
730 * <ul>
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700731 * <li>outLab[0] is L [0 ...100)</li>
Adrian Roos4ff3b122016-02-01 12:26:13 -0800732 * <li>outLab[1] is a [-128...127)</li>
733 * <li>outLab[2] is b [-128...127)</li>
734 * </ul>
735 *
736 * @param r red component value [0..255]
737 * @param g green component value [0..255]
738 * @param b blue component value [0..255]
739 * @param outLab 3-element array which holds the resulting LAB components
740 */
741 public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r,
742 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
743 @NonNull double[] outLab) {
744 // First we convert RGB to XYZ
745 RGBToXYZ(r, g, b, outLab);
746 // outLab now contains XYZ
747 XYZToLAB(outLab[0], outLab[1], outLab[2], outLab);
748 // outLab now contains LAB representation
749 }
750
751 /**
752 * Convert the ARGB color to it's CIE XYZ representative components.
753 *
754 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
755 * 2° Standard Observer (1931).</p>
756 *
757 * <ul>
758 * <li>outXyz[0] is X [0 ...95.047)</li>
759 * <li>outXyz[1] is Y [0...100)</li>
760 * <li>outXyz[2] is Z [0...108.883)</li>
761 * </ul>
762 *
763 * @param color the ARGB color to convert. The alpha component is ignored
764 * @param outXyz 3-element array which holds the resulting LAB components
765 */
766 public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
767 RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
768 }
769
770 /**
771 * Convert RGB components to it's CIE XYZ representative components.
772 *
773 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
774 * 2° Standard Observer (1931).</p>
775 *
776 * <ul>
777 * <li>outXyz[0] is X [0 ...95.047)</li>
778 * <li>outXyz[1] is Y [0...100)</li>
779 * <li>outXyz[2] is Z [0...108.883)</li>
780 * </ul>
781 *
782 * @param r red component value [0..255]
783 * @param g green component value [0..255]
784 * @param b blue component value [0..255]
785 * @param outXyz 3-element array which holds the resulting XYZ components
786 */
787 public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r,
788 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
789 @NonNull double[] outXyz) {
790 if (outXyz.length != 3) {
791 throw new IllegalArgumentException("outXyz must have a length of 3.");
792 }
793
794 double sr = r / 255.0;
795 sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
796 double sg = g / 255.0;
797 sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
798 double sb = b / 255.0;
799 sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
800
801 outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
802 outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
803 outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
804 }
805
806 /**
807 * Converts a color from CIE XYZ to CIE Lab representation.
808 *
809 * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
810 * 2° Standard Observer (1931).</p>
811 *
812 * <ul>
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700813 * <li>outLab[0] is L [0 ...100)</li>
Adrian Roos4ff3b122016-02-01 12:26:13 -0800814 * <li>outLab[1] is a [-128...127)</li>
815 * <li>outLab[2] is b [-128...127)</li>
816 * </ul>
817 *
818 * @param x X component value [0...95.047)
819 * @param y Y component value [0...100)
820 * @param z Z component value [0...108.883)
821 * @param outLab 3-element array which holds the resulting Lab components
822 */
823 public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
824 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
825 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
826 @NonNull double[] outLab) {
827 if (outLab.length != 3) {
828 throw new IllegalArgumentException("outLab must have a length of 3.");
829 }
830 x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
831 y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
832 z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
833 outLab[0] = Math.max(0, 116 * y - 16);
834 outLab[1] = 500 * (x - y);
835 outLab[2] = 200 * (y - z);
836 }
837
838 /**
839 * Converts a color from CIE Lab to CIE XYZ representation.
840 *
841 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
842 * 2° Standard Observer (1931).</p>
843 *
844 * <ul>
845 * <li>outXyz[0] is X [0 ...95.047)</li>
846 * <li>outXyz[1] is Y [0...100)</li>
847 * <li>outXyz[2] is Z [0...108.883)</li>
848 * </ul>
849 *
850 * @param l L component value [0...100)
851 * @param a A component value [-128...127)
852 * @param b B component value [-128...127)
853 * @param outXyz 3-element array which holds the resulting XYZ components
854 */
855 public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l,
856 @FloatRange(from = -128, to = 127) final double a,
857 @FloatRange(from = -128, to = 127) final double b,
858 @NonNull double[] outXyz) {
859 final double fy = (l + 16) / 116;
860 final double fx = a / 500 + fy;
861 final double fz = fy - b / 200;
862
863 double tmp = Math.pow(fx, 3);
864 final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA;
865 final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA;
866
867 tmp = Math.pow(fz, 3);
868 final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA;
869
870 outXyz[0] = xr * XYZ_WHITE_REFERENCE_X;
871 outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y;
872 outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z;
873 }
874
875 /**
876 * Converts a color from CIE XYZ to its RGB representation.
877 *
878 * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
879 * 2° Standard Observer (1931).</p>
880 *
881 * @param x X component value [0...95.047)
882 * @param y Y component value [0...100)
883 * @param z Z component value [0...108.883)
884 * @return int containing the RGB representation
885 */
886 @ColorInt
887 public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
888 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
889 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) {
890 double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100;
891 double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100;
892 double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100;
893
894 r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
895 g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
896 b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;
897
898 return Color.rgb(
899 constrain((int) Math.round(r * 255), 0, 255),
900 constrain((int) Math.round(g * 255), 0, 255),
901 constrain((int) Math.round(b * 255), 0, 255));
902 }
903
904 /**
905 * Converts a color from CIE Lab to its RGB representation.
906 *
907 * @param l L component value [0...100]
908 * @param a A component value [-128...127]
909 * @param b B component value [-128...127]
910 * @return int containing the RGB representation
911 */
912 @ColorInt
913 public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l,
914 @FloatRange(from = -128, to = 127) final double a,
915 @FloatRange(from = -128, to = 127) final double b) {
916 final double[] result = getTempDouble3Array();
917 LABToXYZ(l, a, b, result);
918 return XYZToColor(result[0], result[1], result[2]);
919 }
920
921 private static int constrain(int amount, int low, int high) {
922 return amount < low ? low : (amount > high ? high : amount);
923 }
924
Adrian Roos99d2e642017-02-09 14:58:44 +0100925 private static float constrain(float amount, float low, float high) {
926 return amount < low ? low : (amount > high ? high : amount);
927 }
928
Adrian Roos4ff3b122016-02-01 12:26:13 -0800929 private static double pivotXyzComponent(double component) {
930 return component > XYZ_EPSILON
931 ? Math.pow(component, 1 / 3.0)
932 : (XYZ_KAPPA * component + 16) / 116;
933 }
934
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700935 public static double[] getTempDouble3Array() {
Adrian Roos4ff3b122016-02-01 12:26:13 -0800936 double[] result = TEMP_ARRAY.get();
937 if (result == null) {
938 result = new double[3];
939 TEMP_ARRAY.set(result);
940 }
941 return result;
942 }
943
Adrian Roos99d2e642017-02-09 14:58:44 +0100944 /**
945 * Convert HSL (hue-saturation-lightness) components to a RGB color.
946 * <ul>
947 * <li>hsl[0] is Hue [0 .. 360)</li>
948 * <li>hsl[1] is Saturation [0...1]</li>
949 * <li>hsl[2] is Lightness [0...1]</li>
950 * </ul>
951 * If hsv values are out of range, they are pinned.
952 *
953 * @param hsl 3-element array which holds the input HSL components
954 * @return the resulting RGB color
955 */
956 @ColorInt
957 public static int HSLToColor(@NonNull float[] hsl) {
958 final float h = hsl[0];
959 final float s = hsl[1];
960 final float l = hsl[2];
961
962 final float c = (1f - Math.abs(2 * l - 1f)) * s;
963 final float m = l - 0.5f * c;
964 final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
965
966 final int hueSegment = (int) h / 60;
967
968 int r = 0, g = 0, b = 0;
969
970 switch (hueSegment) {
971 case 0:
972 r = Math.round(255 * (c + m));
973 g = Math.round(255 * (x + m));
974 b = Math.round(255 * m);
975 break;
976 case 1:
977 r = Math.round(255 * (x + m));
978 g = Math.round(255 * (c + m));
979 b = Math.round(255 * m);
980 break;
981 case 2:
982 r = Math.round(255 * m);
983 g = Math.round(255 * (c + m));
984 b = Math.round(255 * (x + m));
985 break;
986 case 3:
987 r = Math.round(255 * m);
988 g = Math.round(255 * (x + m));
989 b = Math.round(255 * (c + m));
990 break;
991 case 4:
992 r = Math.round(255 * (x + m));
993 g = Math.round(255 * m);
994 b = Math.round(255 * (c + m));
995 break;
996 case 5:
997 case 6:
998 r = Math.round(255 * (c + m));
999 g = Math.round(255 * m);
1000 b = Math.round(255 * (x + m));
1001 break;
1002 }
1003
1004 r = constrain(r, 0, 255);
1005 g = constrain(g, 0, 255);
1006 b = constrain(b, 0, 255);
1007
1008 return Color.rgb(r, g, b);
1009 }
1010
1011 /**
1012 * Convert the ARGB color to its HSL (hue-saturation-lightness) components.
1013 * <ul>
1014 * <li>outHsl[0] is Hue [0 .. 360)</li>
1015 * <li>outHsl[1] is Saturation [0...1]</li>
1016 * <li>outHsl[2] is Lightness [0...1]</li>
1017 * </ul>
1018 *
1019 * @param color the ARGB color to convert. The alpha component is ignored
1020 * @param outHsl 3-element array which holds the resulting HSL components
1021 */
1022 public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) {
1023 RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl);
1024 }
1025
1026 /**
1027 * Convert RGB components to HSL (hue-saturation-lightness).
1028 * <ul>
1029 * <li>outHsl[0] is Hue [0 .. 360)</li>
1030 * <li>outHsl[1] is Saturation [0...1]</li>
1031 * <li>outHsl[2] is Lightness [0...1]</li>
1032 * </ul>
1033 *
1034 * @param r red component value [0..255]
1035 * @param g green component value [0..255]
1036 * @param b blue component value [0..255]
1037 * @param outHsl 3-element array which holds the resulting HSL components
1038 */
1039 public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r,
1040 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
1041 @NonNull float[] outHsl) {
1042 final float rf = r / 255f;
1043 final float gf = g / 255f;
1044 final float bf = b / 255f;
1045
1046 final float max = Math.max(rf, Math.max(gf, bf));
1047 final float min = Math.min(rf, Math.min(gf, bf));
1048 final float deltaMaxMin = max - min;
1049
1050 float h, s;
1051 float l = (max + min) / 2f;
1052
1053 if (max == min) {
1054 // Monochromatic
1055 h = s = 0f;
1056 } else {
1057 if (max == rf) {
1058 h = ((gf - bf) / deltaMaxMin) % 6f;
1059 } else if (max == gf) {
1060 h = ((bf - rf) / deltaMaxMin) + 2f;
1061 } else {
1062 h = ((rf - gf) / deltaMaxMin) + 4f;
1063 }
1064
1065 s = deltaMaxMin / (1f - Math.abs(2f * l - 1f));
1066 }
1067
1068 h = (h * 60f) % 360f;
1069 if (h < 0) {
1070 h += 360f;
1071 }
1072
1073 outHsl[0] = constrain(h, 0f, 360f);
1074 outHsl[1] = constrain(s, 0f, 1f);
1075 outHsl[2] = constrain(l, 0f, 1f);
1076 }
1077
Adrian Roos4ff3b122016-02-01 12:26:13 -08001078 }
Jorim Jaggi5c2d8462014-03-21 17:37:00 +01001079}