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 17package com.android.systemui.statusbar; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.animation.ValueAnimator; 23import android.app.Notification; 24import android.content.Context; 25import android.content.pm.ApplicationInfo; 26import android.content.res.ColorStateList; 27import android.content.res.Configuration; 28import android.content.res.Resources; 29import android.graphics.Canvas; 30import android.graphics.Color; 31import android.graphics.Paint; 32import android.graphics.Rect; 33import android.graphics.drawable.Drawable; 34import android.graphics.drawable.Icon; 35import android.os.Parcelable; 36import android.os.UserHandle; 37import android.service.notification.StatusBarNotification; 38import android.support.v4.graphics.ColorUtils; 39import android.text.TextUtils; 40import android.util.AttributeSet; 41import android.util.FloatProperty; 42import android.util.Log; 43import android.util.Property; 44import android.util.TypedValue; 45import android.view.ViewDebug; 46import android.view.accessibility.AccessibilityEvent; 47import android.view.animation.Interpolator; 48 49import com.android.internal.statusbar.StatusBarIcon; 50import com.android.internal.util.NotificationColorUtil; 51import com.android.systemui.Interpolators; 52import com.android.systemui.R; 53import com.android.systemui.statusbar.notification.NotificationIconDozeHelper; 54import com.android.systemui.statusbar.notification.NotificationUtils; 55 56import java.text.NumberFormat; 57 58public 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