blob: 23e9ca5c32ae4d4221c00c17e03b37803dba5ad6 [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 Dupin84b89d92017-05-09 12:16:19 -070029import com.android.internal.graphics.palette.Palette;
Lucas Dupinc40608c2017-04-14 18:33:08 -070030
Lucas Dupinea1fb1e2017-04-05 17:39:44 -070031import java.util.ArrayList;
Lucas Dupin84b89d92017-05-09 12:16:19 -070032import java.util.Collections;
Lucas Dupinc40608c2017-04-14 18:33:08 -070033import java.util.List;
34
35/**
Lucas Dupin84b89d92017-05-09 12:16:19 -070036 * Provides information about the colors of a wallpaper.
37 * <p>
38 * This class contains two main components:
39 * <ul>
40 * <li>Named colors: Most visually representative colors of a wallpaper. Can be either
41 * {@link WallpaperColors#getPrimaryColor()}, {@link WallpaperColors#getSecondaryColor()}
42 * or {@link WallpaperColors#getTertiaryColor()}.
43 * </li>
44 * <li>Hints: How colors may affect other system components. Currently the only supported hint is
45 * {@link WallpaperColors#HINT_SUPPORTS_DARK_TEXT}, which specifies if dark text is preferred
46 * over the wallpaper.</li>
47 * </ul>
Lucas Dupinc40608c2017-04-14 18:33:08 -070048 */
49public final class WallpaperColors implements Parcelable {
50
Lucas Dupin84b89d92017-05-09 12:16:19 -070051 /**
52 * Specifies that dark text is preferred over the current wallpaper for best presentation.
53 * <p>
54 * eg. A launcher may set its text color to black if this flag is specified.
55 */
56 public static final int HINT_SUPPORTS_DARK_TEXT = 0x1;
57
58 // Maximum size that a bitmap can have to keep our calculations sane
59 private static final int MAX_BITMAP_SIZE = 112;
60
61 // Even though we have a maximum size, we'll mainly match bitmap sizes
62 // using the area instead. This way our comparisons are aspect ratio independent.
63 private static final int MAX_WALLPAPER_EXTRACTION_AREA = MAX_BITMAP_SIZE * MAX_BITMAP_SIZE;
64
65 // When extracting the main colors, only consider colors
66 // present in at least MIN_COLOR_OCCURRENCE of the image
67 private static final float MIN_COLOR_OCCURRENCE = 0.05f;
68
69 // Minimum mean luminosity that an image needs to have to support dark text
70 private static final float BRIGHT_IMAGE_MEAN_LUMINANCE = 0.9f;
71 // We also check if the image has dark pixels in it,
72 // to avoid bright images with some dark spots.
73 private static final float DARK_PIXEL_LUMINANCE = 0.45f;
74 private static final float MAX_DARK_AREA = 0.05f;
75
76 private final ArrayList<Color> mMainColors;
77 private int mColorHints;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -070078
Lucas Dupinc40608c2017-04-14 18:33:08 -070079 public WallpaperColors(Parcel parcel) {
Lucas Dupin84b89d92017-05-09 12:16:19 -070080 mMainColors = new ArrayList<>();
81 final int count = parcel.readInt();
82 for (int i = 0; i < count; i++) {
83 final int colorInt = parcel.readInt();
84 Color color = Color.valueOf(colorInt);
85 mMainColors.add(color);
Lucas Dupinea1fb1e2017-04-05 17:39:44 -070086 }
Lucas Dupin84b89d92017-05-09 12:16:19 -070087 mColorHints = parcel.readInt();
Lucas Dupinc40608c2017-04-14 18:33:08 -070088 }
89
90 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -070091 * Constructs {@link WallpaperColors} from a drawable.
92 * <p>
93 * Main colors will be extracted from the drawable and hints will be calculated.
Lucas Dupinc40608c2017-04-14 18:33:08 -070094 *
Lucas Dupin84b89d92017-05-09 12:16:19 -070095 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
96 * @param drawable Source where to extract from.
Lucas Dupinc40608c2017-04-14 18:33:08 -070097 */
Lucas Dupin84b89d92017-05-09 12:16:19 -070098 public static WallpaperColors fromDrawable(Drawable drawable) {
99 int width = drawable.getIntrinsicWidth();
100 int height = drawable.getIntrinsicHeight();
101
102 // Some drawables do not have intrinsic dimensions
103 if (width <= 0 || height <= 0) {
104 width = MAX_BITMAP_SIZE;
105 height = MAX_BITMAP_SIZE;
106 }
107
108 Size optimalSize = calculateOptimalSize(width, height);
109 Bitmap bitmap = Bitmap.createBitmap(optimalSize.getWidth(), optimalSize.getHeight(),
110 Bitmap.Config.ARGB_8888);
111 final Canvas bmpCanvas = new Canvas(bitmap);
112 drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
113 drawable.draw(bmpCanvas);
114
115 final WallpaperColors colors = WallpaperColors.fromBitmap(bitmap);
116 bitmap.recycle();
117
118 return colors;
Lucas Dupinc40608c2017-04-14 18:33:08 -0700119 }
120
121 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -0700122 * Constructs {@link WallpaperColors} from a bitmap.
123 * <p>
124 * Main colors will be extracted from the bitmap and hints will be calculated.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700125 *
Lucas Dupin84b89d92017-05-09 12:16:19 -0700126 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
127 * @param bitmap Source where to extract from.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700128 */
Lucas Dupin84b89d92017-05-09 12:16:19 -0700129 public static WallpaperColors fromBitmap(@NonNull Bitmap bitmap) {
130 if (bitmap == null) {
131 throw new IllegalArgumentException("Bitmap can't be null");
132 }
133
134 final int bitmapArea = bitmap.getWidth() * bitmap.getHeight();
135 if (bitmapArea > MAX_WALLPAPER_EXTRACTION_AREA) {
136 Size optimalSize = calculateOptimalSize(bitmap.getWidth(), bitmap.getHeight());
137 Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, optimalSize.getWidth(),
138 optimalSize.getHeight(), true /* filter */);
139 bitmap.recycle();
140 bitmap = scaledBitmap;
141 }
142
143 final Palette palette = Palette
144 .from(bitmap)
145 .clearFilters()
146 .resizeBitmapArea(MAX_WALLPAPER_EXTRACTION_AREA)
147 .generate();
148
149 // Remove insignificant colors and sort swatches by population
150 final ArrayList<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches());
151 final float minColorArea = bitmap.getWidth() * bitmap.getHeight() * MIN_COLOR_OCCURRENCE;
152 swatches.removeIf(s -> s.getPopulation() < minColorArea);
153 swatches.sort((a, b) -> b.getPopulation() - a.getPopulation());
154
155 final int swatchesSize = swatches.size();
156 Color primary = null, secondary = null, tertiary = null;
157
158 swatchLoop:
159 for (int i = 0; i < swatchesSize; i++) {
160 Color color = Color.valueOf(swatches.get(i).getRgb());
161 switch (i) {
162 case 0:
163 primary = color;
164 break;
165 case 1:
166 secondary = color;
167 break;
168 case 2:
169 tertiary = color;
170 break;
171 default:
172 // out of bounds
173 break swatchLoop;
174 }
175 }
176
177 int hints = 0;
178 if (calculateDarkTextSupport(bitmap)) {
179 hints |= HINT_SUPPORTS_DARK_TEXT;
180 }
181 return new WallpaperColors(primary, secondary, tertiary, hints);
182 }
183
184 /**
185 * Constructs a new object from three colors, where hints can be specified.
186 *
187 * @param primaryColor Primary color.
188 * @param secondaryColor Secondary color.
189 * @param tertiaryColor Tertiary color.
190 * @param colorHints A combination of WallpaperColor hints.
191 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
192 * @see WallpaperColors#fromBitmap(Bitmap)
193 * @see WallpaperColors#fromDrawable(Drawable)
194 */
195 public WallpaperColors(@NonNull Color primaryColor, @Nullable Color secondaryColor,
196 @Nullable Color tertiaryColor, int colorHints) {
197
198 if (primaryColor == null) {
199 throw new IllegalArgumentException("Primary color should never be null.");
200 }
201
202 mMainColors = new ArrayList<>(3);
203 mMainColors.add(primaryColor);
204 if (secondaryColor != null) {
205 mMainColors.add(secondaryColor);
206 }
207 if (tertiaryColor != null) {
208 if (secondaryColor == null) {
209 throw new IllegalArgumentException("tertiaryColor can't be specified when "
210 + "secondaryColor is null");
211 }
212 mMainColors.add(tertiaryColor);
213 }
214
215 mColorHints = colorHints;
Lucas Dupinc40608c2017-04-14 18:33:08 -0700216 }
217
218 public static final Creator<WallpaperColors> CREATOR = new Creator<WallpaperColors>() {
219 @Override
220 public WallpaperColors createFromParcel(Parcel in) {
221 return new WallpaperColors(in);
222 }
223
224 @Override
225 public WallpaperColors[] newArray(int size) {
226 return new WallpaperColors[size];
227 }
228 };
229
230 @Override
231 public int describeContents() {
232 return 0;
233 }
234
235 @Override
236 public void writeToParcel(Parcel dest, int flags) {
Lucas Dupin84b89d92017-05-09 12:16:19 -0700237 List<Color> mainColors = getMainColors();
238 int count = mainColors.size();
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700239 dest.writeInt(count);
Lucas Dupin84b89d92017-05-09 12:16:19 -0700240 for (int i = 0; i < count; i++) {
241 Color color = mainColors.get(i);
242 dest.writeInt(color.toArgb());
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700243 }
Lucas Dupin84b89d92017-05-09 12:16:19 -0700244 dest.writeInt(mColorHints);
Lucas Dupinc40608c2017-04-14 18:33:08 -0700245 }
246
247 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -0700248 * Gets the most visually representative color of the wallpaper.
249 * "Visually representative" means easily noticeable in the image,
250 * probably happening at high frequency.
251 *
252 * @return A color.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700253 */
Lucas Dupin84b89d92017-05-09 12:16:19 -0700254 public @NonNull Color getPrimaryColor() {
255 return mMainColors.get(0);
256 }
257
258 /**
259 * Gets the second most preeminent color of the wallpaper. Can be null.
260 *
261 * @return A color, may be null.
262 */
263 public @Nullable Color getSecondaryColor() {
264 return mMainColors.size() < 2 ? null : mMainColors.get(1);
265 }
266
267 /**
268 * Gets the third most preeminent color of the wallpaper. Can be null.
269 *
270 * @return A color, may be null.
271 */
272 public @Nullable Color getTertiaryColor() {
273 return mMainColors.size() < 3 ? null : mMainColors.get(2);
274 }
275
276 /**
277 * List of most preeminent colors, sorted by importance.
278 *
279 * @return List of colors.
280 * @hide
281 */
282 public @NonNull List<Color> getMainColors() {
283 return Collections.unmodifiableList(mMainColors);
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700284 }
285
286 @Override
287 public boolean equals(Object o) {
288 if (o == null || getClass() != o.getClass()) {
289 return false;
290 }
291
292 WallpaperColors other = (WallpaperColors) o;
Lucas Dupin84b89d92017-05-09 12:16:19 -0700293 return mMainColors.equals(other.mMainColors)
294 && mColorHints == other.mColorHints;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700295 }
296
297 @Override
298 public int hashCode() {
Lucas Dupin84b89d92017-05-09 12:16:19 -0700299 return 31 * mMainColors.hashCode() + mColorHints;
Lucas Dupinc40608c2017-04-14 18:33:08 -0700300 }
301
302 /**
Lucas Dupin84b89d92017-05-09 12:16:19 -0700303 * Combination of WallpaperColor hints.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700304 *
Lucas Dupin84b89d92017-05-09 12:16:19 -0700305 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
306 * @return True if dark text is supported.
Lucas Dupinc40608c2017-04-14 18:33:08 -0700307 */
Lucas Dupin84b89d92017-05-09 12:16:19 -0700308 public int getColorHints() {
309 return mColorHints;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700310 }
311
Lucas Dupin84b89d92017-05-09 12:16:19 -0700312 /**
313 * @param colorHints Combination of WallpaperColors hints.
314 * @see WallpaperColors#HINT_SUPPORTS_DARK_TEXT
315 * @hide
316 */
317 public void setColorHints(int colorHints) {
318 mColorHints = colorHints;
319 }
320
321 /**
322 * Checks if image is bright and clean enough to support light text.
323 *
324 * @param source What to read.
325 * @return Whether image supports dark text or not.
326 */
327 private static boolean calculateDarkTextSupport(Bitmap source) {
328 if (source == null) {
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700329 return false;
330 }
331
Lucas Dupin84b89d92017-05-09 12:16:19 -0700332 int[] pixels = new int[source.getWidth() * source.getHeight()];
333 double totalLuminance = 0;
334 final int maxDarkPixels = (int) (pixels.length * MAX_DARK_AREA);
335 int darkPixels = 0;
336 source.getPixels(pixels, 0 /* offset */, source.getWidth(), 0 /* x */, 0 /* y */,
337 source.getWidth(), source.getHeight());
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700338
Lucas Dupin84b89d92017-05-09 12:16:19 -0700339 // This bitmap was already resized to fit the maximum allowed area.
340 // Let's just loop through the pixels, no sweat!
341 for (int i = 0; i < pixels.length; i++) {
342 final float luminance = Color.luminance(pixels[i]);
343 final int alpha = Color.alpha(pixels[i]);
344
345 // Make sure we don't have a dark pixel mass that will
346 // make text illegible.
347 if (luminance < DARK_PIXEL_LUMINANCE && alpha != 0) {
348 darkPixels++;
349 if (darkPixels > maxDarkPixels) {
350 return false;
351 }
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700352 }
Lucas Dupin84b89d92017-05-09 12:16:19 -0700353
354 totalLuminance += luminance;
Lucas Dupinea1fb1e2017-04-05 17:39:44 -0700355 }
Lucas Dupin84b89d92017-05-09 12:16:19 -0700356 return totalLuminance / pixels.length > BRIGHT_IMAGE_MEAN_LUMINANCE;
357 }
358
359 private static Size calculateOptimalSize(int width, int height) {
360 // Calculate how big the bitmap needs to be.
361 // This avoids unnecessary processing and allocation inside Palette.
362 final int requestedArea = width * height;
363 double scale = 1;
364 if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) {
365 scale = Math.sqrt(MAX_WALLPAPER_EXTRACTION_AREA / (double) requestedArea);
366 }
367 int newWidth = (int) (width * scale);
368 int newHeight = (int) (height * scale);
369 // Dealing with edge cases of the drawable being too wide or too tall.
370 // Width or height would end up being 0, in this case we'll set it to 1.
371 if (newWidth == 0) {
372 newWidth = 1;
373 }
374 if (newHeight == 0) {
375 newHeight = 1;
376 }
377
378 return new Size(newWidth, newHeight);
Lucas Dupinc40608c2017-04-14 18:33:08 -0700379 }
380}