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:
committed by
Sam Mortimer
parent
542742b751
commit
987ecb37eb
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
517
sdk/src/java/lineageos/util/palette/ColorCutQuantizer.java
Normal file
517
sdk/src/java/lineageos/util/palette/ColorCutQuantizer.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
299
sdk/src/java/lineageos/util/palette/ColorUtils.java
Normal file
299
sdk/src/java/lineageos/util/palette/ColorUtils.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
244
sdk/src/java/lineageos/util/palette/DefaultGenerator.java
Normal file
244
sdk/src/java/lineageos/util/palette/DefaultGenerator.java
Normal 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;
|
||||
}
|
||||
}
|
||||
740
sdk/src/java/lineageos/util/palette/Palette.java
Normal file
740
sdk/src/java/lineageos/util/palette/Palette.java
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user