Home | History | Annotate | Download | only in graphics
      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