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