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