/*
 * 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 android.app;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Size;

import com.android.internal.graphics.ColorUtils;
import com.android.internal.graphics.palette.Palette;
import com.android.internal.graphics.palette.VariationalKMeansQuantizer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

Provides information about the colors of a wallpaper.

Exposes the 3 most visually representative colors of a wallpaper. Can be either getPrimaryColor(), getSecondaryColor() or getTertiaryColor().

/** * Provides information about the colors of a wallpaper. * <p> * Exposes the 3 most visually representative colors of a wallpaper. Can be either * {@link WallpaperColors#getPrimaryColor()}, {@link WallpaperColors#getSecondaryColor()} * or {@link WallpaperColors#getTertiaryColor()}. */
public final class WallpaperColors implements Parcelable {
Specifies that dark text is preferred over the current wallpaper for best presentation.

eg. A launcher may set its text color to black if this flag is specified.

@hide
/** * Specifies that dark text is preferred over the current wallpaper for best presentation. * <p> * eg. A launcher may set its text color to black if this flag is specified. * @hide */
public static final int HINT_SUPPORTS_DARK_TEXT = 1 << 0;
Specifies that dark theme is preferred over the current wallpaper for best presentation.

eg. A launcher may set its drawer color to black if this flag is specified.

@hide
/** * Specifies that dark theme is preferred over the current wallpaper for best presentation. * <p> * eg. A launcher may set its drawer color to black if this flag is specified. * @hide */
public static final int HINT_SUPPORTS_DARK_THEME = 1 << 1;
Specifies that this object was generated by extracting colors from a bitmap.
@hide
/** * Specifies that this object was generated by extracting colors from a bitmap. * @hide */
public static final int HINT_FROM_BITMAP = 1 << 2; // Maximum size that a bitmap can have to keep our calculations sane private static final int MAX_BITMAP_SIZE = 112; // Even though we have a maximum size, we'll mainly match bitmap sizes // using the area instead. This way our comparisons are aspect ratio independent. private static final int MAX_WALLPAPER_EXTRACTION_AREA = MAX_BITMAP_SIZE * MAX_BITMAP_SIZE; // When extracting the main colors, only consider colors // present in at least MIN_COLOR_OCCURRENCE of the image private static final float MIN_COLOR_OCCURRENCE = 0.05f; // Decides when dark theme is optimal for this wallpaper private static final float DARK_THEME_MEAN_LUMINANCE = 0.25f; // Minimum mean luminosity that an image needs to have to support dark text private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = 0.75f; // We also check if the image has dark pixels in it, // to avoid bright images with some dark spots. private static final float DARK_PIXEL_LUMINANCE = 0.45f; private static final float MAX_DARK_AREA = 0.05f; private final ArrayList<Color> mMainColors; private int mColorHints; public WallpaperColors(Parcel parcel) { mMainColors = new ArrayList<>(); final int count = parcel.readInt(); for (int i = 0; i < count; i++) { final int colorInt = parcel.readInt(); Color color = Color.valueOf(colorInt); mMainColors.add(color); } mColorHints = parcel.readInt(); }
Constructs WallpaperColors from a drawable.

Main colors will be extracted from the drawable.

Params:
  • drawable – Source where to extract from.
/** * Constructs {@link WallpaperColors} from a drawable. * <p> * Main colors will be extracted from the drawable. * * @param drawable Source where to extract from. */
public static WallpaperColors fromDrawable(Drawable drawable) { if (drawable == null) { throw new IllegalArgumentException("Drawable cannot be null"); } Rect initialBounds = drawable.copyBounds(); int width = drawable.getIntrinsicWidth(); int height = drawable.getIntrinsicHeight(); // Some drawables do not have intrinsic dimensions if (width <= 0 || height <= 0) { width = MAX_BITMAP_SIZE; height = MAX_BITMAP_SIZE; } Size optimalSize = calculateOptimalSize(width, height); Bitmap bitmap = Bitmap.createBitmap(optimalSize.getWidth(), optimalSize.getHeight(), Bitmap.Config.ARGB_8888); final Canvas bmpCanvas = new Canvas(bitmap); drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); drawable.draw(bmpCanvas); final WallpaperColors colors = WallpaperColors.fromBitmap(bitmap); bitmap.recycle(); drawable.setBounds(initialBounds); return colors; }
Constructs WallpaperColors from a bitmap.

Main colors will be extracted from the bitmap.

Params:
  • bitmap – Source where to extract from.
/** * Constructs {@link WallpaperColors} from a bitmap. * <p> * Main colors will be extracted from the bitmap. * * @param bitmap Source where to extract from. */
public static WallpaperColors fromBitmap(@NonNull Bitmap bitmap) { if (bitmap == null) { throw new IllegalArgumentException("Bitmap can't be null"); } final int bitmapArea = bitmap.getWidth() * bitmap.getHeight(); boolean shouldRecycle = false; if (bitmapArea > MAX_WALLPAPER_EXTRACTION_AREA) { shouldRecycle = true; Size optimalSize = calculateOptimalSize(bitmap.getWidth(), bitmap.getHeight()); bitmap = Bitmap.createScaledBitmap(bitmap, optimalSize.getWidth(), optimalSize.getHeight(), true /* filter */); } final Palette palette = Palette .from(bitmap) .setQuantizer(new VariationalKMeansQuantizer()) .maximumColorCount(5) .clearFilters() .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA) .generate(); // Remove insignificant colors and sort swatches by population final ArrayList<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches()); final float minColorArea = bitmap.getWidth() * bitmap.getHeight() * MIN_COLOR_OCCURRENCE; swatches.removeIf(s -> s.getPopulation() < minColorArea); swatches.sort((a, b) -> b.getPopulation() - a.getPopulation()); final int swatchesSize = swatches.size(); Color primary = null, secondary = null, tertiary = null; swatchLoop: for (int i = 0; i < swatchesSize; i++) { Color color = Color.valueOf(swatches.get(i).getRgb()); switch (i) { case 0: primary = color; break; case 1: secondary = color; break; case 2: tertiary = color; break; default: // out of bounds break swatchLoop; } } int hints = calculateDarkHints(bitmap); if (shouldRecycle) { bitmap.recycle(); } return new WallpaperColors(primary, secondary, tertiary, HINT_FROM_BITMAP | hints); }
Constructs a new object from three colors.
Params:
  • primaryColor – Primary color.
  • secondaryColor – Secondary color.
  • tertiaryColor – Tertiary color.
See Also:
/** * Constructs a new object from three colors. * * @param primaryColor Primary color. * @param secondaryColor Secondary color. * @param tertiaryColor Tertiary color. * @see WallpaperColors#fromBitmap(Bitmap) * @see WallpaperColors#fromDrawable(Drawable) */
public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor, @Nullable Color tertiaryColor) { this(primaryColor, secondaryColor, tertiaryColor, 0); }
Constructs a new object from three colors, where hints can be specified.
Params:
  • primaryColor – Primary color.
  • secondaryColor – Secondary color.
  • tertiaryColor – Tertiary color.
  • colorHints – A combination of WallpaperColor hints.
See Also:
@hide
/** * Constructs a new object from three colors, where hints can be specified. * * @param primaryColor Primary color. * @param secondaryColor Secondary color. * @param tertiaryColor Tertiary color. * @param colorHints A combination of WallpaperColor hints. * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT * @see WallpaperColors#fromBitmap(Bitmap) * @see WallpaperColors#fromDrawable(Drawable) * @hide */
public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor, @Nullable Color tertiaryColor, int colorHints) { if (primaryColor == null) { throw new IllegalArgumentException("Primary color should never be null."); } mMainColors = new ArrayList<>(3); mMainColors.add(primaryColor); if (secondaryColor != null) { mMainColors.add(secondaryColor); } if (tertiaryColor != null) { if (secondaryColor == null) { throw new IllegalArgumentException("tertiaryColor can't be specified when " + "secondaryColor is null"); } mMainColors.add(tertiaryColor); } mColorHints = colorHints; } public static final Creator<WallpaperColors> CREATOR = new Creator<WallpaperColors>() { @Override public WallpaperColors createFromParcel(Parcel in) { return new WallpaperColors(in); } @Override public WallpaperColors[] newArray(int size) { return new WallpaperColors[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { List<Color> mainColors = getMainColors(); int count = mainColors.size(); dest.writeInt(count); for (int i = 0; i < count; i++) { Color color = mainColors.get(i); dest.writeInt(color.toArgb()); } dest.writeInt(mColorHints); }
Gets the most visually representative color of the wallpaper. "Visually representative" means easily noticeable in the image, probably happening at high frequency.
Returns:A color.
/** * Gets the most visually representative color of the wallpaper. * "Visually representative" means easily noticeable in the image, * probably happening at high frequency. * * @return A color. */
public @NonNull Color getPrimaryColor() { return mMainColors.get(0); }
Gets the second most preeminent color of the wallpaper. Can be null.
Returns:A color, may be null.
/** * Gets the second most preeminent color of the wallpaper. Can be null. * * @return A color, may be null. */
public @Nullable Color getSecondaryColor() { return mMainColors.size() < 2 ? null : mMainColors.get(1); }
Gets the third most preeminent color of the wallpaper. Can be null.
Returns:A color, may be null.
/** * Gets the third most preeminent color of the wallpaper. Can be null. * * @return A color, may be null. */
public @Nullable Color getTertiaryColor() { return mMainColors.size() < 3 ? null : mMainColors.get(2); }
List of most preeminent colors, sorted by importance.
Returns:List of colors.
@hide
/** * List of most preeminent colors, sorted by importance. * * @return List of colors. * @hide */
public @NonNull List<Color> getMainColors() { return Collections.unmodifiableList(mMainColors); } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } WallpaperColors other = (WallpaperColors) o; return mMainColors.equals(other.mMainColors) && mColorHints == other.mColorHints; } @Override public int hashCode() { return 31 * mMainColors.hashCode() + mColorHints; }
Combination of WallpaperColor hints.
See Also:
  • HINT_SUPPORTS_DARK_TEXT.HINT_SUPPORTS_DARK_TEXT
Returns:True if dark text is supported.
@hide
/** * Combination of WallpaperColor hints. * * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT * @return True if dark text is supported. * @hide */
public int getColorHints() { return mColorHints; }
Params:
  • colorHints – Combination of WallpaperColors hints.
See Also:
@hide
/** * @param colorHints Combination of WallpaperColors hints. * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT * @hide */
public void setColorHints(int colorHints) { mColorHints = colorHints; }
Checks if image is bright and clean enough to support light text.
Params:
  • source – What to read.
Returns:Whether image supports dark text or not.
/** * Checks if image is bright and clean enough to support light text. * * @param source What to read. * @return Whether image supports dark text or not. */
private static int calculateDarkHints(Bitmap source) { if (source == null) { return 0; } int[] pixels = new int[source.getWidth() * source.getHeight()]; double totalLuminance = 0; final int maxDarkPixels = (int) (pixels.length * MAX_DARK_AREA); int darkPixels = 0; source.getPixels(pixels, 0 /* offset */, source.getWidth(), 0 /* x */, 0 /* y */, source.getWidth(), source.getHeight()); // This bitmap was already resized to fit the maximum allowed area. // Let's just loop through the pixels, no sweat! float[] tmpHsl = new float[3]; for (int i = 0; i < pixels.length; i++) { ColorUtils.colorToHSL(pixels[i], tmpHsl); final float luminance = tmpHsl[2]; final int alpha = Color.alpha(pixels[i]); // Make sure we don't have a dark pixel mass that will // make text illegible. if (luminance < DARK_PIXEL_LUMINANCE && alpha != 0) { darkPixels++; } totalLuminance += luminance; } int hints = 0; double meanLuminance = totalLuminance / pixels.length; if (meanLuminance > BRIGHT_IMAGE_MEAN_LUMINANCE && darkPixels < maxDarkPixels) { hints |= HINT_SUPPORTS_DARK_TEXT; } if (meanLuminance < DARK_THEME_MEAN_LUMINANCE) { hints |= HINT_SUPPORTS_DARK_THEME; } return hints; } private static Size calculateOptimalSize(int width, int height) { // Calculate how big the bitmap needs to be. // This avoids unnecessary processing and allocation inside Palette. final int requestedArea = width * height; double scale = 1; if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) { scale = Math.sqrt(MAX_WALLPAPER_EXTRACTION_AREA / (double) requestedArea); } int newWidth = (int) (width * scale); int newHeight = (int) (height * scale); // Dealing with edge cases of the drawable being too wide or too tall. // Width or height would end up being 0, in this case we'll set it to 1. if (newWidth == 0) { newWidth = 1; } if (newHeight == 0) { newHeight = 1; } return new Size(newWidth, newHeight); } @Override public String toString() { final StringBuilder colors = new StringBuilder(); for (int i = 0; i < mMainColors.size(); i++) { colors.append(Integer.toHexString(mMainColors.get(i).toArgb())).append(" "); } return "[WallpaperColors: " + colors.toString() + "h: " + mColorHints + "]"; } }