Add new extensible API to Palette
The buckets are now abstracted into a Target
class, with an associated Builder to allow the
creation of custom targets.
Change-Id: I654a081cef0e836cb867098fc9634eaca5aaf37b
diff --git a/v7/palette/api/current.txt b/v7/palette/api/current.txt
index fac6a55..3b24b77 100644
--- a/v7/palette/api/current.txt
+++ b/v7/palette/api/current.txt
@@ -7,6 +7,7 @@
method public static deprecated android.support.v7.graphics.Palette generate(android.graphics.Bitmap, int);
method public static deprecated android.os.AsyncTask<android.graphics.Bitmap, java.lang.Void, android.support.v7.graphics.Palette> generateAsync(android.graphics.Bitmap, android.support.v7.graphics.Palette.PaletteAsyncListener);
method public static deprecated android.os.AsyncTask<android.graphics.Bitmap, java.lang.Void, android.support.v7.graphics.Palette> generateAsync(android.graphics.Bitmap, int, android.support.v7.graphics.Palette.PaletteAsyncListener);
+ method public int getColorForTarget(android.support.v7.graphics.Target, int);
method public int getDarkMutedColor(int);
method public android.support.v7.graphics.Palette.Swatch getDarkMutedSwatch();
method public int getDarkVibrantColor(int);
@@ -17,7 +18,9 @@
method public android.support.v7.graphics.Palette.Swatch getLightVibrantSwatch();
method public int getMutedColor(int);
method public android.support.v7.graphics.Palette.Swatch getMutedSwatch();
+ method public android.support.v7.graphics.Palette.Swatch getSwatchForTarget(android.support.v7.graphics.Target);
method public java.util.List<android.support.v7.graphics.Palette.Swatch> getSwatches();
+ method public java.util.List<android.support.v7.graphics.Target> getTargets();
method public int getVibrantColor(int);
method public android.support.v7.graphics.Palette.Swatch getVibrantSwatch();
}
@@ -26,12 +29,15 @@
ctor public Palette.Builder(android.graphics.Bitmap);
ctor public Palette.Builder(java.util.List<android.support.v7.graphics.Palette.Swatch>);
method public android.support.v7.graphics.Palette.Builder addFilter(android.support.v7.graphics.Palette.Filter);
+ method public android.support.v7.graphics.Palette.Builder addTarget(android.support.v7.graphics.Target);
method public android.support.v7.graphics.Palette.Builder clearFilters();
method public android.support.v7.graphics.Palette.Builder clearRegion();
+ method public android.support.v7.graphics.Palette.Builder clearTargets();
method public android.support.v7.graphics.Palette generate();
method public android.os.AsyncTask<android.graphics.Bitmap, java.lang.Void, android.support.v7.graphics.Palette> generate(android.support.v7.graphics.Palette.PaletteAsyncListener);
method public android.support.v7.graphics.Palette.Builder maximumColorCount(int);
- method public android.support.v7.graphics.Palette.Builder resizeBitmapSize(int);
+ method public android.support.v7.graphics.Palette.Builder resizeBitmapArea(int);
+ method public deprecated android.support.v7.graphics.Palette.Builder resizeBitmapSize(int);
method public android.support.v7.graphics.Palette.Builder setRegion(int, int, int, int);
}
@@ -52,5 +58,40 @@
method public int getTitleTextColor();
}
+ public final class Target {
+ method public float getLightnessWeight();
+ method public float getMaximumLightness();
+ method public float getMaximumSaturation();
+ method public float getMinimumLightness();
+ method public float getMinimumSaturation();
+ method public float getPopulationWeight();
+ method public float getSaturationWeight();
+ method public float getTargetLightness();
+ method public float getTargetSaturation();
+ method public boolean isExclusive();
+ field public static final android.support.v7.graphics.Target DARK_MUTED;
+ field public static final android.support.v7.graphics.Target DARK_VIBRANT;
+ field public static final android.support.v7.graphics.Target LIGHT_MUTED;
+ field public static final android.support.v7.graphics.Target LIGHT_VIBRANT;
+ field public static final android.support.v7.graphics.Target MUTED;
+ field public static final android.support.v7.graphics.Target VIBRANT;
+ }
+
+ public static final class Target.Builder {
+ ctor public Target.Builder();
+ ctor public Target.Builder(android.support.v7.graphics.Target);
+ method public android.support.v7.graphics.Target build();
+ method public android.support.v7.graphics.Target.Builder setExclusive(boolean);
+ method public android.support.v7.graphics.Target.Builder setLightnessWeight(float);
+ method public android.support.v7.graphics.Target.Builder setMaximumLightness(float);
+ method public android.support.v7.graphics.Target.Builder setMaximumSaturation(float);
+ method public android.support.v7.graphics.Target.Builder setMinimumLightness(float);
+ method public android.support.v7.graphics.Target.Builder setMinimumSaturation(float);
+ method public android.support.v7.graphics.Target.Builder setPopulationWeight(float);
+ method public android.support.v7.graphics.Target.Builder setSaturationWeight(float);
+ method public android.support.v7.graphics.Target.Builder setTargetLightness(float);
+ method public android.support.v7.graphics.Target.Builder setTargetSaturation(float);
+ }
+
}
diff --git a/v7/palette/src/main/java/android/support/v7/graphics/DefaultGenerator.java b/v7/palette/src/main/java/android/support/v7/graphics/DefaultGenerator.java
deleted file mode 100644
index 3ee2bfa..0000000
--- a/v7/palette/src/main/java/android/support/v7/graphics/DefaultGenerator.java
+++ /dev/null
@@ -1,242 +0,0 @@
-/*
- * Copyright 2015 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 android.support.v7.graphics;
-
-import android.support.v4.graphics.ColorUtils;
-import android.support.v7.graphics.Palette.Swatch;
-
-import java.util.List;
-
-class DefaultGenerator extends Palette.Generator {
-
- 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 List<Swatch> mSwatches;
-
- private int mHighestPopulation;
-
- private Swatch mVibrantSwatch;
- private Swatch mMutedSwatch;
- private Swatch mDarkVibrantSwatch;
- private Swatch mDarkMutedSwatch;
- private Swatch mLightVibrantSwatch;
- private Swatch mLightMutedSwatch;
-
- @Override
- public void generate(final List<Swatch> swatches) {
- mSwatches = swatches;
-
- mHighestPopulation = findMaxPopulation();
-
- generateVariationColors();
-
- // Now try and generate any missing colors
- generateEmptySwatches();
- }
-
- @Override
- public Swatch getVibrantSwatch() {
- return mVibrantSwatch;
- }
-
- @Override
- public Swatch getLightVibrantSwatch() {
- return mLightVibrantSwatch;
- }
-
- @Override
- public Swatch getDarkVibrantSwatch() {
- return mDarkVibrantSwatch;
- }
-
- @Override
- public Swatch getMutedSwatch() {
- return mMutedSwatch;
- }
-
- @Override
- public Swatch getLightMutedSwatch() {
- return mLightMutedSwatch;
- }
-
- @Override
- public Swatch getDarkMutedSwatch() {
- return mDarkMutedSwatch;
- }
-
- private void generateVariationColors() {
- mVibrantSwatch = findColorVariation(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA,
- TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f);
-
- mLightVibrantSwatch = findColorVariation(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1f,
- TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f);
-
- mDarkVibrantSwatch = findColorVariation(TARGET_DARK_LUMA, 0f, MAX_DARK_LUMA,
- TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f);
-
- mMutedSwatch = findColorVariation(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA,
- TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION);
-
- mLightMutedSwatch = findColorVariation(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1f,
- TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION);
-
- mDarkMutedSwatch = findColorVariation(TARGET_DARK_LUMA, 0f, MAX_DARK_LUMA,
- TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION);
- }
-
- /**
- * 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.HSLToColor(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.HSLToColor(newHsl), 0);
- }
- }
- }
-
- /**
- * Find the {@link Palette.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;
- }
-
- private Swatch findColorVariation(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 value = createComparisonValue(sat, targetSaturation, luma, targetLuma,
- swatch.getPopulation(), mHighestPopulation);
- if (max == null || value > maxValue) {
- max = swatch;
- maxValue = value;
- }
- }
- }
-
- return max;
- }
-
- /**
- * @return true if we have already selected {@code swatch}
- */
- private boolean isAlreadySelected(Swatch swatch) {
- return mVibrantSwatch == swatch || mDarkVibrantSwatch == swatch ||
- mLightVibrantSwatch == swatch || mMutedSwatch == swatch ||
- mDarkMutedSwatch == swatch || mLightMutedSwatch == swatch;
- }
-
- private static float createComparisonValue(float saturation, float targetSaturation,
- float luma, float targetLuma,
- int population, int maxPopulation) {
- return createComparisonValue(saturation, targetSaturation, WEIGHT_SATURATION,
- luma, targetLuma, WEIGHT_LUMA,
- population, maxPopulation, WEIGHT_POPULATION);
- }
-
- private static float createComparisonValue(
- float saturation, float targetSaturation, float saturationWeight,
- float luma, float targetLuma, float lumaWeight,
- int population, int maxPopulation, float populationWeight) {
- return weightedMean(
- invertDiff(saturation, targetSaturation), saturationWeight,
- invertDiff(luma, targetLuma), lumaWeight,
- population / (float) maxPopulation, populationWeight
- );
- }
-
- /**
- * 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;
- }
-}
diff --git a/v7/palette/src/main/java/android/support/v7/graphics/Palette.java b/v7/palette/src/main/java/android/support/v7/graphics/Palette.java
index 57f34a6..d0dff30 100644
--- a/v7/palette/src/main/java/android/support/v7/graphics/Palette.java
+++ b/v7/palette/src/main/java/android/support/v7/graphics/Palette.java
@@ -21,16 +21,20 @@
import android.graphics.Rect;
import android.os.AsyncTask;
import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.graphics.ColorUtils;
import android.support.v4.os.AsyncTaskCompat;
+import android.support.v4.util.ArrayMap;
import android.util.Log;
+import android.util.SparseBooleanArray;
import android.util.TimingLogger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
/**
* A helper class to extract prominent colors from an image.
@@ -80,7 +84,7 @@
void onGenerated(Palette palette);
}
- private static final int DEFAULT_RESIZE_BITMAP_MAX_DIMENSION = 192;
+ private static final int DEFAULT_RESIZE_BITMAP_AREA = 160 * 160;
private static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16;
private static final float MIN_CONTRAST_TITLE_TEXT = 3.0f;
@@ -140,151 +144,266 @@
}
private final List<Swatch> mSwatches;
- private final Generator mGenerator;
+ private final List<Target> mTargets;
- private Palette(List<Swatch> swatches, Generator generator) {
+ private final Map<Target, Swatch> mSelectedSwatches;
+ private final SparseBooleanArray mUsedColors;
+
+ private final int mMaxPopulation;
+
+ private Palette(List<Swatch> swatches, List<Target> targets) {
mSwatches = swatches;
- mGenerator = generator;
+ mTargets = targets;
+
+ mUsedColors = new SparseBooleanArray();
+ mSelectedSwatches = new ArrayMap<>();
+
+ mMaxPopulation = findMaxPopulation();
}
/**
* Returns all of the swatches which make up the palette.
*/
+ @NonNull
public List<Swatch> getSwatches() {
return Collections.unmodifiableList(mSwatches);
}
/**
+ * Returns the targets used to generate this palette.
+ */
+ @NonNull
+ public List<Target> getTargets() {
+ return Collections.unmodifiableList(mTargets);
+ }
+
+ /**
* Returns the most vibrant swatch in the palette. Might be null.
+ *
+ * @see Target#VIBRANT
*/
@Nullable
public Swatch getVibrantSwatch() {
- return mGenerator.getVibrantSwatch();
+ return getSwatchForTarget(Target.VIBRANT);
}
/**
* Returns a light and vibrant swatch from the palette. Might be null.
+ *
+ * @see Target#LIGHT_VIBRANT
*/
@Nullable
public Swatch getLightVibrantSwatch() {
- return mGenerator.getLightVibrantSwatch();
+ return getSwatchForTarget(Target.LIGHT_VIBRANT);
}
/**
* Returns a dark and vibrant swatch from the palette. Might be null.
+ *
+ * @see Target#DARK_VIBRANT
*/
@Nullable
public Swatch getDarkVibrantSwatch() {
- return mGenerator.getDarkVibrantSwatch();
+ return getSwatchForTarget(Target.DARK_VIBRANT);
}
/**
* Returns a muted swatch from the palette. Might be null.
+ *
+ * @see Target#MUTED
*/
@Nullable
public Swatch getMutedSwatch() {
- return mGenerator.getMutedSwatch();
+ return getSwatchForTarget(Target.MUTED);
}
/**
* Returns a muted and light swatch from the palette. Might be null.
+ *
+ * @see Target#LIGHT_MUTED
*/
@Nullable
public Swatch getLightMutedSwatch() {
- return mGenerator.getLightMutedSwatch();
+ return getSwatchForTarget(Target.LIGHT_MUTED);
}
/**
* Returns a muted and dark swatch from the palette. Might be null.
+ *
+ * @see Target#DARK_MUTED
*/
@Nullable
public Swatch getDarkMutedSwatch() {
- return mGenerator.getDarkMutedSwatch();
+ return getSwatchForTarget(Target.DARK_MUTED);
}
/**
* Returns the most vibrant color in the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
+ * @see #getVibrantSwatch()
*/
@ColorInt
- public int getVibrantColor(@ColorInt int defaultColor) {
- Swatch swatch = getVibrantSwatch();
- return swatch != null ? swatch.getRgb() : defaultColor;
+ public int getVibrantColor(@ColorInt final int defaultColor) {
+ return getColorForTarget(Target.VIBRANT, defaultColor);
}
/**
* Returns a light and vibrant color from the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
+ * @see #getLightVibrantSwatch()
*/
@ColorInt
- public int getLightVibrantColor(@ColorInt int defaultColor) {
- Swatch swatch = getLightVibrantSwatch();
- return swatch != null ? swatch.getRgb() : defaultColor;
+ public int getLightVibrantColor(@ColorInt final int defaultColor) {
+ return getColorForTarget(Target.LIGHT_VIBRANT, defaultColor);
}
/**
* Returns a dark and vibrant color from the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
+ * @see #getDarkVibrantSwatch()
*/
@ColorInt
- public int getDarkVibrantColor(@ColorInt int defaultColor) {
- Swatch swatch = getDarkVibrantSwatch();
- return swatch != null ? swatch.getRgb() : defaultColor;
+ public int getDarkVibrantColor(@ColorInt final int defaultColor) {
+ return getColorForTarget(Target.DARK_VIBRANT, defaultColor);
}
/**
* Returns a muted color from the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
+ * @see #getMutedSwatch()
*/
@ColorInt
- public int getMutedColor(@ColorInt int defaultColor) {
- Swatch swatch = getMutedSwatch();
- return swatch != null ? swatch.getRgb() : defaultColor;
+ public int getMutedColor(@ColorInt final int defaultColor) {
+ return getColorForTarget(Target.MUTED, defaultColor);
}
/**
* Returns a muted and light color from the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
+ * @see #getLightMutedSwatch()
*/
@ColorInt
- public int getLightMutedColor(@ColorInt int defaultColor) {
- Swatch swatch = getLightMutedSwatch();
- return swatch != null ? swatch.getRgb() : defaultColor;
+ public int getLightMutedColor(@ColorInt final int defaultColor) {
+ return getColorForTarget(Target.LIGHT_MUTED, defaultColor);
}
/**
* Returns a muted and dark color from the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
+ * @see #getDarkMutedSwatch()
*/
@ColorInt
- public int getDarkMutedColor(@ColorInt int defaultColor) {
- Swatch swatch = getDarkMutedSwatch();
- return swatch != null ? swatch.getRgb() : defaultColor;
+ public int getDarkMutedColor(@ColorInt final int defaultColor) {
+ return getColorForTarget(Target.DARK_MUTED, defaultColor);
}
/**
- * Scale the bitmap down so that it's largest dimension is {@code targetMaxDimension}.
- * If {@code bitmap} is smaller than this, then it is returned.
+ * Returns the selected swatch for the given target from the palette, or {@code null} if one
+ * could not be found.
*/
- private static Bitmap scaleBitmapDown(Bitmap bitmap, final int targetMaxDimension) {
- final int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+ @Nullable
+ public Swatch getSwatchForTarget(@NonNull final Target target) {
+ return mSelectedSwatches.get(target);
+ }
- if (maxDimension <= targetMaxDimension) {
- // If the bitmap is small enough already, just return it
- return bitmap;
+ /**
+ * Returns the selected color for the given target from the palette as an RGB packed int.
+ *
+ * @param defaultColor value to return if the swatch isn't available
+ */
+ @ColorInt
+ public int getColorForTarget(@NonNull final Target target, @ColorInt final int defaultColor) {
+ Swatch swatch = getSwatchForTarget(target);
+ return swatch != null ? swatch.getRgb() : defaultColor;
+ }
+
+ private 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 Swatch generateScoredTarget(final Target target) {
+ final 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 Swatch getMaxScoredSwatchForTarget(final Target target) {
+ float maxScore = 0;
+ Swatch maxScoreSwatch = null;
+ for (int i = 0, count = mSwatches.size(); i < count; i++) {
+ final 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 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(Swatch swatch, Target target) {
+ final float[] hsl = swatch.getHsl();
+
+ float saturationScore = 0;
+ float luminanceScore = 0;
+ float populationScore = 0;
+
+ 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) mMaxPopulation);
}
- final double scaleRatio = targetMaxDimension / (double) maxDimension;
- return Bitmap.createScaledBitmap(bitmap,
- (int) Math.ceil(bitmap.getWidth() * scaleRatio),
- (int) Math.ceil(bitmap.getHeight() * scaleRatio),
- false);
+ return saturationScore + luminanceScore + populationScore;
+ }
+
+ private int findMaxPopulation() {
+ int max = 0;
+ for (int i = 0, count = mSwatches.size(); i < count; i++) {
+ max = Math.max(mSwatches.get(i).getPopulation(), max);
+ }
+ return max;
+ }
+
+ private static float[] copyHslValues(Swatch color) {
+ final float[] newHsl = new float[3];
+ System.arraycopy(color.getHsl(), 0, newHsl, 0, 3);
+ return newHsl;
}
/**
@@ -318,6 +437,11 @@
mPopulation = population;
}
+ Swatch(float[] hsl, int population) {
+ this(ColorUtils.HSLToColor(hsl), population);
+ mHsl = hsl;
+ }
+
/**
* @return this swatch's RGB color value
*/
@@ -335,8 +459,8 @@
public float[] getHsl() {
if (mHsl == null) {
mHsl = new float[3];
- ColorUtils.RGBToHSL(mRed, mGreen, mBlue, mHsl);
}
+ ColorUtils.RGBToHSL(mRed, mGreen, mBlue, mHsl);
return mHsl;
}
@@ -445,13 +569,16 @@
public static final class Builder {
private final List<Swatch> mSwatches;
private final Bitmap mBitmap;
+
+ private final List<Target> mTargets = new ArrayList<>();
+
private int mMaxColors = DEFAULT_CALCULATE_NUMBER_COLORS;
- private int mResizeMaxDimension = DEFAULT_RESIZE_BITMAP_MAX_DIMENSION;
+ private int mResizeArea = DEFAULT_RESIZE_BITMAP_AREA;
+ private int mResizeMaxDimension = -1;
+
private final List<Filter> mFilters = new ArrayList<>();
private Rect mRegion;
- private Generator mGenerator;
-
/**
* Construct a new {@link Builder} using a source {@link Bitmap}
*/
@@ -462,6 +589,14 @@
mFilters.add(DEFAULT_FILTER);
mBitmap = bitmap;
mSwatches = null;
+
+ // Add the default targets
+ mTargets.add(Target.LIGHT_VIBRANT);
+ mTargets.add(Target.VIBRANT);
+ mTargets.add(Target.DARK_VIBRANT);
+ mTargets.add(Target.LIGHT_MUTED);
+ mTargets.add(Target.MUTED);
+ mTargets.add(Target.DARK_MUTED);
}
/**
@@ -478,15 +613,6 @@
}
/**
- * 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>
@@ -494,6 +620,7 @@
* 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 Builder maximumColorCount(int colors) {
mMaxColors = colors;
return this;
@@ -504,13 +631,38 @@
* 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.
+ *
+ * @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 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 it's area matches {@code area}. 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.
+ *
+ * @param area the number of pixels that the intemediary scaled down Bitmap should cover,
+ * or any value <= 0 to disable resizing.
*/
- public Builder resizeBitmapSize(int maxDimension) {
- mResizeMaxDimension = maxDimension;
+ @NonNull
+ public Builder resizeBitmapArea(final int area) {
+ mResizeArea = area;
+ mResizeMaxDimension = -1;
return this;
}
@@ -518,6 +670,7 @@
* Clear all added filters. This includes any default filters added automatically by
* {@link Palette}.
*/
+ @NonNull
public Builder clearFilters() {
mFilters.clear();
return this;
@@ -529,6 +682,7 @@
*
* @param filter filter to add.
*/
+ @NonNull
public Builder addFilter(Filter filter) {
if (filter != null) {
mFilters.add(filter);
@@ -545,6 +699,7 @@
* @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 Builder setRegion(int left, int top, int right, int bottom) {
if (mBitmap != null) {
if (mRegion == null) mRegion = new Rect();
@@ -562,14 +717,41 @@
/**
* Clear any previously region set via {@link #setRegion(int, int, int, int)}.
*/
+ @NonNull
public Builder clearRegion() {
mRegion = null;
return this;
}
/**
+ * Add a target profile to be generated in the palette.
+ *
+ * <p>You can retrieve the result via {@link Palette#getSwatchForTarget(Target)}.</p>
+ */
+ @NonNull
+ public 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 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")
@@ -578,15 +760,10 @@
List<Swatch> swatches;
if (mBitmap != null) {
- // We have a Bitmap so we need to quantization to reduce the number of colors
+ // We have a Bitmap so we need to use 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 bitmap = scaleBitmapDown(mBitmap, mResizeMaxDimension);
+ // First we'll scale down the bitmap if needed
+ final Bitmap bitmap = scaleBitmapDown(mBitmap);
if (logger != null) {
logger.addSplit("Processed Bitmap");
@@ -613,6 +790,7 @@
if (bitmap != mBitmap) {
bitmap.recycle();
}
+
swatches = quantizer.getQuantizedColors();
if (logger != null) {
@@ -623,20 +801,10 @@
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);
+ final Palette p = new Palette(swatches, mTargets);
+ // And make it generate itself
+ p.generate();
if (logger != null) {
logger.addSplit("Created Palette");
@@ -651,6 +819,7 @@
* {@link PaletteAsyncListener#onGenerated} method will be called with the palette when
* generated.
*/
+ @NonNull
public AsyncTask<Bitmap, Void, Palette> generate(final PaletteAsyncListener listener) {
if (listener == null) {
throw new IllegalArgumentException("listener can not be null");
@@ -702,59 +871,34 @@
return subsetPixels;
}
}
- }
-
- 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.
+ * Scale the bitmap down as needed.
*/
- public abstract void generate(List<Palette.Swatch> swatches);
+ private Bitmap scaleBitmapDown(final Bitmap bitmap) {
+ double scaleRatio = -1;
- /**
- * Return the most vibrant {@link Palette.Swatch}
- */
- public Palette.Swatch getVibrantSwatch() {
- return null;
- }
+ if (mResizeArea > 0) {
+ final int bitmapArea = bitmap.getWidth() * bitmap.getHeight();
+ if (bitmapArea > mResizeArea) {
+ scaleRatio = mResizeArea / (double) bitmapArea;
+ }
+ } else if (mResizeMaxDimension > 0) {
+ final int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+ if (maxDimension > mResizeMaxDimension) {
+ scaleRatio = mResizeMaxDimension / (double) maxDimension;
+ }
+ }
- /**
- * Return a light and vibrant {@link Palette.Swatch}
- */
- public Palette.Swatch getLightVibrantSwatch() {
- return null;
- }
+ if (scaleRatio <= 0) {
+ // Scaling has been disabled or not needed so just return the Bitmap
+ return bitmap;
+ }
- /**
- * 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;
+ return Bitmap.createScaledBitmap(bitmap,
+ (int) Math.ceil(bitmap.getWidth() * scaleRatio),
+ (int) Math.ceil(bitmap.getHeight() * scaleRatio),
+ false);
}
}
diff --git a/v7/palette/src/main/java/android/support/v7/graphics/Target.java b/v7/palette/src/main/java/android/support/v7/graphics/Target.java
new file mode 100644
index 0000000..8ac8205
--- /dev/null
+++ b/v7/palette/src/main/java/android/support/v7/graphics/Target.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright 2015 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 android.support.v7.graphics;
+
+import android.support.annotation.FloatRange;
+
+/**
+ * A class which allows custom selection of colors in a {@link Palette}'s generation. Instances
+ * can be created via the {@link Builder} class.
+ *
+ * <p>To use the target, use the {@link Palette.Builder#addTarget(Target)} API when building a
+ * Palette.</p>
+ */
+public final class Target {
+
+ 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 = 0.24f;
+ private static final float WEIGHT_LUMA = 0.52f;
+ private static final float WEIGHT_POPULATION = 0.24f;
+
+ private static final int INDEX_MIN = 0;
+ private static final int INDEX_TARGET = 1;
+ private static final int INDEX_MAX = 2;
+
+ private static final int INDEX_WEIGHT_SAT = 0;
+ private static final int INDEX_WEIGHT_LUMA = 1;
+ private static final int INDEX_WEIGHT_POP = 2;
+
+ /**
+ * A target which has the characteristics of a vibrant color which is light in luminance.
+ */
+ public static final Target LIGHT_VIBRANT;
+
+ /**
+ * A target which has the characteristics of a vibrant color which is neither light or dark.
+ */
+ public static final Target VIBRANT;
+
+ /**
+ * A target which has the characteristics of a vibrant color which is dark in luminance.
+ */
+ public static final Target DARK_VIBRANT;
+
+ /**
+ * A target which has the characteristics of a muted color which is light in luminance.
+ */
+ public static final Target LIGHT_MUTED;
+
+ /**
+ * A target which has the characteristics of a muted color which is neither light or dark.
+ */
+ public static final Target MUTED;
+
+ /**
+ * A target which has the characteristics of a muted color which is dark in luminance.
+ */
+ public static final Target DARK_MUTED;
+
+ static {
+ LIGHT_VIBRANT = new Target();
+ setDefaultLightLightnessValues(LIGHT_VIBRANT);
+ setDefaultVibrantSaturationValues(LIGHT_VIBRANT);
+
+ VIBRANT = new Target();
+ setDefaultNormalLightnessValues(VIBRANT);
+ setDefaultVibrantSaturationValues(VIBRANT);
+
+ DARK_VIBRANT = new Target();
+ setDefaultDarkLightnessValues(DARK_VIBRANT);
+ setDefaultVibrantSaturationValues(DARK_VIBRANT);
+
+ LIGHT_MUTED = new Target();
+ setDefaultLightLightnessValues(LIGHT_MUTED);
+ setDefaultMutedSaturationValues(LIGHT_MUTED);
+
+ MUTED = new Target();
+ setDefaultNormalLightnessValues(MUTED);
+ setDefaultMutedSaturationValues(MUTED);
+
+ DARK_MUTED = new Target();
+ setDefaultDarkLightnessValues(DARK_MUTED);
+ setDefaultMutedSaturationValues(DARK_MUTED);
+ }
+
+ private final float[] mSaturationTargets = new float[3];
+ private final float[] mLightnessTargets = new float[3];
+ private final float[] mWeights = new float[3];
+ private boolean mIsExclusive = true; // default to true
+
+ private Target() {
+ setTargetDefaultValues(mSaturationTargets);
+ setTargetDefaultValues(mLightnessTargets);
+ setDefaultWeights();
+ }
+
+ private Target(Target from) {
+ System.arraycopy(from.mSaturationTargets, 0, mSaturationTargets, 0,
+ mSaturationTargets.length);
+ System.arraycopy(from.mLightnessTargets, 0, mLightnessTargets, 0,
+ mLightnessTargets.length);
+ System.arraycopy(from.mWeights, 0, mWeights, 0, mWeights.length);
+ }
+
+ /**
+ * The minimum saturation value for this target.
+ */
+ @FloatRange(from = 0, to = 1)
+ public float getMinimumSaturation() {
+ return mSaturationTargets[INDEX_MIN];
+ }
+
+ /**
+ * The target saturation value for this target.
+ */
+ @FloatRange(from = 0, to = 1)
+ public float getTargetSaturation() {
+ return mSaturationTargets[INDEX_TARGET];
+ }
+
+ /**
+ * The maximum saturation value for this target.
+ */
+ @FloatRange(from = 0, to = 1)
+ public float getMaximumSaturation() {
+ return mSaturationTargets[INDEX_MAX];
+ }
+
+ /**
+ * The minimum lightness value for this target.
+ */
+ @FloatRange(from = 0, to = 1)
+ public float getMinimumLightness() {
+ return mLightnessTargets[INDEX_MIN];
+ }
+
+ /**
+ * The target lightness value for this target.
+ */
+ @FloatRange(from = 0, to = 1)
+ public float getTargetLightness() {
+ return mLightnessTargets[INDEX_TARGET];
+ }
+
+ /**
+ * The maximum lightness value for this target.
+ */
+ @FloatRange(from = 0, to = 1)
+ public float getMaximumLightness() {
+ return mLightnessTargets[INDEX_MAX];
+ }
+
+ /**
+ * The weight of important that a color's saturation value has on selection.
+ */
+ public float getSaturationWeight() {
+ return mWeights[INDEX_WEIGHT_SAT];
+ }
+
+ /**
+ * The weight of important that a color's lightness value has on selection.
+ */
+ public float getLightnessWeight() {
+ return mWeights[INDEX_WEIGHT_LUMA];
+ }
+
+ /**
+ * The weight of important that a color's population value has on selection.
+ */
+ public float getPopulationWeight() {
+ return mWeights[INDEX_WEIGHT_POP];
+ }
+
+ /**
+ * Returns whether any color selected for this target is exclusive for this target only.
+ *
+ * <p>If false, then the color can be selected for other targets.</p>
+ */
+ public boolean isExclusive() {
+ return mIsExclusive;
+ }
+
+ private static void setTargetDefaultValues(final float[] values) {
+ values[INDEX_MIN] = 0f;
+ values[INDEX_TARGET] = 0.5f;
+ values[INDEX_MAX] = 1f;
+ }
+
+ private void setDefaultWeights() {
+ mWeights[INDEX_WEIGHT_SAT] = WEIGHT_SATURATION;
+ mWeights[INDEX_WEIGHT_LUMA] = WEIGHT_LUMA;
+ mWeights[INDEX_WEIGHT_POP] = WEIGHT_POPULATION;
+ }
+
+ void normalizeWeights() {
+ float sum = 0;
+ for (int i = 0, z = mWeights.length; i < z; i++) {
+ float weight = mWeights[i];
+ if (weight > 0) {
+ sum += weight;
+ }
+ }
+ if (sum != 0) {
+ for (int i = 0, z = mWeights.length; i < z; i++) {
+ if (mWeights[i] > 0) {
+ mWeights[i] /= sum;
+ }
+ }
+ }
+ }
+
+ private static void setDefaultDarkLightnessValues(Target target) {
+ target.mLightnessTargets[INDEX_TARGET] = TARGET_DARK_LUMA;
+ target.mLightnessTargets[INDEX_MAX] = MAX_DARK_LUMA;
+ }
+
+ private static void setDefaultNormalLightnessValues(Target target) {
+ target.mLightnessTargets[INDEX_MIN] = MIN_NORMAL_LUMA;
+ target.mLightnessTargets[INDEX_TARGET] = TARGET_NORMAL_LUMA;
+ target.mLightnessTargets[INDEX_MAX] = MAX_NORMAL_LUMA;
+ }
+
+ private static void setDefaultLightLightnessValues(Target target) {
+ target.mLightnessTargets[INDEX_MIN] = MIN_LIGHT_LUMA;
+ target.mLightnessTargets[INDEX_TARGET] = TARGET_LIGHT_LUMA;
+ }
+
+ private static void setDefaultVibrantSaturationValues(Target target) {
+ target.mSaturationTargets[INDEX_MIN] = MIN_VIBRANT_SATURATION;
+ target.mSaturationTargets[INDEX_TARGET] = TARGET_VIBRANT_SATURATION;
+ }
+
+ private static void setDefaultMutedSaturationValues(Target target) {
+ target.mSaturationTargets[INDEX_TARGET] = TARGET_MUTED_SATURATION;
+ target.mSaturationTargets[INDEX_MAX] = MAX_MUTED_SATURATION;
+ }
+
+ /**
+ * Builder class for generating custom {@link Target} instances.
+ */
+ public final static class Builder {
+ private final Target mTarget;
+
+ /**
+ * Create a new {@link Target} builder from scratch.
+ */
+ public Builder() {
+ mTarget = new Target();
+ }
+
+ /**
+ * Create a new builder based on an existing {@link Target}.
+ */
+ public Builder(Target target) {
+ mTarget = new Target(target);
+ }
+
+ /**
+ * Set the minimum saturation value for this target.
+ */
+ public Builder setMinimumSaturation(@FloatRange(from = 0, to = 1) float value) {
+ mTarget.mSaturationTargets[INDEX_MIN] = value;
+ return this;
+ }
+
+ /**
+ * Set the target/ideal saturation value for this target.
+ */
+ public Builder setTargetSaturation(@FloatRange(from = 0, to = 1) float value) {
+ mTarget.mSaturationTargets[INDEX_TARGET] = value;
+ return this;
+ }
+
+ /**
+ * Set the maximum saturation value for this target.
+ */
+ public Builder setMaximumSaturation(@FloatRange(from = 0, to = 1) float value) {
+ mTarget.mSaturationTargets[INDEX_MAX] = value;
+ return this;
+ }
+
+ /**
+ * Set the minimum lightness value for this target.
+ */
+ public Builder setMinimumLightness(@FloatRange(from = 0, to = 1) float value) {
+ mTarget.mLightnessTargets[INDEX_MIN] = value;
+ return this;
+ }
+
+ /**
+ * Set the target/ideal lightness value for this target.
+ */
+ public Builder setTargetLightness(@FloatRange(from = 0, to = 1) float value) {
+ mTarget.mLightnessTargets[INDEX_TARGET] = value;
+ return this;
+ }
+
+ /**
+ * Set the maximum lightness value for this target.
+ */
+ public Builder setMaximumLightness(@FloatRange(from = 0, to = 1) float value) {
+ mTarget.mLightnessTargets[INDEX_MAX] = value;
+ return this;
+ }
+
+ /**
+ * Set the weight of important that a color's saturation value has on selection. A weight
+ * of <= 0 means that it has no weight and is ignored.
+ */
+ public Builder setSaturationWeight(@FloatRange(from = 0) float weight) {
+ mTarget.mWeights[INDEX_WEIGHT_SAT] = weight;
+ return this;
+ }
+
+ /**
+ * Set the weight of important that a color's lightness value has on selection. A weight
+ * of <= 0 means that it has no weight and is ignored.
+ */
+ public Builder setLightnessWeight(@FloatRange(from = 0) float weight) {
+ mTarget.mWeights[INDEX_WEIGHT_LUMA] = weight;
+ return this;
+ }
+
+ /**
+ * Set the weight of important that a color's population value has on selection. A weight
+ * of <= 0 means that it has no weight and is ignored.
+ */
+ public Builder setPopulationWeight(@FloatRange(from = 0) float weight) {
+ mTarget.mWeights[INDEX_WEIGHT_POP] = weight;
+ return this;
+ }
+
+ /**
+ * Set whether any color selected for this target is exclusive to this target only.
+ * Defaults to true.
+ *
+ * @param exclusive true if any the color is exclusive to this target, or false is the
+ * color can be selected for other targets.
+ */
+ public Builder setExclusive(boolean exclusive) {
+ mTarget.mIsExclusive = exclusive;
+ return this;
+ }
+
+ /**
+ * Builds and returns the resulting {@link Target}.
+ */
+ public Target build() {
+ return mTarget;
+ }
+ }
+
+}
\ No newline at end of file