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