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