Home | History | Annotate | Download | only in bubbles
      1 /*
      2  * Copyright (C) 2018 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.systemui.bubbles;
     18 
     19 import android.annotation.Nullable;
     20 import android.app.Notification;
     21 import android.content.Context;
     22 import android.graphics.Color;
     23 import android.graphics.drawable.AdaptiveIconDrawable;
     24 import android.graphics.drawable.ColorDrawable;
     25 import android.graphics.drawable.Drawable;
     26 import android.graphics.drawable.Icon;
     27 import android.graphics.drawable.InsetDrawable;
     28 import android.util.AttributeSet;
     29 import android.widget.FrameLayout;
     30 
     31 import com.android.internal.graphics.ColorUtils;
     32 import com.android.systemui.Interpolators;
     33 import com.android.systemui.R;
     34 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
     35 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
     36 
     37 /**
     38  * A floating object on the screen that can post message updates.
     39  */
     40 public class BubbleView extends FrameLayout {
     41     private static final String TAG = "BubbleView";
     42 
     43     private static final int DARK_ICON_ALPHA = 180;
     44     private static final double ICON_MIN_CONTRAST = 4.1;
     45     private static final int DEFAULT_BACKGROUND_COLOR =  Color.LTGRAY;
     46     // Same value as Launcher3 badge code
     47     private static final float WHITE_SCRIM_ALPHA = 0.54f;
     48     private Context mContext;
     49 
     50     private BadgedImageView mBadgedImageView;
     51     private int mBadgeColor;
     52     private int mPadding;
     53     private int mIconInset;
     54 
     55     private boolean mSuppressDot = false;
     56 
     57     private NotificationEntry mEntry;
     58 
     59     public BubbleView(Context context) {
     60         this(context, null);
     61     }
     62 
     63     public BubbleView(Context context, AttributeSet attrs) {
     64         this(context, attrs, 0);
     65     }
     66 
     67     public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
     68         this(context, attrs, defStyleAttr, 0);
     69     }
     70 
     71     public BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
     72         super(context, attrs, defStyleAttr, defStyleRes);
     73         mContext = context;
     74         // XXX: can this padding just be on the view and we look it up?
     75         mPadding = getResources().getDimensionPixelSize(R.dimen.bubble_view_padding);
     76         mIconInset = getResources().getDimensionPixelSize(R.dimen.bubble_icon_inset);
     77     }
     78 
     79     @Override
     80     protected void onFinishInflate() {
     81         super.onFinishInflate();
     82         mBadgedImageView = findViewById(R.id.bubble_image);
     83     }
     84 
     85     @Override
     86     protected void onAttachedToWindow() {
     87         super.onAttachedToWindow();
     88     }
     89 
     90     /**
     91      * Populates this view with a notification.
     92      * <p>
     93      * This should only be called when a new notification is being set on the view, updates to the
     94      * current notification should use {@link #update(NotificationEntry)}.
     95      *
     96      * @param entry the notification to display as a bubble.
     97      */
     98     public void setNotif(NotificationEntry entry) {
     99         mEntry = entry;
    100         updateViews();
    101     }
    102 
    103     /**
    104      * The {@link NotificationEntry} associated with this view, if one exists.
    105      */
    106     @Nullable
    107     public NotificationEntry getEntry() {
    108         return mEntry;
    109     }
    110 
    111     /**
    112      * The key for the {@link NotificationEntry} associated with this view, if one exists.
    113      */
    114     @Nullable
    115     public String getKey() {
    116         return (mEntry != null) ? mEntry.key : null;
    117     }
    118 
    119     /**
    120      * Updates the UI based on the entry, updates badge and animates messages as needed.
    121      */
    122     public void update(NotificationEntry entry) {
    123         mEntry = entry;
    124         updateViews();
    125     }
    126 
    127     /**
    128      * @return the {@link ExpandableNotificationRow} view to display notification content when the
    129      * bubble is expanded.
    130      */
    131     @Nullable
    132     public ExpandableNotificationRow getRowView() {
    133         return (mEntry != null) ? mEntry.getRow() : null;
    134     }
    135 
    136     /** Changes the dot's visibility to match the bubble view's state. */
    137     void updateDotVisibility(boolean animate) {
    138         updateDotVisibility(animate, null /* after */);
    139     }
    140 
    141 
    142     /**
    143      * Sets whether or not to hide the dot even if we'd otherwise show it. This is used while the
    144      * flyout is visible or animating, to hide the dot until the flyout visually transforms into it.
    145      */
    146     void setSuppressDot(boolean suppressDot, boolean animate) {
    147         mSuppressDot = suppressDot;
    148         updateDotVisibility(animate);
    149     }
    150 
    151     /** Sets the position of the 'new' dot, animating it out and back in if requested. */
    152     void setDotPosition(boolean onLeft, boolean animate) {
    153         if (animate && onLeft != mBadgedImageView.getDotPosition() && !mSuppressDot) {
    154             animateDot(false /* showDot */, () -> {
    155                 mBadgedImageView.setDotPosition(onLeft);
    156                 animateDot(true /* showDot */, null);
    157             });
    158         } else {
    159             mBadgedImageView.setDotPosition(onLeft);
    160         }
    161     }
    162 
    163     boolean getDotPositionOnLeft() {
    164         return mBadgedImageView.getDotPosition();
    165     }
    166 
    167     /**
    168      * Changes the dot's visibility to match the bubble view's state, running the provided callback
    169      * after animation if requested.
    170      */
    171     private void updateDotVisibility(boolean animate, Runnable after) {
    172         boolean showDot = getEntry().showInShadeWhenBubble() && !mSuppressDot;
    173 
    174         if (animate) {
    175             animateDot(showDot, after);
    176         } else {
    177             mBadgedImageView.setShowDot(showDot);
    178         }
    179     }
    180 
    181     /**
    182      * Animates the badge to show or hide.
    183      */
    184     private void animateDot(boolean showDot, Runnable after) {
    185         if (mBadgedImageView.isShowingDot() != showDot) {
    186             if (showDot) {
    187                 mBadgedImageView.setShowDot(true);
    188             }
    189 
    190             mBadgedImageView.clearAnimation();
    191             mBadgedImageView.animate().setDuration(200)
    192                     .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
    193                     .setUpdateListener((valueAnimator) -> {
    194                         float fraction = valueAnimator.getAnimatedFraction();
    195                         fraction = showDot ? fraction : 1f - fraction;
    196                         mBadgedImageView.setDotScale(fraction);
    197                     }).withEndAction(() -> {
    198                         if (!showDot) {
    199                             mBadgedImageView.setShowDot(false);
    200                         }
    201 
    202                         if (after != null) {
    203                             after.run();
    204                         }
    205             }).start();
    206         }
    207     }
    208 
    209     void updateViews() {
    210         if (mEntry == null) {
    211             return;
    212         }
    213         Notification.BubbleMetadata metadata = mEntry.getBubbleMetadata();
    214         Notification n = mEntry.notification.getNotification();
    215         Icon ic;
    216         boolean needsTint;
    217         if (metadata != null) {
    218             ic = metadata.getIcon();
    219             needsTint = ic.getType() != Icon.TYPE_ADAPTIVE_BITMAP;
    220         } else {
    221             needsTint = n.getLargeIcon() == null;
    222             ic = needsTint ? n.getSmallIcon() : n.getLargeIcon();
    223         }
    224         Drawable iconDrawable = ic.loadDrawable(mContext);
    225         if (needsTint) {
    226             mBadgedImageView.setImageDrawable(buildIconWithTint(iconDrawable, n.color));
    227         } else {
    228             mBadgedImageView.setImageDrawable(iconDrawable);
    229         }
    230         int badgeColor = determineDominateColor(iconDrawable, n.color);
    231         mBadgeColor = badgeColor;
    232         mBadgedImageView.setDotColor(badgeColor);
    233         animateDot(mEntry.showInShadeWhenBubble() /* showDot */, null /* after */);
    234     }
    235 
    236     int getBadgeColor() {
    237         return mBadgeColor;
    238     }
    239 
    240     private Drawable buildIconWithTint(Drawable iconDrawable, int backgroundColor) {
    241         iconDrawable = checkTint(iconDrawable, backgroundColor);
    242         InsetDrawable foreground = new InsetDrawable(iconDrawable, mIconInset);
    243         ColorDrawable background = new ColorDrawable(backgroundColor);
    244         return new AdaptiveIconDrawable(background, foreground);
    245     }
    246 
    247     private Drawable checkTint(Drawable iconDrawable, int backgroundColor) {
    248         backgroundColor = ColorUtils.setAlphaComponent(backgroundColor, 255 /* alpha */);
    249         if (backgroundColor == Color.TRANSPARENT) {
    250             // ColorUtils throws exception when background is translucent.
    251             backgroundColor = DEFAULT_BACKGROUND_COLOR;
    252         }
    253         iconDrawable.setTint(Color.WHITE);
    254         double contrastRatio = ColorUtils.calculateContrast(Color.WHITE, backgroundColor);
    255         if (contrastRatio < ICON_MIN_CONTRAST) {
    256             int dark = ColorUtils.setAlphaComponent(Color.BLACK, DARK_ICON_ALPHA);
    257             iconDrawable.setTint(dark);
    258         }
    259         return iconDrawable;
    260     }
    261 
    262     private int determineDominateColor(Drawable d, int defaultTint) {
    263         // XXX: should we pull from the drawable, app icon, notif tint?
    264         return ColorUtils.blendARGB(defaultTint, Color.WHITE, WHITE_SCRIM_ALPHA);
    265     }
    266 }
    267