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