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 com.android.launcher3.graphics; 18 19import android.app.Notification; 20import android.content.Context; 21import android.content.res.Resources; 22import android.graphics.Color; 23import android.graphics.ColorMatrix; 24import android.graphics.ColorMatrixColorFilter; 25import android.support.annotation.NonNull; 26import android.support.annotation.Nullable; 27import android.support.v4.graphics.ColorUtils; 28import android.util.Log; 29 30import com.android.launcher3.R; 31import com.android.launcher3.util.Themes; 32 33/** 34 * Contains colors based on the dominant color of an icon. 35 */ 36public 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, 4.5); 173 } 174 /** 175 * Finds a suitable color such that there's enough contrast. 176 * 177 * @param fg the color to start searching from. 178 * @param bg the color to ensure contrast against. 179 * @param minRatio the minimum contrast ratio required. 180 * @return a color with the same hue as {@param color}, potentially darkened to meet the 181 * contrast ratio. 182 * 183 * This was copied from com.android.internal.util.NotificationColorUtil. 184 */ 185 private static int findContrastColor(int fg, int bg, double minRatio) { 186 if (ColorUtils.calculateContrast(fg, bg) >= minRatio) { 187 return fg; 188 } 189 190 double[] lab = new double[3]; 191 ColorUtils.colorToLAB(bg, lab); 192 double bgL = lab[0]; 193 ColorUtils.colorToLAB(fg, lab); 194 double fgL = lab[0]; 195 boolean isBgDark = bgL < 50; 196 197 double low = isBgDark ? fgL : 0, high = isBgDark ? 100 : fgL; 198 final double a = lab[1], b = lab[2]; 199 for (int i = 0; i < 15 && high - low > 0.00001; i++) { 200 final double l = (low + high) / 2; 201 fg = ColorUtils.LABToColor(l, a, b); 202 if (ColorUtils.calculateContrast(fg, bg) > minRatio) { 203 if (isBgDark) high = l; else low = l; 204 } else { 205 if (isBgDark) low = l; else high = l; 206 } 207 } 208 return ColorUtils.LABToColor(low, a, b); 209 } 210 211 private static int getMutedColor(int color, float whiteScrimAlpha) { 212 int whiteScrim = ColorUtils.setAlphaComponent(Color.WHITE, (int) (255 * whiteScrimAlpha)); 213 return ColorUtils.compositeColors(whiteScrim, color); 214 } 215 216 private static int getTextColorForBackground(int backgroundColor) { 217 return getLighterOrDarkerVersionOfColor(backgroundColor, 4.5f); 218 } 219 220 private static int getLowContrastColor(int color) { 221 return getLighterOrDarkerVersionOfColor(color, 1.5f); 222 } 223 224 private static int getLighterOrDarkerVersionOfColor(int color, float contrastRatio) { 225 int whiteMinAlpha = ColorUtils.calculateMinimumAlpha(Color.WHITE, color, contrastRatio); 226 int blackMinAlpha = ColorUtils.calculateMinimumAlpha(Color.BLACK, color, contrastRatio); 227 int translucentWhiteOrBlack; 228 if (whiteMinAlpha >= 0) { 229 translucentWhiteOrBlack = ColorUtils.setAlphaComponent(Color.WHITE, whiteMinAlpha); 230 } else if (blackMinAlpha >= 0) { 231 translucentWhiteOrBlack = ColorUtils.setAlphaComponent(Color.BLACK, blackMinAlpha); 232 } else { 233 translucentWhiteOrBlack = Color.WHITE; 234 } 235 return ColorUtils.compositeColors(translucentWhiteOrBlack, color); 236 } 237} 238