/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.internal.graphics.palette; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Rect; import android.os.AsyncTask; import android.util.ArrayMap; import android.util.Log; import android.util.SparseBooleanArray; import android.util.TimingLogger; import com.android.internal.graphics.ColorUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; /** * Copied from: /frameworks/support/v7/palette/src/main/java/android/support/v7/ * graphics/Palette.java * * A helper class to extract prominent colors from an image. *
* A number of colors with different profiles are extracted from the image: *
* Instances are created with a {@link Palette.Builder} which supports several options to tweak the * generated Palette. See that class' documentation for more information. *
* Generation should always be completed on a background thread, ideally the one in * which you load your image on. {@link Palette.Builder} supports both synchronous and asynchronous * generation: * *
* // Synchronous * Palette p = Palette.from(bitmap).generate(); * * // Asynchronous * Palette.from(bitmap).generate(new PaletteAsyncListener() { * public void onGenerated(Palette p) { * // Use generated instance * } * }); **/ public final class Palette { /** * Listener to be used with {@link #generateAsync(Bitmap, Palette.PaletteAsyncListener)} or * {@link #generateAsync(Bitmap, int, Palette.PaletteAsyncListener)} */ public interface PaletteAsyncListener { /** * Called when the {@link Palette} has been generated. */ void onGenerated(Palette palette); } static final int DEFAULT_RESIZE_BITMAP_AREA = 112 * 112; static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16; static final float MIN_CONTRAST_TITLE_TEXT = 3.0f; static final float MIN_CONTRAST_BODY_TEXT = 4.5f; static final String LOG_TAG = "Palette"; static final boolean LOG_TIMINGS = false; /** * Start generating a {@link Palette} with the returned {@link Palette.Builder} instance. */ public static Palette.Builder from(Bitmap bitmap) { return new Palette.Builder(bitmap); } /** * Generate a {@link Palette} from the pre-generated list of {@link Palette.Swatch} swatches. * This is useful for testing, or if you want to resurrect a {@link Palette} instance from a * list of swatches. Will return null if the {@code swatches} is null. */ public static Palette from(List
The dominant swatch is defined as the swatch with the greatest population (frequency) * within the palette.
*/ @Nullable public Palette.Swatch getDominantSwatch() { return mDominantSwatch; } /** * Returns the color of the dominant swatch from the palette, as an RGB packed int. * * @param defaultColor value to return if the swatch isn't available * @see #getDominantSwatch() */ @ColorInt public int getDominantColor(@ColorInt int defaultColor) { return mDominantSwatch != null ? mDominantSwatch.getRgb() : defaultColor; } void generate() { // We need to make sure that the scored targets are generated first. This is so that // inherited targets have something to inherit from for (int i = 0, count = mTargets.size(); i < count; i++) { final Target target = mTargets.get(i); target.normalizeWeights(); mSelectedSwatches.put(target, generateScoredTarget(target)); } // We now clear out the used colors mUsedColors.clear(); } private Palette.Swatch generateScoredTarget(final Target target) { final Palette.Swatch maxScoreSwatch = getMaxScoredSwatchForTarget(target); if (maxScoreSwatch != null && target.isExclusive()) { // If we have a swatch, and the target is exclusive, add the color to the used list mUsedColors.append(maxScoreSwatch.getRgb(), true); } return maxScoreSwatch; } private Palette.Swatch getMaxScoredSwatchForTarget(final Target target) { float maxScore = 0; Palette.Swatch maxScoreSwatch = null; for (int i = 0, count = mSwatches.size(); i < count; i++) { final Palette.Swatch swatch = mSwatches.get(i); if (shouldBeScoredForTarget(swatch, target)) { final float score = generateScore(swatch, target); if (maxScoreSwatch == null || score > maxScore) { maxScoreSwatch = swatch; maxScore = score; } } } return maxScoreSwatch; } private boolean shouldBeScoredForTarget(final Palette.Swatch swatch, final Target target) { // Check whether the HSL values are within the correct ranges, and this color hasn't // been used yet. final float hsl[] = swatch.getHsl(); return hsl[1] >= target.getMinimumSaturation() && hsl[1] <= target.getMaximumSaturation() && hsl[2] >= target.getMinimumLightness() && hsl[2] <= target.getMaximumLightness() && !mUsedColors.get(swatch.getRgb()); } private float generateScore(Palette.Swatch swatch, Target target) { final float[] hsl = swatch.getHsl(); float saturationScore = 0; float luminanceScore = 0; float populationScore = 0; final int maxPopulation = mDominantSwatch != null ? mDominantSwatch.getPopulation() : 1; if (target.getSaturationWeight() > 0) { saturationScore = target.getSaturationWeight() * (1f - Math.abs(hsl[1] - target.getTargetSaturation())); } if (target.getLightnessWeight() > 0) { luminanceScore = target.getLightnessWeight() * (1f - Math.abs(hsl[2] - target.getTargetLightness())); } if (target.getPopulationWeight() > 0) { populationScore = target.getPopulationWeight() * (swatch.getPopulation() / (float) maxPopulation); } return saturationScore + luminanceScore + populationScore; } private Palette.Swatch findDominantSwatch() { int maxPop = Integer.MIN_VALUE; Palette.Swatch maxSwatch = null; for (int i = 0, count = mSwatches.size(); i < count; i++) { Palette.Swatch swatch = mSwatches.get(i); if (swatch.getPopulation() > maxPop) { maxSwatch = swatch; maxPop = swatch.getPopulation(); } } return maxSwatch; } private static float[] copyHslValues(Palette.Swatch color) { final float[] newHsl = new float[3]; System.arraycopy(color.getHsl(), 0, newHsl, 0, 3); return newHsl; } /** * Represents a color swatch generated from an image's palette. The RGB color can be retrieved * by calling {@link #getRgb()}. */ public static final class Swatch { private final int mRed, mGreen, mBlue; private final int mRgb; private final int mPopulation; private boolean mGeneratedTextColors; private int mTitleTextColor; private int mBodyTextColor; private float[] mHsl; public Swatch(@ColorInt int color, int population) { mRed = Color.red(color); mGreen = Color.green(color); mBlue = Color.blue(color); mRgb = color; mPopulation = population; } Swatch(int red, int green, int blue, int population) { mRed = red; mGreen = green; mBlue = blue; mRgb = Color.rgb(red, green, blue); mPopulation = population; } Swatch(float[] hsl, int population) { this(ColorUtils.HSLToColor(hsl), population); mHsl = hsl; } /** * @return this swatch's RGB color value */ @ColorInt public int getRgb() { return mRgb; } /** * Return this swatch's HSL values. * hsv[0] is Hue [0 .. 360) * hsv[1] is Saturation [0...1] * hsv[2] is Lightness [0...1] */ public float[] getHsl() { if (mHsl == null) { mHsl = new float[3]; } ColorUtils.RGBToHSL(mRed, mGreen, mBlue, mHsl); return mHsl; } /** * @return the number of pixels represented by this swatch */ public int getPopulation() { return mPopulation; } /** * Returns an appropriate color to use for any 'title' text which is displayed over this * {@link Palette.Swatch}'s color. This color is guaranteed to have sufficient contrast. */ @ColorInt public int getTitleTextColor() { ensureTextColorsGenerated(); return mTitleTextColor; } /** * Returns an appropriate color to use for any 'body' text which is displayed over this * {@link Palette.Swatch}'s color. This color is guaranteed to have sufficient contrast. */ @ColorInt public int getBodyTextColor() { ensureTextColorsGenerated(); return mBodyTextColor; } private void ensureTextColorsGenerated() { if (!mGeneratedTextColors) { // First check white, as most colors will be dark final int lightBodyAlpha = ColorUtils.calculateMinimumAlpha( Color.WHITE, mRgb, MIN_CONTRAST_BODY_TEXT); final int lightTitleAlpha = ColorUtils.calculateMinimumAlpha( Color.WHITE, mRgb, MIN_CONTRAST_TITLE_TEXT); if (lightBodyAlpha != -1 && lightTitleAlpha != -1) { // If we found valid light values, use them and return mBodyTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha); mTitleTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha); mGeneratedTextColors = true; return; } final int darkBodyAlpha = ColorUtils.calculateMinimumAlpha( Color.BLACK, mRgb, MIN_CONTRAST_BODY_TEXT); final int darkTitleAlpha = ColorUtils.calculateMinimumAlpha( Color.BLACK, mRgb, MIN_CONTRAST_TITLE_TEXT); if (darkBodyAlpha != -1 && darkTitleAlpha != -1) { // If we found valid dark values, use them and return mBodyTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha); mTitleTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha); mGeneratedTextColors = true; return; } // If we reach here then we can not find title and body values which use the same // lightness, we need to use mismatched values mBodyTextColor = lightBodyAlpha != -1 ? ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha) : ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha); mTitleTextColor = lightTitleAlpha != -1 ? ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha) : ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha); mGeneratedTextColors = true; } } @Override public String toString() { return new StringBuilder(getClass().getSimpleName()) .append(" [RGB: #").append(Integer.toHexString(getRgb())).append(']') .append(" [HSL: ").append(Arrays.toString(getHsl())).append(']') .append(" [Population: ").append(mPopulation).append(']') .append(" [Title Text: #").append(Integer.toHexString(getTitleTextColor())) .append(']') .append(" [Body Text: #").append(Integer.toHexString(getBodyTextColor())) .append(']').toString(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Palette.Swatch swatch = (Palette.Swatch) o; return mPopulation == swatch.mPopulation && mRgb == swatch.mRgb; } @Override public int hashCode() { return 31 * mRgb + mPopulation; } } /** * Builder class for generating {@link Palette} instances. */ public static final class Builder { private final List* Good values for depend on the source image type. For landscapes, good values are in * the range 10-16. For images which are largely made up of people's faces then this * value should be increased to ~24. */ @NonNull public Palette.Builder maximumColorCount(int colors) { mMaxColors = colors; return this; } /** * Set the resize value when using a {@link android.graphics.Bitmap} as the source. * If the bitmap's largest dimension is greater than the value specified, then the bitmap * will be resized so that its largest dimension matches {@code maxDimension}. If the * bitmap is smaller or equal, the original is used as-is. * * @deprecated Using {@link #resizeBitmapArea(int)} is preferred since it can handle * abnormal aspect ratios more gracefully. * * @param maxDimension the number of pixels that the max dimension should be scaled down to, * or any value <= 0 to disable resizing. */ @NonNull @Deprecated public Palette.Builder resizeBitmapSize(final int maxDimension) { mResizeMaxDimension = maxDimension; mResizeArea = -1; return this; } /** * Set the resize value when using a {@link android.graphics.Bitmap} as the source. * If the bitmap's area is greater than the value specified, then the bitmap * will be resized so that its area matches {@code area}. If the * bitmap is smaller or equal, the original is used as-is. *
* This value has a large effect on the processing time. The larger the resized image is, * the greater time it will take to generate the palette. The smaller the image is, the * more detail is lost in the resulting image and thus less precision for color selection. * * @param area the number of pixels that the intermediary scaled down Bitmap should cover, * or any value <= 0 to disable resizing. */ @NonNull public Palette.Builder resizeBitmapArea(final int area) { mResizeArea = area; mResizeMaxDimension = -1; return this; } /** * Clear all added filters. This includes any default filters added automatically by * {@link Palette}. */ @NonNull public Palette.Builder clearFilters() { mFilters.clear(); return this; } /** * Add a filter to be able to have fine grained control over which colors are * allowed in the resulting palette. * * @param filter filter to add. */ @NonNull public Palette.Builder addFilter( Palette.Filter filter) { if (filter != null) { mFilters.add(filter); } return this; } /** * Set a specific quantization algorithm. {@link ColorCutQuantizer} will * be used if unspecified. * * @param quantizer Quantizer implementation. */ @NonNull public Palette.Builder setQuantizer(Quantizer quantizer) { mQuantizer = quantizer; return this; } /** * Set a region of the bitmap to be used exclusively when calculating the palette. *
This only works when the original input is a {@link Bitmap}.
* * @param left The left side of the rectangle used for the region. * @param top The top of the rectangle used for the region. * @param right The right side of the rectangle used for the region. * @param bottom The bottom of the rectangle used for the region. */ @NonNull public Palette.Builder setRegion(int left, int top, int right, int bottom) { if (mBitmap != null) { if (mRegion == null) mRegion = new Rect(); // Set the Rect to be initially the whole Bitmap mRegion.set(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); // Now just get the intersection with the region if (!mRegion.intersect(left, top, right, bottom)) { throw new IllegalArgumentException("The given region must intersect with " + "the Bitmap's dimensions."); } } return this; } /** * Clear any previously region set via {@link #setRegion(int, int, int, int)}. */ @NonNull public Palette.Builder clearRegion() { mRegion = null; return this; } /** * Add a target profile to be generated in the palette. * *You can retrieve the result via {@link Palette#getSwatchForTarget(Target)}.
*/ @NonNull public Palette.Builder addTarget(@NonNull final Target target) { if (!mTargets.contains(target)) { mTargets.add(target); } return this; } /** * Clear all added targets. This includes any default targets added automatically by * {@link Palette}. */ @NonNull public Palette.Builder clearTargets() { if (mTargets != null) { mTargets.clear(); } return this; } /** * Generate and return the {@link Palette} synchronously. */ @NonNull public Palette generate() { final TimingLogger logger = LOG_TIMINGS ? new TimingLogger(LOG_TAG, "Generation") : null; List