diff options
| author | TreeHugger Robot <treehugger-gerrit@google.com> | 2017-06-16 20:04:53 +0000 |
|---|---|---|
| committer | Android (Google) Code Review <android-gerrit@google.com> | 2017-06-16 20:05:03 +0000 |
| commit | 04d2ec59e8ba742917f96b0cd3da0d4151cb80c8 (patch) | |
| tree | 5d9bcd65f3599f409a8b8eb39e8188ca5366484c /core/java/android | |
| parent | 963a20772e6bd0b48674157bf61bed36fe31294c (diff) | |
| parent | 84b89d9d59797483a7e4a1bf82f3819d81e696e9 (diff) | |
Merge "WallpaperColors refactor" into oc-dr1-dev
Diffstat (limited to 'core/java/android')
| -rw-r--r-- | core/java/android/app/WallpaperColors.java | 337 | ||||
| -rw-r--r-- | core/java/android/service/wallpaper/WallpaperService.java | 29 |
2 files changed, 298 insertions, 68 deletions
diff --git a/core/java/android/app/WallpaperColors.java b/core/java/android/app/WallpaperColors.java index b3c70a49f660..23e9ca5c32ae 100644 --- a/core/java/android/app/WallpaperColors.java +++ b/core/java/android/app/WallpaperColors.java @@ -16,63 +16,203 @@ 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.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; +import android.util.Size; -import android.util.Pair; +import com.android.internal.graphics.palette.Palette; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** - * A class containing information about the colors of a wallpaper. + * Provides information about the colors of a wallpaper. + * <p> + * This class contains two main components: + * <ul> + * <li>Named colors: Most visually representative colors of a wallpaper. Can be either + * {@link WallpaperColors#getPrimaryColor()}, {@link WallpaperColors#getSecondaryColor()} + * or {@link WallpaperColors#getTertiaryColor()}. + * </li> + * <li>Hints: How colors may affect other system components. Currently the only supported hint is + * {@link WallpaperColors#HINT_SUPPORTS_DARK_TEXT}, which specifies if dark text is preferred + * over the wallpaper.</li> + * </ul> */ public final class WallpaperColors implements Parcelable { - private static final float BRIGHT_LUMINANCE = 0.9f; - private final List<Pair<Color, Integer>> mColors; - private final boolean mSupportsDarkText; + /** + * 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. + */ + public static final int HINT_SUPPORTS_DARK_TEXT = 0x1; + + // 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; + + // Minimum mean luminosity that an image needs to have to support dark text + private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = 0.9f; + // 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) { - mColors = new ArrayList<>(); - int count = parcel.readInt(); - for (int i=0; i < count; i++) { - Color color = Color.valueOf(parcel.readInt()); - int weight = parcel.readInt(); - mColors.add(new Pair<>(color, weight)); + 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); } - mSupportsDarkText = parcel.readBoolean(); + mColorHints = parcel.readInt(); } /** - * Wallpaper color details containing a list of colors and their weights, - * as if it were an histogram. - * This list can be extracted from a bitmap by the Palette API. + * Constructs {@link WallpaperColors} from a drawable. + * <p> + * Main colors will be extracted from the drawable and hints will be calculated. * - * Dark text support will be calculated internally based on the histogram. + * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT + * @param drawable Source where to extract from. + */ + public static WallpaperColors fromDrawable(Drawable drawable) { + 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(); + + return colors; + } + + /** + * Constructs {@link WallpaperColors} from a bitmap. + * <p> + * Main colors will be extracted from the bitmap and hints will be calculated. * - * @param colors list of pairs where each pair contains a color - * and number of occurrences/influence. + * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT + * @param bitmap Source where to extract from. */ - public WallpaperColors(List<Pair<Color, Integer>> colors) { - this(colors, calculateDarkTextSupport(colors)); + 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(); + if (bitmapArea > MAX_WALLPAPER_EXTRACTION_AREA) { + Size optimalSize = calculateOptimalSize(bitmap.getWidth(), bitmap.getHeight()); + Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, optimalSize.getWidth(), + optimalSize.getHeight(), true /* filter */); + bitmap.recycle(); + bitmap = scaledBitmap; + } + + final Palette palette = Palette + .from(bitmap) + .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 = 0; + if (calculateDarkTextSupport(bitmap)) { + hints |= HINT_SUPPORTS_DARK_TEXT; + } + return new WallpaperColors(primary, secondary, tertiary, hints); } /** - * Wallpaper color details containing a list of colors and their weights, - * as if it were an histogram. - * Explicit dark text support. + * Constructs a new object from three colors, where hints can be specified. * - * @param colors list of pairs where each pair contains a color - * and number of occurrences/influence. - * @param supportsDarkText can have dark text on top or not + * @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) */ - public WallpaperColors(List<Pair<Color, Integer>> colors, boolean supportsDarkText) { - if (colors == null) - colors = new ArrayList<>(); - mColors = colors; - mSupportsDarkText = supportsDarkText; + 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>() { @@ -94,21 +234,53 @@ public final class WallpaperColors implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - int count = mColors.size(); + List<Color> mainColors = getMainColors(); + int count = mainColors.size(); dest.writeInt(count); - for (Pair<Color, Integer> color : mColors) { - dest.writeInt(color.first.toArgb()); - dest.writeInt(color.second); + for (int i = 0; i < count; i++) { + Color color = mainColors.get(i); + dest.writeInt(color.toArgb()); } - dest.writeBoolean(mSupportsDarkText); + 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. + * + * @return A color. + */ + public @NonNull Color getPrimaryColor() { + return mMainColors.get(0); + } + + /** + * 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. + * + * @return A color, may be null. + */ + public @Nullable Color getTertiaryColor() { + return mMainColors.size() < 3 ? null : mMainColors.get(2); } /** - * List of colors with their occurrences. The bigger the int, the more relevant the color. - * @return list of colors paired with their weights. + * List of most preeminent colors, sorted by importance. + * + * @return List of colors. + * @hide */ - public List<Pair<Color, Integer>> getColors() { - return mColors; + public @NonNull List<Color> getMainColors() { + return Collections.unmodifiableList(mMainColors); } @Override @@ -118,38 +290,91 @@ public final class WallpaperColors implements Parcelable { } WallpaperColors other = (WallpaperColors) o; - return mColors.equals(other.mColors) && mSupportsDarkText == other.mSupportsDarkText; + return mMainColors.equals(other.mMainColors) + && mColorHints == other.mColorHints; } @Override public int hashCode() { - return 31 * mColors.hashCode() + (mSupportsDarkText ? 1 : 0); + return 31 * mMainColors.hashCode() + mColorHints; } /** - * Whether or not dark text is legible on top of this wallpaper. + * Combination of WallpaperColor hints. * - * @return true if dark text is supported + * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT + * @return True if dark text is supported. + */ + public int getColorHints() { + return mColorHints; + } + + /** + * @param colorHints Combination of WallpaperColors hints. + * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT + * @hide */ - public boolean supportsDarkText() { - return mSupportsDarkText; + public void setColorHints(int colorHints) { + mColorHints = colorHints; } - private static boolean calculateDarkTextSupport(List<Pair<Color, Integer>> colors) { - if (colors == null) { + /** + * 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 boolean calculateDarkTextSupport(Bitmap source) { + if (source == null) { return false; } - Pair<Color, Integer> mainColor = null; + 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()); - for (Pair<Color, Integer> color : colors) { - if (mainColor == null) { - mainColor = color; - } else if (color.second > mainColor.second) { - mainColor = color; + // This bitmap was already resized to fit the maximum allowed area. + // Let's just loop through the pixels, no sweat! + for (int i = 0; i < pixels.length; i++) { + final float luminance = Color.luminance(pixels[i]); + 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++; + if (darkPixels > maxDarkPixels) { + return false; + } } + + totalLuminance += luminance; + } + return totalLuminance / pixels.length > BRIGHT_IMAGE_MEAN_LUMINANCE; + } + + 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; } - return mainColor != null && - mainColor.first.luminance() > BRIGHT_LUMINANCE; + if (newHeight == 0) { + newHeight = 1; + } + + return new Size(newWidth, newHeight); } } diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index da6b1f5f460e..e4d3142ccefe 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -17,25 +17,19 @@ package android.service.wallpaper; import android.annotation.Nullable; -import android.app.WallpaperColors; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.util.MergedConfiguration; -import android.view.WindowInsets; - -import com.android.internal.R; -import com.android.internal.os.HandlerCaller; -import com.android.internal.view.BaseIWindow; -import com.android.internal.view.BaseSurfaceHolder; - import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.app.Service; +import android.app.WallpaperColors; import android.app.WallpaperManager; import android.content.Context; import android.content.Intent; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.PixelFormat; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; import android.os.Bundle; @@ -44,6 +38,7 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.Log; +import android.util.MergedConfiguration; import android.view.Display; import android.view.Gravity; import android.view.IWindowSession; @@ -55,9 +50,14 @@ import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.View; import android.view.ViewGroup; +import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowManagerGlobal; +import com.android.internal.os.HandlerCaller; +import com.android.internal.view.BaseIWindow; +import com.android.internal.view.BaseSurfaceHolder; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; @@ -563,8 +563,13 @@ public abstract class WallpaperService extends Service { * Notifies the system about what colors the wallpaper is using. * You might return null if no color information is available at the moment. In that case * you might want to call {@link #invalidateColors()} in a near future. + * <p> + * The simplest way of creating A {@link android.app.WallpaperColors} object is by using + * {@link android.app.WallpaperColors#fromBitmap(Bitmap)} or + * {@link android.app.WallpaperColors#fromDrawable(Drawable)}, but you can also specify + * your main colors and dark text support explicitly using one of the constructors. * - * @return List of wallpaper colors and their weights. + * @return Wallpaper colors. * @hide */ public @Nullable WallpaperColors onComputeWallpaperColors() { |
