blob: d0791cf93469b8ea1d29227cf9f0922077a497e4 [file] [log] [blame]
Lucas Dupinc40608c2017-04-14 18:33:08 -07001/*
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
Lucas Dupin84b89d92017-05-09 12:16:19 -070019import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
Lucas Dupinc40608c2017-04-14 18:33:08 -070023import android.graphics.Color;
Lucas Dupin84b89d92017-05-09 12:16:19 -070024import android.graphics.drawable.Drawable;
Lucas Dupinc40608c2017-04-14 18:33:08 -070025import android.os.Parcel;
26import android.os.Parcelable;
Lucas Dupin84b89d92017-05-09 12:16:19 -070027import android.util.Size;
Lucas Dupinc40608c2017-04-14 18:33:08 -070028
Lucas Dupin4bd24f32017-06-29 12:20:29 -070029import com.android.internal.graphics.ColorUtils;
Lucas Dupin84b89d92017-05-09 12:16:19 -070030import com.android.internal.graphics.palette.Palette;
Lucas Dupin1d3c00d52017-06-05 08:40:39 -070031import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
Lucas Dupinc40608c2017-04-14 18:33:08 -070032
Lucas Dupinea1fb1e2017-04-05 17:39:44 -070033import java.util.ArrayList;
Lucas Dupin84b89d92017-05-09 12:16:19 -070034import java.util.Collections;
Lucas Dupinc40608c2017-04-14 18:33:08 -070035import java.util.List;
36
37/**
Lucas Dupin84b89d92017-05-09 12:16:19 -070038 * Provides information about the colors of a wallpaper.
39 * <p>
Lucas Dupinbdffdd52017-06-28 09:49:47 -070040 * Exposes the 3 most visually representative colors of a wallpaper. Can be either
Lucas Dupin84b89d92017-05-09 12:16:19 -070041 * {@link WallpaperColors#getPrimaryColor()}, {@link WallpaperColors#getSecondaryColor()}
42 * or {@link WallpaperColors#getTertiaryColor()}.
Lucas Dupinc40608c2017-04-14 18:33:08 -070043 */
44public final class WallpaperColors implements Parcelable {
45
Lucas Dupin84b89d92017-05-09 12:16:19 -070046 /**
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.
Lucas Dupinbdffdd52017-06-28 09:49:47 -070050 * @hide
Lucas Dupin84b89d92017-05-09 12:16:19 -070051 */
52 public static final int HINT_SUPPORTS_DARK_TEXT = 0x1;
53
Lucas Dupin4bd24f32017-06-29 12:20:29 -070054 /**
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 = 0x2;
61
Lucas Dupin84b89d92017-05-09 12:16:19 -070062 // Maximum size that a bitmap can have to keep our calculations sane
63 private static final int MAX_BITMAP_SIZE = 112;
64
65 // Even though we have a maximum size, we'll mainly match bitmap sizes
66 // using the area instead. This way our comparisons are aspect ratio independent.
67 private static final int MAX_WALLPAPER_EXTRACTION_AREA = MAX_BITMAP_SIZE * MAX_BITMAP_SIZE;
68
69 // When extracting the main colors, only consider colors
70 // present in at least MIN_COLOR_OCCURRENCE of the image
71 private static final float MIN_COLOR_OCCURRENCE = 0.05f;
72
Lucas Dupin4bd24f32017-06-29 12:20:29 -070073 // Decides when dark theme is optimal for this wallpaper
74 private static final float DARK_THEME_MEAN_LUMINANCE = 0.25f;
Lucas Dupin84b89d92017-05-09 12:16:19 -070075 // Minimum mean luminosity that an image needs to have to support dark text
Lucas Dupin4bd24f32017-06-29 12:20:29 -070076 private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = 0.75f;
Lucas Dupin84b89d92017-05-09 12:16:19 -070077 // We also check if the image has dark pixels in it,
78 // to avoid bright images with some dark spots.
79 private static final float DARK_PIXEL_LUMINANCE = 0.45f;
80 private static final float MAX_DARK_AREA = 0.05f;
81
82 private final ArrayList<Color> mMainColors;
83 private int mColorHints;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -070084
Lucas Dupinc40608c2017-04-14 18:33:08 -070085 public WallpaperColors(Parcel parcel) {
Lucas Dupin84b89d92017-05-09 12:16:19 -070086 mMainColors = new ArrayList<>();
87 final int count = parcel.readInt();
88 for (int i = 0; i < count; i++) {
89 final int colorInt = parcel.readInt();
90 Color color = Color.valueOf(colorInt);
91 mMainColors.add(color);
Lucas Dupinea1fb1e2017-04-05 17:39:44 -070092 }
Lucas Dupin84b89d92017-05-09 12:16:19 -070093 mColorHints = parcel.readInt();
Lucas Dupinc40608c2017-04-14 18:33:08 -070094 }
95
96 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -070097 * Constructs {@link WallpaperColors} from a drawable.
98 * <p>
Lucas Dupinbdffdd52017-06-28 09:49:47 -070099 * Main colors will be extracted from the drawable.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700100 *
Lucas Dupin84b89d92017-05-09 12:16:19 -0700101 * @param drawable Source where to extract from.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700102 */
Lucas Dupin84b89d92017-05-09 12:16:19 -0700103 public static WallpaperColors fromDrawable(Drawable drawable) {
104 int width = drawable.getIntrinsicWidth();
105 int height = drawable.getIntrinsicHeight();
106
107 // Some drawables do not have intrinsic dimensions
108 if (width <= 0 || height <= 0) {
109 width = MAX_BITMAP_SIZE;
110 height = MAX_BITMAP_SIZE;
111 }
112
113 Size optimalSize = calculateOptimalSize(width, height);
114 Bitmap bitmap = Bitmap.createBitmap(optimalSize.getWidth(), optimalSize.getHeight(),
115 Bitmap.Config.ARGB_8888);
116 final Canvas bmpCanvas = new Canvas(bitmap);
117 drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
118 drawable.draw(bmpCanvas);
119
120 final WallpaperColors colors = WallpaperColors.fromBitmap(bitmap);
121 bitmap.recycle();
122
123 return colors;
Lucas Dupinc40608c2017-04-14 18:33:08 -0700124 }
125
126 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -0700127 * Constructs {@link WallpaperColors} from a bitmap.
128 * <p>
Lucas Dupinbdffdd52017-06-28 09:49:47 -0700129 * Main colors will be extracted from the bitmap.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700130 *
Lucas Dupin84b89d92017-05-09 12:16:19 -0700131 * @param bitmap Source where to extract from.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700132 */
Lucas Dupin84b89d92017-05-09 12:16:19 -0700133 public static WallpaperColors fromBitmap(@NonNull Bitmap bitmap) {
134 if (bitmap == null) {
135 throw new IllegalArgumentException("Bitmap can't be null");
136 }
137
138 final int bitmapArea = bitmap.getWidth() * bitmap.getHeight();
Lucas Dupin42acf602017-07-13 16:32:44 -0700139 boolean shouldRecycle = false;
Lucas Dupin84b89d92017-05-09 12:16:19 -0700140 if (bitmapArea > MAX_WALLPAPER_EXTRACTION_AREA) {
Lucas Dupin42acf602017-07-13 16:32:44 -0700141 shouldRecycle = true;
Lucas Dupin84b89d92017-05-09 12:16:19 -0700142 Size optimalSize = calculateOptimalSize(bitmap.getWidth(), bitmap.getHeight());
Lucas Dupin42acf602017-07-13 16:32:44 -0700143 bitmap = Bitmap.createScaledBitmap(bitmap, optimalSize.getWidth(),
Lucas Dupin84b89d92017-05-09 12:16:19 -0700144 optimalSize.getHeight(), true /* filter */);
Lucas Dupin84b89d92017-05-09 12:16:19 -0700145 }
146
147 final Palette palette = Palette
148 .from(bitmap)
Lucas Dupin1d3c00d52017-06-05 08:40:39 -0700149 .setQuantizer(new VariationalKMeansQuantizer())
150 .maximumColorCount(5)
Lucas Dupin84b89d92017-05-09 12:16:19 -0700151 .clearFilters()
152 .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA)
153 .generate();
154
155 // Remove insignificant colors and sort swatches by population
156 final ArrayList<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches());
157 final float minColorArea = bitmap.getWidth() * bitmap.getHeight() * MIN_COLOR_OCCURRENCE;
158 swatches.removeIf(s -> s.getPopulation() < minColorArea);
159 swatches.sort((a, b) -> b.getPopulation() - a.getPopulation());
160
161 final int swatchesSize = swatches.size();
162 Color primary = null, secondary = null, tertiary = null;
163
164 swatchLoop:
165 for (int i = 0; i < swatchesSize; i++) {
166 Color color = Color.valueOf(swatches.get(i).getRgb());
167 switch (i) {
168 case 0:
169 primary = color;
170 break;
171 case 1:
172 secondary = color;
173 break;
174 case 2:
175 tertiary = color;
176 break;
177 default:
178 // out of bounds
179 break swatchLoop;
180 }
181 }
182
Lucas Dupin4bd24f32017-06-29 12:20:29 -0700183 int hints = calculateHints(bitmap);
Lucas Dupin42acf602017-07-13 16:32:44 -0700184
185 if (shouldRecycle) {
186 bitmap.recycle();
187 }
188
Lucas Dupin84b89d92017-05-09 12:16:19 -0700189 return new WallpaperColors(primary, secondary, tertiary, hints);
190 }
191
192 /**
Lucas Dupinbdffdd52017-06-28 09:49:47 -0700193 * Constructs a new object from three colors.
194 *
195 * @param primaryColor Primary color.
196 * @param secondaryColor Secondary color.
197 * @param tertiaryColor Tertiary color.
198 * @see WallpaperColors#fromBitmap(Bitmap)
199 * @see WallpaperColors#fromDrawable(Drawable)
200 */
201 public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor,
202 @Nullable Color tertiaryColor) {
203 this(primaryColor, secondaryColor, tertiaryColor, 0);
204 }
205
206 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -0700207 * Constructs a new object from three colors, where hints can be specified.
208 *
209 * @param primaryColor Primary color.
210 * @param secondaryColor Secondary color.
211 * @param tertiaryColor Tertiary color.
212 * @param colorHints A combination of WallpaperColor hints.
213 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
214 * @see WallpaperColors#fromBitmap(Bitmap)
215 * @see WallpaperColors#fromDrawable(Drawable)
Lucas Dupinbdffdd52017-06-28 09:49:47 -0700216 * @hide
Lucas Dupin84b89d92017-05-09 12:16:19 -0700217 */
218 public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor,
219 @Nullable Color tertiaryColor, int colorHints) {
220
221 if (primaryColor == null) {
222 throw new IllegalArgumentException("Primary color should never be null.");
223 }
224
225 mMainColors = new ArrayList<>(3);
226 mMainColors.add(primaryColor);
227 if (secondaryColor != null) {
228 mMainColors.add(secondaryColor);
229 }
230 if (tertiaryColor != null) {
231 if (secondaryColor == null) {
232 throw new IllegalArgumentException("tertiaryColor can't be specified when "
233 + "secondaryColor is null");
234 }
235 mMainColors.add(tertiaryColor);
236 }
237
238 mColorHints = colorHints;
Lucas Dupinc40608c2017-04-14 18:33:08 -0700239 }
240
241 public static final Creator<WallpaperColors> CREATOR = new Creator<WallpaperColors>() {
242 @Override
243 public WallpaperColors createFromParcel(Parcel in) {
244 return new WallpaperColors(in);
245 }
246
247 @Override
248 public WallpaperColors[] newArray(int size) {
249 return new WallpaperColors[size];
250 }
251 };
252
253 @Override
254 public int describeContents() {
255 return 0;
256 }
257
258 @Override
259 public void writeToParcel(Parcel dest, int flags) {
Lucas Dupin84b89d92017-05-09 12:16:19 -0700260 List<Color> mainColors = getMainColors();
261 int count = mainColors.size();
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700262 dest.writeInt(count);
Lucas Dupin84b89d92017-05-09 12:16:19 -0700263 for (int i = 0; i < count; i++) {
264 Color color = mainColors.get(i);
265 dest.writeInt(color.toArgb());
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700266 }
Lucas Dupin84b89d92017-05-09 12:16:19 -0700267 dest.writeInt(mColorHints);
Lucas Dupinc40608c2017-04-14 18:33:08 -0700268 }
269
270 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -0700271 * Gets the most visually representative color of the wallpaper.
272 * "Visually representative" means easily noticeable in the image,
273 * probably happening at high frequency.
274 *
275 * @return A color.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700276 */
Lucas Dupin84b89d92017-05-09 12:16:19 -0700277 public @NonNull Color getPrimaryColor() {
278 return mMainColors.get(0);
279 }
280
281 /**
282 * Gets the second most preeminent color of the wallpaper. Can be null.
283 *
284 * @return A color, may be null.
285 */
286 public @Nullable Color getSecondaryColor() {
287 return mMainColors.size() < 2 ? null : mMainColors.get(1);
288 }
289
290 /**
291 * Gets the third most preeminent color of the wallpaper. Can be null.
292 *
293 * @return A color, may be null.
294 */
295 public @Nullable Color getTertiaryColor() {
296 return mMainColors.size() < 3 ? null : mMainColors.get(2);
297 }
298
299 /**
300 * List of most preeminent colors, sorted by importance.
301 *
302 * @return List of colors.
303 * @hide
304 */
305 public @NonNull List<Color> getMainColors() {
306 return Collections.unmodifiableList(mMainColors);
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700307 }
308
309 @Override
310 public boolean equals(Object o) {
311 if (o == null || getClass() != o.getClass()) {
312 return false;
313 }
314
315 WallpaperColors other = (WallpaperColors) o;
Lucas Dupin84b89d92017-05-09 12:16:19 -0700316 return mMainColors.equals(other.mMainColors)
317 && mColorHints == other.mColorHints;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700318 }
319
320 @Override
321 public int hashCode() {
Lucas Dupin84b89d92017-05-09 12:16:19 -0700322 return 31 * mMainColors.hashCode() + mColorHints;
Lucas Dupinc40608c2017-04-14 18:33:08 -0700323 }
324
325 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -0700326 * Combination of WallpaperColor hints.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700327 *
Lucas Dupin84b89d92017-05-09 12:16:19 -0700328 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
329 * @return True if dark text is supported.
Lucas Dupinbdffdd52017-06-28 09:49:47 -0700330 * @hide
Lucas Dupinc40608c2017-04-14 18:33:08 -0700331 */
Lucas Dupin84b89d92017-05-09 12:16:19 -0700332 public int getColorHints() {
333 return mColorHints;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700334 }
335
Lucas Dupin84b89d92017-05-09 12:16:19 -0700336 /**
337 * @param colorHints Combination of WallpaperColors hints.
338 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
339 * @hide
340 */
341 public void setColorHints(int colorHints) {
342 mColorHints = colorHints;
343 }
344
345 /**
346 * Checks if image is bright and clean enough to support light text.
347 *
348 * @param source What to read.
349 * @return Whether image supports dark text or not.
350 */
Lucas Dupin4bd24f32017-06-29 12:20:29 -0700351 private static int calculateHints(Bitmap source) {
Lucas Dupin84b89d92017-05-09 12:16:19 -0700352 if (source == null) {
Lucas Dupin4bd24f32017-06-29 12:20:29 -0700353 return 0;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700354 }
355
Lucas Dupin84b89d92017-05-09 12:16:19 -0700356 int[] pixels = new int[source.getWidth() * source.getHeight()];
357 double totalLuminance = 0;
358 final int maxDarkPixels = (int) (pixels.length * MAX_DARK_AREA);
359 int darkPixels = 0;
360 source.getPixels(pixels, 0 /* offset */, source.getWidth(), 0 /* x */, 0 /* y */,
361 source.getWidth(), source.getHeight());
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700362
Lucas Dupin84b89d92017-05-09 12:16:19 -0700363 // This bitmap was already resized to fit the maximum allowed area.
364 // Let's just loop through the pixels, no sweat!
Lucas Dupin4bd24f32017-06-29 12:20:29 -0700365 float[] tmpHsl = new float[3];
Lucas Dupin84b89d92017-05-09 12:16:19 -0700366 for (int i = 0; i < pixels.length; i++) {
Lucas Dupin4bd24f32017-06-29 12:20:29 -0700367 ColorUtils.colorToHSL(pixels[i], tmpHsl);
368 final float luminance = tmpHsl[2];
Lucas Dupin84b89d92017-05-09 12:16:19 -0700369 final int alpha = Color.alpha(pixels[i]);
Lucas Dupin84b89d92017-05-09 12:16:19 -0700370 // Make sure we don't have a dark pixel mass that will
371 // make text illegible.
372 if (luminance < DARK_PIXEL_LUMINANCE && alpha != 0) {
373 darkPixels++;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700374 }
Lucas Dupin84b89d92017-05-09 12:16:19 -0700375 totalLuminance += luminance;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700376 }
Lucas Dupin4bd24f32017-06-29 12:20:29 -0700377
378 int hints = 0;
379 double meanLuminance = totalLuminance / pixels.length;
380 if (meanLuminance > BRIGHT_IMAGE_MEAN_LUMINANCE && darkPixels < maxDarkPixels) {
381 hints |= HINT_SUPPORTS_DARK_TEXT;
382 }
383 if (meanLuminance < DARK_THEME_MEAN_LUMINANCE) {
384 hints |= HINT_SUPPORTS_DARK_THEME;
385 }
386
387 return hints;
Lucas Dupin84b89d92017-05-09 12:16:19 -0700388 }
389
390 private static Size calculateOptimalSize(int width, int height) {
391 // Calculate how big the bitmap needs to be.
392 // This avoids unnecessary processing and allocation inside Palette.
393 final int requestedArea = width * height;
394 double scale = 1;
395 if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) {
396 scale = Math.sqrt(MAX_WALLPAPER_EXTRACTION_AREA / (double) requestedArea);
397 }
398 int newWidth = (int) (width * scale);
399 int newHeight = (int) (height * scale);
400 // Dealing with edge cases of the drawable being too wide or too tall.
401 // Width or height would end up being 0, in this case we'll set it to 1.
402 if (newWidth == 0) {
403 newWidth = 1;
404 }
405 if (newHeight == 0) {
406 newHeight = 1;
407 }
408
409 return new Size(newWidth, newHeight);
Lucas Dupinc40608c2017-04-14 18:33:08 -0700410 }
411}