Merge "Internal copy of Palette API." into oc-dev

This commit is contained in:
TreeHugger Robot
2017-04-19 17:24:02 +00:00
committed by Android (Google) Code Review
4 changed files with 2578 additions and 0 deletions

View File

@@ -0,0 +1,618 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.internal.graphics;
import android.annotation.ColorInt;
import android.annotation.FloatRange;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.graphics.Color;
/**
* Copied from: frameworks/support/core-utils/java/android/support/v4/graphics/ColorUtils.java
*
* A set of color-related utility methods, building upon those available in {@code Color}.
*/
public final class ColorUtils {
private static final double XYZ_WHITE_REFERENCE_X = 95.047;
private static final double XYZ_WHITE_REFERENCE_Y = 100;
private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
private static final double XYZ_EPSILON = 0.008856;
private static final double XYZ_KAPPA = 903.3;
private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
private ColorUtils() {}
/**
* Composite two potentially translucent colors over each other and returns the result.
*/
public static int compositeColors(@ColorInt int foreground, @ColorInt 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 as a float between {@code 0.0} and {@code 1.0}.
* <p>Defined as the Y component in the XYZ representation of {@code color}.</p>
*/
@FloatRange(from = 0.0, to = 1.0)
public static double calculateLuminance(@ColorInt int color) {
final double[] result = getTempDouble3Array();
colorToXYZ(color, result);
// Luminance is the Y component
return result[1] / 100;
}
/**
* 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(@ColorInt int foreground, @ColorInt int background) {
if (Color.alpha(background) != 255) {
throw new IllegalArgumentException("background can not be translucent: #"
+ Integer.toHexString(background));
}
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 opaque background color
* @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(@ColorInt int foreground, @ColorInt int background,
float minContrastRatio) {
if (Color.alpha(background) != 255) {
throw new IllegalArgumentException("background can not be translucent: #"
+ Integer.toHexString(background));
}
// 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>outHsl[0] is Hue [0 .. 360)</li>
* <li>outHsl[1] is Saturation [0...1]</li>
* <li>outHsl[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 outHsl 3-element array which holds the resulting HSL components
*/
public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r,
@IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
@NonNull float[] outHsl) {
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;
}
outHsl[0] = constrain(h, 0f, 360f);
outHsl[1] = constrain(s, 0f, 1f);
outHsl[2] = constrain(l, 0f, 1f);
}
/**
* Convert the ARGB color to its HSL (hue-saturation-lightness) components.
* <ul>
* <li>outHsl[0] is Hue [0 .. 360)</li>
* <li>outHsl[1] is Saturation [0...1]</li>
* <li>outHsl[2] is Lightness [0...1]</li>
* </ul>
*
* @param color the ARGB color to convert. The alpha component is ignored
* @param outHsl 3-element array which holds the resulting HSL components
*/
public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) {
RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl);
}
/**
* 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
*/
@ColorInt
public static int HSLToColor(@NonNull 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}.
*/
@ColorInt
public static int setAlphaComponent(@ColorInt int color,
@IntRange(from = 0x0, to = 0xFF) int alpha) {
if (alpha < 0 || alpha > 255) {
throw new IllegalArgumentException("alpha must be between 0 and 255.");
}
return (color & 0x00ffffff) | (alpha << 24);
}
/**
* Convert the ARGB color to its CIE Lab representative components.
*
* @param color the ARGB color to convert. The alpha component is ignored
* @param outLab 3-element array which holds the resulting LAB components
*/
public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) {
RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab);
}
/**
* Convert RGB components to its CIE Lab representative components.
*
* <ul>
* <li>outLab[0] is L [0 ...1)</li>
* <li>outLab[1] is a [-128...127)</li>
* <li>outLab[2] is b [-128...127)</li>
* </ul>
*
* @param r red component value [0..255]
* @param g green component value [0..255]
* @param b blue component value [0..255]
* @param outLab 3-element array which holds the resulting LAB components
*/
public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r,
@IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
@NonNull double[] outLab) {
// First we convert RGB to XYZ
RGBToXYZ(r, g, b, outLab);
// outLab now contains XYZ
XYZToLAB(outLab[0], outLab[1], outLab[2], outLab);
// outLab now contains LAB representation
}
/**
* Convert the ARGB color to its CIE XYZ representative components.
*
* <p>The resulting XYZ representation will use the D65 illuminant and the CIE
* 2° Standard Observer (1931).</p>
*
* <ul>
* <li>outXyz[0] is X [0 ...95.047)</li>
* <li>outXyz[1] is Y [0...100)</li>
* <li>outXyz[2] is Z [0...108.883)</li>
* </ul>
*
* @param color the ARGB color to convert. The alpha component is ignored
* @param outXyz 3-element array which holds the resulting LAB components
*/
public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
}
/**
* Convert RGB components to its CIE XYZ representative components.
*
* <p>The resulting XYZ representation will use the D65 illuminant and the CIE
* 2° Standard Observer (1931).</p>
*
* <ul>
* <li>outXyz[0] is X [0 ...95.047)</li>
* <li>outXyz[1] is Y [0...100)</li>
* <li>outXyz[2] is Z [0...108.883)</li>
* </ul>
*
* @param r red component value [0..255]
* @param g green component value [0..255]
* @param b blue component value [0..255]
* @param outXyz 3-element array which holds the resulting XYZ components
*/
public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r,
@IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
@NonNull double[] outXyz) {
if (outXyz.length != 3) {
throw new IllegalArgumentException("outXyz must have a length of 3.");
}
double sr = r / 255.0;
sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
double sg = g / 255.0;
sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
double sb = b / 255.0;
sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
}
/**
* Converts a color from CIE XYZ to CIE Lab representation.
*
* <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
* 2° Standard Observer (1931).</p>
*
* <ul>
* <li>outLab[0] is L [0 ...1)</li>
* <li>outLab[1] is a [-128...127)</li>
* <li>outLab[2] is b [-128...127)</li>
* </ul>
*
* @param x X component value [0...95.047)
* @param y Y component value [0...100)
* @param z Z component value [0...108.883)
* @param outLab 3-element array which holds the resulting Lab components
*/
public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
@NonNull double[] outLab) {
if (outLab.length != 3) {
throw new IllegalArgumentException("outLab must have a length of 3.");
}
x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
outLab[0] = Math.max(0, 116 * y - 16);
outLab[1] = 500 * (x - y);
outLab[2] = 200 * (y - z);
}
/**
* Converts a color from CIE Lab to CIE XYZ representation.
*
* <p>The resulting XYZ representation will use the D65 illuminant and the CIE
* 2° Standard Observer (1931).</p>
*
* <ul>
* <li>outXyz[0] is X [0 ...95.047)</li>
* <li>outXyz[1] is Y [0...100)</li>
* <li>outXyz[2] is Z [0...108.883)</li>
* </ul>
*
* @param l L component value [0...100)
* @param a A component value [-128...127)
* @param b B component value [-128...127)
* @param outXyz 3-element array which holds the resulting XYZ components
*/
public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l,
@FloatRange(from = -128, to = 127) final double a,
@FloatRange(from = -128, to = 127) final double b,
@NonNull double[] outXyz) {
final double fy = (l + 16) / 116;
final double fx = a / 500 + fy;
final double fz = fy - b / 200;
double tmp = Math.pow(fx, 3);
final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA;
final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA;
tmp = Math.pow(fz, 3);
final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA;
outXyz[0] = xr * XYZ_WHITE_REFERENCE_X;
outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y;
outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z;
}
/**
* Converts a color from CIE XYZ to its RGB representation.
*
* <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
* 2° Standard Observer (1931).</p>
*
* @param x X component value [0...95.047)
* @param y Y component value [0...100)
* @param z Z component value [0...108.883)
* @return int containing the RGB representation
*/
@ColorInt
public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) {
double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100;
double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100;
double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100;
r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;
return Color.rgb(
constrain((int) Math.round(r * 255), 0, 255),
constrain((int) Math.round(g * 255), 0, 255),
constrain((int) Math.round(b * 255), 0, 255));
}
/**
* Converts a color from CIE Lab to its RGB representation.
*
* @param l L component value [0...100]
* @param a A component value [-128...127]
* @param b B component value [-128...127]
* @return int containing the RGB representation
*/
@ColorInt
public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l,
@FloatRange(from = -128, to = 127) final double a,
@FloatRange(from = -128, to = 127) final double b) {
final double[] result = getTempDouble3Array();
LABToXYZ(l, a, b, result);
return XYZToColor(result[0], result[1], result[2]);
}
/**
* Returns the euclidean distance between two LAB colors.
*/
public static double distanceEuclidean(@NonNull double[] labX, @NonNull double[] labY) {
return Math.sqrt(Math.pow(labX[0] - labY[0], 2)
+ Math.pow(labX[1] - labY[1], 2)
+ Math.pow(labX[2] - labY[2], 2));
}
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);
}
private static double pivotXyzComponent(double component) {
return component > XYZ_EPSILON
? Math.pow(component, 1 / 3.0)
: (XYZ_KAPPA * component + 16) / 116;
}
/**
* Blend between two ARGB colors using the given ratio.
*
* <p>A blend ratio of 0.0 will result in {@code color1}, 0.5 will give an even blend,
* 1.0 will result in {@code color2}.</p>
*
* @param color1 the first ARGB color
* @param color2 the second ARGB color
* @param ratio the blend ratio of {@code color1} to {@code color2}
*/
@ColorInt
public static int blendARGB(@ColorInt int color1, @ColorInt int color2,
@FloatRange(from = 0.0, to = 1.0) float ratio) {
final float inverseRatio = 1 - ratio;
float a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio;
float r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio;
float g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio;
float b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio;
return Color.argb((int) a, (int) r, (int) g, (int) b);
}
/**
* Blend between {@code hsl1} and {@code hsl2} using the given ratio. This will interpolate
* the hue using the shortest angle.
*
* <p>A blend ratio of 0.0 will result in {@code hsl1}, 0.5 will give an even blend,
* 1.0 will result in {@code hsl2}.</p>
*
* @param hsl1 3-element array which holds the first HSL color
* @param hsl2 3-element array which holds the second HSL color
* @param ratio the blend ratio of {@code hsl1} to {@code hsl2}
* @param outResult 3-element array which holds the resulting HSL components
*/
public static void blendHSL(@NonNull float[] hsl1, @NonNull float[] hsl2,
@FloatRange(from = 0.0, to = 1.0) float ratio, @NonNull float[] outResult) {
if (outResult.length != 3) {
throw new IllegalArgumentException("result must have a length of 3.");
}
final float inverseRatio = 1 - ratio;
// Since hue is circular we will need to interpolate carefully
outResult[0] = circularInterpolate(hsl1[0], hsl2[0], ratio);
outResult[1] = hsl1[1] * inverseRatio + hsl2[1] * ratio;
outResult[2] = hsl1[2] * inverseRatio + hsl2[2] * ratio;
}
/**
* Blend between two CIE-LAB colors using the given ratio.
*
* <p>A blend ratio of 0.0 will result in {@code lab1}, 0.5 will give an even blend,
* 1.0 will result in {@code lab2}.</p>
*
* @param lab1 3-element array which holds the first LAB color
* @param lab2 3-element array which holds the second LAB color
* @param ratio the blend ratio of {@code lab1} to {@code lab2}
* @param outResult 3-element array which holds the resulting LAB components
*/
public static void blendLAB(@NonNull double[] lab1, @NonNull double[] lab2,
@FloatRange(from = 0.0, to = 1.0) double ratio, @NonNull double[] outResult) {
if (outResult.length != 3) {
throw new IllegalArgumentException("outResult must have a length of 3.");
}
final double inverseRatio = 1 - ratio;
outResult[0] = lab1[0] * inverseRatio + lab2[0] * ratio;
outResult[1] = lab1[1] * inverseRatio + lab2[1] * ratio;
outResult[2] = lab1[2] * inverseRatio + lab2[2] * ratio;
}
static float circularInterpolate(float a, float b, float f) {
if (Math.abs(b - a) > 180) {
if (b > a) {
a += 360;
} else {
b += 360;
}
}
return (a + ((b - a) * f)) % 360;
}
private static double[] getTempDouble3Array() {
double[] result = TEMP_ARRAY.get();
if (result == null) {
result = new double[3];
TEMP_ARRAY.set(result);
}
return result;
}
}

View File

@@ -0,0 +1,535 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.internal.graphics.palette;
/*
* 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.
*/
import android.graphics.Color;
import android.util.TimingLogger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import com.android.internal.graphics.ColorUtils;
import com.android.internal.graphics.palette.Palette.Swatch;
/**
* Copied from: frameworks/support/v7/palette/src/main/java/android/support/v7/
* graphics/ColorCutQuantizer.java
*
* 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.
*/
final class ColorCutQuantizer {
private static final String LOG_TAG = "ColorCutQuantizer";
private static final boolean LOG_TIMINGS = false;
static final int COMPONENT_RED = -3;
static final int COMPONENT_GREEN = -2;
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 its 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
// its 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()
*/
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}.
*/
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
*/
static int quantizedRed(int color) {
return (color >> (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)) & QUANTIZE_WORD_MASK;
}
/**
* @return green component of a quantized color
*/
static int quantizedGreen(int color) {
return (color >> QUANTIZE_WORD_WIDTH) & QUANTIZE_WORD_MASK;
}
/**
* @return blue component of a quantized color
*/
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,990 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.internal.graphics.palette;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.util.ArrayMap;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.util.TimingLogger;
import com.android.internal.graphics.ColorUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Copied from: /frameworks/support/v7/palette/src/main/java/android/support/v7/
* graphics/Palette.java
*
* A helper class to extract prominent colors from an image.
* <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 Palette.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 Palette.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>
*/
public final class Palette {
/**
* Listener to be used with {@link #generateAsync(Bitmap, Palette.PaletteAsyncListener)} or
* {@link #generateAsync(Bitmap, int, Palette.PaletteAsyncListener)}
*/
public interface PaletteAsyncListener {
/**
* Called when the {@link Palette} has been generated.
*/
void onGenerated(Palette palette);
}
static final int DEFAULT_RESIZE_BITMAP_AREA = 112 * 112;
static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16;
static final float MIN_CONTRAST_TITLE_TEXT = 3.0f;
static final float MIN_CONTRAST_BODY_TEXT = 4.5f;
static final String LOG_TAG = "Palette";
static final boolean LOG_TIMINGS = false;
/**
* Start generating a {@link Palette} with the returned {@link Palette.Builder} instance.
*/
public static Palette.Builder from(Bitmap bitmap) {
return new Palette.Builder(bitmap);
}
/**
* Generate a {@link Palette} from the pre-generated list of {@link Palette.Swatch} swatches.
* This is useful for testing, or if you want to resurrect a {@link Palette} instance from a
* list of swatches. Will return null if the {@code swatches} is null.
*/
public static Palette from(List<Palette.Swatch> swatches) {
return new Palette.Builder(swatches).generate();
}
/**
* @deprecated Use {@link Palette.Builder} to generate the Palette.
*/
@Deprecated
public static Palette generate(Bitmap bitmap) {
return from(bitmap).generate();
}
/**
* @deprecated Use {@link Palette.Builder} to generate the Palette.
*/
@Deprecated
public static Palette generate(Bitmap bitmap, int numColors) {
return from(bitmap).maximumColorCount(numColors).generate();
}
/**
* @deprecated Use {@link Palette.Builder} to generate the Palette.
*/
@Deprecated
public static AsyncTask<Bitmap, Void, Palette> generateAsync(
Bitmap bitmap, Palette.PaletteAsyncListener listener) {
return from(bitmap).generate(listener);
}
/**
* @deprecated Use {@link Palette.Builder} to generate the Palette.
*/
@Deprecated
public static AsyncTask<Bitmap, Void, Palette> generateAsync(
final Bitmap bitmap, final int numColors, final Palette.PaletteAsyncListener listener) {
return from(bitmap).maximumColorCount(numColors).generate(listener);
}
private final List<Palette.Swatch> mSwatches;
private final List<Target> mTargets;
private final Map<Target, Palette.Swatch> mSelectedSwatches;
private final SparseBooleanArray mUsedColors;
private final Palette.Swatch mDominantSwatch;
Palette(List<Palette.Swatch> swatches, List<Target> targets) {
mSwatches = swatches;
mTargets = targets;
mUsedColors = new SparseBooleanArray();
mSelectedSwatches = new ArrayMap<>();
mDominantSwatch = findDominantSwatch();
}
/**
* Returns all of the swatches which make up the palette.
*/
@NonNull
public List<Palette.Swatch> getSwatches() {
return Collections.unmodifiableList(mSwatches);
}
/**
* Returns the targets used to generate this palette.
*/
@NonNull
public List<Target> getTargets() {
return Collections.unmodifiableList(mTargets);
}
/**
* Returns the most vibrant swatch in the palette. Might be null.
*
* @see Target#VIBRANT
*/
@Nullable
public Palette.Swatch getVibrantSwatch() {
return getSwatchForTarget(Target.VIBRANT);
}
/**
* Returns a light and vibrant swatch from the palette. Might be null.
*
* @see Target#LIGHT_VIBRANT
*/
@Nullable
public Palette.Swatch getLightVibrantSwatch() {
return getSwatchForTarget(Target.LIGHT_VIBRANT);
}
/**
* Returns a dark and vibrant swatch from the palette. Might be null.
*
* @see Target#DARK_VIBRANT
*/
@Nullable
public Palette.Swatch getDarkVibrantSwatch() {
return getSwatchForTarget(Target.DARK_VIBRANT);
}
/**
* Returns a muted swatch from the palette. Might be null.
*
* @see Target#MUTED
*/
@Nullable
public Palette.Swatch getMutedSwatch() {
return getSwatchForTarget(Target.MUTED);
}
/**
* Returns a muted and light swatch from the palette. Might be null.
*
* @see Target#LIGHT_MUTED
*/
@Nullable
public Palette.Swatch getLightMutedSwatch() {
return getSwatchForTarget(Target.LIGHT_MUTED);
}
/**
* Returns a muted and dark swatch from the palette. Might be null.
*
* @see Target#DARK_MUTED
*/
@Nullable
public Palette.Swatch getDarkMutedSwatch() {
return getSwatchForTarget(Target.DARK_MUTED);
}
/**
* Returns the most vibrant color in the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
* @see #getVibrantSwatch()
*/
@ColorInt
public int getVibrantColor(@ColorInt final int defaultColor) {
return getColorForTarget(Target.VIBRANT, defaultColor);
}
/**
* Returns a light and vibrant color from the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
* @see #getLightVibrantSwatch()
*/
@ColorInt
public int getLightVibrantColor(@ColorInt final int defaultColor) {
return getColorForTarget(Target.LIGHT_VIBRANT, defaultColor);
}
/**
* Returns a dark and vibrant color from the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
* @see #getDarkVibrantSwatch()
*/
@ColorInt
public int getDarkVibrantColor(@ColorInt final int defaultColor) {
return getColorForTarget(Target.DARK_VIBRANT, defaultColor);
}
/**
* Returns a muted color from the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
* @see #getMutedSwatch()
*/
@ColorInt
public int getMutedColor(@ColorInt final int defaultColor) {
return getColorForTarget(Target.MUTED, defaultColor);
}
/**
* Returns a muted and light color from the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
* @see #getLightMutedSwatch()
*/
@ColorInt
public int getLightMutedColor(@ColorInt final int defaultColor) {
return getColorForTarget(Target.LIGHT_MUTED, defaultColor);
}
/**
* Returns a muted and dark color from the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
* @see #getDarkMutedSwatch()
*/
@ColorInt
public int getDarkMutedColor(@ColorInt final int defaultColor) {
return getColorForTarget(Target.DARK_MUTED, defaultColor);
}
/**
* Returns the selected swatch for the given target from the palette, or {@code null} if one
* could not be found.
*/
@Nullable
public Palette.Swatch getSwatchForTarget(@NonNull final Target target) {
return mSelectedSwatches.get(target);
}
/**
* Returns the selected color for the given target from the palette as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
*/
@ColorInt
public int getColorForTarget(@NonNull final Target target, @ColorInt final int defaultColor) {
Palette.Swatch swatch = getSwatchForTarget(target);
return swatch != null ? swatch.getRgb() : defaultColor;
}
/**
* Returns the dominant swatch from the palette.
*
* <p>The dominant swatch is defined as the swatch with the greatest population (frequency)
* within the palette.</p>
*/
@Nullable
public Palette.Swatch getDominantSwatch() {
return mDominantSwatch;
}
/**
* Returns the color of the dominant swatch from the palette, as an RGB packed int.
*
* @param defaultColor value to return if the swatch isn't available
* @see #getDominantSwatch()
*/
@ColorInt
public int getDominantColor(@ColorInt int defaultColor) {
return mDominantSwatch != null ? mDominantSwatch.getRgb() : defaultColor;
}
void generate() {
// We need to make sure that the scored targets are generated first. This is so that
// inherited targets have something to inherit from
for (int i = 0, count = mTargets.size(); i < count; i++) {
final Target target = mTargets.get(i);
target.normalizeWeights();
mSelectedSwatches.put(target, generateScoredTarget(target));
}
// We now clear out the used colors
mUsedColors.clear();
}
private Palette.Swatch generateScoredTarget(final Target target) {
final Palette.Swatch maxScoreSwatch = getMaxScoredSwatchForTarget(target);
if (maxScoreSwatch != null && target.isExclusive()) {
// If we have a swatch, and the target is exclusive, add the color to the used list
mUsedColors.append(maxScoreSwatch.getRgb(), true);
}
return maxScoreSwatch;
}
private Palette.Swatch getMaxScoredSwatchForTarget(final Target target) {
float maxScore = 0;
Palette.Swatch maxScoreSwatch = null;
for (int i = 0, count = mSwatches.size(); i < count; i++) {
final Palette.Swatch swatch = mSwatches.get(i);
if (shouldBeScoredForTarget(swatch, target)) {
final float score = generateScore(swatch, target);
if (maxScoreSwatch == null || score > maxScore) {
maxScoreSwatch = swatch;
maxScore = score;
}
}
}
return maxScoreSwatch;
}
private boolean shouldBeScoredForTarget(final Palette.Swatch swatch, final Target target) {
// Check whether the HSL values are within the correct ranges, and this color hasn't
// been used yet.
final float hsl[] = swatch.getHsl();
return hsl[1] >= target.getMinimumSaturation() && hsl[1] <= target.getMaximumSaturation()
&& hsl[2] >= target.getMinimumLightness() && hsl[2] <= target.getMaximumLightness()
&& !mUsedColors.get(swatch.getRgb());
}
private float generateScore(Palette.Swatch swatch, Target target) {
final float[] hsl = swatch.getHsl();
float saturationScore = 0;
float luminanceScore = 0;
float populationScore = 0;
final int maxPopulation = mDominantSwatch != null ? mDominantSwatch.getPopulation() : 1;
if (target.getSaturationWeight() > 0) {
saturationScore = target.getSaturationWeight()
* (1f - Math.abs(hsl[1] - target.getTargetSaturation()));
}
if (target.getLightnessWeight() > 0) {
luminanceScore = target.getLightnessWeight()
* (1f - Math.abs(hsl[2] - target.getTargetLightness()));
}
if (target.getPopulationWeight() > 0) {
populationScore = target.getPopulationWeight()
* (swatch.getPopulation() / (float) maxPopulation);
}
return saturationScore + luminanceScore + populationScore;
}
private Palette.Swatch findDominantSwatch() {
int maxPop = Integer.MIN_VALUE;
Palette.Swatch maxSwatch = null;
for (int i = 0, count = mSwatches.size(); i < count; i++) {
Palette.Swatch swatch = mSwatches.get(i);
if (swatch.getPopulation() > maxPop) {
maxSwatch = swatch;
maxPop = swatch.getPopulation();
}
}
return maxSwatch;
}
private static float[] copyHslValues(Palette.Swatch color) {
final float[] newHsl = new float[3];
System.arraycopy(color.getHsl(), 0, newHsl, 0, 3);
return newHsl;
}
/**
* Represents a color swatch generated from an image's palette. The RGB color can be retrieved
* by calling {@link #getRgb()}.
*/
public static final class Swatch {
private final int mRed, mGreen, mBlue;
private final int mRgb;
private final int mPopulation;
private boolean mGeneratedTextColors;
private int mTitleTextColor;
private int mBodyTextColor;
private float[] mHsl;
public Swatch(@ColorInt int color, int population) {
mRed = Color.red(color);
mGreen = Color.green(color);
mBlue = Color.blue(color);
mRgb = color;
mPopulation = population;
}
Swatch(int red, int green, int blue, int population) {
mRed = red;
mGreen = green;
mBlue = blue;
mRgb = Color.rgb(red, green, blue);
mPopulation = population;
}
Swatch(float[] hsl, int population) {
this(ColorUtils.HSLToColor(hsl), population);
mHsl = hsl;
}
/**
* @return this swatch's RGB color value
*/
@ColorInt
public int getRgb() {
return mRgb;
}
/**
* Return this swatch's HSL values.
* hsv[0] is Hue [0 .. 360)
* hsv[1] is Saturation [0...1]
* hsv[2] is Lightness [0...1]
*/
public float[] getHsl() {
if (mHsl == null) {
mHsl = new float[3];
}
ColorUtils.RGBToHSL(mRed, mGreen, mBlue, mHsl);
return mHsl;
}
/**
* @return the number of pixels represented by this swatch
*/
public int getPopulation() {
return mPopulation;
}
/**
* Returns an appropriate color to use for any 'title' text which is displayed over this
* {@link Palette.Swatch}'s color. This color is guaranteed to have sufficient contrast.
*/
@ColorInt
public int getTitleTextColor() {
ensureTextColorsGenerated();
return mTitleTextColor;
}
/**
* Returns an appropriate color to use for any 'body' text which is displayed over this
* {@link Palette.Swatch}'s color. This color is guaranteed to have sufficient contrast.
*/
@ColorInt
public int getBodyTextColor() {
ensureTextColorsGenerated();
return mBodyTextColor;
}
private void ensureTextColorsGenerated() {
if (!mGeneratedTextColors) {
// First check white, as most colors will be dark
final int lightBodyAlpha = ColorUtils.calculateMinimumAlpha(
Color.WHITE, mRgb, MIN_CONTRAST_BODY_TEXT);
final int lightTitleAlpha = ColorUtils.calculateMinimumAlpha(
Color.WHITE, mRgb, MIN_CONTRAST_TITLE_TEXT);
if (lightBodyAlpha != -1 && lightTitleAlpha != -1) {
// If we found valid light values, use them and return
mBodyTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha);
mTitleTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha);
mGeneratedTextColors = true;
return;
}
final int darkBodyAlpha = ColorUtils.calculateMinimumAlpha(
Color.BLACK, mRgb, MIN_CONTRAST_BODY_TEXT);
final int darkTitleAlpha = ColorUtils.calculateMinimumAlpha(
Color.BLACK, mRgb, MIN_CONTRAST_TITLE_TEXT);
if (darkBodyAlpha != -1 && darkTitleAlpha != -1) {
// If we found valid dark values, use them and return
mBodyTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha);
mTitleTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha);
mGeneratedTextColors = true;
return;
}
// If we reach here then we can not find title and body values which use the same
// lightness, we need to use mismatched values
mBodyTextColor = lightBodyAlpha != -1
? ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha)
: ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha);
mTitleTextColor = lightTitleAlpha != -1
? ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha)
: ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha);
mGeneratedTextColors = true;
}
}
@Override
public String toString() {
return new StringBuilder(getClass().getSimpleName())
.append(" [RGB: #").append(Integer.toHexString(getRgb())).append(']')
.append(" [HSL: ").append(Arrays.toString(getHsl())).append(']')
.append(" [Population: ").append(mPopulation).append(']')
.append(" [Title Text: #").append(Integer.toHexString(getTitleTextColor()))
.append(']')
.append(" [Body Text: #").append(Integer.toHexString(getBodyTextColor()))
.append(']').toString();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Palette.Swatch
swatch = (Palette.Swatch) o;
return mPopulation == swatch.mPopulation && mRgb == swatch.mRgb;
}
@Override
public int hashCode() {
return 31 * mRgb + mPopulation;
}
}
/**
* Builder class for generating {@link Palette} instances.
*/
public static final class Builder {
private final List<Palette.Swatch> mSwatches;
private final Bitmap mBitmap;
private final List<Target> mTargets = new ArrayList<>();
private int mMaxColors = DEFAULT_CALCULATE_NUMBER_COLORS;
private int mResizeArea = DEFAULT_RESIZE_BITMAP_AREA;
private int mResizeMaxDimension = -1;
private final List<Palette.Filter> mFilters = new ArrayList<>();
private Rect mRegion;
/**
* Construct a new {@link Palette.Builder} using a source {@link Bitmap}
*/
public Builder(Bitmap bitmap) {
if (bitmap == null || bitmap.isRecycled()) {
throw new IllegalArgumentException("Bitmap is not valid");
}
mFilters.add(DEFAULT_FILTER);
mBitmap = bitmap;
mSwatches = null;
// Add the default targets
mTargets.add(Target.LIGHT_VIBRANT);
mTargets.add(Target.VIBRANT);
mTargets.add(Target.DARK_VIBRANT);
mTargets.add(Target.LIGHT_MUTED);
mTargets.add(Target.MUTED);
mTargets.add(Target.DARK_MUTED);
}
/**
* Construct a new {@link Palette.Builder} using a list of {@link Palette.Swatch} instances.
* Typically only used for testing.
*/
public Builder(List<Palette.Swatch> swatches) {
if (swatches == null || swatches.isEmpty()) {
throw new IllegalArgumentException("List of Swatches is not valid");
}
mFilters.add(DEFAULT_FILTER);
mSwatches = swatches;
mBitmap = null;
}
/**
* 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.
*/
@NonNull
public Palette.Builder maximumColorCount(int colors) {
mMaxColors = colors;
return this;
}
/**
* Set the resize value when using a {@link android.graphics.Bitmap} as the source.
* If the bitmap's largest dimension is greater than the value specified, then the bitmap
* will be resized so that its largest dimension matches {@code maxDimension}. If the
* bitmap is smaller or equal, the original is used as-is.
*
* @deprecated Using {@link #resizeBitmapArea(int)} is preferred since it can handle
* abnormal aspect ratios more gracefully.
*
* @param maxDimension the number of pixels that the max dimension should be scaled down to,
* or any value <= 0 to disable resizing.
*/
@NonNull
@Deprecated
public Palette.Builder resizeBitmapSize(final int maxDimension) {
mResizeMaxDimension = maxDimension;
mResizeArea = -1;
return this;
}
/**
* Set the resize value when using a {@link android.graphics.Bitmap} as the source.
* If the bitmap's area is greater than the value specified, then the bitmap
* will be resized so that its area matches {@code area}. If the
* bitmap is smaller or equal, the original is used as-is.
* <p>
* This value has a large effect on the processing time. The larger the resized image is,
* the greater time it will take to generate the palette. The smaller the image is, the
* more detail is lost in the resulting image and thus less precision for color selection.
*
* @param area the number of pixels that the intermediary scaled down Bitmap should cover,
* or any value <= 0 to disable resizing.
*/
@NonNull
public Palette.Builder resizeBitmapArea(final int area) {
mResizeArea = area;
mResizeMaxDimension = -1;
return this;
}
/**
* Clear all added filters. This includes any default filters added automatically by
* {@link Palette}.
*/
@NonNull
public Palette.Builder clearFilters() {
mFilters.clear();
return this;
}
/**
* Add a filter to be able to have fine grained control over which colors are
* allowed in the resulting palette.
*
* @param filter filter to add.
*/
@NonNull
public Palette.Builder addFilter(
Palette.Filter filter) {
if (filter != null) {
mFilters.add(filter);
}
return this;
}
/**
* Set a region of the bitmap to be used exclusively when calculating the palette.
* <p>This only works when the original input is a {@link Bitmap}.</p>
*
* @param left The left side of the rectangle used for the region.
* @param top The top of the rectangle used for the region.
* @param right The right side of the rectangle used for the region.
* @param bottom The bottom of the rectangle used for the region.
*/
@NonNull
public Palette.Builder setRegion(int left, int top, int right, int bottom) {
if (mBitmap != null) {
if (mRegion == null) mRegion = new Rect();
// Set the Rect to be initially the whole Bitmap
mRegion.set(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
// Now just get the intersection with the region
if (!mRegion.intersect(left, top, right, bottom)) {
throw new IllegalArgumentException("The given region must intersect with "
+ "the Bitmap's dimensions.");
}
}
return this;
}
/**
* Clear any previously region set via {@link #setRegion(int, int, int, int)}.
*/
@NonNull
public Palette.Builder clearRegion() {
mRegion = null;
return this;
}
/**
* Add a target profile to be generated in the palette.
*
* <p>You can retrieve the result via {@link Palette#getSwatchForTarget(Target)}.</p>
*/
@NonNull
public Palette.Builder addTarget(@NonNull final Target target) {
if (!mTargets.contains(target)) {
mTargets.add(target);
}
return this;
}
/**
* Clear all added targets. This includes any default targets added automatically by
* {@link Palette}.
*/
@NonNull
public Palette.Builder clearTargets() {
if (mTargets != null) {
mTargets.clear();
}
return this;
}
/**
* Generate and return the {@link Palette} synchronously.
*/
@NonNull
public Palette generate() {
final TimingLogger logger = LOG_TIMINGS
? new TimingLogger(LOG_TAG, "Generation")
: null;
List<Palette.Swatch> swatches;
if (mBitmap != null) {
// We have a Bitmap so we need to use quantization to reduce the number of colors
// First we'll scale down the bitmap if needed
final Bitmap bitmap = scaleBitmapDown(mBitmap);
if (logger != null) {
logger.addSplit("Processed Bitmap");
}
final Rect region = mRegion;
if (bitmap != mBitmap && region != null) {
// If we have a scaled bitmap and a selected region, we need to scale down the
// region to match the new scale
final double scale = bitmap.getWidth() / (double) mBitmap.getWidth();
region.left = (int) Math.floor(region.left * scale);
region.top = (int) Math.floor(region.top * scale);
region.right = Math.min((int) Math.ceil(region.right * scale),
bitmap.getWidth());
region.bottom = Math.min((int) Math.ceil(region.bottom * scale),
bitmap.getHeight());
}
// Now generate a quantizer from the Bitmap
final ColorCutQuantizer quantizer = new ColorCutQuantizer(
getPixelsFromBitmap(bitmap),
mMaxColors,
mFilters.isEmpty() ? null : mFilters.toArray(new Palette.Filter[mFilters.size()]));
// If created a new bitmap, recycle it
if (bitmap != mBitmap) {
bitmap.recycle();
}
swatches = quantizer.getQuantizedColors();
if (logger != null) {
logger.addSplit("Color quantization completed");
}
} else {
// Else we're using the provided swatches
swatches = mSwatches;
}
// Now create a Palette instance
final Palette p = new Palette(swatches, mTargets);
// And make it generate itself
p.generate();
if (logger != null) {
logger.addSplit("Created Palette");
logger.dumpToLog();
}
return p;
}
/**
* Generate the {@link Palette} asynchronously. The provided listener's
* {@link Palette.PaletteAsyncListener#onGenerated} method will be called with the palette when
* generated.
*/
@NonNull
public AsyncTask<Bitmap, Void, Palette> generate(final Palette.PaletteAsyncListener listener) {
if (listener == null) {
throw new IllegalArgumentException("listener can not be null");
}
return new AsyncTask<Bitmap, Void, Palette>() {
@Override
protected Palette doInBackground(Bitmap... params) {
try {
return generate();
} catch (Exception e) {
Log.e(LOG_TAG, "Exception thrown during async generate", e);
return null;
}
}
@Override
protected void onPostExecute(Palette colorExtractor) {
listener.onGenerated(colorExtractor);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mBitmap);
}
private int[] getPixelsFromBitmap(Bitmap bitmap) {
final int bitmapWidth = bitmap.getWidth();
final int bitmapHeight = bitmap.getHeight();
final int[] pixels = new int[bitmapWidth * bitmapHeight];
bitmap.getPixels(pixels, 0, bitmapWidth, 0, 0, bitmapWidth, bitmapHeight);
if (mRegion == null) {
// If we don't have a region, return all of the pixels
return pixels;
} else {
// If we do have a region, lets create a subset array containing only the region's
// pixels
final int regionWidth = mRegion.width();
final int regionHeight = mRegion.height();
// pixels contains all of the pixels, so we need to iterate through each row and
// copy the regions pixels into a new smaller array
final int[] subsetPixels = new int[regionWidth * regionHeight];
for (int row = 0; row < regionHeight; row++) {
System.arraycopy(pixels, ((row + mRegion.top) * bitmapWidth) + mRegion.left,
subsetPixels, row * regionWidth, regionWidth);
}
return subsetPixels;
}
}
/**
* Scale the bitmap down as needed.
*/
private Bitmap scaleBitmapDown(final Bitmap bitmap) {
double scaleRatio = -1;
if (mResizeArea > 0) {
final int bitmapArea = bitmap.getWidth() * bitmap.getHeight();
if (bitmapArea > mResizeArea) {
scaleRatio = Math.sqrt(mResizeArea / (double) bitmapArea);
}
} else if (mResizeMaxDimension > 0) {
final int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
if (maxDimension > mResizeMaxDimension) {
scaleRatio = mResizeMaxDimension / (double) maxDimension;
}
}
if (scaleRatio <= 0) {
// Scaling has been disabled or not needed so just return the Bitmap
return bitmap;
}
return Bitmap.createScaledBitmap(bitmap,
(int) Math.ceil(bitmap.getWidth() * scaleRatio),
(int) Math.ceil(bitmap.getHeight() * scaleRatio),
false);
}
}
/**
* 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 Palette.Builder#addFilter(Palette.Filter)
*/
boolean isAllowed(int rgb, float[] hsl);
}
/**
* The default filter.
*/
static final Palette.Filter
DEFAULT_FILTER = new Palette.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;
}
};
}

View File

@@ -0,0 +1,435 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.internal.graphics.palette;
/*
* 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.
*/
import android.annotation.FloatRange;
/**
* Copied from: frameworks/support/v7/palette/src/main/java/android/support/v7/graphics/Target.java
*
* A class which allows custom selection of colors in a {@link Palette}'s generation. Instances
* can be created via the {@link android.support.v7.graphics.Target.Builder} class.
*
* <p>To use the target, use the {@link Palette.Builder#addTarget(Target)} API when building a
* Palette.</p>
*/
public final class Target {
private static final float TARGET_DARK_LUMA = 0.26f;
private static final float MAX_DARK_LUMA = 0.45f;
private static final float MIN_LIGHT_LUMA = 0.55f;
private static final float TARGET_LIGHT_LUMA = 0.74f;
private static final float MIN_NORMAL_LUMA = 0.3f;
private static final float TARGET_NORMAL_LUMA = 0.5f;
private static final float MAX_NORMAL_LUMA = 0.7f;
private static final float TARGET_MUTED_SATURATION = 0.3f;
private static final float MAX_MUTED_SATURATION = 0.4f;
private static final float TARGET_VIBRANT_SATURATION = 1f;
private static final float MIN_VIBRANT_SATURATION = 0.35f;
private static final float WEIGHT_SATURATION = 0.24f;
private static final float WEIGHT_LUMA = 0.52f;
private static final float WEIGHT_POPULATION = 0.24f;
static final int INDEX_MIN = 0;
static final int INDEX_TARGET = 1;
static final int INDEX_MAX = 2;
static final int INDEX_WEIGHT_SAT = 0;
static final int INDEX_WEIGHT_LUMA = 1;
static final int INDEX_WEIGHT_POP = 2;
/**
* A target which has the characteristics of a vibrant color which is light in luminance.
*/
public static final Target LIGHT_VIBRANT;
/**
* A target which has the characteristics of a vibrant color which is neither light or dark.
*/
public static final Target VIBRANT;
/**
* A target which has the characteristics of a vibrant color which is dark in luminance.
*/
public static final Target DARK_VIBRANT;
/**
* A target which has the characteristics of a muted color which is light in luminance.
*/
public static final Target LIGHT_MUTED;
/**
* A target which has the characteristics of a muted color which is neither light or dark.
*/
public static final Target MUTED;
/**
* A target which has the characteristics of a muted color which is dark in luminance.
*/
public static final Target DARK_MUTED;
static {
LIGHT_VIBRANT = new Target();
setDefaultLightLightnessValues(LIGHT_VIBRANT);
setDefaultVibrantSaturationValues(LIGHT_VIBRANT);
VIBRANT = new Target();
setDefaultNormalLightnessValues(VIBRANT);
setDefaultVibrantSaturationValues(VIBRANT);
DARK_VIBRANT = new Target();
setDefaultDarkLightnessValues(DARK_VIBRANT);
setDefaultVibrantSaturationValues(DARK_VIBRANT);
LIGHT_MUTED = new Target();
setDefaultLightLightnessValues(LIGHT_MUTED);
setDefaultMutedSaturationValues(LIGHT_MUTED);
MUTED = new Target();
setDefaultNormalLightnessValues(MUTED);
setDefaultMutedSaturationValues(MUTED);
DARK_MUTED = new Target();
setDefaultDarkLightnessValues(DARK_MUTED);
setDefaultMutedSaturationValues(DARK_MUTED);
}
final float[] mSaturationTargets = new float[3];
final float[] mLightnessTargets = new float[3];
final float[] mWeights = new float[3];
boolean mIsExclusive = true; // default to true
Target() {
setTargetDefaultValues(mSaturationTargets);
setTargetDefaultValues(mLightnessTargets);
setDefaultWeights();
}
Target(Target from) {
System.arraycopy(from.mSaturationTargets, 0, mSaturationTargets, 0,
mSaturationTargets.length);
System.arraycopy(from.mLightnessTargets, 0, mLightnessTargets, 0,
mLightnessTargets.length);
System.arraycopy(from.mWeights, 0, mWeights, 0, mWeights.length);
}
/**
* The minimum saturation value for this target.
*/
@FloatRange(from = 0, to = 1)
public float getMinimumSaturation() {
return mSaturationTargets[INDEX_MIN];
}
/**
* The target saturation value for this target.
*/
@FloatRange(from = 0, to = 1)
public float getTargetSaturation() {
return mSaturationTargets[INDEX_TARGET];
}
/**
* The maximum saturation value for this target.
*/
@FloatRange(from = 0, to = 1)
public float getMaximumSaturation() {
return mSaturationTargets[INDEX_MAX];
}
/**
* The minimum lightness value for this target.
*/
@FloatRange(from = 0, to = 1)
public float getMinimumLightness() {
return mLightnessTargets[INDEX_MIN];
}
/**
* The target lightness value for this target.
*/
@FloatRange(from = 0, to = 1)
public float getTargetLightness() {
return mLightnessTargets[INDEX_TARGET];
}
/**
* The maximum lightness value for this target.
*/
@FloatRange(from = 0, to = 1)
public float getMaximumLightness() {
return mLightnessTargets[INDEX_MAX];
}
/**
* Returns the weight of importance that this target places on a color's saturation within
* the image.
*
* <p>The larger the weight, relative to the other weights, the more important that a color
* being close to the target value has on selection.</p>
*
* @see #getTargetSaturation()
*/
public float getSaturationWeight() {
return mWeights[INDEX_WEIGHT_SAT];
}
/**
* Returns the weight of importance that this target places on a color's lightness within
* the image.
*
* <p>The larger the weight, relative to the other weights, the more important that a color
* being close to the target value has on selection.</p>
*
* @see #getTargetLightness()
*/
public float getLightnessWeight() {
return mWeights[INDEX_WEIGHT_LUMA];
}
/**
* Returns the weight of importance that this target places on a color's population within
* the image.
*
* <p>The larger the weight, relative to the other weights, the more important that a
* color's population being close to the most populous has on selection.</p>
*/
public float getPopulationWeight() {
return mWeights[INDEX_WEIGHT_POP];
}
/**
* Returns whether any color selected for this target is exclusive for this target only.
*
* <p>If false, then the color can be selected for other targets.</p>
*/
public boolean isExclusive() {
return mIsExclusive;
}
private static void setTargetDefaultValues(final float[] values) {
values[INDEX_MIN] = 0f;
values[INDEX_TARGET] = 0.5f;
values[INDEX_MAX] = 1f;
}
private void setDefaultWeights() {
mWeights[INDEX_WEIGHT_SAT] = WEIGHT_SATURATION;
mWeights[INDEX_WEIGHT_LUMA] = WEIGHT_LUMA;
mWeights[INDEX_WEIGHT_POP] = WEIGHT_POPULATION;
}
void normalizeWeights() {
float sum = 0;
for (int i = 0, z = mWeights.length; i < z; i++) {
float weight = mWeights[i];
if (weight > 0) {
sum += weight;
}
}
if (sum != 0) {
for (int i = 0, z = mWeights.length; i < z; i++) {
if (mWeights[i] > 0) {
mWeights[i] /= sum;
}
}
}
}
private static void setDefaultDarkLightnessValues(Target target) {
target.mLightnessTargets[INDEX_TARGET] = TARGET_DARK_LUMA;
target.mLightnessTargets[INDEX_MAX] = MAX_DARK_LUMA;
}
private static void setDefaultNormalLightnessValues(Target target) {
target.mLightnessTargets[INDEX_MIN] = MIN_NORMAL_LUMA;
target.mLightnessTargets[INDEX_TARGET] = TARGET_NORMAL_LUMA;
target.mLightnessTargets[INDEX_MAX] = MAX_NORMAL_LUMA;
}
private static void setDefaultLightLightnessValues(Target target) {
target.mLightnessTargets[INDEX_MIN] = MIN_LIGHT_LUMA;
target.mLightnessTargets[INDEX_TARGET] = TARGET_LIGHT_LUMA;
}
private static void setDefaultVibrantSaturationValues(Target target) {
target.mSaturationTargets[INDEX_MIN] = MIN_VIBRANT_SATURATION;
target.mSaturationTargets[INDEX_TARGET] = TARGET_VIBRANT_SATURATION;
}
private static void setDefaultMutedSaturationValues(Target target) {
target.mSaturationTargets[INDEX_TARGET] = TARGET_MUTED_SATURATION;
target.mSaturationTargets[INDEX_MAX] = MAX_MUTED_SATURATION;
}
/**
* Builder class for generating custom {@link Target} instances.
*/
public final static class Builder {
private final Target mTarget;
/**
* Create a new {@link Target} builder from scratch.
*/
public Builder() {
mTarget = new Target();
}
/**
* Create a new builder based on an existing {@link Target}.
*/
public Builder(Target target) {
mTarget = new Target(target);
}
/**
* Set the minimum saturation value for this target.
*/
public Target.Builder setMinimumSaturation(@FloatRange(from = 0, to = 1) float value) {
mTarget.mSaturationTargets[INDEX_MIN] = value;
return this;
}
/**
* Set the target/ideal saturation value for this target.
*/
public Target.Builder setTargetSaturation(@FloatRange(from = 0, to = 1) float value) {
mTarget.mSaturationTargets[INDEX_TARGET] = value;
return this;
}
/**
* Set the maximum saturation value for this target.
*/
public Target.Builder setMaximumSaturation(@FloatRange(from = 0, to = 1) float value) {
mTarget.mSaturationTargets[INDEX_MAX] = value;
return this;
}
/**
* Set the minimum lightness value for this target.
*/
public Target.Builder setMinimumLightness(@FloatRange(from = 0, to = 1) float value) {
mTarget.mLightnessTargets[INDEX_MIN] = value;
return this;
}
/**
* Set the target/ideal lightness value for this target.
*/
public Target.Builder setTargetLightness(@FloatRange(from = 0, to = 1) float value) {
mTarget.mLightnessTargets[INDEX_TARGET] = value;
return this;
}
/**
* Set the maximum lightness value for this target.
*/
public Target.Builder setMaximumLightness(@FloatRange(from = 0, to = 1) float value) {
mTarget.mLightnessTargets[INDEX_MAX] = value;
return this;
}
/**
* Set the weight of importance that this target will place on saturation values.
*
* <p>The larger the weight, relative to the other weights, the more important that a color
* being close to the target value has on selection.</p>
*
* <p>A weight of 0 means that it has no weight, and thus has no
* bearing on the selection.</p>
*
* @see #setTargetSaturation(float)
*/
public Target.Builder setSaturationWeight(@FloatRange(from = 0) float weight) {
mTarget.mWeights[INDEX_WEIGHT_SAT] = weight;
return this;
}
/**
* Set the weight of importance that this target will place on lightness values.
*
* <p>The larger the weight, relative to the other weights, the more important that a color
* being close to the target value has on selection.</p>
*
* <p>A weight of 0 means that it has no weight, and thus has no
* bearing on the selection.</p>
*
* @see #setTargetLightness(float)
*/
public Target.Builder setLightnessWeight(@FloatRange(from = 0) float weight) {
mTarget.mWeights[INDEX_WEIGHT_LUMA] = weight;
return this;
}
/**
* Set the weight of importance that this target will place on a color's population within
* the image.
*
* <p>The larger the weight, relative to the other weights, the more important that a
* color's population being close to the most populous has on selection.</p>
*
* <p>A weight of 0 means that it has no weight, and thus has no
* bearing on the selection.</p>
*/
public Target.Builder setPopulationWeight(@FloatRange(from = 0) float weight) {
mTarget.mWeights[INDEX_WEIGHT_POP] = weight;
return this;
}
/**
* Set whether any color selected for this target is exclusive to this target only.
* Defaults to true.
*
* @param exclusive true if any the color is exclusive to this target, or false is the
* color can be selected for other targets.
*/
public Target.Builder setExclusive(boolean exclusive) {
mTarget.mIsExclusive = exclusive;
return this;
}
/**
* Builds and returns the resulting {@link Target}.
*/
public Target build() {
return mTarget;
}
}
}