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