summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author TreeHugger Robot <treehugger-gerrit@google.com> 2017-04-19 17:24:02 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2017-04-19 17:24:07 +0000
commit8b4cca11f3d9cf58ee0c005e66d811d233e79d21 (patch)
treec427dc158d204bfe41bc827a0f4b8f143a93aa12
parent7790695bf2f44a0f9bbfe304a86bd0da05335339 (diff)
parent440e8e9dbc4ed4ecb20284607251f746833cd472 (diff)
Merge "Internal copy of Palette API." into oc-dev
-rw-r--r--core/java/com/android/internal/graphics/ColorUtils.java618
-rw-r--r--core/java/com/android/internal/graphics/palette/ColorCutQuantizer.java535
-rw-r--r--core/java/com/android/internal/graphics/palette/Palette.java990
-rw-r--r--core/java/com/android/internal/graphics/palette/Target.java435
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