1 /* 2 * Copyright 2015 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 androidx.core.graphics; 18 19 import android.annotation.SuppressLint; 20 import android.graphics.Color; 21 22 import androidx.annotation.ColorInt; 23 import androidx.annotation.FloatRange; 24 import androidx.annotation.IntRange; 25 import androidx.annotation.NonNull; 26 import androidx.annotation.RequiresApi; 27 import androidx.annotation.VisibleForTesting; 28 29 import java.util.Objects; 30 31 /** 32 * A set of color-related utility methods, building upon those available in {@code Color}. 33 */ 34 public final class ColorUtils { 35 36 private static final double XYZ_WHITE_REFERENCE_X = 95.047; 37 private static final double XYZ_WHITE_REFERENCE_Y = 100; 38 private static final double XYZ_WHITE_REFERENCE_Z = 108.883; 39 private static final double XYZ_EPSILON = 0.008856; 40 private static final double XYZ_KAPPA = 903.3; 41 42 private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10; 43 private static final int MIN_ALPHA_SEARCH_PRECISION = 1; 44 45 private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>(); 46 47 private ColorUtils() {} 48 49 /** 50 * Composite two potentially translucent colors over each other and returns the result. 51 */ 52 public static int compositeColors(@ColorInt int foreground, @ColorInt int background) { 53 int bgAlpha = Color.alpha(background); 54 int fgAlpha = Color.alpha(foreground); 55 int a = compositeAlpha(fgAlpha, bgAlpha); 56 57 int r = compositeComponent(Color.red(foreground), fgAlpha, 58 Color.red(background), bgAlpha, a); 59 int g = compositeComponent(Color.green(foreground), fgAlpha, 60 Color.green(background), bgAlpha, a); 61 int b = compositeComponent(Color.blue(foreground), fgAlpha, 62 Color.blue(background), bgAlpha, a); 63 64 return Color.argb(a, r, g, b); 65 } 66 67 /** 68 * Composites two translucent colors together. More specifically, adds two colors using 69 * the {@linkplain android.graphics.PorterDuff.Mode#SRC_OVER source over} blending mode. The 70 * colors must not be pre-multiplied and the result is a non pre-multiplied color. 71 * <p> 72 * If the two colors have different color spaces, the foreground color is converted to the 73 * color space of the background color. 74 * <p> 75 * The following example creates a purple color by blending opaque blue with 76 * semi-translucent red: 77 * 78 * <pre>{@code 79 * Color purple = ColorUtils.compositeColors( 80 * Color.valueOf(1f, 0f, 0f, 0.5f), 81 * Color.valueOf(0f, 0f, 1f)); 82 * }</pre> 83 * 84 * <em>Note:</em> This method requires API 26 or newer. 85 * 86 * @throws IllegalArgumentException if the 87 * {@linkplain android.graphics.Color#getModel models} of the colors do not match 88 */ 89 @RequiresApi(26) 90 @NonNull 91 public static Color compositeColors(@NonNull Color foreground, @NonNull Color background) { 92 if (!Objects.equals(foreground.getModel(), background.getModel())) { 93 throw new IllegalArgumentException( 94 "Color models must match (" + foreground.getModel() + " vs. " 95 + background.getModel() + ")"); 96 } 97 98 Color s = Objects.equals(background.getColorSpace(), foreground.getColorSpace()) 99 ? foreground 100 : foreground.convert(background.getColorSpace()); 101 102 float[] src = s.getComponents(); 103 float[] dst = background.getComponents(); 104 105 float sa = s.alpha(); 106 // Destination alpha pre-composited 107 float da = background.alpha() * (1.0f - sa); 108 109 // Index of the alpha component 110 @SuppressLint("Range") // TODO Remove after upgrading Android Gradle Plugin to 3.1 or newer. 111 int ai = background.getComponentCount() - 1; 112 113 // Final alpha: src_alpha + dst_alpha * (1 - src_alpha) 114 dst[ai] = sa + da; 115 116 // Divide by final alpha to return non pre-multiplied color 117 if (dst[ai] > 0) { 118 sa /= dst[ai]; 119 da /= dst[ai]; 120 } 121 122 // Composite non-alpha components 123 for (int i = 0; i < ai; i++) { 124 dst[i] = src[i] * sa + dst[i] * da; 125 } 126 127 return Color.valueOf(dst, background.getColorSpace()); 128 } 129 130 private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) { 131 return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF); 132 } 133 134 private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) { 135 if (a == 0) return 0; 136 return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF); 137 } 138 139 /** 140 * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}. 141 * <p>Defined as the Y component in the XYZ representation of {@code color}.</p> 142 */ 143 @FloatRange(from = 0.0, to = 1.0) 144 public static double calculateLuminance(@ColorInt int color) { 145 final double[] result = getTempDouble3Array(); 146 colorToXYZ(color, result); 147 // Luminance is the Y component 148 return result[1] / 100; 149 } 150 151 /** 152 * Returns the contrast ratio between {@code foreground} and {@code background}. 153 * {@code background} must be opaque. 154 * <p> 155 * Formula defined 156 * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>. 157 */ 158 public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) { 159 if (Color.alpha(background) != 255) { 160 throw new IllegalArgumentException("background can not be translucent: #" 161 + Integer.toHexString(background)); 162 } 163 if (Color.alpha(foreground) < 255) { 164 // If the foreground is translucent, composite the foreground over the background 165 foreground = compositeColors(foreground, background); 166 } 167 168 final double luminance1 = calculateLuminance(foreground) + 0.05; 169 final double luminance2 = calculateLuminance(background) + 0.05; 170 171 // Now return the lighter luminance divided by the darker luminance 172 return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2); 173 } 174 175 /** 176 * Calculates the minimum alpha value which can be applied to {@code foreground} so that would 177 * have a contrast value of at least {@code minContrastRatio} when compared to 178 * {@code background}. 179 * 180 * @param foreground the foreground color 181 * @param background the opaque background color 182 * @param minContrastRatio the minimum contrast ratio 183 * @return the alpha value in the range 0-255, or -1 if no value could be calculated 184 */ 185 public static int calculateMinimumAlpha(@ColorInt int foreground, @ColorInt int background, 186 float minContrastRatio) { 187 if (Color.alpha(background) != 255) { 188 throw new IllegalArgumentException("background can not be translucent: #" 189 + Integer.toHexString(background)); 190 } 191 192 // First lets check that a fully opaque foreground has sufficient contrast 193 int testForeground = setAlphaComponent(foreground, 255); 194 double testRatio = calculateContrast(testForeground, background); 195 if (testRatio < minContrastRatio) { 196 // Fully opaque foreground does not have sufficient contrast, return error 197 return -1; 198 } 199 200 // Binary search to find a value with the minimum value which provides sufficient contrast 201 int numIterations = 0; 202 int minAlpha = 0; 203 int maxAlpha = 255; 204 205 while (numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS && 206 (maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION) { 207 final int testAlpha = (minAlpha + maxAlpha) / 2; 208 209 testForeground = setAlphaComponent(foreground, testAlpha); 210 testRatio = calculateContrast(testForeground, background); 211 212 if (testRatio < minContrastRatio) { 213 minAlpha = testAlpha; 214 } else { 215 maxAlpha = testAlpha; 216 } 217 218 numIterations++; 219 } 220 221 // Conservatively return the max of the range of possible alphas, which is known to pass. 222 return maxAlpha; 223 } 224 225 /** 226 * Convert RGB components to HSL (hue-saturation-lightness). 227 * <ul> 228 * <li>outHsl[0] is Hue [0 .. 360)</li> 229 * <li>outHsl[1] is Saturation [0...1]</li> 230 * <li>outHsl[2] is Lightness [0...1]</li> 231 * </ul> 232 * 233 * @param r red component value [0..255] 234 * @param g green component value [0..255] 235 * @param b blue component value [0..255] 236 * @param outHsl 3-element array which holds the resulting HSL components 237 */ 238 public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r, 239 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, 240 @NonNull float[] outHsl) { 241 final float rf = r / 255f; 242 final float gf = g / 255f; 243 final float bf = b / 255f; 244 245 final float max = Math.max(rf, Math.max(gf, bf)); 246 final float min = Math.min(rf, Math.min(gf, bf)); 247 final float deltaMaxMin = max - min; 248 249 float h, s; 250 float l = (max + min) / 2f; 251 252 if (max == min) { 253 // Monochromatic 254 h = s = 0f; 255 } else { 256 if (max == rf) { 257 h = ((gf - bf) / deltaMaxMin) % 6f; 258 } else if (max == gf) { 259 h = ((bf - rf) / deltaMaxMin) + 2f; 260 } else { 261 h = ((rf - gf) / deltaMaxMin) + 4f; 262 } 263 264 s = deltaMaxMin / (1f - Math.abs(2f * l - 1f)); 265 } 266 267 h = (h * 60f) % 360f; 268 if (h < 0) { 269 h += 360f; 270 } 271 272 outHsl[0] = constrain(h, 0f, 360f); 273 outHsl[1] = constrain(s, 0f, 1f); 274 outHsl[2] = constrain(l, 0f, 1f); 275 } 276 277 /** 278 * Convert the ARGB color to its HSL (hue-saturation-lightness) components. 279 * <ul> 280 * <li>outHsl[0] is Hue [0 .. 360)</li> 281 * <li>outHsl[1] is Saturation [0...1]</li> 282 * <li>outHsl[2] is Lightness [0...1]</li> 283 * </ul> 284 * 285 * @param color the ARGB color to convert. The alpha component is ignored 286 * @param outHsl 3-element array which holds the resulting HSL components 287 */ 288 public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) { 289 RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl); 290 } 291 292 /** 293 * Convert HSL (hue-saturation-lightness) components to a RGB color. 294 * <ul> 295 * <li>hsl[0] is Hue [0 .. 360)</li> 296 * <li>hsl[1] is Saturation [0...1]</li> 297 * <li>hsl[2] is Lightness [0...1]</li> 298 * </ul> 299 * If hsv values are out of range, they are pinned. 300 * 301 * @param hsl 3-element array which holds the input HSL components 302 * @return the resulting RGB color 303 */ 304 @ColorInt 305 public static int HSLToColor(@NonNull float[] hsl) { 306 final float h = hsl[0]; 307 final float s = hsl[1]; 308 final float l = hsl[2]; 309 310 final float c = (1f - Math.abs(2 * l - 1f)) * s; 311 final float m = l - 0.5f * c; 312 final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f)); 313 314 final int hueSegment = (int) h / 60; 315 316 int r = 0, g = 0, b = 0; 317 318 switch (hueSegment) { 319 case 0: 320 r = Math.round(255 * (c + m)); 321 g = Math.round(255 * (x + m)); 322 b = Math.round(255 * m); 323 break; 324 case 1: 325 r = Math.round(255 * (x + m)); 326 g = Math.round(255 * (c + m)); 327 b = Math.round(255 * m); 328 break; 329 case 2: 330 r = Math.round(255 * m); 331 g = Math.round(255 * (c + m)); 332 b = Math.round(255 * (x + m)); 333 break; 334 case 3: 335 r = Math.round(255 * m); 336 g = Math.round(255 * (x + m)); 337 b = Math.round(255 * (c + m)); 338 break; 339 case 4: 340 r = Math.round(255 * (x + m)); 341 g = Math.round(255 * m); 342 b = Math.round(255 * (c + m)); 343 break; 344 case 5: 345 case 6: 346 r = Math.round(255 * (c + m)); 347 g = Math.round(255 * m); 348 b = Math.round(255 * (x + m)); 349 break; 350 } 351 352 r = constrain(r, 0, 255); 353 g = constrain(g, 0, 255); 354 b = constrain(b, 0, 255); 355 356 return Color.rgb(r, g, b); 357 } 358 359 /** 360 * Set the alpha component of {@code color} to be {@code alpha}. 361 */ 362 @ColorInt 363 public static int setAlphaComponent(@ColorInt int color, 364 @IntRange(from = 0x0, to = 0xFF) int alpha) { 365 if (alpha < 0 || alpha > 255) { 366 throw new IllegalArgumentException("alpha must be between 0 and 255."); 367 } 368 return (color & 0x00ffffff) | (alpha << 24); 369 } 370 371 /** 372 * Convert the ARGB color to its CIE Lab representative components. 373 * 374 * @param color the ARGB color to convert. The alpha component is ignored 375 * @param outLab 3-element array which holds the resulting LAB components 376 */ 377 public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) { 378 RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab); 379 } 380 381 /** 382 * Convert RGB components to its CIE Lab representative components. 383 * 384 * <ul> 385 * <li>outLab[0] is L [0 ...1)</li> 386 * <li>outLab[1] is a [-128...127)</li> 387 * <li>outLab[2] is b [-128...127)</li> 388 * </ul> 389 * 390 * @param r red component value [0..255] 391 * @param g green component value [0..255] 392 * @param b blue component value [0..255] 393 * @param outLab 3-element array which holds the resulting LAB components 394 */ 395 public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r, 396 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, 397 @NonNull double[] outLab) { 398 // First we convert RGB to XYZ 399 RGBToXYZ(r, g, b, outLab); 400 // outLab now contains XYZ 401 XYZToLAB(outLab[0], outLab[1], outLab[2], outLab); 402 // outLab now contains LAB representation 403 } 404 405 /** 406 * Convert the ARGB color to its CIE XYZ representative components. 407 * 408 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE 409 * 2 Standard Observer (1931).</p> 410 * 411 * <ul> 412 * <li>outXyz[0] is X [0 ...95.047)</li> 413 * <li>outXyz[1] is Y [0...100)</li> 414 * <li>outXyz[2] is Z [0...108.883)</li> 415 * </ul> 416 * 417 * @param color the ARGB color to convert. The alpha component is ignored 418 * @param outXyz 3-element array which holds the resulting LAB components 419 */ 420 public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) { 421 RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz); 422 } 423 424 /** 425 * Convert RGB components to its CIE XYZ representative components. 426 * 427 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE 428 * 2 Standard Observer (1931).</p> 429 * 430 * <ul> 431 * <li>outXyz[0] is X [0 ...95.047)</li> 432 * <li>outXyz[1] is Y [0...100)</li> 433 * <li>outXyz[2] is Z [0...108.883)</li> 434 * </ul> 435 * 436 * @param r red component value [0..255] 437 * @param g green component value [0..255] 438 * @param b blue component value [0..255] 439 * @param outXyz 3-element array which holds the resulting XYZ components 440 */ 441 public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r, 442 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, 443 @NonNull double[] outXyz) { 444 if (outXyz.length != 3) { 445 throw new IllegalArgumentException("outXyz must have a length of 3."); 446 } 447 448 double sr = r / 255.0; 449 sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4); 450 double sg = g / 255.0; 451 sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4); 452 double sb = b / 255.0; 453 sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4); 454 455 outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805); 456 outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722); 457 outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505); 458 } 459 460 /** 461 * Converts a color from CIE XYZ to CIE Lab representation. 462 * 463 * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE 464 * 2 Standard Observer (1931).</p> 465 * 466 * <ul> 467 * <li>outLab[0] is L [0 ...1)</li> 468 * <li>outLab[1] is a [-128...127)</li> 469 * <li>outLab[2] is b [-128...127)</li> 470 * </ul> 471 * 472 * @param x X component value [0...95.047) 473 * @param y Y component value [0...100) 474 * @param z Z component value [0...108.883) 475 * @param outLab 3-element array which holds the resulting Lab components 476 */ 477 public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, 478 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, 479 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z, 480 @NonNull double[] outLab) { 481 if (outLab.length != 3) { 482 throw new IllegalArgumentException("outLab must have a length of 3."); 483 } 484 x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X); 485 y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y); 486 z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z); 487 outLab[0] = Math.max(0, 116 * y - 16); 488 outLab[1] = 500 * (x - y); 489 outLab[2] = 200 * (y - z); 490 } 491 492 /** 493 * Converts a color from CIE Lab to CIE XYZ representation. 494 * 495 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE 496 * 2 Standard Observer (1931).</p> 497 * 498 * <ul> 499 * <li>outXyz[0] is X [0 ...95.047)</li> 500 * <li>outXyz[1] is Y [0...100)</li> 501 * <li>outXyz[2] is Z [0...108.883)</li> 502 * </ul> 503 * 504 * @param l L component value [0...100) 505 * @param a A component value [-128...127) 506 * @param b B component value [-128...127) 507 * @param outXyz 3-element array which holds the resulting XYZ components 508 */ 509 public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l, 510 @FloatRange(from = -128, to = 127) final double a, 511 @FloatRange(from = -128, to = 127) final double b, 512 @NonNull double[] outXyz) { 513 final double fy = (l + 16) / 116; 514 final double fx = a / 500 + fy; 515 final double fz = fy - b / 200; 516 517 double tmp = Math.pow(fx, 3); 518 final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA; 519 final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA; 520 521 tmp = Math.pow(fz, 3); 522 final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA; 523 524 outXyz[0] = xr * XYZ_WHITE_REFERENCE_X; 525 outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y; 526 outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z; 527 } 528 529 /** 530 * Converts a color from CIE XYZ to its RGB representation. 531 * 532 * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE 533 * 2 Standard Observer (1931).</p> 534 * 535 * @param x X component value [0...95.047) 536 * @param y Y component value [0...100) 537 * @param z Z component value [0...108.883) 538 * @return int containing the RGB representation 539 */ 540 @ColorInt 541 public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, 542 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, 543 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) { 544 double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100; 545 double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100; 546 double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100; 547 548 r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r; 549 g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g; 550 b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b; 551 552 return Color.rgb( 553 constrain((int) Math.round(r * 255), 0, 255), 554 constrain((int) Math.round(g * 255), 0, 255), 555 constrain((int) Math.round(b * 255), 0, 255)); 556 } 557 558 /** 559 * Converts a color from CIE Lab to its RGB representation. 560 * 561 * @param l L component value [0...100] 562 * @param a A component value [-128...127] 563 * @param b B component value [-128...127] 564 * @return int containing the RGB representation 565 */ 566 @ColorInt 567 public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l, 568 @FloatRange(from = -128, to = 127) final double a, 569 @FloatRange(from = -128, to = 127) final double b) { 570 final double[] result = getTempDouble3Array(); 571 LABToXYZ(l, a, b, result); 572 return XYZToColor(result[0], result[1], result[2]); 573 } 574 575 /** 576 * Returns the euclidean distance between two LAB colors. 577 */ 578 public static double distanceEuclidean(@NonNull double[] labX, @NonNull double[] labY) { 579 return Math.sqrt(Math.pow(labX[0] - labY[0], 2) 580 + Math.pow(labX[1] - labY[1], 2) 581 + Math.pow(labX[2] - labY[2], 2)); 582 } 583 584 private static float constrain(float amount, float low, float high) { 585 return amount < low ? low : (amount > high ? high : amount); 586 } 587 588 private static int constrain(int amount, int low, int high) { 589 return amount < low ? low : (amount > high ? high : amount); 590 } 591 592 private static double pivotXyzComponent(double component) { 593 return component > XYZ_EPSILON 594 ? Math.pow(component, 1 / 3.0) 595 : (XYZ_KAPPA * component + 16) / 116; 596 } 597 598 /** 599 * Blend between two ARGB colors using the given ratio. 600 * 601 * <p>A blend ratio of 0.0 will result in {@code color1}, 0.5 will give an even blend, 602 * 1.0 will result in {@code color2}.</p> 603 * 604 * @param color1 the first ARGB color 605 * @param color2 the second ARGB color 606 * @param ratio the blend ratio of {@code color1} to {@code color2} 607 */ 608 @ColorInt 609 public static int blendARGB(@ColorInt int color1, @ColorInt int color2, 610 @FloatRange(from = 0.0, to = 1.0) float ratio) { 611 final float inverseRatio = 1 - ratio; 612 float a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio; 613 float r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio; 614 float g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio; 615 float b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio; 616 return Color.argb((int) a, (int) r, (int) g, (int) b); 617 } 618 619 /** 620 * Blend between {@code hsl1} and {@code hsl2} using the given ratio. This will interpolate 621 * the hue using the shortest angle. 622 * 623 * <p>A blend ratio of 0.0 will result in {@code hsl1}, 0.5 will give an even blend, 624 * 1.0 will result in {@code hsl2}.</p> 625 * 626 * @param hsl1 3-element array which holds the first HSL color 627 * @param hsl2 3-element array which holds the second HSL color 628 * @param ratio the blend ratio of {@code hsl1} to {@code hsl2} 629 * @param outResult 3-element array which holds the resulting HSL components 630 */ 631 public static void blendHSL(@NonNull float[] hsl1, @NonNull float[] hsl2, 632 @FloatRange(from = 0.0, to = 1.0) float ratio, @NonNull float[] outResult) { 633 if (outResult.length != 3) { 634 throw new IllegalArgumentException("result must have a length of 3."); 635 } 636 final float inverseRatio = 1 - ratio; 637 // Since hue is circular we will need to interpolate carefully 638 outResult[0] = circularInterpolate(hsl1[0], hsl2[0], ratio); 639 outResult[1] = hsl1[1] * inverseRatio + hsl2[1] * ratio; 640 outResult[2] = hsl1[2] * inverseRatio + hsl2[2] * ratio; 641 } 642 643 /** 644 * Blend between two CIE-LAB colors using the given ratio. 645 * 646 * <p>A blend ratio of 0.0 will result in {@code lab1}, 0.5 will give an even blend, 647 * 1.0 will result in {@code lab2}.</p> 648 * 649 * @param lab1 3-element array which holds the first LAB color 650 * @param lab2 3-element array which holds the second LAB color 651 * @param ratio the blend ratio of {@code lab1} to {@code lab2} 652 * @param outResult 3-element array which holds the resulting LAB components 653 */ 654 public static void blendLAB(@NonNull double[] lab1, @NonNull double[] lab2, 655 @FloatRange(from = 0.0, to = 1.0) double ratio, @NonNull double[] outResult) { 656 if (outResult.length != 3) { 657 throw new IllegalArgumentException("outResult must have a length of 3."); 658 } 659 final double inverseRatio = 1 - ratio; 660 outResult[0] = lab1[0] * inverseRatio + lab2[0] * ratio; 661 outResult[1] = lab1[1] * inverseRatio + lab2[1] * ratio; 662 outResult[2] = lab1[2] * inverseRatio + lab2[2] * ratio; 663 } 664 665 @VisibleForTesting 666 static float circularInterpolate(float a, float b, float f) { 667 if (Math.abs(b - a) > 180) { 668 if (b > a) { 669 a += 360; 670 } else { 671 b += 360; 672 } 673 } 674 return (a + ((b - a) * f)) % 360; 675 } 676 677 private static double[] getTempDouble3Array() { 678 double[] result = TEMP_ARRAY.get(); 679 if (result == null) { 680 result = new double[3]; 681 TEMP_ARRAY.set(result); 682 } 683 return result; 684 } 685 686 } 687