lineage-sdk: Add private copy of Palette

This was in frameworks/base on lineage 14.1 and
earlier (https://review.lineageos.org/#/c/65797/).

Update api/lineage_current.txt to match the new location.

Change-Id: Ib852ab1bc04936828ef00e24f71783e6a41de33c
This commit is contained in:
Steve Kondik
2016-08-29 00:27:00 -07:00
committed by Sam Mortimer
parent 542742b751
commit 987ecb37eb
5 changed files with 1801 additions and 1 deletions

View File

@@ -1622,7 +1622,7 @@ package lineageos.util {
method public static int findPerceptuallyNearestColor(int, int[]);
method public static int findPerceptuallyNearestSolidColor(int);
method public static int generateAlertColorFromDrawable(android.graphics.drawable.Drawable);
method public static com.android.internal.util.cm.palette.Palette.Swatch getDominantSwatch(com.android.internal.util.cm.palette.Palette);
method public static lineageos.util.palette.Palette.Swatch getDominantSwatch(lineageos.util.palette.Palette);
method public static float[] temperatureToRGB(int);
}

View File

@@ -0,0 +1,517 @@
/*
* Copyright 2014 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 lineageos.util.palette;
import android.graphics.Color;
import android.util.TimingLogger;
import lineageos.util.palette.Palette.Swatch;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
/**
* An color quantizer based on the Median-cut algorithm, but optimized for picking out distinct
* colors rather than representation colors.
*
* The color space is represented as a 3-dimensional cube with each dimension being an RGB
* component. The cube is then repeatedly divided until we have reduced the color space to the
* requested number of colors. An average color is then generated from each cube.
*
* What makes this different to median-cut is that median-cut divided cubes so that all of the cubes
* have roughly the same population, where this quantizer divides boxes based on their color volume.
* This means that the color space is divided into distinct colors, rather than representative
* colors.
*
* @hide
*/
final class ColorCutQuantizer {
private static final String LOG_TAG = "ColorCutQuantizer";
private static final boolean LOG_TIMINGS = false;
private static final int COMPONENT_RED = -3;
private static final int COMPONENT_GREEN = -2;
private static final int COMPONENT_BLUE = -1;
private static final int QUANTIZE_WORD_WIDTH = 5;
private static final int QUANTIZE_WORD_MASK = (1 << QUANTIZE_WORD_WIDTH) - 1;
final int[] mColors;
final int[] mHistogram;
final List<Swatch> mQuantizedColors;
final TimingLogger mTimingLogger;
final Palette.Filter[] mFilters;
private final float[] mTempHsl = new float[3];
/**
* Constructor.
*
* @param pixels histogram representing an image's pixel data
* @param maxColors The maximum number of colors that should be in the result palette.
* @param filters Set of filters to use in the quantization stage
*/
ColorCutQuantizer(final int[] pixels, final int maxColors, final Palette.Filter[] filters) {
mTimingLogger = LOG_TIMINGS ? new TimingLogger(LOG_TAG, "Creation") : null;
mFilters = filters;
final int[] hist = mHistogram = new int[1 << (QUANTIZE_WORD_WIDTH * 3)];
for (int i = 0; i < pixels.length; i++) {
final int quantizedColor = quantizeFromRgb888(pixels[i]);
// Now update the pixel value to the quantized value
pixels[i] = quantizedColor;
// And update the histogram
hist[quantizedColor]++;
}
if (LOG_TIMINGS) {
mTimingLogger.addSplit("Histogram created");
}
// Now let's count the number of distinct colors
int distinctColorCount = 0;
for (int color = 0; color < hist.length; color++) {
if (hist[color] > 0 && shouldIgnoreColor(color)) {
// If we should ignore the color, set the population to 0
hist[color] = 0;
}
if (hist[color] > 0) {
// If the color has population, increase the distinct color count
distinctColorCount++;
}
}
if (LOG_TIMINGS) {
mTimingLogger.addSplit("Filtered colors and distinct colors counted");
}
// Now lets go through create an array consisting of only distinct colors
final int[] colors = mColors = new int[distinctColorCount];
int distinctColorIndex = 0;
for (int color = 0; color < hist.length; color++) {
if (hist[color] > 0) {
colors[distinctColorIndex++] = color;
}
}
if (LOG_TIMINGS) {
mTimingLogger.addSplit("Distinct colors copied into array");
}
if (distinctColorCount <= maxColors) {
// The image has fewer colors than the maximum requested, so just return the colors
mQuantizedColors = new ArrayList<>();
for (int color : colors) {
mQuantizedColors.add(new Swatch(approximateToRgb888(color), hist[color]));
}
if (LOG_TIMINGS) {
mTimingLogger.addSplit("Too few colors present. Copied to Swatches");
mTimingLogger.dumpToLog();
}
} else {
// We need use quantization to reduce the number of colors
mQuantizedColors = quantizePixels(maxColors);
if (LOG_TIMINGS) {
mTimingLogger.addSplit("Quantized colors computed");
mTimingLogger.dumpToLog();
}
}
}
/**
* @return the list of quantized colors
*/
List<Swatch> getQuantizedColors() {
return mQuantizedColors;
}
private List<Swatch> quantizePixels(int maxColors) {
// Create the priority queue which is sorted by volume descending. This means we always
// split the largest box in the queue
final PriorityQueue<Vbox> pq = new PriorityQueue<>(maxColors, VBOX_COMPARATOR_VOLUME);
// To start, offer a box which contains all of the colors
pq.offer(new Vbox(0, mColors.length - 1));
// Now go through the boxes, splitting them until we have reached maxColors or there are no
// more boxes to split
splitBoxes(pq, maxColors);
// Finally, return the average colors of the color boxes
return generateAverageColors(pq);
}
/**
* Iterate through the {@link java.util.Queue}, popping
* {@link ColorCutQuantizer.Vbox} objects from the queue
* and splitting them. Once split, the new box and the remaining box are offered back to the
* queue.
*
* @param queue {@link java.util.PriorityQueue} to poll for boxes
* @param maxSize Maximum amount of boxes to split
*/
private void splitBoxes(final PriorityQueue<Vbox> queue, final int maxSize) {
while (queue.size() < maxSize) {
final Vbox vbox = queue.poll();
if (vbox != null && vbox.canSplit()) {
// First split the box, and offer the result
queue.offer(vbox.splitBox());
if (LOG_TIMINGS) {
mTimingLogger.addSplit("Box split");
}
// Then offer the box back
queue.offer(vbox);
} else {
if (LOG_TIMINGS) {
mTimingLogger.addSplit("All boxes split");
}
// If we get here then there are no more boxes to split, so return
return;
}
}
}
private List<Swatch> generateAverageColors(Collection<Vbox> vboxes) {
ArrayList<Swatch> colors = new ArrayList<>(vboxes.size());
for (Vbox vbox : vboxes) {
Swatch swatch = vbox.getAverageColor();
if (!shouldIgnoreColor(swatch)) {
// As we're averaging a color box, we can still get colors which we do not want, so
// we check again here
colors.add(swatch);
}
}
return colors;
}
/**
* Represents a tightly fitting box around a color space.
*/
private class Vbox {
// lower and upper index are inclusive
private int mLowerIndex;
private int mUpperIndex;
// Population of colors within this box
private int mPopulation;
private int mMinRed, mMaxRed;
private int mMinGreen, mMaxGreen;
private int mMinBlue, mMaxBlue;
Vbox(int lowerIndex, int upperIndex) {
mLowerIndex = lowerIndex;
mUpperIndex = upperIndex;
fitBox();
}
final int getVolume() {
return (mMaxRed - mMinRed + 1) * (mMaxGreen - mMinGreen + 1) *
(mMaxBlue - mMinBlue + 1);
}
final boolean canSplit() {
return getColorCount() > 1;
}
final int getColorCount() {
return 1 + mUpperIndex - mLowerIndex;
}
/**
* Recomputes the boundaries of this box to tightly fit the colors within the box.
*/
final void fitBox() {
final int[] colors = mColors;
final int[] hist = mHistogram;
// Reset the min and max to opposite values
int minRed, minGreen, minBlue;
minRed = minGreen = minBlue = Integer.MAX_VALUE;
int maxRed, maxGreen, maxBlue;
maxRed = maxGreen = maxBlue = Integer.MIN_VALUE;
int count = 0;
for (int i = mLowerIndex; i <= mUpperIndex; i++) {
final int color = colors[i];
count += hist[color];
final int r = quantizedRed(color);
final int g = quantizedGreen(color);
final int b = quantizedBlue(color);
if (r > maxRed) {
maxRed = r;
}
if (r < minRed) {
minRed = r;
}
if (g > maxGreen) {
maxGreen = g;
}
if (g < minGreen) {
minGreen = g;
}
if (b > maxBlue) {
maxBlue = b;
}
if (b < minBlue) {
minBlue = b;
}
}
mMinRed = minRed;
mMaxRed = maxRed;
mMinGreen = minGreen;
mMaxGreen = maxGreen;
mMinBlue = minBlue;
mMaxBlue = maxBlue;
mPopulation = count;
}
/**
* Split this color box at the mid-point along it's longest dimension
*
* @return the new ColorBox
*/
final Vbox splitBox() {
if (!canSplit()) {
throw new IllegalStateException("Can not split a box with only 1 color");
}
// find median along the longest dimension
final int splitPoint = findSplitPoint();
Vbox newBox = new Vbox(splitPoint + 1, mUpperIndex);
// Now change this box's upperIndex and recompute the color boundaries
mUpperIndex = splitPoint;
fitBox();
return newBox;
}
/**
* @return the dimension which this box is largest in
*/
final int getLongestColorDimension() {
final int redLength = mMaxRed - mMinRed;
final int greenLength = mMaxGreen - mMinGreen;
final int blueLength = mMaxBlue - mMinBlue;
if (redLength >= greenLength && redLength >= blueLength) {
return COMPONENT_RED;
} else if (greenLength >= redLength && greenLength >= blueLength) {
return COMPONENT_GREEN;
} else {
return COMPONENT_BLUE;
}
}
/**
* Finds the point within this box's lowerIndex and upperIndex index of where to split.
*
* This is calculated by finding the longest color dimension, and then sorting the
* sub-array based on that dimension value in each color. The colors are then iterated over
* until a color is found with at least the midpoint of the whole box's dimension midpoint.
*
* @return the index of the colors array to split from
*/
final int findSplitPoint() {
final int longestDimension = getLongestColorDimension();
final int[] colors = mColors;
final int[] hist = mHistogram;
// We need to sort the colors in this box based on the longest color dimension.
// As we can't use a Comparator to define the sort logic, we modify each color so that
// it's most significant is the desired dimension
modifySignificantOctet(colors, longestDimension, mLowerIndex, mUpperIndex);
// Now sort... Arrays.sort uses a exclusive toIndex so we need to add 1
Arrays.sort(colors, mLowerIndex, mUpperIndex + 1);
// Now revert all of the colors so that they are packed as RGB again
modifySignificantOctet(colors, longestDimension, mLowerIndex, mUpperIndex);
final int midPoint = mPopulation / 2;
for (int i = mLowerIndex, count = 0; i <= mUpperIndex; i++) {
count += hist[colors[i]];
if (count >= midPoint) {
return i;
}
}
return mLowerIndex;
}
/**
* @return the average color of this box.
*/
final Swatch getAverageColor() {
final int[] colors = mColors;
final int[] hist = mHistogram;
int redSum = 0;
int greenSum = 0;
int blueSum = 0;
int totalPopulation = 0;
for (int i = mLowerIndex; i <= mUpperIndex; i++) {
final int color = colors[i];
final int colorPopulation = hist[color];
totalPopulation += colorPopulation;
redSum += colorPopulation * quantizedRed(color);
greenSum += colorPopulation * quantizedGreen(color);
blueSum += colorPopulation * quantizedBlue(color);
}
final int redMean = Math.round(redSum / (float) totalPopulation);
final int greenMean = Math.round(greenSum / (float) totalPopulation);
final int blueMean = Math.round(blueSum / (float) totalPopulation);
return new Swatch(approximateToRgb888(redMean, greenMean, blueMean), totalPopulation);
}
}
/**
* Modify the significant octet in a packed color int. Allows sorting based on the value of a
* single color component. This relies on all components being the same word size.
*
* @see Vbox#findSplitPoint()
*/
private static void modifySignificantOctet(final int[] a, final int dimension,
final int lower, final int upper) {
switch (dimension) {
case COMPONENT_RED:
// Already in RGB, no need to do anything
break;
case COMPONENT_GREEN:
// We need to do a RGB to GRB swap, or vice-versa
for (int i = lower; i <= upper; i++) {
final int color = a[i];
a[i] = quantizedGreen(color) << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)
| quantizedRed(color) << QUANTIZE_WORD_WIDTH
| quantizedBlue(color);
}
break;
case COMPONENT_BLUE:
// We need to do a RGB to BGR swap, or vice-versa
for (int i = lower; i <= upper; i++) {
final int color = a[i];
a[i] = quantizedBlue(color) << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)
| quantizedGreen(color) << QUANTIZE_WORD_WIDTH
| quantizedRed(color);
}
break;
}
}
private boolean shouldIgnoreColor(int color565) {
final int rgb = approximateToRgb888(color565);
ColorUtils.colorToHSL(rgb, mTempHsl);
return shouldIgnoreColor(rgb, mTempHsl);
}
private boolean shouldIgnoreColor(Swatch color) {
return shouldIgnoreColor(color.getRgb(), color.getHsl());
}
private boolean shouldIgnoreColor(int rgb, float[] hsl) {
if (mFilters != null && mFilters.length > 0) {
for (int i = 0, count = mFilters.length; i < count; i++) {
if (!mFilters[i].isAllowed(rgb, hsl)) {
return true;
}
}
}
return false;
}
/**
* Comparator which sorts {@link Vbox} instances based on their volume, in descending order
*/
private static final Comparator<Vbox> VBOX_COMPARATOR_VOLUME = new Comparator<Vbox>() {
@Override
public int compare(Vbox lhs, Vbox rhs) {
return rhs.getVolume() - lhs.getVolume();
}
};
/**
* Quantized a RGB888 value to have a word width of {@value #QUANTIZE_WORD_WIDTH}.
*/
private static int quantizeFromRgb888(int color) {
int r = modifyWordWidth(Color.red(color), 8, QUANTIZE_WORD_WIDTH);
int g = modifyWordWidth(Color.green(color), 8, QUANTIZE_WORD_WIDTH);
int b = modifyWordWidth(Color.blue(color), 8, QUANTIZE_WORD_WIDTH);
return r << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH) | g << QUANTIZE_WORD_WIDTH | b;
}
/**
* Quantized RGB888 values to have a word width of {@value #QUANTIZE_WORD_WIDTH}.
*/
private static int approximateToRgb888(int r, int g, int b) {
return Color.rgb(modifyWordWidth(r, QUANTIZE_WORD_WIDTH, 8),
modifyWordWidth(g, QUANTIZE_WORD_WIDTH, 8),
modifyWordWidth(b, QUANTIZE_WORD_WIDTH, 8));
}
private static int approximateToRgb888(int color) {
return approximateToRgb888(quantizedRed(color), quantizedGreen(color), quantizedBlue(color));
}
/**
* @return red component of the quantized color
*/
private static int quantizedRed(int color) {
return (color >> (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)) & QUANTIZE_WORD_MASK;
}
/**
* @return green component of a quantized color
*/
private static int quantizedGreen(int color) {
return (color >> QUANTIZE_WORD_WIDTH) & QUANTIZE_WORD_MASK;
}
/**
* @return blue component of a quantized color
*/
private static int quantizedBlue(int color) {
return color & QUANTIZE_WORD_MASK;
}
private static int modifyWordWidth(int value, int currentWidth, int targetWidth) {
final int newValue;
if (targetWidth > currentWidth) {
// If we're approximating up in word width, we'll shift up
newValue = value << (targetWidth - currentWidth);
} else {
// Else, we will just shift and keep the MSB
newValue = value >> (currentWidth - targetWidth);
}
return newValue & ((1 << targetWidth) - 1);
}
}

View File

@@ -0,0 +1,299 @@
/*
* 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 lineageos.util.palette;
import android.graphics.Color;
/**
* A set of color-related utility methods, building upon those available in {@code Color}.
*
* @hide
*/
public class ColorUtils {
private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
private static final int MIN_ALPHA_SEARCH_PRECISION = 10;
private ColorUtils() {}
/**
* Composite two potentially translucent colors over each other and returns the result.
*/
public static int compositeColors(int foreground, int background) {
int bgAlpha = Color.alpha(background);
int fgAlpha = Color.alpha(foreground);
int a = compositeAlpha(fgAlpha, bgAlpha);
int r = compositeComponent(Color.red(foreground), fgAlpha,
Color.red(background), bgAlpha, a);
int g = compositeComponent(Color.green(foreground), fgAlpha,
Color.green(background), bgAlpha, a);
int b = compositeComponent(Color.blue(foreground), fgAlpha,
Color.blue(background), bgAlpha, a);
return Color.argb(a, r, g, b);
}
private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
}
private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
if (a == 0) return 0;
return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
}
/**
* Returns the luminance of a color.
*
* Formula defined here: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
*/
public static double calculateLuminance(int color) {
double red = Color.red(color) / 255d;
red = red < 0.03928 ? red / 12.92 : Math.pow((red + 0.055) / 1.055, 2.4);
double green = Color.green(color) / 255d;
green = green < 0.03928 ? green / 12.92 : Math.pow((green + 0.055) / 1.055, 2.4);
double blue = Color.blue(color) / 255d;
blue = blue < 0.03928 ? blue / 12.92 : Math.pow((blue + 0.055) / 1.055, 2.4);
return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue);
}
/**
* Returns the contrast ratio between {@code foreground} and {@code background}.
* {@code background} must be opaque.
* <p>
* Formula defined
* <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
*/
public static double calculateContrast(int foreground, int background) {
if (Color.alpha(background) != 255) {
throw new IllegalArgumentException("background can not be translucent");
}
if (Color.alpha(foreground) < 255) {
// If the foreground is translucent, composite the foreground over the background
foreground = compositeColors(foreground, background);
}
final double luminance1 = calculateLuminance(foreground) + 0.05;
final double luminance2 = calculateLuminance(background) + 0.05;
// Now return the lighter luminance divided by the darker luminance
return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
}
/**
* Calculates the minimum alpha value which can be applied to {@code foreground} so that would
* have a contrast value of at least {@code minContrastRatio} when compared to
* {@code background}.
*
* @param foreground the foreground color.
* @param background the background color. Should be opaque.
* @param minContrastRatio the minimum contrast ratio.
* @return the alpha value in the range 0-255, or -1 if no value could be calculated.
*/
public static int calculateMinimumAlpha(int foreground, int background,
float minContrastRatio) {
if (Color.alpha(background) != 255) {
throw new IllegalArgumentException("background can not be translucent");
}
// First lets check that a fully opaque foreground has sufficient contrast
int testForeground = setAlphaComponent(foreground, 255);
double testRatio = calculateContrast(testForeground, background);
if (testRatio < minContrastRatio) {
// Fully opaque foreground does not have sufficient contrast, return error
return -1;
}
// Binary search to find a value with the minimum value which provides sufficient contrast
int numIterations = 0;
int minAlpha = 0;
int maxAlpha = 255;
while (numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS &&
(maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION) {
final int testAlpha = (minAlpha + maxAlpha) / 2;
testForeground = setAlphaComponent(foreground, testAlpha);
testRatio = calculateContrast(testForeground, background);
if (testRatio < minContrastRatio) {
minAlpha = testAlpha;
} else {
maxAlpha = testAlpha;
}
numIterations++;
}
// Conservatively return the max of the range of possible alphas, which is known to pass.
return maxAlpha;
}
/**
* Convert RGB components to HSL (hue-saturation-lightness).
* <ul>
* <li>hsl[0] is Hue [0 .. 360)</li>
* <li>hsl[1] is Saturation [0...1]</li>
* <li>hsl[2] is Lightness [0...1]</li>
* </ul>
*
* @param r red component value [0..255]
* @param g green component value [0..255]
* @param b blue component value [0..255]
* @param hsl 3 element array which holds the resulting HSL components.
*/
public static void RGBToHSL(int r, int g, int b, float[] hsl) {
final float rf = r / 255f;
final float gf = g / 255f;
final float bf = b / 255f;
final float max = Math.max(rf, Math.max(gf, bf));
final float min = Math.min(rf, Math.min(gf, bf));
final float deltaMaxMin = max - min;
float h, s;
float l = (max + min) / 2f;
if (max == min) {
// Monochromatic
h = s = 0f;
} else {
if (max == rf) {
h = ((gf - bf) / deltaMaxMin) % 6f;
} else if (max == gf) {
h = ((bf - rf) / deltaMaxMin) + 2f;
} else {
h = ((rf - gf) / deltaMaxMin) + 4f;
}
s = deltaMaxMin / (1f - Math.abs(2f * l - 1f));
}
h = (h * 60f) % 360f;
if (h < 0) {
h += 360f;
}
hsl[0] = constrain(h, 0f, 360f);
hsl[1] = constrain(s, 0f, 1f);
hsl[2] = constrain(l, 0f, 1f);
}
/**
* Convert the ARGB color to its HSL (hue-saturation-lightness) components.
* <ul>
* <li>hsl[0] is Hue [0 .. 360)</li>
* <li>hsl[1] is Saturation [0...1]</li>
* <li>hsl[2] is Lightness [0...1]</li>
* </ul>
*
* @param color the ARGB color to convert. The alpha component is ignored.
* @param hsl 3 element array which holds the resulting HSL components.
*/
public static void colorToHSL(int color, float[] hsl) {
RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl);
}
/**
* Convert HSL (hue-saturation-lightness) components to a RGB color.
* <ul>
* <li>hsl[0] is Hue [0 .. 360)</li>
* <li>hsl[1] is Saturation [0...1]</li>
* <li>hsl[2] is Lightness [0...1]</li>
* </ul>
* If hsv values are out of range, they are pinned.
*
* @param hsl 3 element array which holds the input HSL components.
* @return the resulting RGB color
*/
public static int HSLToColor(float[] hsl) {
final float h = hsl[0];
final float s = hsl[1];
final float l = hsl[2];
final float c = (1f - Math.abs(2 * l - 1f)) * s;
final float m = l - 0.5f * c;
final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
final int hueSegment = (int) h / 60;
int r = 0, g = 0, b = 0;
switch (hueSegment) {
case 0:
r = Math.round(255 * (c + m));
g = Math.round(255 * (x + m));
b = Math.round(255 * m);
break;
case 1:
r = Math.round(255 * (x + m));
g = Math.round(255 * (c + m));
b = Math.round(255 * m);
break;
case 2:
r = Math.round(255 * m);
g = Math.round(255 * (c + m));
b = Math.round(255 * (x + m));
break;
case 3:
r = Math.round(255 * m);
g = Math.round(255 * (x + m));
b = Math.round(255 * (c + m));
break;
case 4:
r = Math.round(255 * (x + m));
g = Math.round(255 * m);
b = Math.round(255 * (c + m));
break;
case 5:
case 6:
r = Math.round(255 * (c + m));
g = Math.round(255 * m);
b = Math.round(255 * (x + m));
break;
}
r = constrain(r, 0, 255);
g = constrain(g, 0, 255);
b = constrain(b, 0, 255);
return Color.rgb(r, g, b);
}
/**
* Set the alpha component of {@code color} to be {@code alpha}.
*/
public static int setAlphaComponent(int color, int alpha) {
if (alpha < 0 || alpha > 255) {
throw new IllegalArgumentException("alpha must be between 0 and 255.");
}
return (color & 0x00ffffff) | (alpha << 24);
}
private static float constrain(float amount, float low, float high) {
return amount < low ? low : (amount > high ? high : amount);
}
private static int constrain(int amount, int low, int high) {
return amount < low ? low : (amount > high ? high : amount);
}
}

View File

@@ -0,0 +1,244 @@
/*
* 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 lineageos.util.palette;
import lineageos.util.palette.Palette.Swatch;
import java.util.List;
/**
* @hide
*/
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;
}
}

View File

@@ -0,0 +1,740 @@
/*
* Copyright 2014 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 lineageos.util.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;
/**
* A helper class to extract prominent colors from an image.
* <p>
* A number of colors with different profiles are extracted from the image:
* <ul>
* <li>Vibrant</li>
* <li>Vibrant Dark</li>
* <li>Vibrant Light</li>
* <li>Muted</li>
* <li>Muted Dark</li>
* <li>Muted Light</li>
* </ul>
* These can be retrieved from the appropriate getter method.
*
* <p>
* Instances are created with a {@link Builder} which supports several options to tweak the
* generated Palette. See that class' documentation for more information.
* <p>
* 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>
* // Synchronous
* Palette p = Palette.from(bitmap).generate();
*
* // Asynchronous
* Palette.from(bitmap).generate(new PaletteAsyncListener() {
* public void onGenerated(Palette p) {
* // Use generated instance
* }
* });
* </pre>
*
* @hide
*/
public final class Palette {
/**
* Listener to be used with {@link #generateAsync(Bitmap, PaletteAsyncListener)} or
* {@link #generateAsync(Bitmap, int, PaletteAsyncListener)}
*/
public interface PaletteAsyncListener {
/**
* Called when the {@link Palette} has been generated.
*/
void onGenerated(Palette palette);
}
private static final int DEFAULT_RESIZE_BITMAP_MAX_DIMENSION = 192;
private static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16;
private static final float MIN_CONTRAST_TITLE_TEXT = 3.0f;
private static final float MIN_CONTRAST_BODY_TEXT = 4.5f;
private static final String LOG_TAG = "Palette";
private static final boolean LOG_TIMINGS = false;
/**
* Start generating a {@link Palette} with the returned {@link Builder} instance.
*/
public static Builder from(Bitmap bitmap) {
return new 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<Swatch> swatches) {
return new Builder(swatches).generate();
}
/**
* @deprecated Use {@link Builder} to generate the Palette.
*/
@Deprecated
public static Palette generate(Bitmap bitmap) {
return from(bitmap).generate();
}
/**
* @deprecated Use {@link Builder} to generate the Palette.
*/
@Deprecated
public static Palette generate(Bitmap bitmap, int numColors) {
return from(bitmap).maximumColorCount(numColors).generate();
}
/**
* @deprecated Use {@link Builder} to generate the Palette.
*/
@Deprecated
public static AsyncTask<Bitmap, Void, Palette> generateAsync(
Bitmap bitmap, PaletteAsyncListener listener) {
return from(bitmap).generate(listener);
}
/**
* @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) {
return from(bitmap).maximumColorCount(numColors).generate(listener);
}
private final List<Swatch> mSwatches;
private final Generator mGenerator;
private Palette(List<Swatch> swatches, Generator generator) {
mSwatches = swatches;
mGenerator = generator;
}
/**
* Returns all of the swatches which make up the palette.
*/
public List<Swatch> getSwatches() {
return Collections.unmodifiableList(mSwatches);
}
/**
* Returns the most vibrant swatch in the palette. Might be null.
*/
@Nullable
public Swatch getVibrantSwatch() {
return mGenerator.getVibrantSwatch();
}
/**
* Returns a light and vibrant swatch from the palette. Might be null.
*/
@Nullable
public Swatch getLightVibrantSwatch() {
return mGenerator.getLightVibrantSwatch();
}
/**
* Returns a dark and vibrant swatch from the palette. Might be null.
*/
@Nullable
public Swatch getDarkVibrantSwatch() {
return mGenerator.getDarkVibrantSwatch();
}
/**
* Returns a muted swatch from the palette. Might be null.
*/
@Nullable
public Swatch getMutedSwatch() {
return mGenerator.getMutedSwatch();
}
/**
* Returns a muted and light swatch from the palette. Might be null.
*/
@Nullable
public Swatch getLightMutedSwatch() {
return mGenerator.getLightMutedSwatch();
}
/**
* Returns a muted and dark swatch from the palette. Might be null.
*/
@Nullable
public Swatch getDarkMutedSwatch() {
return mGenerator.getDarkMutedSwatch();
}
/**
* Returns the most vibrant color in the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
*/
@ColorInt
public int getVibrantColor(@ColorInt int defaultColor) {
Swatch swatch = getVibrantSwatch();
return swatch != null ? swatch.getRgb() : 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
*/
@ColorInt
public int getLightVibrantColor(@ColorInt int defaultColor) {
Swatch swatch = getLightVibrantSwatch();
return swatch != null ? swatch.getRgb() : 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
*/
@ColorInt
public int getDarkVibrantColor(@ColorInt int defaultColor) {
Swatch swatch = getDarkVibrantSwatch();
return swatch != null ? swatch.getRgb() : defaultColor;
}
/**
* Returns a muted color from the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
*/
@ColorInt
public int getMutedColor(@ColorInt int defaultColor) {
Swatch swatch = getMutedSwatch();
return swatch != null ? swatch.getRgb() : 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
*/
@ColorInt
public int getLightMutedColor(@ColorInt int defaultColor) {
Swatch swatch = getLightMutedSwatch();
return swatch != null ? swatch.getRgb() : 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
*/
@ColorInt
public int getDarkMutedColor(@ColorInt int defaultColor) {
Swatch swatch = getDarkMutedSwatch();
return swatch != null ? swatch.getRgb() : 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.
*/
private static Bitmap scaleBitmapDown(Bitmap bitmap, final int targetMaxDimension) {
final int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
if (maxDimension <= targetMaxDimension) {
// If the bitmap is small enough already, just return it
return bitmap;
}
final float scaleRatio = targetMaxDimension / (float) maxDimension;
return Bitmap.createScaledBitmap(bitmap,
Math.round(bitmap.getWidth() * scaleRatio),
Math.round(bitmap.getHeight() * scaleRatio),
false);
}
/**
* 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;
}
/**
* @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 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 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 && darkBodyAlpha != -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;
}
Swatch swatch = (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 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;
}
};
}