WallpaperColors.java revision b5e5053ebc442ced1ad702f551919bc533bee164
1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17package android.app; 18 19import android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.graphics.Bitmap; 22import android.graphics.Canvas; 23import android.graphics.Color; 24import android.graphics.drawable.Drawable; 25import android.os.Parcel; 26import android.os.Parcelable; 27import android.util.Size; 28 29import com.android.internal.graphics.ColorUtils; 30import com.android.internal.graphics.palette.Palette; 31import com.android.internal.graphics.palette.VariationalKMeansQuantizer; 32 33import java.util.ArrayList; 34import java.util.Collections; 35import java.util.List; 36 37/** 38 * Provides information about the colors of a wallpaper. 39 * <p> 40 * Exposes the 3 most visually representative colors of a wallpaper. Can be either 41 * {@link WallpaperColors#getPrimaryColor()}, {@link WallpaperColors#getSecondaryColor()} 42 * or {@link WallpaperColors#getTertiaryColor()}. 43 */ 44public final class WallpaperColors implements Parcelable { 45 46 /** 47 * Specifies that dark text is preferred over the current wallpaper for best presentation. 48 * <p> 49 * eg. A launcher may set its text color to black if this flag is specified. 50 * @hide 51 */ 52 public static final int HINT_SUPPORTS_DARK_TEXT = 1 << 0; 53 54 /** 55 * Specifies that dark theme is preferred over the current wallpaper for best presentation. 56 * <p> 57 * eg. A launcher may set its drawer color to black if this flag is specified. 58 * @hide 59 */ 60 public static final int HINT_SUPPORTS_DARK_THEME = 1 << 1; 61 62 /** 63 * Specifies that this object was generated by extracting colors from a bitmap. 64 * @hide 65 */ 66 public static final int HINT_FROM_BITMAP = 1 << 2; 67 68 // Maximum size that a bitmap can have to keep our calculations sane 69 private static final int MAX_BITMAP_SIZE = 112; 70 71 // Even though we have a maximum size, we'll mainly match bitmap sizes 72 // using the area instead. This way our comparisons are aspect ratio independent. 73 private static final int MAX_WALLPAPER_EXTRACTION_AREA = MAX_BITMAP_SIZE * MAX_BITMAP_SIZE; 74 75 // When extracting the main colors, only consider colors 76 // present in at least MIN_COLOR_OCCURRENCE of the image 77 private static final float MIN_COLOR_OCCURRENCE = 0.05f; 78 79 // Decides when dark theme is optimal for this wallpaper 80 private static final float DARK_THEME_MEAN_LUMINANCE = 0.25f; 81 // Minimum mean luminosity that an image needs to have to support dark text 82 private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = 0.75f; 83 // We also check if the image has dark pixels in it, 84 // to avoid bright images with some dark spots. 85 private static final float DARK_PIXEL_LUMINANCE = 0.45f; 86 private static final float MAX_DARK_AREA = 0.05f; 87 88 private final ArrayList<Color> mMainColors; 89 private int mColorHints; 90 91 public WallpaperColors(Parcel parcel) { 92 mMainColors = new ArrayList<>(); 93 final int count = parcel.readInt(); 94 for (int i = 0; i < count; i++) { 95 final int colorInt = parcel.readInt(); 96 Color color = Color.valueOf(colorInt); 97 mMainColors.add(color); 98 } 99 mColorHints = parcel.readInt(); 100 } 101 102 /** 103 * Constructs {@link WallpaperColors} from a drawable. 104 * <p> 105 * Main colors will be extracted from the drawable. 106 * 107 * @param drawable Source where to extract from. 108 */ 109 public static WallpaperColors fromDrawable(Drawable drawable) { 110 int width = drawable.getIntrinsicWidth(); 111 int height = drawable.getIntrinsicHeight(); 112 113 // Some drawables do not have intrinsic dimensions 114 if (width <= 0 || height <= 0) { 115 width = MAX_BITMAP_SIZE; 116 height = MAX_BITMAP_SIZE; 117 } 118 119 Size optimalSize = calculateOptimalSize(width, height); 120 Bitmap bitmap = Bitmap.createBitmap(optimalSize.getWidth(), optimalSize.getHeight(), 121 Bitmap.Config.ARGB_8888); 122 final Canvas bmpCanvas = new Canvas(bitmap); 123 drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); 124 drawable.draw(bmpCanvas); 125 126 final WallpaperColors colors = WallpaperColors.fromBitmap(bitmap); 127 bitmap.recycle(); 128 129 return colors; 130 } 131 132 /** 133 * Constructs {@link WallpaperColors} from a bitmap. 134 * <p> 135 * Main colors will be extracted from the bitmap. 136 * 137 * @param bitmap Source where to extract from. 138 */ 139 public static WallpaperColors fromBitmap(@NonNull Bitmap bitmap) { 140 if (bitmap == null) { 141 throw new IllegalArgumentException("Bitmap can't be null"); 142 } 143 144 final int bitmapArea = bitmap.getWidth() * bitmap.getHeight(); 145 boolean shouldRecycle = false; 146 if (bitmapArea > MAX_WALLPAPER_EXTRACTION_AREA) { 147 shouldRecycle = true; 148 Size optimalSize = calculateOptimalSize(bitmap.getWidth(), bitmap.getHeight()); 149 bitmap = Bitmap.createScaledBitmap(bitmap, optimalSize.getWidth(), 150 optimalSize.getHeight(), true /* filter */); 151 } 152 153 final Palette palette = Palette 154 .from(bitmap) 155 .setQuantizer(new VariationalKMeansQuantizer()) 156 .maximumColorCount(5) 157 .clearFilters() 158 .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA) 159 .generate(); 160 161 // Remove insignificant colors and sort swatches by population 162 final ArrayList<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches()); 163 final float minColorArea = bitmap.getWidth() * bitmap.getHeight() * MIN_COLOR_OCCURRENCE; 164 swatches.removeIf(s -> s.getPopulation() < minColorArea); 165 swatches.sort((a, b) -> b.getPopulation() - a.getPopulation()); 166 167 final int swatchesSize = swatches.size(); 168 Color primary = null, secondary = null, tertiary = null; 169 170 swatchLoop: 171 for (int i = 0; i < swatchesSize; i++) { 172 Color color = Color.valueOf(swatches.get(i).getRgb()); 173 switch (i) { 174 case 0: 175 primary = color; 176 break; 177 case 1: 178 secondary = color; 179 break; 180 case 2: 181 tertiary = color; 182 break; 183 default: 184 // out of bounds 185 break swatchLoop; 186 } 187 } 188 189 int hints = calculateDarkHints(bitmap); 190 191 if (shouldRecycle) { 192 bitmap.recycle(); 193 } 194 195 return new WallpaperColors(primary, secondary, tertiary, HINT_FROM_BITMAP | hints); 196 } 197 198 /** 199 * Constructs a new object from three colors. 200 * 201 * @param primaryColor Primary color. 202 * @param secondaryColor Secondary color. 203 * @param tertiaryColor Tertiary color. 204 * @see WallpaperColors#fromBitmap(Bitmap) 205 * @see WallpaperColors#fromDrawable(Drawable) 206 */ 207 public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor, 208 @Nullable Color tertiaryColor) { 209 this(primaryColor, secondaryColor, tertiaryColor, 0); 210 } 211 212 /** 213 * Constructs a new object from three colors, where hints can be specified. 214 * 215 * @param primaryColor Primary color. 216 * @param secondaryColor Secondary color. 217 * @param tertiaryColor Tertiary color. 218 * @param colorHints A combination of WallpaperColor hints. 219 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT 220 * @see WallpaperColors#fromBitmap(Bitmap) 221 * @see WallpaperColors#fromDrawable(Drawable) 222 * @hide 223 */ 224 public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor, 225 @Nullable Color tertiaryColor, int colorHints) { 226 227 if (primaryColor == null) { 228 throw new IllegalArgumentException("Primary color should never be null."); 229 } 230 231 mMainColors = new ArrayList<>(3); 232 mMainColors.add(primaryColor); 233 if (secondaryColor != null) { 234 mMainColors.add(secondaryColor); 235 } 236 if (tertiaryColor != null) { 237 if (secondaryColor == null) { 238 throw new IllegalArgumentException("tertiaryColor can't be specified when " 239 + "secondaryColor is null"); 240 } 241 mMainColors.add(tertiaryColor); 242 } 243 244 mColorHints = colorHints; 245 } 246 247 public static final Creator<WallpaperColors> CREATOR = new Creator<WallpaperColors>() { 248 @Override 249 public WallpaperColors createFromParcel(Parcel in) { 250 return new WallpaperColors(in); 251 } 252 253 @Override 254 public WallpaperColors[] newArray(int size) { 255 return new WallpaperColors[size]; 256 } 257 }; 258 259 @Override 260 public int describeContents() { 261 return 0; 262 } 263 264 @Override 265 public void writeToParcel(Parcel dest, int flags) { 266 List<Color> mainColors = getMainColors(); 267 int count = mainColors.size(); 268 dest.writeInt(count); 269 for (int i = 0; i < count; i++) { 270 Color color = mainColors.get(i); 271 dest.writeInt(color.toArgb()); 272 } 273 dest.writeInt(mColorHints); 274 } 275 276 /** 277 * Gets the most visually representative color of the wallpaper. 278 * "Visually representative" means easily noticeable in the image, 279 * probably happening at high frequency. 280 * 281 * @return A color. 282 */ 283 public @NonNull Color getPrimaryColor() { 284 return mMainColors.get(0); 285 } 286 287 /** 288 * Gets the second most preeminent color of the wallpaper. Can be null. 289 * 290 * @return A color, may be null. 291 */ 292 public @Nullable Color getSecondaryColor() { 293 return mMainColors.size() < 2 ? null : mMainColors.get(1); 294 } 295 296 /** 297 * Gets the third most preeminent color of the wallpaper. Can be null. 298 * 299 * @return A color, may be null. 300 */ 301 public @Nullable Color getTertiaryColor() { 302 return mMainColors.size() < 3 ? null : mMainColors.get(2); 303 } 304 305 /** 306 * List of most preeminent colors, sorted by importance. 307 * 308 * @return List of colors. 309 * @hide 310 */ 311 public @NonNull List<Color> getMainColors() { 312 return Collections.unmodifiableList(mMainColors); 313 } 314 315 @Override 316 public boolean equals(Object o) { 317 if (o == null || getClass() != o.getClass()) { 318 return false; 319 } 320 321 WallpaperColors other = (WallpaperColors) o; 322 return mMainColors.equals(other.mMainColors) 323 && mColorHints == other.mColorHints; 324 } 325 326 @Override 327 public int hashCode() { 328 return 31 * mMainColors.hashCode() + mColorHints; 329 } 330 331 /** 332 * Combination of WallpaperColor hints. 333 * 334 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT 335 * @return True if dark text is supported. 336 * @hide 337 */ 338 public int getColorHints() { 339 return mColorHints; 340 } 341 342 /** 343 * @param colorHints Combination of WallpaperColors hints. 344 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT 345 * @hide 346 */ 347 public void setColorHints(int colorHints) { 348 mColorHints = colorHints; 349 } 350 351 /** 352 * Checks if image is bright and clean enough to support light text. 353 * 354 * @param source What to read. 355 * @return Whether image supports dark text or not. 356 */ 357 private static int calculateDarkHints(Bitmap source) { 358 if (source == null) { 359 return 0; 360 } 361 362 int[] pixels = new int[source.getWidth() * source.getHeight()]; 363 double totalLuminance = 0; 364 final int maxDarkPixels = (int) (pixels.length * MAX_DARK_AREA); 365 int darkPixels = 0; 366 source.getPixels(pixels, 0 /* offset */, source.getWidth(), 0 /* x */, 0 /* y */, 367 source.getWidth(), source.getHeight()); 368 369 // This bitmap was already resized to fit the maximum allowed area. 370 // Let's just loop through the pixels, no sweat! 371 float[] tmpHsl = new float[3]; 372 for (int i = 0; i < pixels.length; i++) { 373 ColorUtils.colorToHSL(pixels[i], tmpHsl); 374 final float luminance = tmpHsl[2]; 375 final int alpha = Color.alpha(pixels[i]); 376 // Make sure we don't have a dark pixel mass that will 377 // make text illegible. 378 if (luminance < DARK_PIXEL_LUMINANCE && alpha != 0) { 379 darkPixels++; 380 } 381 totalLuminance += luminance; 382 } 383 384 int hints = 0; 385 double meanLuminance = totalLuminance / pixels.length; 386 if (meanLuminance > BRIGHT_IMAGE_MEAN_LUMINANCE && darkPixels < maxDarkPixels) { 387 hints |= HINT_SUPPORTS_DARK_TEXT; 388 } 389 if (meanLuminance < DARK_THEME_MEAN_LUMINANCE) { 390 hints |= HINT_SUPPORTS_DARK_THEME; 391 } 392 393 return hints; 394 } 395 396 private static Size calculateOptimalSize(int width, int height) { 397 // Calculate how big the bitmap needs to be. 398 // This avoids unnecessary processing and allocation inside Palette. 399 final int requestedArea = width * height; 400 double scale = 1; 401 if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) { 402 scale = Math.sqrt(MAX_WALLPAPER_EXTRACTION_AREA / (double) requestedArea); 403 } 404 int newWidth = (int) (width * scale); 405 int newHeight = (int) (height * scale); 406 // Dealing with edge cases of the drawable being too wide or too tall. 407 // Width or height would end up being 0, in this case we'll set it to 1. 408 if (newWidth == 0) { 409 newWidth = 1; 410 } 411 if (newHeight == 0) { 412 newHeight = 1; 413 } 414 415 return new Size(newWidth, newHeight); 416 } 417 418 @Override 419 public String toString() { 420 final StringBuilder colors = new StringBuilder(); 421 for (int i = 0; i < mMainColors.size(); i++) { 422 colors.append(Integer.toHexString(mMainColors.get(i).toArgb())).append(" "); 423 } 424 return "[WallpaperColors: " + colors.toString() + "h: " + mColorHints + "]"; 425 } 426} 427