blob: e0ba317f5eaad58219973eb277cdcfaca3d190a1 [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
Adrian Roos487374f2017-01-11 15:48:14 -0800525 public static int resolveAmbientColor(Context context, int notificationColor) {
Lucas Dupin1596b652018-09-24 14:33:05 -0700526 final int resolvedColor = notificationColor == Notification.COLOR_DEFAULT
527 ? context.getColor(com.android.internal.R.color.notification_default_color_dark)
528 : notificationColor;
Adrian Roos487374f2017-01-11 15:48:14 -0800529
530 int color = resolvedColor;
Lucas Dupina291d192018-06-07 13:59:42 -0700531 color = ContrastColorUtil.ensureTextContrastOnBlack(color);
Adrian Roos487374f2017-01-11 15:48:14 -0800532
533 if (color != resolvedColor) {
534 if (DEBUG){
535 Log.w(TAG, String.format(
536 "Ambient contrast of notification for %s is %s (over black)"
537 + " by changing #%s to #%s",
538 context.getPackageName(),
Lucas Dupina291d192018-06-07 13:59:42 -0700539 ContrastColorUtil.contrastChange(resolvedColor, color, Color.BLACK),
Adrian Roos487374f2017-01-11 15:48:14 -0800540 Integer.toHexString(resolvedColor), Integer.toHexString(color)));
541 }
542 }
543 return color;
544 }
545
Lucas Dupinf03e7522018-06-25 16:21:13 -0700546 public static int resolvePrimaryColor(Context context, int backgroundColor,
547 boolean defaultBackgroundIsDark) {
548 boolean useDark = shouldUseDark(backgroundColor, defaultBackgroundIsDark);
Selim Cinek7b9605b2017-01-19 17:36:00 -0800549 if (useDark) {
550 return context.getColor(
551 com.android.internal.R.color.notification_primary_text_color_light);
552 } else {
553 return context.getColor(
554 com.android.internal.R.color.notification_primary_text_color_dark);
555 }
556 }
557
Lucas Dupinf03e7522018-06-25 16:21:13 -0700558 public static int resolveSecondaryColor(Context context, int backgroundColor,
559 boolean defaultBackgroundIsDark) {
560 boolean useDark = shouldUseDark(backgroundColor, defaultBackgroundIsDark);
Selim Cinek7b9605b2017-01-19 17:36:00 -0800561 if (useDark) {
562 return context.getColor(
563 com.android.internal.R.color.notification_secondary_text_color_light);
564 } else {
565 return context.getColor(
566 com.android.internal.R.color.notification_secondary_text_color_dark);
567 }
568 }
569
Lucas Dupinf03e7522018-06-25 16:21:13 -0700570 public static int resolveDefaultColor(Context context, int backgroundColor,
571 boolean defaultBackgroundIsDark) {
572 boolean useDark = shouldUseDark(backgroundColor, defaultBackgroundIsDark);
Selim Cinekc7f5a822018-03-20 19:32:06 -0700573 if (useDark) {
574 return context.getColor(
575 com.android.internal.R.color.notification_default_color_light);
576 } else {
577 return context.getColor(
578 com.android.internal.R.color.notification_default_color_dark);
579 }
580 }
581
Selim Cinek622c64a2017-04-17 17:10:05 -0700582 /**
583 * Get a color that stays in the same tint, but darkens or lightens it by a certain
584 * amount.
585 * This also looks at the lightness of the provided color and shifts it appropriately.
586 *
587 * @param color the base color to use
588 * @param amount the amount from 1 to 100 how much to modify the color
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800589 * @return the new color that was modified
Selim Cinek622c64a2017-04-17 17:10:05 -0700590 */
591 public static int getShiftedColor(int color, int amount) {
Selim Cinek7b9605b2017-01-19 17:36:00 -0800592 final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
Selim Cinek622c64a2017-04-17 17:10:05 -0700593 ColorUtilsFromCompat.colorToLAB(color, result);
594 if (result[0] >= 4) {
595 result[0] = Math.max(0, result[0] - amount);
Selim Cinek7b9605b2017-01-19 17:36:00 -0800596 } else {
Selim Cinek622c64a2017-04-17 17:10:05 -0700597 result[0] = Math.min(100, result[0] + amount);
Selim Cinek7b9605b2017-01-19 17:36:00 -0800598 }
599 return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
600 }
601
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800602 /**
603 * Blends the provided color with white to create a muted version.
604 *
605 * @param color the color to mute
606 * @param alpha the amount from 0 to 1 to set the alpha component of the white scrim
607 * @return the new color that was modified
608 */
609 public static int getMutedColor(int color, float alpha) {
610 int whiteScrim = ColorUtilsFromCompat.setAlphaComponent(
611 Color.WHITE, (int) (255 * alpha));
612 return compositeColors(whiteScrim, color);
613 }
614
Lucas Dupinf03e7522018-06-25 16:21:13 -0700615 private static boolean shouldUseDark(int backgroundColor, boolean defaultBackgroundIsDark) {
616 if (backgroundColor == Notification.COLOR_DEFAULT) {
617 return !defaultBackgroundIsDark;
Selim Cinek7b9605b2017-01-19 17:36:00 -0800618 }
Lucas Dupinf03e7522018-06-25 16:21:13 -0700619 return ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5;
Selim Cinek7b9605b2017-01-19 17:36:00 -0800620 }
621
Selim Cinek5fb73f82017-04-20 16:55:38 -0700622 public static double calculateLuminance(int backgroundColor) {
623 return ColorUtilsFromCompat.calculateLuminance(backgroundColor);
624 }
625
626
627 public static double calculateContrast(int foregroundColor, int backgroundColor) {
628 return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor);
629 }
630
Selim Cinek389edcd2017-05-11 19:16:44 -0700631 public static boolean satisfiesTextContrast(int backgroundColor, int foregroundColor) {
Lucas Dupina291d192018-06-07 13:59:42 -0700632 return ContrastColorUtil.calculateContrast(foregroundColor, backgroundColor) >= 4.5;
Selim Cinek389edcd2017-05-11 19:16:44 -0700633 }
634
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700635 /**
Selim Cinekac5f0272017-05-02 16:05:41 -0700636 * Composite two potentially translucent colors over each other and returns the result.
637 */
638 public static int compositeColors(int foreground, int background) {
639 return ColorUtilsFromCompat.compositeColors(foreground, background);
640 }
641
Selim Cinek389edcd2017-05-11 19:16:44 -0700642 public static boolean isColorLight(int backgroundColor) {
643 return calculateLuminance(backgroundColor) > 0.5f;
644 }
645
Selim Cinekac5f0272017-05-02 16:05:41 -0700646 /**
Adrian Roos4ff3b122016-02-01 12:26:13 -0800647 * Framework copy of functions needed from android.support.v4.graphics.ColorUtils.
648 */
649 private static class ColorUtilsFromCompat {
650 private static final double XYZ_WHITE_REFERENCE_X = 95.047;
651 private static final double XYZ_WHITE_REFERENCE_Y = 100;
652 private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
653 private static final double XYZ_EPSILON = 0.008856;
654 private static final double XYZ_KAPPA = 903.3;
655
656 private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
657 private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
658
659 private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
660
661 private ColorUtilsFromCompat() {}
662
663 /**
664 * Composite two potentially translucent colors over each other and returns the result.
665 */
666 public static int compositeColors(@ColorInt int foreground, @ColorInt int background) {
667 int bgAlpha = Color.alpha(background);
668 int fgAlpha = Color.alpha(foreground);
669 int a = compositeAlpha(fgAlpha, bgAlpha);
670
671 int r = compositeComponent(Color.red(foreground), fgAlpha,
672 Color.red(background), bgAlpha, a);
673 int g = compositeComponent(Color.green(foreground), fgAlpha,
674 Color.green(background), bgAlpha, a);
675 int b = compositeComponent(Color.blue(foreground), fgAlpha,
676 Color.blue(background), bgAlpha, a);
677
678 return Color.argb(a, r, g, b);
679 }
680
681 private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
682 return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
683 }
684
685 private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
686 if (a == 0) return 0;
687 return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
688 }
689
690 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800691 * Set the alpha component of {@code color} to be {@code alpha}.
692 */
693 @ColorInt
694 public static int setAlphaComponent(@ColorInt int color,
695 @IntRange(from = 0x0, to = 0xFF) int alpha) {
696 if (alpha < 0 || alpha > 255) {
697 throw new IllegalArgumentException("alpha must be between 0 and 255.");
698 }
699 return (color & 0x00ffffff) | (alpha << 24);
700 }
701
702 /**
Adrian Roos4ff3b122016-02-01 12:26:13 -0800703 * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}.
704 * <p>Defined as the Y component in the XYZ representation of {@code color}.</p>
705 */
706 @FloatRange(from = 0.0, to = 1.0)
707 public static double calculateLuminance(@ColorInt int color) {
708 final double[] result = getTempDouble3Array();
709 colorToXYZ(color, result);
710 // Luminance is the Y component
711 return result[1] / 100;
712 }
713
714 /**
715 * Returns the contrast ratio between {@code foreground} and {@code background}.
716 * {@code background} must be opaque.
717 * <p>
718 * Formula defined
719 * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
720 */
721 public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) {
722 if (Color.alpha(background) != 255) {
Selim Cinek4c807912017-06-23 17:22:38 -0700723 Log.wtf(TAG, "background can not be translucent: #"
Adrian Roos4ff3b122016-02-01 12:26:13 -0800724 + Integer.toHexString(background));
725 }
726 if (Color.alpha(foreground) < 255) {
727 // If the foreground is translucent, composite the foreground over the background
728 foreground = compositeColors(foreground, background);
729 }
730
731 final double luminance1 = calculateLuminance(foreground) + 0.05;
732 final double luminance2 = calculateLuminance(background) + 0.05;
733
734 // Now return the lighter luminance divided by the darker luminance
735 return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
736 }
737
738 /**
739 * Convert the ARGB color to its CIE Lab representative components.
740 *
741 * @param color the ARGB color to convert. The alpha component is ignored
742 * @param outLab 3-element array which holds the resulting LAB components
743 */
744 public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) {
745 RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab);
746 }
747
748 /**
749 * Convert RGB components to its CIE Lab representative components.
750 *
751 * <ul>
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700752 * <li>outLab[0] is L [0 ...100)</li>
Adrian Roos4ff3b122016-02-01 12:26:13 -0800753 * <li>outLab[1] is a [-128...127)</li>
754 * <li>outLab[2] is b [-128...127)</li>
755 * </ul>
756 *
757 * @param r red component value [0..255]
758 * @param g green component value [0..255]
759 * @param b blue component value [0..255]
760 * @param outLab 3-element array which holds the resulting LAB components
761 */
762 public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r,
763 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
764 @NonNull double[] outLab) {
765 // First we convert RGB to XYZ
766 RGBToXYZ(r, g, b, outLab);
767 // outLab now contains XYZ
768 XYZToLAB(outLab[0], outLab[1], outLab[2], outLab);
769 // outLab now contains LAB representation
770 }
771
772 /**
773 * Convert the ARGB color to it's CIE XYZ representative components.
774 *
775 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
776 * 2° Standard Observer (1931).</p>
777 *
778 * <ul>
779 * <li>outXyz[0] is X [0 ...95.047)</li>
780 * <li>outXyz[1] is Y [0...100)</li>
781 * <li>outXyz[2] is Z [0...108.883)</li>
782 * </ul>
783 *
784 * @param color the ARGB color to convert. The alpha component is ignored
785 * @param outXyz 3-element array which holds the resulting LAB components
786 */
787 public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
788 RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
789 }
790
791 /**
792 * Convert RGB components to it's CIE XYZ representative components.
793 *
794 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
795 * 2° Standard Observer (1931).</p>
796 *
797 * <ul>
798 * <li>outXyz[0] is X [0 ...95.047)</li>
799 * <li>outXyz[1] is Y [0...100)</li>
800 * <li>outXyz[2] is Z [0...108.883)</li>
801 * </ul>
802 *
803 * @param r red component value [0..255]
804 * @param g green component value [0..255]
805 * @param b blue component value [0..255]
806 * @param outXyz 3-element array which holds the resulting XYZ components
807 */
808 public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r,
809 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
810 @NonNull double[] outXyz) {
811 if (outXyz.length != 3) {
812 throw new IllegalArgumentException("outXyz must have a length of 3.");
813 }
814
815 double sr = r / 255.0;
816 sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
817 double sg = g / 255.0;
818 sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
819 double sb = b / 255.0;
820 sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
821
822 outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
823 outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
824 outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
825 }
826
827 /**
828 * Converts a color from CIE XYZ to CIE Lab representation.
829 *
830 * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
831 * 2° Standard Observer (1931).</p>
832 *
833 * <ul>
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700834 * <li>outLab[0] is L [0 ...100)</li>
Adrian Roos4ff3b122016-02-01 12:26:13 -0800835 * <li>outLab[1] is a [-128...127)</li>
836 * <li>outLab[2] is b [-128...127)</li>
837 * </ul>
838 *
839 * @param x X component value [0...95.047)
840 * @param y Y component value [0...100)
841 * @param z Z component value [0...108.883)
842 * @param outLab 3-element array which holds the resulting Lab components
843 */
844 public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
845 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
846 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
847 @NonNull double[] outLab) {
848 if (outLab.length != 3) {
849 throw new IllegalArgumentException("outLab must have a length of 3.");
850 }
851 x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
852 y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
853 z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
854 outLab[0] = Math.max(0, 116 * y - 16);
855 outLab[1] = 500 * (x - y);
856 outLab[2] = 200 * (y - z);
857 }
858
859 /**
860 * Converts a color from CIE Lab to CIE XYZ representation.
861 *
862 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
863 * 2° Standard Observer (1931).</p>
864 *
865 * <ul>
866 * <li>outXyz[0] is X [0 ...95.047)</li>
867 * <li>outXyz[1] is Y [0...100)</li>
868 * <li>outXyz[2] is Z [0...108.883)</li>
869 * </ul>
870 *
871 * @param l L component value [0...100)
872 * @param a A component value [-128...127)
873 * @param b B component value [-128...127)
874 * @param outXyz 3-element array which holds the resulting XYZ components
875 */
876 public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l,
877 @FloatRange(from = -128, to = 127) final double a,
878 @FloatRange(from = -128, to = 127) final double b,
879 @NonNull double[] outXyz) {
880 final double fy = (l + 16) / 116;
881 final double fx = a / 500 + fy;
882 final double fz = fy - b / 200;
883
884 double tmp = Math.pow(fx, 3);
885 final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA;
886 final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA;
887
888 tmp = Math.pow(fz, 3);
889 final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA;
890
891 outXyz[0] = xr * XYZ_WHITE_REFERENCE_X;
892 outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y;
893 outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z;
894 }
895
896 /**
897 * Converts a color from CIE XYZ to its RGB representation.
898 *
899 * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
900 * 2° Standard Observer (1931).</p>
901 *
902 * @param x X component value [0...95.047)
903 * @param y Y component value [0...100)
904 * @param z Z component value [0...108.883)
905 * @return int containing the RGB representation
906 */
907 @ColorInt
908 public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
909 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
910 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) {
911 double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100;
912 double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100;
913 double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100;
914
915 r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
916 g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
917 b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;
918
919 return Color.rgb(
920 constrain((int) Math.round(r * 255), 0, 255),
921 constrain((int) Math.round(g * 255), 0, 255),
922 constrain((int) Math.round(b * 255), 0, 255));
923 }
924
925 /**
926 * Converts a color from CIE Lab to its RGB representation.
927 *
928 * @param l L component value [0...100]
929 * @param a A component value [-128...127]
930 * @param b B component value [-128...127]
931 * @return int containing the RGB representation
932 */
933 @ColorInt
934 public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l,
935 @FloatRange(from = -128, to = 127) final double a,
936 @FloatRange(from = -128, to = 127) final double b) {
937 final double[] result = getTempDouble3Array();
938 LABToXYZ(l, a, b, result);
939 return XYZToColor(result[0], result[1], result[2]);
940 }
941
942 private static int constrain(int amount, int low, int high) {
943 return amount < low ? low : (amount > high ? high : amount);
944 }
945
Adrian Roos99d2e642017-02-09 14:58:44 +0100946 private static float constrain(float amount, float low, float high) {
947 return amount < low ? low : (amount > high ? high : amount);
948 }
949
Adrian Roos4ff3b122016-02-01 12:26:13 -0800950 private static double pivotXyzComponent(double component) {
951 return component > XYZ_EPSILON
952 ? Math.pow(component, 1 / 3.0)
953 : (XYZ_KAPPA * component + 16) / 116;
954 }
955
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700956 public static double[] getTempDouble3Array() {
Adrian Roos4ff3b122016-02-01 12:26:13 -0800957 double[] result = TEMP_ARRAY.get();
958 if (result == null) {
959 result = new double[3];
960 TEMP_ARRAY.set(result);
961 }
962 return result;
963 }
964
Adrian Roos99d2e642017-02-09 14:58:44 +0100965 /**
966 * Convert HSL (hue-saturation-lightness) components to a RGB color.
967 * <ul>
968 * <li>hsl[0] is Hue [0 .. 360)</li>
969 * <li>hsl[1] is Saturation [0...1]</li>
970 * <li>hsl[2] is Lightness [0...1]</li>
971 * </ul>
972 * If hsv values are out of range, they are pinned.
973 *
974 * @param hsl 3-element array which holds the input HSL components
975 * @return the resulting RGB color
976 */
977 @ColorInt
978 public static int HSLToColor(@NonNull float[] hsl) {
979 final float h = hsl[0];
980 final float s = hsl[1];
981 final float l = hsl[2];
982
983 final float c = (1f - Math.abs(2 * l - 1f)) * s;
984 final float m = l - 0.5f * c;
985 final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
986
987 final int hueSegment = (int) h / 60;
988
989 int r = 0, g = 0, b = 0;
990
991 switch (hueSegment) {
992 case 0:
993 r = Math.round(255 * (c + m));
994 g = Math.round(255 * (x + m));
995 b = Math.round(255 * m);
996 break;
997 case 1:
998 r = Math.round(255 * (x + m));
999 g = Math.round(255 * (c + m));
1000 b = Math.round(255 * m);
1001 break;
1002 case 2:
1003 r = Math.round(255 * m);
1004 g = Math.round(255 * (c + m));
1005 b = Math.round(255 * (x + m));
1006 break;
1007 case 3:
1008 r = Math.round(255 * m);
1009 g = Math.round(255 * (x + m));
1010 b = Math.round(255 * (c + m));
1011 break;
1012 case 4:
1013 r = Math.round(255 * (x + m));
1014 g = Math.round(255 * m);
1015 b = Math.round(255 * (c + m));
1016 break;
1017 case 5:
1018 case 6:
1019 r = Math.round(255 * (c + m));
1020 g = Math.round(255 * m);
1021 b = Math.round(255 * (x + m));
1022 break;
1023 }
1024
1025 r = constrain(r, 0, 255);
1026 g = constrain(g, 0, 255);
1027 b = constrain(b, 0, 255);
1028
1029 return Color.rgb(r, g, b);
1030 }
1031
1032 /**
1033 * Convert the ARGB color to its HSL (hue-saturation-lightness) components.
1034 * <ul>
1035 * <li>outHsl[0] is Hue [0 .. 360)</li>
1036 * <li>outHsl[1] is Saturation [0...1]</li>
1037 * <li>outHsl[2] is Lightness [0...1]</li>
1038 * </ul>
1039 *
1040 * @param color the ARGB color to convert. The alpha component is ignored
1041 * @param outHsl 3-element array which holds the resulting HSL components
1042 */
1043 public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) {
1044 RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl);
1045 }
1046
1047 /**
1048 * Convert RGB components to HSL (hue-saturation-lightness).
1049 * <ul>
1050 * <li>outHsl[0] is Hue [0 .. 360)</li>
1051 * <li>outHsl[1] is Saturation [0...1]</li>
1052 * <li>outHsl[2] is Lightness [0...1]</li>
1053 * </ul>
1054 *
1055 * @param r red component value [0..255]
1056 * @param g green component value [0..255]
1057 * @param b blue component value [0..255]
1058 * @param outHsl 3-element array which holds the resulting HSL components
1059 */
1060 public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r,
1061 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
1062 @NonNull float[] outHsl) {
1063 final float rf = r / 255f;
1064 final float gf = g / 255f;
1065 final float bf = b / 255f;
1066
1067 final float max = Math.max(rf, Math.max(gf, bf));
1068 final float min = Math.min(rf, Math.min(gf, bf));
1069 final float deltaMaxMin = max - min;
1070
1071 float h, s;
1072 float l = (max + min) / 2f;
1073
1074 if (max == min) {
1075 // Monochromatic
1076 h = s = 0f;
1077 } else {
1078 if (max == rf) {
1079 h = ((gf - bf) / deltaMaxMin) % 6f;
1080 } else if (max == gf) {
1081 h = ((bf - rf) / deltaMaxMin) + 2f;
1082 } else {
1083 h = ((rf - gf) / deltaMaxMin) + 4f;
1084 }
1085
1086 s = deltaMaxMin / (1f - Math.abs(2f * l - 1f));
1087 }
1088
1089 h = (h * 60f) % 360f;
1090 if (h < 0) {
1091 h += 360f;
1092 }
1093
1094 outHsl[0] = constrain(h, 0f, 360f);
1095 outHsl[1] = constrain(s, 0f, 1f);
1096 outHsl[2] = constrain(l, 0f, 1f);
1097 }
1098
Adrian Roos4ff3b122016-02-01 12:26:13 -08001099 }
Jorim Jaggi5c2d8462014-03-21 17:37:00 +01001100}