Merge "Internal copy of Palette API." into oc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
8b4cca11f3
618
core/java/com/android/internal/graphics/ColorUtils.java
Normal file
618
core/java/com/android/internal/graphics/ColorUtils.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
990
core/java/com/android/internal/graphics/palette/Palette.java
Normal file
990
core/java/com/android/internal/graphics/palette/Palette.java
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
435
core/java/com/android/internal/graphics/palette/Target.java
Normal file
435
core/java/com/android/internal/graphics/palette/Target.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user