1 /* 2 * Copyright (C) 2017 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.graphics.palette; 18 19 import android.annotation.ColorInt; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.graphics.Bitmap; 23 import android.graphics.Color; 24 import android.graphics.Rect; 25 import android.os.AsyncTask; 26 import android.util.ArrayMap; 27 import android.util.Log; 28 import android.util.SparseBooleanArray; 29 import android.util.TimingLogger; 30 31 import com.android.internal.graphics.ColorUtils; 32 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.Collections; 36 import java.util.List; 37 import java.util.Map; 38 39 40 /** 41 * Copied from: /frameworks/support/v7/palette/src/main/java/android/support/v7/ 42 * graphics/Palette.java 43 * 44 * A helper class to extract prominent colors from an image. 45 * <p> 46 * A number of colors with different profiles are extracted from the image: 47 * <ul> 48 * <li>Vibrant</li> 49 * <li>Vibrant Dark</li> 50 * <li>Vibrant Light</li> 51 * <li>Muted</li> 52 * <li>Muted Dark</li> 53 * <li>Muted Light</li> 54 * </ul> 55 * These can be retrieved from the appropriate getter method. 56 * 57 * <p> 58 * Instances are created with a {@link Palette.Builder} which supports several options to tweak the 59 * generated Palette. See that class' documentation for more information. 60 * <p> 61 * Generation should always be completed on a background thread, ideally the one in 62 * which you load your image on. {@link Palette.Builder} supports both synchronous and asynchronous 63 * generation: 64 * 65 * <pre> 66 * // Synchronous 67 * Palette p = Palette.from(bitmap).generate(); 68 * 69 * // Asynchronous 70 * Palette.from(bitmap).generate(new PaletteAsyncListener() { 71 * public void onGenerated(Palette p) { 72 * // Use generated instance 73 * } 74 * }); 75 * </pre> 76 */ 77 public final class Palette { 78 79 /** 80 * Listener to be used with {@link #generateAsync(Bitmap, Palette.PaletteAsyncListener)} or 81 * {@link #generateAsync(Bitmap, int, Palette.PaletteAsyncListener)} 82 */ 83 public interface PaletteAsyncListener { 84 85 /** 86 * Called when the {@link Palette} has been generated. 87 */ onGenerated(Palette palette)88 void onGenerated(Palette palette); 89 } 90 91 static final int DEFAULT_RESIZE_BITMAP_AREA = 112 * 112; 92 static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16; 93 94 static final float MIN_CONTRAST_TITLE_TEXT = 3.0f; 95 static final float MIN_CONTRAST_BODY_TEXT = 4.5f; 96 97 static final String LOG_TAG = "Palette"; 98 static final boolean LOG_TIMINGS = false; 99 100 /** 101 * Start generating a {@link Palette} with the returned {@link Palette.Builder} instance. 102 */ from(Bitmap bitmap)103 public static Palette.Builder from(Bitmap bitmap) { 104 return new Palette.Builder(bitmap); 105 } 106 107 /** 108 * Generate a {@link Palette} from the pre-generated list of {@link Palette.Swatch} swatches. 109 * This is useful for testing, or if you want to resurrect a {@link Palette} instance from a 110 * list of swatches. Will return null if the {@code swatches} is null. 111 */ from(List<Palette.Swatch> swatches)112 public static Palette from(List<Palette.Swatch> swatches) { 113 return new Palette.Builder(swatches).generate(); 114 } 115 116 /** 117 * @deprecated Use {@link Palette.Builder} to generate the Palette. 118 */ 119 @Deprecated generate(Bitmap bitmap)120 public static Palette generate(Bitmap bitmap) { 121 return from(bitmap).generate(); 122 } 123 124 /** 125 * @deprecated Use {@link Palette.Builder} to generate the Palette. 126 */ 127 @Deprecated generate(Bitmap bitmap, int numColors)128 public static Palette generate(Bitmap bitmap, int numColors) { 129 return from(bitmap).maximumColorCount(numColors).generate(); 130 } 131 132 /** 133 * @deprecated Use {@link Palette.Builder} to generate the Palette. 134 */ 135 @Deprecated generateAsync( Bitmap bitmap, Palette.PaletteAsyncListener listener)136 public static AsyncTask<Bitmap, Void, Palette> generateAsync( 137 Bitmap bitmap, Palette.PaletteAsyncListener listener) { 138 return from(bitmap).generate(listener); 139 } 140 141 /** 142 * @deprecated Use {@link Palette.Builder} to generate the Palette. 143 */ 144 @Deprecated generateAsync( final Bitmap bitmap, final int numColors, final Palette.PaletteAsyncListener listener)145 public static AsyncTask<Bitmap, Void, Palette> generateAsync( 146 final Bitmap bitmap, final int numColors, final Palette.PaletteAsyncListener listener) { 147 return from(bitmap).maximumColorCount(numColors).generate(listener); 148 } 149 150 private final List<Palette.Swatch> mSwatches; 151 private final List<Target> mTargets; 152 153 private final Map<Target, Palette.Swatch> mSelectedSwatches; 154 private final SparseBooleanArray mUsedColors; 155 156 private final Palette.Swatch mDominantSwatch; 157 Palette(List<Palette.Swatch> swatches, List<Target> targets)158 Palette(List<Palette.Swatch> swatches, List<Target> targets) { 159 mSwatches = swatches; 160 mTargets = targets; 161 162 mUsedColors = new SparseBooleanArray(); 163 mSelectedSwatches = new ArrayMap<>(); 164 165 mDominantSwatch = findDominantSwatch(); 166 } 167 168 /** 169 * Returns all of the swatches which make up the palette. 170 */ 171 @NonNull getSwatches()172 public List<Palette.Swatch> getSwatches() { 173 return Collections.unmodifiableList(mSwatches); 174 } 175 176 /** 177 * Returns the targets used to generate this palette. 178 */ 179 @NonNull getTargets()180 public List<Target> getTargets() { 181 return Collections.unmodifiableList(mTargets); 182 } 183 184 /** 185 * Returns the most vibrant swatch in the palette. Might be null. 186 * 187 * @see Target#VIBRANT 188 */ 189 @Nullable getVibrantSwatch()190 public Palette.Swatch getVibrantSwatch() { 191 return getSwatchForTarget(Target.VIBRANT); 192 } 193 194 /** 195 * Returns a light and vibrant swatch from the palette. Might be null. 196 * 197 * @see Target#LIGHT_VIBRANT 198 */ 199 @Nullable getLightVibrantSwatch()200 public Palette.Swatch getLightVibrantSwatch() { 201 return getSwatchForTarget(Target.LIGHT_VIBRANT); 202 } 203 204 /** 205 * Returns a dark and vibrant swatch from the palette. Might be null. 206 * 207 * @see Target#DARK_VIBRANT 208 */ 209 @Nullable getDarkVibrantSwatch()210 public Palette.Swatch getDarkVibrantSwatch() { 211 return getSwatchForTarget(Target.DARK_VIBRANT); 212 } 213 214 /** 215 * Returns a muted swatch from the palette. Might be null. 216 * 217 * @see Target#MUTED 218 */ 219 @Nullable getMutedSwatch()220 public Palette.Swatch getMutedSwatch() { 221 return getSwatchForTarget(Target.MUTED); 222 } 223 224 /** 225 * Returns a muted and light swatch from the palette. Might be null. 226 * 227 * @see Target#LIGHT_MUTED 228 */ 229 @Nullable getLightMutedSwatch()230 public Palette.Swatch getLightMutedSwatch() { 231 return getSwatchForTarget(Target.LIGHT_MUTED); 232 } 233 234 /** 235 * Returns a muted and dark swatch from the palette. Might be null. 236 * 237 * @see Target#DARK_MUTED 238 */ 239 @Nullable getDarkMutedSwatch()240 public Palette.Swatch getDarkMutedSwatch() { 241 return getSwatchForTarget(Target.DARK_MUTED); 242 } 243 244 /** 245 * Returns the most vibrant color in the palette as an RGB packed int. 246 * 247 * @param defaultColor value to return if the swatch isn't available 248 * @see #getVibrantSwatch() 249 */ 250 @ColorInt getVibrantColor(@olorInt final int defaultColor)251 public int getVibrantColor(@ColorInt final int defaultColor) { 252 return getColorForTarget(Target.VIBRANT, defaultColor); 253 } 254 255 /** 256 * Returns a light and vibrant color from the palette as an RGB packed int. 257 * 258 * @param defaultColor value to return if the swatch isn't available 259 * @see #getLightVibrantSwatch() 260 */ 261 @ColorInt getLightVibrantColor(@olorInt final int defaultColor)262 public int getLightVibrantColor(@ColorInt final int defaultColor) { 263 return getColorForTarget(Target.LIGHT_VIBRANT, defaultColor); 264 } 265 266 /** 267 * Returns a dark and vibrant color from the palette as an RGB packed int. 268 * 269 * @param defaultColor value to return if the swatch isn't available 270 * @see #getDarkVibrantSwatch() 271 */ 272 @ColorInt getDarkVibrantColor(@olorInt final int defaultColor)273 public int getDarkVibrantColor(@ColorInt final int defaultColor) { 274 return getColorForTarget(Target.DARK_VIBRANT, defaultColor); 275 } 276 277 /** 278 * Returns a muted color from the palette as an RGB packed int. 279 * 280 * @param defaultColor value to return if the swatch isn't available 281 * @see #getMutedSwatch() 282 */ 283 @ColorInt getMutedColor(@olorInt final int defaultColor)284 public int getMutedColor(@ColorInt final int defaultColor) { 285 return getColorForTarget(Target.MUTED, defaultColor); 286 } 287 288 /** 289 * Returns a muted and light color from the palette as an RGB packed int. 290 * 291 * @param defaultColor value to return if the swatch isn't available 292 * @see #getLightMutedSwatch() 293 */ 294 @ColorInt getLightMutedColor(@olorInt final int defaultColor)295 public int getLightMutedColor(@ColorInt final int defaultColor) { 296 return getColorForTarget(Target.LIGHT_MUTED, defaultColor); 297 } 298 299 /** 300 * Returns a muted and dark color from the palette as an RGB packed int. 301 * 302 * @param defaultColor value to return if the swatch isn't available 303 * @see #getDarkMutedSwatch() 304 */ 305 @ColorInt getDarkMutedColor(@olorInt final int defaultColor)306 public int getDarkMutedColor(@ColorInt final int defaultColor) { 307 return getColorForTarget(Target.DARK_MUTED, defaultColor); 308 } 309 310 /** 311 * Returns the selected swatch for the given target from the palette, or {@code null} if one 312 * could not be found. 313 */ 314 @Nullable getSwatchForTarget(@onNull final Target target)315 public Palette.Swatch getSwatchForTarget(@NonNull final Target target) { 316 return mSelectedSwatches.get(target); 317 } 318 319 /** 320 * Returns the selected color for the given target from the palette as an RGB packed int. 321 * 322 * @param defaultColor value to return if the swatch isn't available 323 */ 324 @ColorInt getColorForTarget(@onNull final Target target, @ColorInt final int defaultColor)325 public int getColorForTarget(@NonNull final Target target, @ColorInt final int defaultColor) { 326 Palette.Swatch swatch = getSwatchForTarget(target); 327 return swatch != null ? swatch.getRgb() : defaultColor; 328 } 329 330 /** 331 * Returns the dominant swatch from the palette. 332 * 333 * <p>The dominant swatch is defined as the swatch with the greatest population (frequency) 334 * within the palette.</p> 335 */ 336 @Nullable getDominantSwatch()337 public Palette.Swatch getDominantSwatch() { 338 return mDominantSwatch; 339 } 340 341 /** 342 * Returns the color of the dominant swatch from the palette, as an RGB packed int. 343 * 344 * @param defaultColor value to return if the swatch isn't available 345 * @see #getDominantSwatch() 346 */ 347 @ColorInt getDominantColor(@olorInt int defaultColor)348 public int getDominantColor(@ColorInt int defaultColor) { 349 return mDominantSwatch != null ? mDominantSwatch.getRgb() : defaultColor; 350 } 351 generate()352 void generate() { 353 // We need to make sure that the scored targets are generated first. This is so that 354 // inherited targets have something to inherit from 355 for (int i = 0, count = mTargets.size(); i < count; i++) { 356 final Target target = mTargets.get(i); 357 target.normalizeWeights(); 358 mSelectedSwatches.put(target, generateScoredTarget(target)); 359 } 360 // We now clear out the used colors 361 mUsedColors.clear(); 362 } 363 generateScoredTarget(final Target target)364 private Palette.Swatch generateScoredTarget(final Target target) { 365 final Palette.Swatch maxScoreSwatch = getMaxScoredSwatchForTarget(target); 366 if (maxScoreSwatch != null && target.isExclusive()) { 367 // If we have a swatch, and the target is exclusive, add the color to the used list 368 mUsedColors.append(maxScoreSwatch.getRgb(), true); 369 } 370 return maxScoreSwatch; 371 } 372 getMaxScoredSwatchForTarget(final Target target)373 private Palette.Swatch getMaxScoredSwatchForTarget(final Target target) { 374 float maxScore = 0; 375 Palette.Swatch maxScoreSwatch = null; 376 for (int i = 0, count = mSwatches.size(); i < count; i++) { 377 final Palette.Swatch swatch = mSwatches.get(i); 378 if (shouldBeScoredForTarget(swatch, target)) { 379 final float score = generateScore(swatch, target); 380 if (maxScoreSwatch == null || score > maxScore) { 381 maxScoreSwatch = swatch; 382 maxScore = score; 383 } 384 } 385 } 386 return maxScoreSwatch; 387 } 388 shouldBeScoredForTarget(final Palette.Swatch swatch, final Target target)389 private boolean shouldBeScoredForTarget(final Palette.Swatch swatch, final Target target) { 390 // Check whether the HSL values are within the correct ranges, and this color hasn't 391 // been used yet. 392 final float hsl[] = swatch.getHsl(); 393 return hsl[1] >= target.getMinimumSaturation() && hsl[1] <= target.getMaximumSaturation() 394 && hsl[2] >= target.getMinimumLightness() && hsl[2] <= target.getMaximumLightness() 395 && !mUsedColors.get(swatch.getRgb()); 396 } 397 generateScore(Palette.Swatch swatch, Target target)398 private float generateScore(Palette.Swatch swatch, Target target) { 399 final float[] hsl = swatch.getHsl(); 400 401 float saturationScore = 0; 402 float luminanceScore = 0; 403 float populationScore = 0; 404 405 final int maxPopulation = mDominantSwatch != null ? mDominantSwatch.getPopulation() : 1; 406 407 if (target.getSaturationWeight() > 0) { 408 saturationScore = target.getSaturationWeight() 409 * (1f - Math.abs(hsl[1] - target.getTargetSaturation())); 410 } 411 if (target.getLightnessWeight() > 0) { 412 luminanceScore = target.getLightnessWeight() 413 * (1f - Math.abs(hsl[2] - target.getTargetLightness())); 414 } 415 if (target.getPopulationWeight() > 0) { 416 populationScore = target.getPopulationWeight() 417 * (swatch.getPopulation() / (float) maxPopulation); 418 } 419 420 return saturationScore + luminanceScore + populationScore; 421 } 422 findDominantSwatch()423 private Palette.Swatch findDominantSwatch() { 424 int maxPop = Integer.MIN_VALUE; 425 Palette.Swatch maxSwatch = null; 426 for (int i = 0, count = mSwatches.size(); i < count; i++) { 427 Palette.Swatch swatch = mSwatches.get(i); 428 if (swatch.getPopulation() > maxPop) { 429 maxSwatch = swatch; 430 maxPop = swatch.getPopulation(); 431 } 432 } 433 return maxSwatch; 434 } 435 copyHslValues(Palette.Swatch color)436 private static float[] copyHslValues(Palette.Swatch color) { 437 final float[] newHsl = new float[3]; 438 System.arraycopy(color.getHsl(), 0, newHsl, 0, 3); 439 return newHsl; 440 } 441 442 /** 443 * Represents a color swatch generated from an image's palette. The RGB color can be retrieved 444 * by calling {@link #getRgb()}. 445 */ 446 public static final class Swatch { 447 private final int mRed, mGreen, mBlue; 448 private final int mRgb; 449 private final int mPopulation; 450 451 private boolean mGeneratedTextColors; 452 private int mTitleTextColor; 453 private int mBodyTextColor; 454 455 private float[] mHsl; 456 Swatch(@olorInt int color, int population)457 public Swatch(@ColorInt int color, int population) { 458 mRed = Color.red(color); 459 mGreen = Color.green(color); 460 mBlue = Color.blue(color); 461 mRgb = color; 462 mPopulation = population; 463 } 464 Swatch(int red, int green, int blue, int population)465 Swatch(int red, int green, int blue, int population) { 466 mRed = red; 467 mGreen = green; 468 mBlue = blue; 469 mRgb = Color.rgb(red, green, blue); 470 mPopulation = population; 471 } 472 Swatch(float[] hsl, int population)473 Swatch(float[] hsl, int population) { 474 this(ColorUtils.HSLToColor(hsl), population); 475 mHsl = hsl; 476 } 477 478 /** 479 * @return this swatch's RGB color value 480 */ 481 @ColorInt getRgb()482 public int getRgb() { 483 return mRgb; 484 } 485 486 /** 487 * Return this swatch's HSL values. 488 * hsv[0] is Hue [0 .. 360) 489 * hsv[1] is Saturation [0...1] 490 * hsv[2] is Lightness [0...1] 491 */ getHsl()492 public float[] getHsl() { 493 if (mHsl == null) { 494 mHsl = new float[3]; 495 } 496 ColorUtils.RGBToHSL(mRed, mGreen, mBlue, mHsl); 497 return mHsl; 498 } 499 500 /** 501 * @return the number of pixels represented by this swatch 502 */ getPopulation()503 public int getPopulation() { 504 return mPopulation; 505 } 506 507 /** 508 * Returns an appropriate color to use for any 'title' text which is displayed over this 509 * {@link Palette.Swatch}'s color. This color is guaranteed to have sufficient contrast. 510 */ 511 @ColorInt getTitleTextColor()512 public int getTitleTextColor() { 513 ensureTextColorsGenerated(); 514 return mTitleTextColor; 515 } 516 517 /** 518 * Returns an appropriate color to use for any 'body' text which is displayed over this 519 * {@link Palette.Swatch}'s color. This color is guaranteed to have sufficient contrast. 520 */ 521 @ColorInt getBodyTextColor()522 public int getBodyTextColor() { 523 ensureTextColorsGenerated(); 524 return mBodyTextColor; 525 } 526 ensureTextColorsGenerated()527 private void ensureTextColorsGenerated() { 528 if (!mGeneratedTextColors) { 529 // First check white, as most colors will be dark 530 final int lightBodyAlpha = ColorUtils.calculateMinimumAlpha( 531 Color.WHITE, mRgb, MIN_CONTRAST_BODY_TEXT); 532 final int lightTitleAlpha = ColorUtils.calculateMinimumAlpha( 533 Color.WHITE, mRgb, MIN_CONTRAST_TITLE_TEXT); 534 535 if (lightBodyAlpha != -1 && lightTitleAlpha != -1) { 536 // If we found valid light values, use them and return 537 mBodyTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha); 538 mTitleTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha); 539 mGeneratedTextColors = true; 540 return; 541 } 542 543 final int darkBodyAlpha = ColorUtils.calculateMinimumAlpha( 544 Color.BLACK, mRgb, MIN_CONTRAST_BODY_TEXT); 545 final int darkTitleAlpha = ColorUtils.calculateMinimumAlpha( 546 Color.BLACK, mRgb, MIN_CONTRAST_TITLE_TEXT); 547 548 if (darkBodyAlpha != -1 && darkTitleAlpha != -1) { 549 // If we found valid dark values, use them and return 550 mBodyTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha); 551 mTitleTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha); 552 mGeneratedTextColors = true; 553 return; 554 } 555 556 // If we reach here then we can not find title and body values which use the same 557 // lightness, we need to use mismatched values 558 mBodyTextColor = lightBodyAlpha != -1 559 ? ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha) 560 : ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha); 561 mTitleTextColor = lightTitleAlpha != -1 562 ? ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha) 563 : ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha); 564 mGeneratedTextColors = true; 565 } 566 } 567 568 @Override toString()569 public String toString() { 570 return new StringBuilder(getClass().getSimpleName()) 571 .append(" [RGB: #").append(Integer.toHexString(getRgb())).append(']') 572 .append(" [HSL: ").append(Arrays.toString(getHsl())).append(']') 573 .append(" [Population: ").append(mPopulation).append(']') 574 .append(" [Title Text: #").append(Integer.toHexString(getTitleTextColor())) 575 .append(']') 576 .append(" [Body Text: #").append(Integer.toHexString(getBodyTextColor())) 577 .append(']').toString(); 578 } 579 580 @Override equals(Object o)581 public boolean equals(Object o) { 582 if (this == o) { 583 return true; 584 } 585 if (o == null || getClass() != o.getClass()) { 586 return false; 587 } 588 589 Palette.Swatch 590 swatch = (Palette.Swatch) o; 591 return mPopulation == swatch.mPopulation && mRgb == swatch.mRgb; 592 } 593 594 @Override hashCode()595 public int hashCode() { 596 return 31 * mRgb + mPopulation; 597 } 598 } 599 600 /** 601 * Builder class for generating {@link Palette} instances. 602 */ 603 public static final class Builder { 604 private final List<Palette.Swatch> mSwatches; 605 private final Bitmap mBitmap; 606 607 private final List<Target> mTargets = new ArrayList<>(); 608 609 private int mMaxColors = DEFAULT_CALCULATE_NUMBER_COLORS; 610 private int mResizeArea = DEFAULT_RESIZE_BITMAP_AREA; 611 private int mResizeMaxDimension = -1; 612 613 private final List<Palette.Filter> mFilters = new ArrayList<>(); 614 private Rect mRegion; 615 616 private Quantizer mQuantizer; 617 618 /** 619 * Construct a new {@link Palette.Builder} using a source {@link Bitmap} 620 */ Builder(Bitmap bitmap)621 public Builder(Bitmap bitmap) { 622 if (bitmap == null || bitmap.isRecycled()) { 623 throw new IllegalArgumentException("Bitmap is not valid"); 624 } 625 mFilters.add(DEFAULT_FILTER); 626 mBitmap = bitmap; 627 mSwatches = null; 628 629 // Add the default targets 630 mTargets.add(Target.LIGHT_VIBRANT); 631 mTargets.add(Target.VIBRANT); 632 mTargets.add(Target.DARK_VIBRANT); 633 mTargets.add(Target.LIGHT_MUTED); 634 mTargets.add(Target.MUTED); 635 mTargets.add(Target.DARK_MUTED); 636 } 637 638 /** 639 * Construct a new {@link Palette.Builder} using a list of {@link Palette.Swatch} instances. 640 * Typically only used for testing. 641 */ Builder(List<Palette.Swatch> swatches)642 public Builder(List<Palette.Swatch> swatches) { 643 if (swatches == null || swatches.isEmpty()) { 644 throw new IllegalArgumentException("List of Swatches is not valid"); 645 } 646 mFilters.add(DEFAULT_FILTER); 647 mSwatches = swatches; 648 mBitmap = null; 649 } 650 651 /** 652 * Set the maximum number of colors to use in the quantization step when using a 653 * {@link android.graphics.Bitmap} as the source. 654 * <p> 655 * Good values for depend on the source image type. For landscapes, good values are in 656 * the range 10-16. For images which are largely made up of people's faces then this 657 * value should be increased to ~24. 658 */ 659 @NonNull maximumColorCount(int colors)660 public Palette.Builder maximumColorCount(int colors) { 661 mMaxColors = colors; 662 return this; 663 } 664 665 /** 666 * Set the resize value when using a {@link android.graphics.Bitmap} as the source. 667 * If the bitmap's largest dimension is greater than the value specified, then the bitmap 668 * will be resized so that its largest dimension matches {@code maxDimension}. If the 669 * bitmap is smaller or equal, the original is used as-is. 670 * 671 * @deprecated Using {@link #resizeBitmapArea(int)} is preferred since it can handle 672 * abnormal aspect ratios more gracefully. 673 * 674 * @param maxDimension the number of pixels that the max dimension should be scaled down to, 675 * or any value <= 0 to disable resizing. 676 */ 677 @NonNull 678 @Deprecated resizeBitmapSize(final int maxDimension)679 public Palette.Builder resizeBitmapSize(final int maxDimension) { 680 mResizeMaxDimension = maxDimension; 681 mResizeArea = -1; 682 return this; 683 } 684 685 /** 686 * Set the resize value when using a {@link android.graphics.Bitmap} as the source. 687 * If the bitmap's area is greater than the value specified, then the bitmap 688 * will be resized so that its area matches {@code area}. If the 689 * bitmap is smaller or equal, the original is used as-is. 690 * <p> 691 * This value has a large effect on the processing time. The larger the resized image is, 692 * the greater time it will take to generate the palette. The smaller the image is, the 693 * more detail is lost in the resulting image and thus less precision for color selection. 694 * 695 * @param area the number of pixels that the intermediary scaled down Bitmap should cover, 696 * or any value <= 0 to disable resizing. 697 */ 698 @NonNull resizeBitmapArea(final int area)699 public Palette.Builder resizeBitmapArea(final int area) { 700 mResizeArea = area; 701 mResizeMaxDimension = -1; 702 return this; 703 } 704 705 /** 706 * Clear all added filters. This includes any default filters added automatically by 707 * {@link Palette}. 708 */ 709 @NonNull clearFilters()710 public Palette.Builder clearFilters() { 711 mFilters.clear(); 712 return this; 713 } 714 715 /** 716 * Add a filter to be able to have fine grained control over which colors are 717 * allowed in the resulting palette. 718 * 719 * @param filter filter to add. 720 */ 721 @NonNull addFilter( Palette.Filter filter)722 public Palette.Builder addFilter( 723 Palette.Filter filter) { 724 if (filter != null) { 725 mFilters.add(filter); 726 } 727 return this; 728 } 729 730 /** 731 * Set a specific quantization algorithm. {@link ColorCutQuantizer} will 732 * be used if unspecified. 733 * 734 * @param quantizer Quantizer implementation. 735 */ 736 @NonNull setQuantizer(Quantizer quantizer)737 public Palette.Builder setQuantizer(Quantizer quantizer) { 738 mQuantizer = quantizer; 739 return this; 740 } 741 742 /** 743 * Set a region of the bitmap to be used exclusively when calculating the palette. 744 * <p>This only works when the original input is a {@link Bitmap}.</p> 745 * 746 * @param left The left side of the rectangle used for the region. 747 * @param top The top of the rectangle used for the region. 748 * @param right The right side of the rectangle used for the region. 749 * @param bottom The bottom of the rectangle used for the region. 750 */ 751 @NonNull setRegion(int left, int top, int right, int bottom)752 public Palette.Builder setRegion(int left, int top, int right, int bottom) { 753 if (mBitmap != null) { 754 if (mRegion == null) mRegion = new Rect(); 755 // Set the Rect to be initially the whole Bitmap 756 mRegion.set(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 757 // Now just get the intersection with the region 758 if (!mRegion.intersect(left, top, right, bottom)) { 759 throw new IllegalArgumentException("The given region must intersect with " 760 + "the Bitmap's dimensions."); 761 } 762 } 763 return this; 764 } 765 766 /** 767 * Clear any previously region set via {@link #setRegion(int, int, int, int)}. 768 */ 769 @NonNull clearRegion()770 public Palette.Builder clearRegion() { 771 mRegion = null; 772 return this; 773 } 774 775 /** 776 * Add a target profile to be generated in the palette. 777 * 778 * <p>You can retrieve the result via {@link Palette#getSwatchForTarget(Target)}.</p> 779 */ 780 @NonNull addTarget(@onNull final Target target)781 public Palette.Builder addTarget(@NonNull final Target target) { 782 if (!mTargets.contains(target)) { 783 mTargets.add(target); 784 } 785 return this; 786 } 787 788 /** 789 * Clear all added targets. This includes any default targets added automatically by 790 * {@link Palette}. 791 */ 792 @NonNull clearTargets()793 public Palette.Builder clearTargets() { 794 if (mTargets != null) { 795 mTargets.clear(); 796 } 797 return this; 798 } 799 800 /** 801 * Generate and return the {@link Palette} synchronously. 802 */ 803 @NonNull generate()804 public Palette generate() { 805 final TimingLogger logger = LOG_TIMINGS 806 ? new TimingLogger(LOG_TAG, "Generation") 807 : null; 808 809 List<Palette.Swatch> swatches; 810 811 if (mBitmap != null) { 812 // We have a Bitmap so we need to use quantization to reduce the number of colors 813 814 // First we'll scale down the bitmap if needed 815 final Bitmap bitmap = scaleBitmapDown(mBitmap); 816 817 if (logger != null) { 818 logger.addSplit("Processed Bitmap"); 819 } 820 821 final Rect region = mRegion; 822 if (bitmap != mBitmap && region != null) { 823 // If we have a scaled bitmap and a selected region, we need to scale down the 824 // region to match the new scale 825 final double scale = bitmap.getWidth() / (double) mBitmap.getWidth(); 826 region.left = (int) Math.floor(region.left * scale); 827 region.top = (int) Math.floor(region.top * scale); 828 region.right = Math.min((int) Math.ceil(region.right * scale), 829 bitmap.getWidth()); 830 region.bottom = Math.min((int) Math.ceil(region.bottom * scale), 831 bitmap.getHeight()); 832 } 833 834 // Now generate a quantizer from the Bitmap 835 if (mQuantizer == null) { 836 mQuantizer = new ColorCutQuantizer(); 837 } 838 mQuantizer.quantize(getPixelsFromBitmap(bitmap), 839 mMaxColors, mFilters.isEmpty() ? null : 840 mFilters.toArray(new Palette.Filter[mFilters.size()])); 841 842 // If created a new bitmap, recycle it 843 if (bitmap != mBitmap) { 844 bitmap.recycle(); 845 } 846 847 swatches = mQuantizer.getQuantizedColors(); 848 849 if (logger != null) { 850 logger.addSplit("Color quantization completed"); 851 } 852 } else { 853 // Else we're using the provided swatches 854 swatches = mSwatches; 855 } 856 857 // Now create a Palette instance 858 final Palette p = new Palette(swatches, mTargets); 859 // And make it generate itself 860 p.generate(); 861 862 if (logger != null) { 863 logger.addSplit("Created Palette"); 864 logger.dumpToLog(); 865 } 866 867 return p; 868 } 869 870 /** 871 * Generate the {@link Palette} asynchronously. The provided listener's 872 * {@link Palette.PaletteAsyncListener#onGenerated} method will be called with the palette when 873 * generated. 874 */ 875 @NonNull generate(final Palette.PaletteAsyncListener listener)876 public AsyncTask<Bitmap, Void, Palette> generate(final Palette.PaletteAsyncListener listener) { 877 if (listener == null) { 878 throw new IllegalArgumentException("listener can not be null"); 879 } 880 881 return new AsyncTask<Bitmap, Void, Palette>() { 882 @Override 883 protected Palette doInBackground(Bitmap... params) { 884 try { 885 return generate(); 886 } catch (Exception e) { 887 Log.e(LOG_TAG, "Exception thrown during async generate", e); 888 return null; 889 } 890 } 891 892 @Override 893 protected void onPostExecute(Palette colorExtractor) { 894 listener.onGenerated(colorExtractor); 895 } 896 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mBitmap); 897 } 898 899 private int[] getPixelsFromBitmap(Bitmap bitmap) { 900 final int bitmapWidth = bitmap.getWidth(); 901 final int bitmapHeight = bitmap.getHeight(); 902 final int[] pixels = new int[bitmapWidth * bitmapHeight]; 903 bitmap.getPixels(pixels, 0, bitmapWidth, 0, 0, bitmapWidth, bitmapHeight); 904 905 if (mRegion == null) { 906 // If we don't have a region, return all of the pixels 907 return pixels; 908 } else { 909 // If we do have a region, lets create a subset array containing only the region's 910 // pixels 911 final int regionWidth = mRegion.width(); 912 final int regionHeight = mRegion.height(); 913 // pixels contains all of the pixels, so we need to iterate through each row and 914 // copy the regions pixels into a new smaller array 915 final int[] subsetPixels = new int[regionWidth * regionHeight]; 916 for (int row = 0; row < regionHeight; row++) { 917 System.arraycopy(pixels, ((row + mRegion.top) * bitmapWidth) + mRegion.left, 918 subsetPixels, row * regionWidth, regionWidth); 919 } 920 return subsetPixels; 921 } 922 } 923 924 /** 925 * Scale the bitmap down as needed. 926 */ 927 private Bitmap scaleBitmapDown(final Bitmap bitmap) { 928 double scaleRatio = -1; 929 930 if (mResizeArea > 0) { 931 final int bitmapArea = bitmap.getWidth() * bitmap.getHeight(); 932 if (bitmapArea > mResizeArea) { 933 scaleRatio = Math.sqrt(mResizeArea / (double) bitmapArea); 934 } 935 } else if (mResizeMaxDimension > 0) { 936 final int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight()); 937 if (maxDimension > mResizeMaxDimension) { 938 scaleRatio = mResizeMaxDimension / (double) maxDimension; 939 } 940 } 941 942 if (scaleRatio <= 0) { 943 // Scaling has been disabled or not needed so just return the Bitmap 944 return bitmap; 945 } 946 947 return Bitmap.createScaledBitmap(bitmap, 948 (int) Math.ceil(bitmap.getWidth() * scaleRatio), 949 (int) Math.ceil(bitmap.getHeight() * scaleRatio), 950 false); 951 } 952 } 953 954 /** 955 * A Filter provides a mechanism for exercising fine-grained control over which colors 956 * are valid within a resulting {@link Palette}. 957 */ 958 public interface Filter { 959 /** 960 * Hook to allow clients to be able filter colors from resulting palette. 961 * 962 * @param rgb the color in RGB888. 963 * @param hsl HSL representation of the color. 964 * 965 * @return true if the color is allowed, false if not. 966 * 967 * @see Palette.Builder#addFilter(Palette.Filter) 968 */ 969 boolean isAllowed(int rgb, float[] hsl); 970 } 971 972 /** 973 * The default filter. 974 */ 975 static final Palette.Filter 976 DEFAULT_FILTER = new Palette.Filter() { 977 private static final float BLACK_MAX_LIGHTNESS = 0.05f; 978 private static final float WHITE_MIN_LIGHTNESS = 0.95f; 979 980 @Override 981 public boolean isAllowed(int rgb, float[] hsl) { 982 return !isWhite(hsl) && !isBlack(hsl) && !isNearRedILine(hsl); 983 } 984 985 /** 986 * @return true if the color represents a color which is close to black. 987 */ 988 private boolean isBlack(float[] hslColor) { 989 return hslColor[2] <= BLACK_MAX_LIGHTNESS; 990 } 991 992 /** 993 * @return true if the color represents a color which is close to white. 994 */ 995 private boolean isWhite(float[] hslColor) { 996 return hslColor[2] >= WHITE_MIN_LIGHTNESS; 997 } 998 999 /** 1000 * @return true if the color lies close to the red side of the I line. 1001 */ 1002 private boolean isNearRedILine(float[] hslColor) { 1003 return hslColor[0] >= 10f && hslColor[0] <= 37f && hslColor[1] <= 0.82f; 1004 } 1005 }; 1006 } 1007