diff options
author | Wolfgang Wiedmeyer <wolfgit@wiedmeyer.de> | 2016-01-14 22:13:02 +0100 |
---|---|---|
committer | Wolfgang Wiedmeyer <wolfgit@wiedmeyer.de> | 2016-01-14 22:13:02 +0100 |
commit | 69a68b55dfbb1603ca549610bfd14dd4698a3c67 (patch) | |
tree | 28c1bcec9bd0af829cd642df8fddc54032ad5730 /core/java/com/android/internal/util/cm/palette/Palette.java | |
parent | 85e8acfb6b7f12598c11ca6f88b06407de32db90 (diff) | |
parent | 0f4b580ed9af38dabb217e4cb843c0b0acf210ee (diff) | |
download | frameworks_base-7dcac68b2bd4ec4f57af73ec22bd22305430f441.zip frameworks_base-7dcac68b2bd4ec4f57af73ec22bd22305430f441.tar.gz frameworks_base-7dcac68b2bd4ec4f57af73ec22bd22305430f441.tar.bz2 |
Merge branch 'cm-13.0' of https://github.com/CyanogenMod/android_frameworks_base into replicant-6.0replicant-6.0-alpha-0001
Diffstat (limited to 'core/java/com/android/internal/util/cm/palette/Palette.java')
-rw-r--r-- | core/java/com/android/internal/util/cm/palette/Palette.java | 788 |
1 files changed, 420 insertions, 368 deletions
diff --git a/core/java/com/android/internal/util/cm/palette/Palette.java b/core/java/com/android/internal/util/cm/palette/Palette.java index d0a62f0..2cbd2b8 100644 --- a/core/java/com/android/internal/util/cm/palette/Palette.java +++ b/core/java/com/android/internal/util/cm/palette/Palette.java @@ -19,7 +19,11 @@ package com.android.internal.util.cm.palette; import android.graphics.Bitmap; import android.graphics.Color; import android.os.AsyncTask; +import android.annotation.ColorInt; +import android.annotation.Nullable; +import android.util.TimingLogger; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -39,18 +43,21 @@ import java.util.List; * These can be retrieved from the appropriate getter method. * * <p> - * Instances can be created with the synchronous factory methods {@link #generate(Bitmap)} and - * {@link #generate(Bitmap, int)}. + * Instances are created with a {@link Builder} which supports several options to tweak the + * generated Palette. See that class' documentation for more information. * <p> - * These should be called on a background thread, ideally the one in - * which you load your images on. Sometimes that is not possible, so asynchronous factory methods - * have also been provided: {@link #generateAsync(Bitmap, PaletteAsyncListener)} and - * {@link #generateAsync(Bitmap, int, PaletteAsyncListener)}. These can be used as so: + * Generation should always be completed on a background thread, ideally the one in + * which you load your image on. {@link Builder} supports both synchronous and asynchronous + * generation: * * <pre> - * Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() { - * public void onGenerated(Palette palette) { - * // Do something with colors... + * // Synchronous + * Palette p = Palette.from(bitmap).generate(); + * + * // Asynchronous + * Palette.from(bitmap).generate(new PaletteAsyncListener() { + * public void onGenerated(Palette p) { + * // Use generated instance * } * }); * </pre> @@ -71,160 +78,71 @@ public final class Palette { void onGenerated(Palette palette); } - private static final int CALCULATE_BITMAP_MIN_DIMENSION = 100; + private static final int DEFAULT_RESIZE_BITMAP_MAX_DIMENSION = 192; private static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16; - private static final float TARGET_DARK_LUMA = 0.26f; - private static final float MAX_DARK_LUMA = 0.45f; - - private static final float MIN_LIGHT_LUMA = 0.55f; - private static final float TARGET_LIGHT_LUMA = 0.74f; - - private static final float MIN_NORMAL_LUMA = 0.3f; - private static final float TARGET_NORMAL_LUMA = 0.5f; - private static final float MAX_NORMAL_LUMA = 0.7f; - - private static final float TARGET_MUTED_SATURATION = 0.3f; - private static final float MAX_MUTED_SATURATION = 0.4f; - - private static final float TARGET_VIBRANT_SATURATION = 1f; - private static final float MIN_VIBRANT_SATURATION = 0.35f; - - private static final float WEIGHT_SATURATION = 3f; - private static final float WEIGHT_LUMA = 6f; - private static final float WEIGHT_POPULATION = 1f; - private static final float MIN_CONTRAST_TITLE_TEXT = 3.0f; private static final float MIN_CONTRAST_BODY_TEXT = 4.5f; - private final List<Swatch> mSwatches; - private final int mHighestPopulation; - - private Swatch mVibrantSwatch; - private Swatch mMutedSwatch; + private static final String LOG_TAG = "Palette"; + private static final boolean LOG_TIMINGS = false; - private Swatch mDarkVibrantSwatch; - private Swatch mDarkMutedSwatch; + /** + * Start generating a {@link Palette} with the returned {@link Builder} instance. + */ + public static Builder from(Bitmap bitmap) { + return new Builder(bitmap); + } - private Swatch mLightVibrantSwatch; - private Swatch mLightMutedColor; + /** + * 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<Swatch> swatches) { + return new Builder(swatches).generate(); + } /** - * Generate a {@link Palette} from a {@link Bitmap} using the default number of colors. + * @deprecated Use {@link Builder} to generate the Palette. */ + @Deprecated public static Palette generate(Bitmap bitmap) { - return generate(bitmap, DEFAULT_CALCULATE_NUMBER_COLORS); + return from(bitmap).generate(); } /** - * Generate a {@link Palette} from a {@link Bitmap} using the specified {@code numColors}. - * Good values for {@code numColors} depend on the source image type. - * For landscapes, a good values are in the range 12-16. For images which are largely made up - * of people's faces then this value should be increased to 24-32. - * - * @param numColors The maximum number of colors in the generated palette. Increasing this - * number will increase the time needed to compute the values. + * @deprecated Use {@link Builder} to generate the Palette. */ + @Deprecated public static Palette generate(Bitmap bitmap, int numColors) { - checkBitmapParam(bitmap); - checkNumberColorsParam(numColors); - - // First we'll scale down the bitmap so it's shortest dimension is 100px - final Bitmap scaledBitmap = scaleBitmapDown(bitmap); - - // Now generate a quantizer from the Bitmap - ColorCutQuantizer quantizer = ColorCutQuantizer.fromBitmap(scaledBitmap, numColors); - - // If created a new bitmap, recycle it - if (scaledBitmap != bitmap) { - scaledBitmap.recycle(); - } - - // Now return a ColorExtractor instance - return new Palette(quantizer.getQuantizedColors()); + return from(bitmap).maximumColorCount(numColors).generate(); } /** - * Generate a {@link Palette} asynchronously. {@link PaletteAsyncListener#onGenerated(Palette)} - * will be called with the created instance. The resulting {@link Palette} is the same as - * what would be created by calling {@link #generate(Bitmap)}. - * - * @param listener Listener to be invoked when the {@link Palette} has been generated. - * - * @return the {@link AsyncTask} used to asynchronously generate the instance. + * @deprecated Use {@link Builder} to generate the Palette. */ + @Deprecated public static AsyncTask<Bitmap, Void, Palette> generateAsync( Bitmap bitmap, PaletteAsyncListener listener) { - return generateAsync(bitmap, DEFAULT_CALCULATE_NUMBER_COLORS, listener); + return from(bitmap).generate(listener); } /** - * Generate a {@link Palette} asynchronously. {@link PaletteAsyncListener#onGenerated(Palette)} - * will be called with the created instance. The resulting {@link Palette} is the same as what - * would be created by calling {@link #generate(Bitmap, int)}. - * - * @param listener Listener to be invoked when the {@link Palette} has been generated. - * - * @return the {@link AsyncTask} used to asynchronously generate the instance. + * @deprecated Use {@link Builder} to generate the Palette. */ + @Deprecated public static AsyncTask<Bitmap, Void, Palette> generateAsync( final Bitmap bitmap, final int numColors, final PaletteAsyncListener listener) { - checkBitmapParam(bitmap); - checkNumberColorsParam(numColors); - checkAsyncListenerParam(listener); - - AsyncTask<Bitmap, Void, Palette> task = new AsyncTask<Bitmap, Void, Palette>() { - @Override - protected Palette doInBackground(Bitmap... params) { - return generate(params[0], numColors); - } - - @Override - protected void onPostExecute(Palette colorExtractor) { - listener.onGenerated(colorExtractor); - } - }; - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, bitmap); - - return task; + return from(bitmap).maximumColorCount(numColors).generate(listener); } - /** - * 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<Swatch> swatches) { - if (swatches == null) { - return null; - } - return new Palette(swatches); - } + private final List<Swatch> mSwatches; + private final Generator mGenerator; - private Palette(List<Swatch> swatches) { + private Palette(List<Swatch> swatches, Generator generator) { mSwatches = swatches; - mHighestPopulation = findMaxPopulation(); - - mVibrantSwatch = findColor(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA, - TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f); - - mLightVibrantSwatch = findColor(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1f, - TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f); - - mDarkVibrantSwatch = findColor(TARGET_DARK_LUMA, 0f, MAX_DARK_LUMA, - TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f); - - mMutedSwatch = findColor(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA, - TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION); - - mLightMutedColor = findColor(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1f, - TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION); - - mDarkMutedSwatch = findColor(TARGET_DARK_LUMA, 0f, MAX_DARK_LUMA, - TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION); - - // Now try and generate any missing colors - generateEmptySwatches(); + mGenerator = generator; } /** @@ -237,43 +155,49 @@ public final class Palette { /** * Returns the most vibrant swatch in the palette. Might be null. */ + @Nullable public Swatch getVibrantSwatch() { - return mVibrantSwatch; + return mGenerator.getVibrantSwatch(); } /** * Returns a light and vibrant swatch from the palette. Might be null. */ + @Nullable public Swatch getLightVibrantSwatch() { - return mLightVibrantSwatch; + return mGenerator.getLightVibrantSwatch(); } /** * Returns a dark and vibrant swatch from the palette. Might be null. */ + @Nullable public Swatch getDarkVibrantSwatch() { - return mDarkVibrantSwatch; + return mGenerator.getDarkVibrantSwatch(); } /** * Returns a muted swatch from the palette. Might be null. */ + @Nullable public Swatch getMutedSwatch() { - return mMutedSwatch; + return mGenerator.getMutedSwatch(); } /** * Returns a muted and light swatch from the palette. Might be null. */ + @Nullable public Swatch getLightMutedSwatch() { - return mLightMutedColor; + return mGenerator.getLightMutedSwatch(); } /** * Returns a muted and dark swatch from the palette. Might be null. */ + @Nullable public Swatch getDarkMutedSwatch() { - return mDarkMutedSwatch; + return mGenerator.getDarkMutedSwatch(); } /** @@ -281,8 +205,10 @@ public final class Palette { * * @param defaultColor value to return if the swatch isn't available */ - public int getVibrantColor(int defaultColor) { - return mVibrantSwatch != null ? mVibrantSwatch.getRgb() : defaultColor; + @ColorInt + public int getVibrantColor(@ColorInt int defaultColor) { + Swatch swatch = getVibrantSwatch(); + return swatch != null ? swatch.getRgb() : defaultColor; } /** @@ -290,8 +216,10 @@ public final class Palette { * * @param defaultColor value to return if the swatch isn't available */ - public int getLightVibrantColor(int defaultColor) { - return mLightVibrantSwatch != null ? mLightVibrantSwatch.getRgb() : defaultColor; + @ColorInt + public int getLightVibrantColor(@ColorInt int defaultColor) { + Swatch swatch = getLightVibrantSwatch(); + return swatch != null ? swatch.getRgb() : defaultColor; } /** @@ -299,8 +227,10 @@ public final class Palette { * * @param defaultColor value to return if the swatch isn't available */ - public int getDarkVibrantColor(int defaultColor) { - return mDarkVibrantSwatch != null ? mDarkVibrantSwatch.getRgb() : defaultColor; + @ColorInt + public int getDarkVibrantColor(@ColorInt int defaultColor) { + Swatch swatch = getDarkVibrantSwatch(); + return swatch != null ? swatch.getRgb() : defaultColor; } /** @@ -308,8 +238,10 @@ public final class Palette { * * @param defaultColor value to return if the swatch isn't available */ - public int getMutedColor(int defaultColor) { - return mMutedSwatch != null ? mMutedSwatch.getRgb() : defaultColor; + @ColorInt + public int getMutedColor(@ColorInt int defaultColor) { + Swatch swatch = getMutedSwatch(); + return swatch != null ? swatch.getRgb() : defaultColor; } /** @@ -317,8 +249,10 @@ public final class Palette { * * @param defaultColor value to return if the swatch isn't available */ - public int getLightMutedColor(int defaultColor) { - return mLightMutedColor != null ? mLightMutedColor.getRgb() : defaultColor; + @ColorInt + public int getLightMutedColor(@ColorInt int defaultColor) { + Swatch swatch = getLightMutedSwatch(); + return swatch != null ? swatch.getRgb() : defaultColor; } /** @@ -326,220 +260,31 @@ public final class Palette { * * @param defaultColor value to return if the swatch isn't available */ - public int getDarkMutedColor(int defaultColor) { - return mDarkMutedSwatch != null ? mDarkMutedSwatch.getRgb() : defaultColor; + @ColorInt + public int getDarkMutedColor(@ColorInt int defaultColor) { + Swatch swatch = getDarkMutedSwatch(); + return swatch != null ? swatch.getRgb() : defaultColor; } /** - * @return true if we have already selected {@code swatch} + * Scale the bitmap down so that it's largest dimension is {@code targetMaxDimension}. + * If {@code bitmap} is smaller than this, then it is returned. */ - private boolean isAlreadySelected(Swatch swatch) { - return mVibrantSwatch == swatch || mDarkVibrantSwatch == swatch || - mLightVibrantSwatch == swatch || mMutedSwatch == swatch || - mDarkMutedSwatch == swatch || mLightMutedColor == swatch; - } - - private Swatch findColor(float targetLuma, float minLuma, float maxLuma, - float targetSaturation, float minSaturation, float maxSaturation) { - Swatch max = null; - float maxValue = 0f; - - for (Swatch swatch : mSwatches) { - final float sat = swatch.getHsl()[1]; - final float luma = swatch.getHsl()[2]; - - if (sat >= minSaturation && sat <= maxSaturation && - luma >= minLuma && luma <= maxLuma && - !isAlreadySelected(swatch)) { - float thisValue = createComparisonValue(sat, targetSaturation, luma, targetLuma, - swatch.getPopulation(), mHighestPopulation); - if (max == null || thisValue > maxValue) { - max = swatch; - maxValue = thisValue; - } - } - } + private static Bitmap scaleBitmapDown(Bitmap bitmap, final int targetMaxDimension) { + final int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight()); - return max; - } - - /** - * Try and generate any missing swatches from the swatches we did find. - */ - private void generateEmptySwatches() { - if (mVibrantSwatch == null) { - // If we do not have a vibrant color... - if (mDarkVibrantSwatch != null) { - // ...but we do have a dark vibrant, generate the value by modifying the luma - final float[] newHsl = copyHslValues(mDarkVibrantSwatch); - newHsl[2] = TARGET_NORMAL_LUMA; - mVibrantSwatch = new Swatch(ColorUtils.HSLtoRGB(newHsl), 0); - } - } - - if (mDarkVibrantSwatch == null) { - // If we do not have a dark vibrant color... - if (mVibrantSwatch != null) { - // ...but we do have a vibrant, generate the value by modifying the luma - final float[] newHsl = copyHslValues(mVibrantSwatch); - newHsl[2] = TARGET_DARK_LUMA; - mDarkVibrantSwatch = new Swatch(ColorUtils.HSLtoRGB(newHsl), 0); - } - } - } - - /** - * Find the {@link Swatch} with the highest population value and return the population. - */ - private int findMaxPopulation() { - int population = 0; - for (Swatch swatch : mSwatches) { - population = Math.max(population, swatch.getPopulation()); - } - return population; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - Palette palette = (Palette) o; - - if (mSwatches != null ? !mSwatches.equals(palette.mSwatches) : palette.mSwatches != null) { - return false; - } - if (mDarkMutedSwatch != null ? !mDarkMutedSwatch.equals(palette.mDarkMutedSwatch) - : palette.mDarkMutedSwatch != null) { - return false; - } - if (mDarkVibrantSwatch != null ? !mDarkVibrantSwatch.equals(palette.mDarkVibrantSwatch) - : palette.mDarkVibrantSwatch != null) { - return false; - } - if (mLightMutedColor != null ? !mLightMutedColor.equals(palette.mLightMutedColor) - : palette.mLightMutedColor != null) { - return false; - } - if (mLightVibrantSwatch != null ? !mLightVibrantSwatch.equals(palette.mLightVibrantSwatch) - : palette.mLightVibrantSwatch != null) { - return false; - } - if (mMutedSwatch != null ? !mMutedSwatch.equals(palette.mMutedSwatch) - : palette.mMutedSwatch != null) { - return false; - } - if (mVibrantSwatch != null ? !mVibrantSwatch.equals(palette.mVibrantSwatch) - : palette.mVibrantSwatch != null) { - return false; - } - - return true; - } - - @Override - public int hashCode() { - int result = mSwatches != null ? mSwatches.hashCode() : 0; - result = 31 * result + (mVibrantSwatch != null ? mVibrantSwatch.hashCode() : 0); - result = 31 * result + (mMutedSwatch != null ? mMutedSwatch.hashCode() : 0); - result = 31 * result + (mDarkVibrantSwatch != null ? mDarkVibrantSwatch.hashCode() : 0); - result = 31 * result + (mDarkMutedSwatch != null ? mDarkMutedSwatch.hashCode() : 0); - result = 31 * result + (mLightVibrantSwatch != null ? mLightVibrantSwatch.hashCode() : 0); - result = 31 * result + (mLightMutedColor != null ? mLightMutedColor.hashCode() : 0); - return result; - } - - /** - * Scale the bitmap down so that it's smallest dimension is - * {@value #CALCULATE_BITMAP_MIN_DIMENSION}px. If {@code bitmap} is smaller than this, than it - * is returned. - */ - private static Bitmap scaleBitmapDown(Bitmap bitmap) { - final int minDimension = Math.min(bitmap.getWidth(), bitmap.getHeight()); - - if (minDimension <= CALCULATE_BITMAP_MIN_DIMENSION) { + if (maxDimension <= targetMaxDimension) { // If the bitmap is small enough already, just return it return bitmap; } - final float scaleRatio = CALCULATE_BITMAP_MIN_DIMENSION / (float) minDimension; + final float scaleRatio = targetMaxDimension / (float) maxDimension; return Bitmap.createScaledBitmap(bitmap, Math.round(bitmap.getWidth() * scaleRatio), Math.round(bitmap.getHeight() * scaleRatio), false); } - private static float createComparisonValue(float saturation, float targetSaturation, - float luma, float targetLuma, - int population, int highestPopulation) { - return weightedMean( - invertDiff(saturation, targetSaturation), WEIGHT_SATURATION, - invertDiff(luma, targetLuma), WEIGHT_LUMA, - population / (float) highestPopulation, WEIGHT_POPULATION - ); - } - - /** - * Copy a {@link Swatch}'s HSL values into a new float[]. - */ - private static float[] copyHslValues(Swatch color) { - final float[] newHsl = new float[3]; - System.arraycopy(color.getHsl(), 0, newHsl, 0, 3); - return newHsl; - } - - /** - * Returns a value in the range 0-1. 1 is returned when {@code value} equals the - * {@code targetValue} and then decreases as the absolute difference between {@code value} and - * {@code targetValue} increases. - * - * @param value the item's value - * @param targetValue the value which we desire - */ - private static float invertDiff(float value, float targetValue) { - return 1f - Math.abs(value - targetValue); - } - - private static float weightedMean(float... values) { - float sum = 0f; - float sumWeight = 0f; - - for (int i = 0; i < values.length; i += 2) { - float value = values[i]; - float weight = values[i + 1]; - - sum += (value * weight); - sumWeight += weight; - } - - return sum / sumWeight; - } - - private static void checkBitmapParam(Bitmap bitmap) { - if (bitmap == null) { - throw new IllegalArgumentException("bitmap can not be null"); - } - if (bitmap.isRecycled()) { - throw new IllegalArgumentException("bitmap can not be recycled"); - } - } - - private static void checkNumberColorsParam(int numColors) { - if (numColors < 1) { - throw new IllegalArgumentException("numColors must be 1 of greater"); - } - } - - private static void checkAsyncListenerParam(PaletteAsyncListener listener) { - if (listener == null) { - throw new IllegalArgumentException("listener can not be null"); - } - } - /** * Represents a color swatch generated from an image's palette. The RGB color can be retrieved * by calling {@link #getRgb()}. @@ -555,7 +300,7 @@ public final class Palette { private float[] mHsl; - public Swatch(int color, int population) { + public Swatch(@ColorInt int color, int population) { mRed = Color.red(color); mGreen = Color.green(color); mBlue = Color.blue(color); @@ -574,6 +319,7 @@ public final class Palette { /** * @return this swatch's RGB color value */ + @ColorInt public int getRgb() { return mRgb; } @@ -586,9 +332,8 @@ public final class Palette { */ public float[] getHsl() { if (mHsl == null) { - // Lazily generate HSL values from RGB mHsl = new float[3]; - ColorUtils.RGBtoHSL(mRed, mGreen, mBlue, mHsl); + ColorUtils.RGBToHSL(mRed, mGreen, mBlue, mHsl); } return mHsl; } @@ -604,6 +349,7 @@ public final class Palette { * Returns an appropriate color to use for any 'title' text which is displayed over this * {@link Swatch}'s color. This color is guaranteed to have sufficient contrast. */ + @ColorInt public int getTitleTextColor() { ensureTextColorsGenerated(); return mTitleTextColor; @@ -613,6 +359,7 @@ public final class Palette { * Returns an appropriate color to use for any 'body' text which is displayed over this * {@link Swatch}'s color. This color is guaranteed to have sufficient contrast. */ + @ColorInt public int getBodyTextColor() { ensureTextColorsGenerated(); return mBodyTextColor; @@ -621,36 +368,40 @@ public final class Palette { private void ensureTextColorsGenerated() { if (!mGeneratedTextColors) { // First check white, as most colors will be dark - final int lightBody = ColorUtils.getTextColorForBackground( - mRgb, Color.WHITE, MIN_CONTRAST_BODY_TEXT); - final int lightTitle = ColorUtils.getTextColorForBackground( - mRgb, Color.WHITE, MIN_CONTRAST_TITLE_TEXT); + 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 (lightBody != -1 && lightTitle != -1) { + if (lightBodyAlpha != -1 && lightTitleAlpha != -1) { // If we found valid light values, use them and return - mBodyTextColor = lightBody; - mTitleTextColor = lightTitle; + mBodyTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha); + mTitleTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha); mGeneratedTextColors = true; return; } - final int darkBody = ColorUtils.getTextColorForBackground( - mRgb, Color.BLACK, MIN_CONTRAST_BODY_TEXT); - final int darkTitle = ColorUtils.getTextColorForBackground( - mRgb, Color.BLACK, MIN_CONTRAST_TITLE_TEXT); + 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 (darkBody != -1 && darkBody != -1) { + if (darkBodyAlpha != -1 && darkBodyAlpha != -1) { // If we found valid dark values, use them and return - mBodyTextColor = darkBody; - mTitleTextColor = darkTitle; + 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 = lightBody != -1 ? lightBody : darkBody; - mTitleTextColor = lightTitle != -1 ? lightTitle : darkTitle; + 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; } } @@ -661,9 +412,10 @@ public final class Palette { .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(mTitleTextColor)).append(']') - .append(" [Body Text: #").append(Integer.toHexString(mBodyTextColor)).append(']') - .toString(); + .append(" [Title Text: #").append(Integer.toHexString(getTitleTextColor())) + .append(']') + .append(" [Body Text: #").append(Integer.toHexString(getBodyTextColor())) + .append(']').toString(); } @Override @@ -685,4 +437,304 @@ public final class Palette { } } + /** + * Builder class for generating {@link Palette} instances. + */ + public static final class Builder { + private List<Swatch> mSwatches; + private Bitmap mBitmap; + private int mMaxColors = DEFAULT_CALCULATE_NUMBER_COLORS; + private int mResizeMaxDimension = DEFAULT_RESIZE_BITMAP_MAX_DIMENSION; + private final List<Filter> mFilters = new ArrayList<>(); + + private Generator mGenerator; + + /** + * Construct a new {@link Builder} using a source {@link Bitmap} + */ + public Builder(Bitmap bitmap) { + this(); + if (bitmap == null || bitmap.isRecycled()) { + throw new IllegalArgumentException("Bitmap is not valid"); + } + mBitmap = bitmap; + } + + /** + * Construct a new {@link Builder} using a list of {@link Swatch} instances. + * Typically only used for testing. + */ + public Builder(List<Swatch> swatches) { + this(); + if (swatches == null || swatches.isEmpty()) { + throw new IllegalArgumentException("List of Swatches is not valid"); + } + mSwatches = swatches; + } + + private Builder() { + mFilters.add(DEFAULT_FILTER); + } + + /** + * Set the {@link Generator} to use when generating the {@link Palette}. If this is called + * with {@code null} then the default generator will be used. + */ + Builder generator(Generator generator) { + mGenerator = generator; + return this; + } + + /** + * Set the maximum number of colors to use in the quantization step when using a + * {@link android.graphics.Bitmap} as the source. + * <p> + * 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. + */ + public 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 it's largest dimension matches {@code maxDimension}. If the + * bitmap is smaller or equal, the original is used as-is. + * <p> + * 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. + */ + public Builder resizeBitmapSize(int maxDimension) { + mResizeMaxDimension = maxDimension; + return this; + } + + /** + * Clear all added filters. This includes any default filters added automatically by + * {@link Palette}. + */ + public Builder clearFilters() { + mFilters.clear(); + return this; + } + + /** + * Add a filter to be able to have fine grained controlled over the colors which are + * allowed in the resulting palette. + * + * @param filter filter to add. + */ + public Builder addFilter(Filter filter) { + if (filter != null) { + mFilters.add(filter); + } + return this; + } + + /** + * Generate and return the {@link Palette} synchronously. + */ + public Palette generate() { + final TimingLogger logger = LOG_TIMINGS + ? new TimingLogger(LOG_TAG, "Generation") + : null; + + List<Swatch> swatches; + + if (mBitmap != null) { + // We have a Bitmap so we need to quantization to reduce the number of colors + + if (mResizeMaxDimension <= 0) { + throw new IllegalArgumentException( + "Minimum dimension size for resizing should should be >= 1"); + } + + // First we'll scale down the bitmap so it's largest dimension is as specified + final Bitmap scaledBitmap = scaleBitmapDown(mBitmap, mResizeMaxDimension); + + if (logger != null) { + logger.addSplit("Processed Bitmap"); + } + + // Now generate a quantizer from the Bitmap + final int width = scaledBitmap.getWidth(); + final int height = scaledBitmap.getHeight(); + final int[] pixels = new int[width * height]; + scaledBitmap.getPixels(pixels, 0, width, 0, 0, width, height); + + final ColorCutQuantizer quantizer = new ColorCutQuantizer(pixels, mMaxColors, + mFilters.isEmpty() ? null : mFilters.toArray(new Filter[mFilters.size()])); + + // If created a new bitmap, recycle it + if (scaledBitmap != mBitmap) { + scaledBitmap.recycle(); + } + swatches = quantizer.getQuantizedColors(); + + if (logger != null) { + logger.addSplit("Color quantization completed"); + } + } else { + // Else we're using the provided swatches + swatches = mSwatches; + } + + // If we haven't been provided with a generator, use the default + if (mGenerator == null) { + mGenerator = new DefaultGenerator(); + } + + // Now call let the Generator do it's thing + mGenerator.generate(swatches); + + if (logger != null) { + logger.addSplit("Generator.generate() completed"); + } + + // Now create a Palette instance + Palette p = new Palette(swatches, mGenerator); + + if (logger != null) { + logger.addSplit("Created Palette"); + logger.dumpToLog(); + } + + return p; + } + + /** + * Generate the {@link Palette} asynchronously. The provided listener's + * {@link PaletteAsyncListener#onGenerated} method will be called with the palette when + * generated. + */ + public AsyncTask<Bitmap, Void, Palette> generate(final PaletteAsyncListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener can not be null"); + } + + AsyncTask<Bitmap, Void, Palette> task = new AsyncTask<Bitmap, Void, Palette>() { + @Override + protected Palette doInBackground(Bitmap... params) { + return generate(); + } + + @Override + protected void onPostExecute(Palette colorExtractor) { + listener.onGenerated(colorExtractor); + } + }; + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mBitmap); + return task; + } + } + + static abstract class Generator { + + /** + * This method will be called with the {@link Palette.Swatch} that represent an image. + * You should process this list so that you have appropriate values when the other methods in + * class are called. + * <p> + * This method will probably be called on a background thread. + */ + public abstract void generate(List<Palette.Swatch> swatches); + + /** + * Return the most vibrant {@link Palette.Swatch} + */ + public Palette.Swatch getVibrantSwatch() { + return null; + } + + /** + * Return a light and vibrant {@link Palette.Swatch} + */ + public Palette.Swatch getLightVibrantSwatch() { + return null; + } + + /** + * Return a dark and vibrant {@link Palette.Swatch} + */ + public Palette.Swatch getDarkVibrantSwatch() { + return null; + } + + /** + * Return a muted {@link Palette.Swatch} + */ + public Palette.Swatch getMutedSwatch() { + return null; + } + + /** + * Return a muted and light {@link Palette.Swatch} + */ + public Palette.Swatch getLightMutedSwatch() { + return null; + } + + /** + * Return a muted and dark {@link Palette.Swatch} + */ + public Palette.Swatch getDarkMutedSwatch() { + return null; + } + } + + /** + * A Filter provides a mechanism for exercising fine-grained control over which colors + * are valid within a resulting {@link Palette}. + */ + public interface Filter { + /** + * Hook to allow clients to be able filter colors from resulting palette. + * + * @param rgb the color in RGB888. + * @param hsl HSL representation of the color. + * + * @return true if the color is allowed, false if not. + * + * @see Builder#addFilter(Filter) + */ + boolean isAllowed(int rgb, float[] hsl); + } + + /** + * The default filter. + */ + private static final Filter DEFAULT_FILTER = new Filter() { + private static final float BLACK_MAX_LIGHTNESS = 0.05f; + private static final float WHITE_MIN_LIGHTNESS = 0.95f; + + @Override + public boolean isAllowed(int rgb, float[] hsl) { + return !isWhite(hsl) && !isBlack(hsl) && !isNearRedILine(hsl); + } + + /** + * @return true if the color represents a color which is close to black. + */ + private boolean isBlack(float[] hslColor) { + return hslColor[2] <= BLACK_MAX_LIGHTNESS; + } + + /** + * @return true if the color represents a color which is close to white. + */ + private boolean isWhite(float[] hslColor) { + return hslColor[2] >= WHITE_MIN_LIGHTNESS; + } + + /** + * @return true if the color lies close to the red side of the I line. + */ + private boolean isNearRedILine(float[] hslColor) { + return hslColor[0] >= 10f && hslColor[0] <= 37f && hslColor[1] <= 0.82f; + } + }; } |