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 
StatusBarIconView(Context context, String slot, StatusBarNotification sbn)135     public StatusBarIconView(Context context, String slot, StatusBarNotification sbn) {
136         this(context, slot, sbn, false);
137     }
138 
StatusBarIconView(Context context, String slot, StatusBarNotification sbn, boolean blocked)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 
maybeUpdateIconScaleDimens()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 
updateIconScaleDimens()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 
updateIconScale()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 
getIconScaleFullyDark()187     public float getIconScaleFullyDark() {
188         return (float) mStatusBarIconDrawingSizeDark / mStatusBarIconDrawingSize;
189     }
190 
getIconScale()191     public float getIconScale() {
192         return mIconScale;
193     }
194 
195     @Override
onConfigurationChanged(Configuration newConfig)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 
reloadDimens()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 
setNotification(StatusBarNotification notification)215     public void setNotification(StatusBarNotification notification) {
216         mNotification = notification;
217         if (notification != null) {
218             setContentDescription(notification.getNotification());
219         }
220     }
221 
StatusBarIconView(Context context, AttributeSet attrs)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 
streq(String a, String b)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 
equalIcons(Icon a, Icon b)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      */
set(StatusBarIcon icon)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 
updateDrawable()298     public void updateDrawable() {
299         updateDrawable(true /* with clear */);
300     }
301 
updateDrawable(boolean withClear)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 
getSourceIcon()325     public Icon getSourceIcon() {
326         return mIcon.icon;
327     }
328 
getIcon(StatusBarIcon icon)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      */
getIcon(Context context, StatusBarIcon statusBarIcon)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 
getStatusBarIcon()360     public StatusBarIcon getStatusBarIcon() {
361         return mIcon;
362     }
363 
364     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)365     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
366         super.onInitializeAccessibilityEvent(event);
367         if (mNotification != null) {
368             event.setParcelableData(mNotification.getNotification());
369         }
370     }
371 
372     @Override
onSizeChanged(int w, int h, int oldw, int oldh)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
onRtlPropertiesChanged(int layoutDirection)381     public void onRtlPropertiesChanged(int layoutDirection) {
382         super.onRtlPropertiesChanged(layoutDirection);
383         updateDrawable();
384     }
385 
386     @Override
onDraw(Canvas canvas)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
debug(int depth)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 
placeNumber()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 
setContentDescription(Notification notification)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 
toString()465     public String toString() {
466         return "StatusBarIconView(slot=" + mSlot + " icon=" + mIcon
467             + " notification=" + mNotification + ")";
468     }
469 
getNotification()470     public StatusBarNotification getNotification() {
471         return mNotification;
472     }
473 
getSlot()474     public String getSlot() {
475         return mSlot;
476     }
477 
478 
contentDescForNotification(Context c, Notification n)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      */
setDecorColor(int iconTint)512     public void setDecorColor(int iconTint) {
513         mDecorColor = iconTint;
514         updateDecorColor();
515     }
516 
updateDecorColor()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      */
setStaticDrawableColor(int color)532     public void setStaticDrawableColor(int color) {
533         mDrawableColor = color;
534         setColorInternal(color);
535         updateContrastedStaticColor();
536         mIconColor = color;
537         mDozer.setColor(color);
538     }
539 
setColorInternal(int color)540     private void setColorInternal(int color) {
541         mCurrentSetColor = color;
542         updateIconColor();
543     }
544 
updateIconColor()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 
setIconColor(int iconColor, boolean animate)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 
getStaticDrawableColor()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      */
getContrastedStaticDrawableColor(int backgroundColor)595     int getContrastedStaticDrawableColor(int backgroundColor) {
596         if (mCachedContrastBackgroundColor != backgroundColor) {
597             mCachedContrastBackgroundColor = backgroundColor;
598             updateContrastedStaticColor();
599         }
600         return mContrastedDrawableColor;
601     }
602 
updateContrastedStaticColor()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 
setVisibleState(int state)625     public void setVisibleState(int state) {
626         setVisibleState(state, true /* animate */, null /* endRunnable */);
627     }
628 
setVisibleState(int state, boolean animate)629     public void setVisibleState(int state, boolean animate) {
630         setVisibleState(state, animate, null);
631     }
632 
633     @Override
hasOverlappingRendering()634     public boolean hasOverlappingRendering() {
635         return false;
636     }
637 
setVisibleState(int visibleState, boolean animate, Runnable endRunnable)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 
runRunnable(Runnable runnable)709     private void runRunnable(Runnable runnable) {
710         if (runnable != null) {
711             runnable.run();
712         }
713     }
714 
setIconAppearAmount(float iconAppearAmount)715     public void setIconAppearAmount(float iconAppearAmount) {
716         if (mIconAppearAmount != iconAppearAmount) {
717             mIconAppearAmount = iconAppearAmount;
718             invalidate();
719         }
720     }
721 
getIconAppearAmount()722     public float getIconAppearAmount() {
723         return mIconAppearAmount;
724     }
725 
getVisibleState()726     public int getVisibleState() {
727         return mVisibleState;
728     }
729 
setDotAppearAmount(float dotAppearAmount)730     public void setDotAppearAmount(float dotAppearAmount) {
731         if (mDotAppearAmount != dotAppearAmount) {
732             mDotAppearAmount = dotAppearAmount;
733             invalidate();
734         }
735     }
736 
737     @Override
setVisibility(int visibility)738     public void setVisibility(int visibility) {
739         super.setVisibility(visibility);
740         if (mOnVisibilityChangedListener != null) {
741             mOnVisibilityChangedListener.onVisibilityChanged(visibility);
742         }
743     }
744 
getDotAppearAmount()745     public float getDotAppearAmount() {
746         return mDotAppearAmount;
747     }
748 
setOnVisibilityChangedListener(OnVisibilityChangedListener listener)749     public void setOnVisibilityChangedListener(OnVisibilityChangedListener listener) {
750         mOnVisibilityChangedListener = listener;
751     }
752 
setDark(boolean dark, boolean fade, long delay)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 {
onVisibilityChanged(int newVisibility)763         void onVisibilityChanged(int newVisibility);
764     }
765 }
766