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 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 */ 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 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 */ 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 */ 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 */ 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 */ 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. */ 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 */ 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 */ 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 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 219 private static int getTextColorForBackground(int backgroundColor) { 220 return getLighterOrDarkerVersionOfColor(backgroundColor, 4.5f); 221 } 222 223 private static int getLowContrastColor(int color) { 224 return getLighterOrDarkerVersionOfColor(color, 1.5f); 225 } 226 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