OSDN Git Service

Ensure notification icons have enough contrast with background.
authorTony Wickham <twickham@google.com>
Fri, 27 Jan 2017 16:45:49 +0000 (08:45 -0800)
committerTony Wickham <twickham@google.com>
Fri, 27 Jan 2017 20:43:31 +0000 (12:43 -0800)
This uses the same color calculations as the system, except that
we use the extracted notification background instead of assuming
it is white.

Bug: 32410600
Change-Id: I7be8b9459ca38d01a6780758898541e69ec42576

res/values/colors.xml
src/com/android/launcher3/graphics/IconPalette.java
src/com/android/launcher3/notification/NotificationFooterLayout.java
src/com/android/launcher3/notification/NotificationInfo.java
src/com/android/launcher3/notification/NotificationItemView.java
src/com/android/launcher3/notification/NotificationMainView.java

index 809fc6d..a7b507c 100644 (file)
@@ -50,4 +50,6 @@
 
     <!-- Used as a fallback since colorSecondary doesn't exist pre-API 25 -->
     <color name="fallback_secondary_color">#FF37474F</color>
+
+    <color name="notification_icon_default_color">#757575</color> <!-- Gray 600 -->
 </resources>
index 58ad449..27aeaba 100644 (file)
 
 package com.android.launcher3.graphics;
 
+import android.app.Notification;
+import android.content.Context;
 import android.graphics.Color;
 import android.support.v4.graphics.ColorUtils;
+import android.util.Log;
+
+import com.android.launcher3.R;
 
 /**
  * Contains colors based on the dominant color of an icon.
  */
 public class IconPalette {
 
+    private static final boolean DEBUG = false;
+    private static final String TAG = "IconPalette";
+
     public int backgroundColor;
     public int textColor;
     public int secondaryColor;
@@ -36,6 +44,100 @@ public class IconPalette {
         return palette;
     }
 
+    /**
+     * Resolves a color such that it has enough contrast to be used as the
+     * color of an icon or text on the given background color.
+     *
+     * @return a color of the same hue with enough contrast against the background.
+     *
+     * This was copied from com.android.internal.util.NotificationColorUtil.
+     */
+    public static int resolveContrastColor(Context context, int color, int background) {
+        final int resolvedColor = resolveColor(context, color);
+
+        int contrastingColor = ensureTextContrast(resolvedColor, background);
+
+        if (contrastingColor != resolvedColor) {
+            if (DEBUG){
+                Log.w(TAG, String.format(
+                        "Enhanced contrast of notification for %s " +
+                                "%s (over background) by changing #%s to %s",
+                        context.getPackageName(),
+                        contrastChange(resolvedColor, contrastingColor, background),
+                        Integer.toHexString(resolvedColor), Integer.toHexString(contrastingColor)));
+            }
+        }
+        return contrastingColor;
+    }
+
+    /**
+     * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT}
+     *
+     * This was copied from com.android.internal.util.NotificationColorUtil.
+     */
+    private static int resolveColor(Context context, int color) {
+        if (color == Notification.COLOR_DEFAULT) {
+            return context.getColor(R.color.notification_icon_default_color);
+        }
+        return color;
+    }
+
+    /** For debugging. This was copied from com.android.internal.util.NotificationColorUtil. */
+    private static String contrastChange(int colorOld, int colorNew, int bg) {
+        return String.format("from %.2f:1 to %.2f:1",
+                ColorUtils.calculateContrast(colorOld, bg),
+                ColorUtils.calculateContrast(colorNew, bg));
+    }
+
+    /**
+     * Finds a text color with sufficient contrast over bg that has the same hue as the original
+     * color.
+     *
+     * This was copied from com.android.internal.util.NotificationColorUtil.
+     */
+    private static int ensureTextContrast(int color, int bg) {
+        return findContrastColor(color, bg, true, 4.5);
+    }
+    /**
+     * Finds a suitable color such that there's enough contrast.
+     *
+     * @param color the color to start searching from.
+     * @param other the color to ensure contrast against. Assumed to be lighter than {@param color}
+     * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
+     * @param minRatio the minimum contrast ratio required.
+     * @return a color with the same hue as {@param color}, potentially darkened to meet the
+     *          contrast ratio.
+     *
+     * This was copied from com.android.internal.util.NotificationColorUtil.
+     */
+    private static int findContrastColor(int color, int other, boolean findFg, double minRatio) {
+        int fg = findFg ? color : other;
+        int bg = findFg ? other : color;
+        if (ColorUtils.calculateContrast(fg, bg) >= minRatio) {
+            return color;
+        }
+
+        double[] lab = new double[3];
+        ColorUtils.colorToLAB(findFg ? fg : bg, lab);
+
+        double low = 0, high = lab[0];
+        final double a = lab[1], b = lab[2];
+        for (int i = 0; i < 15 && high - low > 0.00001; i++) {
+            final double l = (low + high) / 2;
+            if (findFg) {
+                fg = ColorUtils.LABToColor(l, a, b);
+            } else {
+                bg = ColorUtils.LABToColor(l, a, b);
+            }
+            if (ColorUtils.calculateContrast(fg, bg) > minRatio) {
+                low = l;
+            } else {
+                high = l;
+            }
+        }
+        return ColorUtils.LABToColor(low, a, b);
+    }
+
     private static int getMutedColor(int color) {
         int alpha = (int) (255 * 0.15f);
         return ColorUtils.compositeColors(ColorUtils.setAlphaComponent(color, alpha), Color.WHITE);
index cd610bd..07178ce 100644 (file)
@@ -61,6 +61,7 @@ public class NotificationFooterLayout extends LinearLayout {
 
     LinearLayout.LayoutParams mIconLayoutParams;
     private LinearLayout mIconRow;
+    private int mBackgroundColor;
     private int mTextColor;
 
     public NotificationFooterLayout(Context context) {
@@ -90,7 +91,8 @@ public class NotificationFooterLayout extends LinearLayout {
     }
 
     public void applyColors(IconPalette iconPalette) {
-        setBackgroundTintList(ColorStateList.valueOf(iconPalette.backgroundColor));
+        mBackgroundColor = iconPalette.backgroundColor;
+        setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
         findViewById(R.id.divider).setBackgroundColor(iconPalette.secondaryColor);
         mTextColor = iconPalette.textColor;
     }
@@ -130,7 +132,7 @@ public class NotificationFooterLayout extends LinearLayout {
 
     private void addNotificationIconForInfo(NotificationInfo info, boolean fromOverflow) {
         View icon = new View(getContext());
-        icon.setBackground(info.iconDrawable);
+        icon.setBackground(info.getIconForBackground(getContext(), mBackgroundColor));
         icon.setOnClickListener(info);
         int addIndex = mIconRow.getChildCount();
         if (fromOverflow) {
index bf57b2a..af5e817 100644 (file)
@@ -25,6 +25,7 @@ import android.service.notification.StatusBarNotification;
 import android.view.View;
 
 import com.android.launcher3.Launcher;
+import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.util.PackageUserKey;
 
@@ -41,11 +42,14 @@ public class NotificationInfo implements View.OnClickListener {
     public final String notificationKey;
     public final CharSequence title;
     public final CharSequence text;
-    public final Drawable iconDrawable;
     public final PendingIntent intent;
     public final boolean autoCancel;
     public final boolean dismissable;
 
+    private final Drawable mIconDrawable;
+    private boolean mShouldTintIcon;
+    private int mIconColor;
+
     /**
      * Extracts the data that we need from the StatusBarNotification.
      */
@@ -60,10 +64,12 @@ public class NotificationInfo implements View.OnClickListener {
         Icon icon = notification.getLargeIcon();
         if (icon == null) {
             icon = notification.getSmallIcon();
-            iconDrawable = icon.loadDrawable(context);
-            iconDrawable.setTint(statusBarNotification.getNotification().color);
+            mIconDrawable = icon.loadDrawable(context);
+            mIconColor = statusBarNotification.getNotification().color;
+            mShouldTintIcon = true;
         } else {
-            iconDrawable = icon.loadDrawable(context);
+            mIconDrawable = icon.loadDrawable(context);
+            mShouldTintIcon = false;
         }
         intent = notification.contentIntent;
         autoCancel = (notification.flags & Notification.FLAG_AUTO_CANCEL) != 0;
@@ -83,4 +89,18 @@ public class NotificationInfo implements View.OnClickListener {
         }
         PopupContainerWithArrow.getOpen(launcher).close(true);
     }
+
+    public Drawable getIconForBackground(Context context, int background) {
+        if (!mShouldTintIcon) {
+            return mIconDrawable;
+        }
+        mIconColor = IconPalette.resolveContrastColor(context, mIconColor, background);
+        Drawable icon = mIconDrawable.mutate();
+        // DrawableContainer ignores the color filter if it's already set, so clear it first to
+        // get it set and invalidated properly.
+        icon.setTintList(null);
+        icon.setTint(mIconColor);
+        mShouldTintIcon = false;
+        return icon;
+    }
 }
index b74cd4e..422722d 100644 (file)
@@ -35,7 +35,9 @@ import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.popup.PopupItemView;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import static com.android.launcher3.LauncherAnimUtils.animateViewHeight;
 
@@ -117,7 +119,7 @@ public class NotificationItemView extends PopupItemView {
         mHeader.setBackgroundTintList(ColorStateList.valueOf(iconPalette.backgroundColor));
         mHeader.setTextColor(ColorStateList.valueOf(iconPalette.textColor));
         mDivider.setBackgroundColor(iconPalette.secondaryColor);
-        mMainView.setBackgroundColor(iconPalette.backgroundColor);
+        mMainView.applyColors(iconPalette);
         mFooter.applyColors(iconPalette);
     }
 
@@ -135,7 +137,7 @@ public class NotificationItemView extends PopupItemView {
                 @Override
                 public void onIconAnimationEnd(NotificationInfo newMainNotification) {
                     if (newMainNotification != null) {
-                        mMainView.applyNotificationInfo(newMainNotification, mIconView, mIconPalette);
+                        mMainView.applyNotificationInfo(newMainNotification, mIconView, true);
                         // Remove the animated notification from the footer by calling trim
                         // TODO: Remove the notification in NotificationFooterLayout directly
                         // instead of relying on this hack.
index 2997d40..76a84b7 100644 (file)
@@ -41,6 +41,7 @@ public class NotificationMainView extends LinearLayout implements SwipeHelper.Ca
     private NotificationInfo mNotificationInfo;
     private TextView mTitleView;
     private TextView mTextView;
+    private IconPalette mIconPalette;
 
     public NotificationMainView(Context context) {
         this(context, null, 0);
@@ -62,35 +63,38 @@ public class NotificationMainView extends LinearLayout implements SwipeHelper.Ca
         mTextView = (TextView) findViewById(R.id.text);
     }
 
+    public void applyColors(IconPalette iconPalette) {
+        setBackgroundColor(iconPalette.backgroundColor);
+        mIconPalette = iconPalette;
+    }
+
     public void applyNotificationInfo(NotificationInfo mainNotification, View iconView) {
-        applyNotificationInfo(mainNotification, iconView, null);
+        applyNotificationInfo(mainNotification, iconView, false);
     }
 
     /**
-     * @param iconPalette if not null, indicates that the new info should be animated in,
-     *                    and that part of this animation includes animating the background
-     *                    from iconPalette.secondaryColor to iconPalette.backgroundColor.
+     * Sets the content of this view, animating it after a new icon shifts up if necessary.
      */
     public void applyNotificationInfo(NotificationInfo mainNotification, View iconView,
-            @Nullable IconPalette iconPalette) {
-        boolean animate = iconPalette != null;
+           boolean animate) {
         if (animate) {
             mTitleView.setAlpha(0);
             mTextView.setAlpha(0);
-            setBackgroundColor(iconPalette.secondaryColor);
+            setBackgroundColor(mIconPalette.secondaryColor);
         }
         mNotificationInfo = mainNotification;
         mTitleView.setText(mNotificationInfo.title);
         mTextView.setText(mNotificationInfo.text);
-        iconView.setBackground(mNotificationInfo.iconDrawable);
+        iconView.setBackground(mNotificationInfo.getIconForBackground(
+                getContext(), mIconPalette.backgroundColor));
         setOnClickListener(mNotificationInfo);
         setTranslationX(0);
         if (animate) {
             AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
             Animator textFade = new LauncherViewPropertyAnimator(mTextView).alpha(1);
             Animator titleFade = new LauncherViewPropertyAnimator(mTitleView).alpha(1);
-            ValueAnimator colorChange = ValueAnimator.ofArgb(iconPalette.secondaryColor,
-                    iconPalette.backgroundColor);
+            ValueAnimator colorChange = ValueAnimator.ofArgb(mIconPalette.secondaryColor,
+                    mIconPalette.backgroundColor);
             colorChange.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                 @Override
                 public void onAnimationUpdate(ValueAnimator valueAnimator) {