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