• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.launcher3.graphics;
18 
19 import android.app.Notification;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.graphics.Color;
23 import android.graphics.ColorMatrix;
24 import android.graphics.ColorMatrixColorFilter;
25 import android.support.annotation.NonNull;
26 import android.support.annotation.Nullable;
27 import android.support.v4.graphics.ColorUtils;
28 import android.util.Log;
29 
30 import com.android.launcher3.R;
31 import com.android.launcher3.util.Themes;
32 
33 /**
34  * Contains colors based on the dominant color of an icon.
35  */
36 public class IconPalette {
37 
38     private static final boolean DEBUG = false;
39     private static final String TAG = "IconPalette";
40 
41     private static final float MIN_PRELOAD_COLOR_SATURATION = 0.2f;
42     private static final float MIN_PRELOAD_COLOR_LIGHTNESS = 0.6f;
43 
44     private static IconPalette sBadgePalette;
45     private static IconPalette sFolderBadgePalette;
46 
47     public final int dominantColor;
48     public final int backgroundColor;
49     public final ColorMatrixColorFilter backgroundColorMatrixFilter;
50     public final ColorMatrixColorFilter saturatedBackgroundColorMatrixFilter;
51     public final int textColor;
52     public final int secondaryColor;
53 
IconPalette(int color, boolean desaturateBackground)54     private IconPalette(int color, boolean desaturateBackground) {
55         dominantColor = color;
56         backgroundColor = desaturateBackground ? getMutedColor(dominantColor, 0.87f) : dominantColor;
57         ColorMatrix backgroundColorMatrix = new ColorMatrix();
58         Themes.setColorScaleOnMatrix(backgroundColor, backgroundColorMatrix);
59         backgroundColorMatrixFilter = new ColorMatrixColorFilter(backgroundColorMatrix);
60         if (!desaturateBackground) {
61             saturatedBackgroundColorMatrixFilter = backgroundColorMatrixFilter;
62         } else {
63             // Get slightly more saturated background color.
64             Themes.setColorScaleOnMatrix(getMutedColor(dominantColor, 0.54f), backgroundColorMatrix);
65             saturatedBackgroundColorMatrixFilter = new ColorMatrixColorFilter(backgroundColorMatrix);
66         }
67         textColor = getTextColorForBackground(backgroundColor);
68         secondaryColor = getLowContrastColor(backgroundColor);
69     }
70 
71     /**
72      * Returns a color suitable for the progress bar color of preload icon.
73      */
getPreloadProgressColor(Context context)74     public int getPreloadProgressColor(Context context) {
75         int result = dominantColor;
76 
77         // Make sure that the dominant color has enough saturation to be visible properly.
78         float[] hsv = new float[3];
79         Color.colorToHSV(result, hsv);
80         if (hsv[1] < MIN_PRELOAD_COLOR_SATURATION) {
81             result = Themes.getColorAccent(context);
82         } else {
83             hsv[2] = Math.max(MIN_PRELOAD_COLOR_LIGHTNESS, hsv[2]);
84             result = Color.HSVToColor(hsv);
85         }
86         return result;
87     }
88 
fromDominantColor(int dominantColor, boolean desaturateBackground)89     public static IconPalette fromDominantColor(int dominantColor, boolean desaturateBackground) {
90         return new IconPalette(dominantColor, desaturateBackground);
91     }
92 
93     /**
94      * Returns an IconPalette based on the badge_color in colors.xml.
95      * If that color is Color.TRANSPARENT, then returns null instead.
96      */
getBadgePalette(Resources resources)97     public static @Nullable IconPalette getBadgePalette(Resources resources) {
98         int badgeColor = resources.getColor(R.color.badge_color);
99         if (badgeColor == Color.TRANSPARENT) {
100             // Colors will be extracted per app icon, so a static palette won't work.
101             return null;
102         }
103         if (sBadgePalette == null) {
104             sBadgePalette = fromDominantColor(badgeColor, false);
105         }
106         return sBadgePalette;
107     }
108 
109     /**
110      * Returns an IconPalette based on the folder_badge_color in colors.xml.
111      */
getFolderBadgePalette(Resources resources)112     public static @NonNull IconPalette getFolderBadgePalette(Resources resources) {
113         if (sFolderBadgePalette == null) {
114             int badgeColor = resources.getColor(R.color.folder_badge_color);
115             sFolderBadgePalette = fromDominantColor(badgeColor, false);
116         }
117         return sFolderBadgePalette;
118     }
119 
120     /**
121      * Resolves a color such that it has enough contrast to be used as the
122      * color of an icon or text on the given background color.
123      *
124      * @return a color of the same hue with enough contrast against the background.
125      *
126      * This was copied from com.android.internal.util.NotificationColorUtil.
127      */
resolveContrastColor(Context context, int color, int background)128     public static int resolveContrastColor(Context context, int color, int background) {
129         final int resolvedColor = resolveColor(context, color);
130 
131         int contrastingColor = ensureTextContrast(resolvedColor, background);
132 
133         if (contrastingColor != resolvedColor) {
134             if (DEBUG){
135                 Log.w(TAG, String.format(
136                         "Enhanced contrast of notification for %s " +
137                                 "%s (over background) by changing #%s to %s",
138                         context.getPackageName(),
139                         contrastChange(resolvedColor, contrastingColor, background),
140                         Integer.toHexString(resolvedColor), Integer.toHexString(contrastingColor)));
141             }
142         }
143         return contrastingColor;
144     }
145 
146     /**
147      * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT}
148      *
149      * This was copied from com.android.internal.util.NotificationColorUtil.
150      */
resolveColor(Context context, int color)151     private static int resolveColor(Context context, int color) {
152         if (color == Notification.COLOR_DEFAULT) {
153             return context.getColor(R.color.notification_icon_default_color);
154         }
155         return color;
156     }
157 
158     /** For debugging. This was copied from com.android.internal.util.NotificationColorUtil. */
contrastChange(int colorOld, int colorNew, int bg)159     private static String contrastChange(int colorOld, int colorNew, int bg) {
160         return String.format("from %.2f:1 to %.2f:1",
161                 ColorUtils.calculateContrast(colorOld, bg),
162                 ColorUtils.calculateContrast(colorNew, bg));
163     }
164 
165     /**
166      * Finds a text color with sufficient contrast over bg that has the same hue as the original
167      * color.
168      *
169      * This was copied from com.android.internal.util.NotificationColorUtil.
170      */
ensureTextContrast(int color, int bg)171     private static int ensureTextContrast(int color, int bg) {
172         return findContrastColor(color, bg, true, 4.5);
173     }
174     /**
175      * Finds a suitable color such that there's enough contrast.
176      *
177      * @param color the color to start searching from.
178      * @param other the color to ensure contrast against. Assumed to be lighter than {@param color}
179      * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
180      * @param minRatio the minimum contrast ratio required.
181      * @return a color with the same hue as {@param color}, potentially darkened to meet the
182      *          contrast ratio.
183      *
184      * This was copied from com.android.internal.util.NotificationColorUtil.
185      */
findContrastColor(int color, int other, boolean findFg, double minRatio)186     private static int findContrastColor(int color, int other, boolean findFg, double minRatio) {
187         int fg = findFg ? color : other;
188         int bg = findFg ? other : color;
189         if (ColorUtils.calculateContrast(fg, bg) >= minRatio) {
190             return color;
191         }
192 
193         double[] lab = new double[3];
194         ColorUtils.colorToLAB(findFg ? fg : bg, lab);
195 
196         double low = 0, high = lab[0];
197         final double a = lab[1], b = lab[2];
198         for (int i = 0; i < 15 && high - low > 0.00001; i++) {
199             final double l = (low + high) / 2;
200             if (findFg) {
201                 fg = ColorUtils.LABToColor(l, a, b);
202             } else {
203                 bg = ColorUtils.LABToColor(l, a, b);
204             }
205             if (ColorUtils.calculateContrast(fg, bg) > minRatio) {
206                 low = l;
207             } else {
208                 high = l;
209             }
210         }
211         return ColorUtils.LABToColor(low, a, b);
212     }
213 
getMutedColor(int color, float whiteScrimAlpha)214     private static int getMutedColor(int color, float whiteScrimAlpha) {
215         int whiteScrim = ColorUtils.setAlphaComponent(Color.WHITE, (int) (255 * whiteScrimAlpha));
216         return ColorUtils.compositeColors(whiteScrim, color);
217     }
218 
getTextColorForBackground(int backgroundColor)219     private static int getTextColorForBackground(int backgroundColor) {
220         return getLighterOrDarkerVersionOfColor(backgroundColor, 4.5f);
221     }
222 
getLowContrastColor(int color)223     private static int getLowContrastColor(int color) {
224         return getLighterOrDarkerVersionOfColor(color, 1.5f);
225     }
226 
getLighterOrDarkerVersionOfColor(int color, float contrastRatio)227     private static int getLighterOrDarkerVersionOfColor(int color, float contrastRatio) {
228         int whiteMinAlpha = ColorUtils.calculateMinimumAlpha(Color.WHITE, color, contrastRatio);
229         int blackMinAlpha = ColorUtils.calculateMinimumAlpha(Color.BLACK, color, contrastRatio);
230         int translucentWhiteOrBlack;
231         if (whiteMinAlpha >= 0) {
232             translucentWhiteOrBlack = ColorUtils.setAlphaComponent(Color.WHITE, whiteMinAlpha);
233         } else if (blackMinAlpha >= 0) {
234             translucentWhiteOrBlack = ColorUtils.setAlphaComponent(Color.BLACK, blackMinAlpha);
235         } else {
236             translucentWhiteOrBlack = Color.WHITE;
237         }
238         return ColorUtils.compositeColors(translucentWhiteOrBlack, color);
239     }
240 }
241