diff options
author | 2017-04-19 17:24:02 +0000 | |
---|---|---|
committer | 2017-04-19 17:24:07 +0000 | |
commit | 8b4cca11f3d9cf58ee0c005e66d811d233e79d21 (patch) | |
tree | c427dc158d204bfe41bc827a0f4b8f143a93aa12 | |
parent | 7790695bf2f44a0f9bbfe304a86bd0da05335339 (diff) | |
parent | 440e8e9dbc4ed4ecb20284607251f746833cd472 (diff) |
Merge "Internal copy of Palette API." into oc-dev
4 files changed, 2578 insertions, 0 deletions
diff --git a/core/java/com/android/internal/graphics/ColorUtils.java b/core/java/com/android/internal/graphics/ColorUtils.java new file mode 100644 index 000000000000..6c1efa43ac86 --- /dev/null +++ b/core/java/com/android/internal/graphics/ColorUtils.java @@ -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; + } + +}
\ No newline at end of file diff --git a/core/java/com/android/internal/graphics/palette/ColorCutQuantizer.java b/core/java/com/android/internal/graphics/palette/ColorCutQuantizer.java new file mode 100644 index 000000000000..2d0ad660de8a --- /dev/null +++ b/core/java/com/android/internal/graphics/palette/ColorCutQuantizer.java @@ -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); + } + +}
\ No newline at end of file diff --git a/core/java/com/android/internal/graphics/palette/Palette.java b/core/java/com/android/internal/graphics/palette/Palette.java new file mode 100644 index 000000000000..9f1504a0495c --- /dev/null +++ b/core/java/com/android/internal/graphics/palette/Palette.java @@ -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; + } + }; +} diff --git a/core/java/com/android/internal/graphics/palette/Target.java b/core/java/com/android/internal/graphics/palette/Target.java new file mode 100644 index 000000000000..0540d80ef6f0 --- /dev/null +++ b/core/java/com/android/internal/graphics/palette/Target.java @@ -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; + } + } + +}
\ No newline at end of file |