Home | History | Annotate | Download | only in statusbar
      1 /*
      2  * Copyright (C) 2008 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.statusbar;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ObjectAnimator;
     22 import android.animation.ValueAnimator;
     23 import android.app.Notification;
     24 import android.content.Context;
     25 import android.content.pm.ApplicationInfo;
     26 import android.content.res.ColorStateList;
     27 import android.content.res.Configuration;
     28 import android.content.res.Resources;
     29 import android.graphics.Canvas;
     30 import android.graphics.Color;
     31 import android.graphics.Paint;
     32 import android.graphics.Rect;
     33 import android.graphics.drawable.Drawable;
     34 import android.graphics.drawable.Icon;
     35 import android.os.Parcelable;
     36 import android.os.UserHandle;
     37 import android.service.notification.StatusBarNotification;
     38 import android.support.v4.graphics.ColorUtils;
     39 import android.text.TextUtils;
     40 import android.util.AttributeSet;
     41 import android.util.FloatProperty;
     42 import android.util.Log;
     43 import android.util.Property;
     44 import android.util.TypedValue;
     45 import android.view.ViewDebug;
     46 import android.view.accessibility.AccessibilityEvent;
     47 import android.view.animation.Interpolator;
     48 
     49 import com.android.internal.statusbar.StatusBarIcon;
     50 import com.android.internal.util.NotificationColorUtil;
     51 import com.android.systemui.Interpolators;
     52 import com.android.systemui.R;
     53 import com.android.systemui.statusbar.notification.NotificationIconDozeHelper;
     54 import com.android.systemui.statusbar.notification.NotificationUtils;
     55 
     56 import java.text.NumberFormat;
     57 
     58 public class StatusBarIconView extends AnimatedImageView {
     59     public static final int NO_COLOR = 0;
     60     private final int ANIMATION_DURATION_FAST = 100;
     61 
     62     public static final int STATE_ICON = 0;
     63     public static final int STATE_DOT = 1;
     64     public static final int STATE_HIDDEN = 2;
     65 
     66     private static final String TAG = "StatusBarIconView";
     67     private static final Property<StatusBarIconView, Float> ICON_APPEAR_AMOUNT
     68             = new FloatProperty<StatusBarIconView>("iconAppearAmount") {
     69 
     70         @Override
     71         public void setValue(StatusBarIconView object, float value) {
     72             object.setIconAppearAmount(value);
     73         }
     74 
     75         @Override
     76         public Float get(StatusBarIconView object) {
     77             return object.getIconAppearAmount();
     78         }
     79     };
     80     private static final Property<StatusBarIconView, Float> DOT_APPEAR_AMOUNT
     81             = new FloatProperty<StatusBarIconView>("dot_appear_amount") {
     82 
     83         @Override
     84         public void setValue(StatusBarIconView object, float value) {
     85             object.setDotAppearAmount(value);
     86         }
     87 
     88         @Override
     89         public Float get(StatusBarIconView object) {
     90             return object.getDotAppearAmount();
     91         }
     92     };
     93 
     94     private boolean mAlwaysScaleIcon;
     95     private int mStatusBarIconDrawingSizeDark = 1;
     96     private int mStatusBarIconDrawingSize = 1;
     97     private int mStatusBarIconSize = 1;
     98     private StatusBarIcon mIcon;
     99     @ViewDebug.ExportedProperty private String mSlot;
    100     private Drawable mNumberBackground;
    101     private Paint mNumberPain;
    102     private int mNumberX;
    103     private int mNumberY;
    104     private String mNumberText;
    105     private StatusBarNotification mNotification;
    106     private final boolean mBlocked;
    107     private int mDensity;
    108     private float mIconScale = 1.0f;
    109     private final Paint mDotPaint = new Paint();
    110     private float mDotRadius;
    111     private int mStaticDotRadius;
    112     private int mVisibleState = STATE_ICON;
    113     private float mIconAppearAmount = 1.0f;
    114     private ObjectAnimator mIconAppearAnimator;
    115     private ObjectAnimator mDotAnimator;
    116     private float mDotAppearAmount;
    117     private OnVisibilityChangedListener mOnVisibilityChangedListener;
    118     private int mDrawableColor;
    119     private int mIconColor;
    120     private int mDecorColor;
    121     private float mDarkAmount;
    122     private ValueAnimator mColorAnimator;
    123     private int mCurrentSetColor = NO_COLOR;
    124     private int mAnimationStartColor = NO_COLOR;
    125     private final ValueAnimator.AnimatorUpdateListener mColorUpdater
    126             = animation -> {
    127         int newColor = NotificationUtils.interpolateColors(mAnimationStartColor, mIconColor,
    128                 animation.getAnimatedFraction());
    129         setColorInternal(newColor);
    130     };
    131     private final NotificationIconDozeHelper mDozer;
    132     private int mContrastedDrawableColor;
    133     private int mCachedContrastBackgroundColor = NO_COLOR;
    134 
    135     public StatusBarIconView(Context context, String slot, StatusBarNotification sbn) {
    136         this(context, slot, sbn, false);
    137     }
    138 
    139     public StatusBarIconView(Context context, String slot, StatusBarNotification sbn,
    140             boolean blocked) {
    141         super(context);
    142         mDozer = new NotificationIconDozeHelper(context);
    143         mBlocked = blocked;
    144         mSlot = slot;
    145         mNumberPain = new Paint();
    146         mNumberPain.setTextAlign(Paint.Align.CENTER);
    147         mNumberPain.setColor(context.getColor(R.drawable.notification_number_text_color));
    148         mNumberPain.setAntiAlias(true);
    149         setNotification(sbn);
    150         maybeUpdateIconScaleDimens();
    151         setScaleType(ScaleType.CENTER);
    152         mDensity = context.getResources().getDisplayMetrics().densityDpi;
    153         if (mNotification != null) {
    154             setDecorColor(getContext().getColor(
    155                     com.android.internal.R.color.notification_icon_default_color));
    156         }
    157         reloadDimens();
    158     }
    159 
    160     private void maybeUpdateIconScaleDimens() {
    161         // We do not resize and scale system icons (on the right), only notification icons (on the
    162         // left).
    163         if (mNotification != null || mAlwaysScaleIcon) {
    164             updateIconScaleDimens();
    165         }
    166     }
    167 
    168     private void updateIconScaleDimens() {
    169         Resources res = mContext.getResources();
    170         mStatusBarIconSize = res.getDimensionPixelSize(R.dimen.status_bar_icon_size);
    171         mStatusBarIconDrawingSizeDark =
    172                 res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size_dark);
    173         mStatusBarIconDrawingSize =
    174                 res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size);
    175         updateIconScale();
    176     }
    177 
    178     private void updateIconScale() {
    179         final float imageBounds = NotificationUtils.interpolate(
    180                 mStatusBarIconDrawingSize,
    181                 mStatusBarIconDrawingSizeDark,
    182                 mDarkAmount);
    183         final int outerBounds = mStatusBarIconSize;
    184         mIconScale = (float)imageBounds / (float)outerBounds;
    185     }
    186 
    187     public float getIconScaleFullyDark() {
    188         return (float) mStatusBarIconDrawingSizeDark / mStatusBarIconDrawingSize;
    189     }
    190 
    191     public float getIconScale() {
    192         return mIconScale;
    193     }
    194 
    195     @Override
    196     protected void onConfigurationChanged(Configuration newConfig) {
    197         super.onConfigurationChanged(newConfig);
    198         int density = newConfig.densityDpi;
    199         if (density != mDensity) {
    200             mDensity = density;
    201             maybeUpdateIconScaleDimens();
    202             updateDrawable();
    203             reloadDimens();
    204         }
    205     }
    206 
    207     private void reloadDimens() {
    208         boolean applyRadius = mDotRadius == mStaticDotRadius;
    209         mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
    210         if (applyRadius) {
    211             mDotRadius = mStaticDotRadius;
    212         }
    213     }
    214 
    215     public void setNotification(StatusBarNotification notification) {
    216         mNotification = notification;
    217         if (notification != null) {
    218             setContentDescription(notification.getNotification());
    219         }
    220     }
    221 
    222     public StatusBarIconView(Context context, AttributeSet attrs) {
    223         super(context, attrs);
    224         mDozer = new NotificationIconDozeHelper(context);
    225         mBlocked = false;
    226         mAlwaysScaleIcon = true;
    227         updateIconScaleDimens();
    228         mDensity = context.getResources().getDisplayMetrics().densityDpi;
    229     }
    230 
    231     private static boolean streq(String a, String b) {
    232         if (a == b) {
    233             return true;
    234         }
    235         if (a == null && b != null) {
    236             return false;
    237         }
    238         if (a != null && b == null) {
    239             return false;
    240         }
    241         return a.equals(b);
    242     }
    243 
    244     public boolean equalIcons(Icon a, Icon b) {
    245         if (a == b) return true;
    246         if (a.getType() != b.getType()) return false;
    247         switch (a.getType()) {
    248             case Icon.TYPE_RESOURCE:
    249                 return a.getResPackage().equals(b.getResPackage()) && a.getResId() == b.getResId();
    250             case Icon.TYPE_URI:
    251                 return a.getUriString().equals(b.getUriString());
    252             default:
    253                 return false;
    254         }
    255     }
    256     /**
    257      * Returns whether the set succeeded.
    258      */
    259     public boolean set(StatusBarIcon icon) {
    260         final boolean iconEquals = mIcon != null && equalIcons(mIcon.icon, icon.icon);
    261         final boolean levelEquals = iconEquals
    262                 && mIcon.iconLevel == icon.iconLevel;
    263         final boolean visibilityEquals = mIcon != null
    264                 && mIcon.visible == icon.visible;
    265         final boolean numberEquals = mIcon != null
    266                 && mIcon.number == icon.number;
    267         mIcon = icon.clone();
    268         setContentDescription(icon.contentDescription);
    269         if (!iconEquals) {
    270             if (!updateDrawable(false /* no clear */)) return false;
    271             // we have to clear the grayscale tag since it may have changed
    272             setTag(R.id.icon_is_grayscale, null);
    273         }
    274         if (!levelEquals) {
    275             setImageLevel(icon.iconLevel);
    276         }
    277 
    278         if (!numberEquals) {
    279             if (icon.number > 0 && getContext().getResources().getBoolean(
    280                         R.bool.config_statusBarShowNumber)) {
    281                 if (mNumberBackground == null) {
    282                     mNumberBackground = getContext().getResources().getDrawable(
    283                             R.drawable.ic_notification_overlay);
    284                 }
    285                 placeNumber();
    286             } else {
    287                 mNumberBackground = null;
    288                 mNumberText = null;
    289             }
    290             invalidate();
    291         }
    292         if (!visibilityEquals) {
    293             setVisibility(icon.visible && !mBlocked ? VISIBLE : GONE);
    294         }
    295         return true;
    296     }
    297 
    298     public void updateDrawable() {
    299         updateDrawable(true /* with clear */);
    300     }
    301 
    302     private boolean updateDrawable(boolean withClear) {
    303         if (mIcon == null) {
    304             return false;
    305         }
    306         Drawable drawable;
    307         try {
    308             drawable = getIcon(mIcon);
    309         } catch (OutOfMemoryError e) {
    310             Log.w(TAG, "OOM while inflating " + mIcon.icon + " for slot " + mSlot);
    311             return false;
    312         }
    313 
    314         if (drawable == null) {
    315             Log.w(TAG, "No icon for slot " + mSlot + "; " + mIcon.icon);
    316             return false;
    317         }
    318         if (withClear) {
    319             setImageDrawable(null);
    320         }
    321         setImageDrawable(drawable);
    322         return true;
    323     }
    324 
    325     public Icon getSourceIcon() {
    326         return mIcon.icon;
    327     }
    328 
    329     private Drawable getIcon(StatusBarIcon icon) {
    330         return getIcon(getContext(), icon);
    331     }
    332 
    333     /**
    334      * Returns the right icon to use for this item
    335      *
    336      * @param context Context to use to get resources
    337      * @return Drawable for this item, or null if the package or item could not
    338      *         be found
    339      */
    340     public static Drawable getIcon(Context context, StatusBarIcon statusBarIcon) {
    341         int userId = statusBarIcon.user.getIdentifier();
    342         if (userId == UserHandle.USER_ALL) {
    343             userId = UserHandle.USER_SYSTEM;
    344         }
    345 
    346         Drawable icon = statusBarIcon.icon.loadDrawableAsUser(context, userId);
    347 
    348         TypedValue typedValue = new TypedValue();
    349         context.getResources().getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
    350         float scaleFactor = typedValue.getFloat();
    351 
    352         // No need to scale the icon, so return it as is.
    353         if (scaleFactor == 1.f) {
    354             return icon;
    355         }
    356 
    357         return new ScalingDrawableWrapper(icon, scaleFactor);
    358     }
    359 
    360     public StatusBarIcon getStatusBarIcon() {
    361         return mIcon;
    362     }
    363 
    364     @Override
    365     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    366         super.onInitializeAccessibilityEvent(event);
    367         if (mNotification != null) {
    368             event.setParcelableData(mNotification.getNotification());
    369         }
    370     }
    371 
    372     @Override
    373     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    374         super.onSizeChanged(w, h, oldw, oldh);
    375         if (mNumberBackground != null) {
    376             placeNumber();
    377         }
    378     }
    379 
    380     @Override
    381     public void onRtlPropertiesChanged(int layoutDirection) {
    382         super.onRtlPropertiesChanged(layoutDirection);
    383         updateDrawable();
    384     }
    385 
    386     @Override
    387     protected void onDraw(Canvas canvas) {
    388         if (mIconAppearAmount > 0.0f) {
    389             canvas.save();
    390             canvas.scale(mIconScale * mIconAppearAmount, mIconScale * mIconAppearAmount,
    391                     getWidth() / 2, getHeight() / 2);
    392             super.onDraw(canvas);
    393             canvas.restore();
    394         }
    395 
    396         if (mNumberBackground != null) {
    397             mNumberBackground.draw(canvas);
    398             canvas.drawText(mNumberText, mNumberX, mNumberY, mNumberPain);
    399         }
    400         if (mDotAppearAmount != 0.0f) {
    401             float radius;
    402             float alpha;
    403             if (mDotAppearAmount <= 1.0f) {
    404                 radius = mDotRadius * mDotAppearAmount;
    405                 alpha = 1.0f;
    406             } else {
    407                 float fadeOutAmount = mDotAppearAmount - 1.0f;
    408                 alpha = 1.0f - fadeOutAmount;
    409                 radius = NotificationUtils.interpolate(mDotRadius, getWidth() / 4, fadeOutAmount);
    410             }
    411             mDotPaint.setAlpha((int) (alpha * 255));
    412             canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mDotPaint);
    413         }
    414     }
    415 
    416     @Override
    417     protected void debug(int depth) {
    418         super.debug(depth);
    419         Log.d("View", debugIndent(depth) + "slot=" + mSlot);
    420         Log.d("View", debugIndent(depth) + "icon=" + mIcon);
    421     }
    422 
    423     void placeNumber() {
    424         final String str;
    425         final int tooBig = getContext().getResources().getInteger(
    426                 android.R.integer.status_bar_notification_info_maxnum);
    427         if (mIcon.number > tooBig) {
    428             str = getContext().getResources().getString(
    429                         android.R.string.status_bar_notification_info_overflow);
    430         } else {
    431             NumberFormat f = NumberFormat.getIntegerInstance();
    432             str = f.format(mIcon.number);
    433         }
    434         mNumberText = str;
    435 
    436         final int w = getWidth();
    437         final int h = getHeight();
    438         final Rect r = new Rect();
    439         mNumberPain.getTextBounds(str, 0, str.length(), r);
    440         final int tw = r.right - r.left;
    441         final int th = r.bottom - r.top;
    442         mNumberBackground.getPadding(r);
    443         int dw = r.left + tw + r.right;
    444         if (dw < mNumberBackground.getMinimumWidth()) {
    445             dw = mNumberBackground.getMinimumWidth();
    446         }
    447         mNumberX = w-r.right-((dw-r.right-r.left)/2);
    448         int dh = r.top + th + r.bottom;
    449         if (dh < mNumberBackground.getMinimumWidth()) {
    450             dh = mNumberBackground.getMinimumWidth();
    451         }
    452         mNumberY = h-r.bottom-((dh-r.top-th-r.bottom)/2);
    453         mNumberBackground.setBounds(w-dw, h-dh, w, h);
    454     }
    455 
    456     private void setContentDescription(Notification notification) {
    457         if (notification != null) {
    458             String d = contentDescForNotification(mContext, notification);
    459             if (!TextUtils.isEmpty(d)) {
    460                 setContentDescription(d);
    461             }
    462         }
    463     }
    464 
    465     public String toString() {
    466         return "StatusBarIconView(slot=" + mSlot + " icon=" + mIcon
    467             + " notification=" + mNotification + ")";
    468     }
    469 
    470     public StatusBarNotification getNotification() {
    471         return mNotification;
    472     }
    473 
    474     public String getSlot() {
    475         return mSlot;
    476     }
    477 
    478 
    479     public static String contentDescForNotification(Context c, Notification n) {
    480         String appName = "";
    481         try {
    482             Notification.Builder builder = Notification.Builder.recoverBuilder(c, n);
    483             appName = builder.loadHeaderAppName();
    484         } catch (RuntimeException e) {
    485             Log.e(TAG, "Unable to recover builder", e);
    486             // Trying to get the app name from the app info instead.
    487             Parcelable appInfo = n.extras.getParcelable(
    488                     Notification.EXTRA_BUILDER_APPLICATION_INFO);
    489             if (appInfo instanceof ApplicationInfo) {
    490                 appName = String.valueOf(((ApplicationInfo) appInfo).loadLabel(
    491                         c.getPackageManager()));
    492             }
    493         }
    494 
    495         CharSequence title = n.extras.getCharSequence(Notification.EXTRA_TITLE);
    496         CharSequence text = n.extras.getCharSequence(Notification.EXTRA_TEXT);
    497         CharSequence ticker = n.tickerText;
    498 
    499         // Some apps just put the app name into the title
    500         CharSequence titleOrText = TextUtils.equals(title, appName) ? text : title;
    501 
    502         CharSequence desc = !TextUtils.isEmpty(titleOrText) ? titleOrText
    503                 : !TextUtils.isEmpty(ticker) ? ticker : "";
    504 
    505         return c.getString(R.string.accessibility_desc_notification_icon, appName, desc);
    506     }
    507 
    508     /**
    509      * Set the color that is used to draw decoration like the overflow dot. This will not be applied
    510      * to the drawable.
    511      */
    512     public void setDecorColor(int iconTint) {
    513         mDecorColor = iconTint;
    514         updateDecorColor();
    515     }
    516 
    517     private void updateDecorColor() {
    518         int color = NotificationUtils.interpolateColors(mDecorColor, Color.WHITE, mDarkAmount);
    519         if (mDotPaint.getColor() != color) {
    520             mDotPaint.setColor(color);
    521 
    522             if (mDotAppearAmount != 0) {
    523                 invalidate();
    524             }
    525         }
    526     }
    527 
    528     /**
    529      * Set the static color that should be used for the drawable of this icon if it's not
    530      * transitioning this also immediately sets the color.
    531      */
    532     public void setStaticDrawableColor(int color) {
    533         mDrawableColor = color;
    534         setColorInternal(color);
    535         updateContrastedStaticColor();
    536         mIconColor = color;
    537         mDozer.setColor(color);
    538     }
    539 
    540     private void setColorInternal(int color) {
    541         mCurrentSetColor = color;
    542         updateIconColor();
    543     }
    544 
    545     private void updateIconColor() {
    546         if (mCurrentSetColor != NO_COLOR) {
    547             setImageTintList(ColorStateList.valueOf(NotificationUtils.interpolateColors(
    548                     mCurrentSetColor, Color.WHITE, mDarkAmount)));
    549         } else {
    550             setImageTintList(null);
    551             mDozer.updateGrayscale(this, mDarkAmount);
    552         }
    553     }
    554 
    555     public void setIconColor(int iconColor, boolean animate) {
    556         if (mIconColor != iconColor) {
    557             mIconColor = iconColor;
    558             if (mColorAnimator != null) {
    559                 mColorAnimator.cancel();
    560             }
    561             if (mCurrentSetColor == iconColor) {
    562                 return;
    563             }
    564             if (animate && mCurrentSetColor != NO_COLOR) {
    565                 mAnimationStartColor = mCurrentSetColor;
    566                 mColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
    567                 mColorAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
    568                 mColorAnimator.setDuration(ANIMATION_DURATION_FAST);
    569                 mColorAnimator.addUpdateListener(mColorUpdater);
    570                 mColorAnimator.addListener(new AnimatorListenerAdapter() {
    571                     @Override
    572                     public void onAnimationEnd(Animator animation) {
    573                         mColorAnimator = null;
    574                         mAnimationStartColor = NO_COLOR;
    575                     }
    576                 });
    577                 mColorAnimator.start();
    578             } else {
    579                 setColorInternal(iconColor);
    580             }
    581         }
    582     }
    583 
    584     public int getStaticDrawableColor() {
    585         return mDrawableColor;
    586     }
    587 
    588     /**
    589      * A drawable color that passes GAR on a specific background.
    590      * This value is cached.
    591      *
    592      * @param backgroundColor Background to test against.
    593      * @return GAR safe version of {@link StatusBarIconView#getStaticDrawableColor()}.
    594      */
    595     int getContrastedStaticDrawableColor(int backgroundColor) {
    596         if (mCachedContrastBackgroundColor != backgroundColor) {
    597             mCachedContrastBackgroundColor = backgroundColor;
    598             updateContrastedStaticColor();
    599         }
    600         return mContrastedDrawableColor;
    601     }
    602 
    603     private void updateContrastedStaticColor() {
    604         if (Color.alpha(mCachedContrastBackgroundColor) != 255) {
    605             mContrastedDrawableColor = mDrawableColor;
    606             return;
    607         }
    608         // We'll modify the color if it doesn't pass GAR
    609         int contrastedColor = mDrawableColor;
    610         if (!NotificationColorUtil.satisfiesTextContrast(mCachedContrastBackgroundColor,
    611                 contrastedColor)) {
    612             float[] hsl = new float[3];
    613             ColorUtils.colorToHSL(mDrawableColor, hsl);
    614             // This is basically a light grey, pushing the color will only distort it.
    615             // Best thing to do in here is to fallback to the default color.
    616             if (hsl[1] < 0.2f) {
    617                 contrastedColor = Notification.COLOR_DEFAULT;
    618             }
    619             contrastedColor = NotificationColorUtil.resolveContrastColor(mContext,
    620                     contrastedColor, mCachedContrastBackgroundColor);
    621         }
    622         mContrastedDrawableColor = contrastedColor;
    623     }
    624 
    625     public void setVisibleState(int state) {
    626         setVisibleState(state, true /* animate */, null /* endRunnable */);
    627     }
    628 
    629     public void setVisibleState(int state, boolean animate) {
    630         setVisibleState(state, animate, null);
    631     }
    632 
    633     @Override
    634     public boolean hasOverlappingRendering() {
    635         return false;
    636     }
    637 
    638     public void setVisibleState(int visibleState, boolean animate, Runnable endRunnable) {
    639         boolean runnableAdded = false;
    640         if (visibleState != mVisibleState) {
    641             mVisibleState = visibleState;
    642             if (mIconAppearAnimator != null) {
    643                 mIconAppearAnimator.cancel();
    644             }
    645             if (mDotAnimator != null) {
    646                 mDotAnimator.cancel();
    647             }
    648             if (animate) {
    649                 float targetAmount = 0.0f;
    650                 Interpolator interpolator = Interpolators.FAST_OUT_LINEAR_IN;
    651                 if (visibleState == STATE_ICON) {
    652                     targetAmount = 1.0f;
    653                     interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
    654                 }
    655                 float currentAmount = getIconAppearAmount();
    656                 if (targetAmount != currentAmount) {
    657                     mIconAppearAnimator = ObjectAnimator.ofFloat(this, ICON_APPEAR_AMOUNT,
    658                             currentAmount, targetAmount);
    659                     mIconAppearAnimator.setInterpolator(interpolator);
    660                     mIconAppearAnimator.setDuration(ANIMATION_DURATION_FAST);
    661                     mIconAppearAnimator.addListener(new AnimatorListenerAdapter() {
    662                         @Override
    663                         public void onAnimationEnd(Animator animation) {
    664                             mIconAppearAnimator = null;
    665                             runRunnable(endRunnable);
    666                         }
    667                     });
    668                     mIconAppearAnimator.start();
    669                     runnableAdded = true;
    670                 }
    671 
    672                 targetAmount = visibleState == STATE_ICON ? 2.0f : 0.0f;
    673                 interpolator = Interpolators.FAST_OUT_LINEAR_IN;
    674                 if (visibleState == STATE_DOT) {
    675                     targetAmount = 1.0f;
    676                     interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
    677                 }
    678                 currentAmount = getDotAppearAmount();
    679                 if (targetAmount != currentAmount) {
    680                     mDotAnimator = ObjectAnimator.ofFloat(this, DOT_APPEAR_AMOUNT,
    681                             currentAmount, targetAmount);
    682                     mDotAnimator.setInterpolator(interpolator);
    683                     mDotAnimator.setDuration(ANIMATION_DURATION_FAST);
    684                     final boolean runRunnable = !runnableAdded;
    685                     mDotAnimator.addListener(new AnimatorListenerAdapter() {
    686                         @Override
    687                         public void onAnimationEnd(Animator animation) {
    688                             mDotAnimator = null;
    689                             if (runRunnable) {
    690                                 runRunnable(endRunnable);
    691                             }
    692                         }
    693                     });
    694                     mDotAnimator.start();
    695                     runnableAdded = true;
    696                 }
    697             } else {
    698                 setIconAppearAmount(visibleState == STATE_ICON ? 1.0f : 0.0f);
    699                 setDotAppearAmount(visibleState == STATE_DOT ? 1.0f
    700                         : visibleState == STATE_ICON ? 2.0f
    701                         : 0.0f);
    702             }
    703         }
    704         if (!runnableAdded) {
    705             runRunnable(endRunnable);
    706         }
    707     }
    708 
    709     private void runRunnable(Runnable runnable) {
    710         if (runnable != null) {
    711             runnable.run();
    712         }
    713     }
    714 
    715     public void setIconAppearAmount(float iconAppearAmount) {
    716         if (mIconAppearAmount != iconAppearAmount) {
    717             mIconAppearAmount = iconAppearAmount;
    718             invalidate();
    719         }
    720     }
    721 
    722     public float getIconAppearAmount() {
    723         return mIconAppearAmount;
    724     }
    725 
    726     public int getVisibleState() {
    727         return mVisibleState;
    728     }
    729 
    730     public void setDotAppearAmount(float dotAppearAmount) {
    731         if (mDotAppearAmount != dotAppearAmount) {
    732             mDotAppearAmount = dotAppearAmount;
    733             invalidate();
    734         }
    735     }
    736 
    737     @Override
    738     public void setVisibility(int visibility) {
    739         super.setVisibility(visibility);
    740         if (mOnVisibilityChangedListener != null) {
    741             mOnVisibilityChangedListener.onVisibilityChanged(visibility);
    742         }
    743     }
    744 
    745     public float getDotAppearAmount() {
    746         return mDotAppearAmount;
    747     }
    748 
    749     public void setOnVisibilityChangedListener(OnVisibilityChangedListener listener) {
    750         mOnVisibilityChangedListener = listener;
    751     }
    752 
    753     public void setDark(boolean dark, boolean fade, long delay) {
    754         mDozer.setIntensityDark(f -> {
    755             mDarkAmount = f;
    756             updateIconScale();
    757             updateDecorColor();
    758             updateIconColor();
    759         }, dark, fade, delay);
    760     }
    761 
    762     public interface OnVisibilityChangedListener {
    763         void onVisibilityChanged(int newVisibility);
    764     }
    765 }
    766