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