/* * Copyright 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v7.graphics; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Rect; import android.os.AsyncTask; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.graphics.ColorUtils; import android.support.v4.os.AsyncTaskCompat; import android.support.v4.util.ArrayMap; import android.util.Log; import android.util.SparseBooleanArray; import android.util.TimingLogger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; /** * A helper class to extract prominent colors from an image. *

* A number of colors with different profiles are extracted from the image: *

* These can be retrieved from the appropriate getter method. * *

* Instances are created with a {@link Builder} which supports several options to tweak the * generated Palette. See that class' documentation for more information. *

* Generation should always be completed on a background thread, ideally the one in * which you load your image on. {@link Builder} supports both synchronous and asynchronous * generation: * *

 * // Synchronous
 * Palette p = Palette.from(bitmap).generate();
 *
 * // Asynchronous
 * Palette.from(bitmap).generate(new PaletteAsyncListener() {
 *     public void onGenerated(Palette p) {
 *         // Use generated instance
 *     }
 * });
 * 
*/ public final class Palette { /** * Listener to be used with {@link #generateAsync(Bitmap, PaletteAsyncListener)} or * {@link #generateAsync(Bitmap, int, PaletteAsyncListener)} */ public interface PaletteAsyncListener { /** * Called when the {@link Palette} has been generated. */ void onGenerated(Palette palette); } private static final int DEFAULT_RESIZE_BITMAP_AREA = 160 * 160; private static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16; private static final float MIN_CONTRAST_TITLE_TEXT = 3.0f; private static final float MIN_CONTRAST_BODY_TEXT = 4.5f; private static final String LOG_TAG = "Palette"; private static final boolean LOG_TIMINGS = false; /** * Start generating a {@link Palette} with the returned {@link Builder} instance. */ public static Builder from(Bitmap bitmap) { return new Builder(bitmap); } /** * Generate a {@link Palette} from the pre-generated list of {@link Palette.Swatch} swatches. * This is useful for testing, or if you want to resurrect a {@link Palette} instance from a * list of swatches. Will return null if the {@code swatches} is null. */ public static Palette from(List swatches) { return new Builder(swatches).generate(); } /** * @deprecated Use {@link Builder} to generate the Palette. */ @Deprecated public static Palette generate(Bitmap bitmap) { return from(bitmap).generate(); } /** * @deprecated Use {@link Builder} to generate the Palette. */ @Deprecated public static Palette generate(Bitmap bitmap, int numColors) { return from(bitmap).maximumColorCount(numColors).generate(); } /** * @deprecated Use {@link Builder} to generate the Palette. */ @Deprecated public static AsyncTask generateAsync( Bitmap bitmap, PaletteAsyncListener listener) { return from(bitmap).generate(listener); } /** * @deprecated Use {@link Builder} to generate the Palette. */ @Deprecated public static AsyncTask generateAsync( final Bitmap bitmap, final int numColors, final PaletteAsyncListener listener) { return from(bitmap).maximumColorCount(numColors).generate(listener); } private final List mSwatches; private final List mTargets; private final Map mSelectedSwatches; private final SparseBooleanArray mUsedColors; private final int mMaxPopulation; private Palette(List swatches, List targets) { mSwatches = swatches; mTargets = targets; mUsedColors = new SparseBooleanArray(); mSelectedSwatches = new ArrayMap<>(); mMaxPopulation = findMaxPopulation(); } /** * Returns all of the swatches which make up the palette. */ @NonNull public List getSwatches() { return Collections.unmodifiableList(mSwatches); } /** * Returns the targets used to generate this palette. */ @NonNull public List getTargets() { return Collections.unmodifiableList(mTargets); } /** * Returns the most vibrant swatch in the palette. Might be null. * * @see Target#VIBRANT */ @Nullable public Swatch getVibrantSwatch() { return getSwatchForTarget(Target.VIBRANT); } /** * Returns a light and vibrant swatch from the palette. Might be null. * * @see Target#LIGHT_VIBRANT */ @Nullable public 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 Swatch getDarkVibrantSwatch() { return getSwatchForTarget(Target.DARK_VIBRANT); } /** * Returns a muted swatch from the palette. Might be null. * * @see Target#MUTED */ @Nullable public Swatch getMutedSwatch() { return getSwatchForTarget(Target.MUTED); } /** * Returns a muted and light swatch from the palette. Might be null. * * @see Target#LIGHT_MUTED */ @Nullable public 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 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 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) { Swatch swatch = getSwatchForTarget(target); return swatch != null ? swatch.getRgb() : defaultColor; } private 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 Swatch generateScoredTarget(final Target target) { final 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 Swatch getMaxScoredSwatchForTarget(final Target target) { float maxScore = 0; Swatch maxScoreSwatch = null; for (int i = 0, count = mSwatches.size(); i < count; i++) { final 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 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(Swatch swatch, Target target) { final float[] hsl = swatch.getHsl(); float saturationScore = 0; float luminanceScore = 0; float populationScore = 0; 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) mMaxPopulation); } return saturationScore + luminanceScore + populationScore; } private int findMaxPopulation() { int max = 0; for (int i = 0, count = mSwatches.size(); i < count; i++) { max = Math.max(mSwatches.get(i).getPopulation(), max); } return max; } private static float[] copyHslValues(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 Swatch}'s color. This color is guaranteed to have sufficient contrast. */ @ColorInt public int getTitleTextColor() { ensureTextColorsGenerated(); return mTitleTextColor; } /** * Returns an appropriate color to use for any 'body' text which is displayed over this * {@link Swatch}'s color. This color is guaranteed to have sufficient contrast. */ @ColorInt public int getBodyTextColor() { ensureTextColorsGenerated(); return mBodyTextColor; } private void ensureTextColorsGenerated() { if (!mGeneratedTextColors) { // First check white, as most colors will be dark final int lightBodyAlpha = ColorUtils.calculateMinimumAlpha( Color.WHITE, mRgb, MIN_CONTRAST_BODY_TEXT); final int lightTitleAlpha = ColorUtils.calculateMinimumAlpha( Color.WHITE, mRgb, MIN_CONTRAST_TITLE_TEXT); if (lightBodyAlpha != -1 && lightTitleAlpha != -1) { // If we found valid light values, use them and return mBodyTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha); mTitleTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha); mGeneratedTextColors = true; return; } final int darkBodyAlpha = ColorUtils.calculateMinimumAlpha( Color.BLACK, mRgb, MIN_CONTRAST_BODY_TEXT); final int darkTitleAlpha = ColorUtils.calculateMinimumAlpha( Color.BLACK, mRgb, MIN_CONTRAST_TITLE_TEXT); if (darkBodyAlpha != -1 && darkBodyAlpha != -1) { // If we found valid dark values, use them and return mBodyTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha); mTitleTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha); mGeneratedTextColors = true; return; } // If we reach here then we can not find title and body values which use the same // lightness, we need to use mismatched values mBodyTextColor = lightBodyAlpha != -1 ? ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha) : ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha); mTitleTextColor = lightTitleAlpha != -1 ? ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha) : ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha); mGeneratedTextColors = true; } } @Override public String toString() { return new StringBuilder(getClass().getSimpleName()) .append(" [RGB: #").append(Integer.toHexString(getRgb())).append(']') .append(" [HSL: ").append(Arrays.toString(getHsl())).append(']') .append(" [Population: ").append(mPopulation).append(']') .append(" [Title Text: #").append(Integer.toHexString(getTitleTextColor())) .append(']') .append(" [Body Text: #").append(Integer.toHexString(getBodyTextColor())) .append(']').toString(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Swatch swatch = (Swatch) o; return mPopulation == swatch.mPopulation && mRgb == swatch.mRgb; } @Override public int hashCode() { return 31 * mRgb + mPopulation; } } /** * Builder class for generating {@link Palette} instances. */ public static final class Builder { private final List mSwatches; private final Bitmap mBitmap; private final List mTargets = new ArrayList<>(); private int mMaxColors = DEFAULT_CALCULATE_NUMBER_COLORS; private int mResizeArea = DEFAULT_RESIZE_BITMAP_AREA; private int mResizeMaxDimension = -1; private final List mFilters = new ArrayList<>(); private Rect mRegion; /** * Construct a new {@link 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 Builder} using a list of {@link Swatch} instances. * Typically only used for testing. */ public Builder(List 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. *

* 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 Builder maximumColorCount(int colors) { mMaxColors = colors; return this; } /** * Set the resize value when using a {@link android.graphics.Bitmap} as the source. * If the bitmap's largest dimension is greater than the value specified, then the bitmap * will be resized so that it's largest dimension matches {@code maxDimension}. If the * bitmap is smaller or equal, the original is used as-is. * * @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 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 it's area matches {@code area}. If the * bitmap is smaller or equal, the original is used as-is. *

* 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 intemediary scaled down Bitmap should cover, * or any value <= 0 to disable resizing. */ @NonNull public 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 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 Builder addFilter(Filter filter) { if (filter != null) { mFilters.add(filter); } return this; } /** * Set a region of the bitmap to be used exclusively when calculating the palette. *

This only works when the original input is a {@link Bitmap}.

* * @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 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 Builder clearRegion() { mRegion = null; return this; } /** * Add a target profile to be generated in the palette. * *

You can retrieve the result via {@link Palette#getSwatchForTarget(Target)}.

*/ @NonNull public 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 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 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 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 PaletteAsyncListener#onGenerated} method will be called with the palette when * generated. */ @NonNull public AsyncTask generate(final PaletteAsyncListener listener) { if (listener == null) { throw new IllegalArgumentException("listener can not be null"); } return AsyncTaskCompat.executeParallel( new AsyncTask() { @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); } }, 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 = 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 Builder#addFilter(Filter) */ boolean isAllowed(int rgb, float[] hsl); } /** * The default filter. */ private static final Filter DEFAULT_FILTER = new Filter() { private static final float BLACK_MAX_LIGHTNESS = 0.05f; private static final float WHITE_MIN_LIGHTNESS = 0.95f; @Override public boolean isAllowed(int rgb, float[] hsl) { return !isWhite(hsl) && !isBlack(hsl) && !isNearRedILine(hsl); } /** * @return true if the color represents a color which is close to black. */ private boolean isBlack(float[] hslColor) { return hslColor[2] <= BLACK_MAX_LIGHTNESS; } /** * @return true if the color represents a color which is close to white. */ private boolean isWhite(float[] hslColor) { return hslColor[2] >= WHITE_MIN_LIGHTNESS; } /** * @return true if the color lies close to the red side of the I line. */ private boolean isNearRedILine(float[] hslColor) { return hslColor[0] >= 10f && hslColor[0] <= 37f && hslColor[1] <= 0.82f; } }; }