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