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