1 /* 2 * Copyright (C) 2016 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 package com.android.systemui.statusbar.phone; 17 18 import static com.android.systemui.statusbar.phone.HeadsUpAppearanceController.CONTENT_FADE_DELAY; 19 import static com.android.systemui.statusbar.phone.HeadsUpAppearanceController.CONTENT_FADE_DURATION; 20 21 import android.content.Context; 22 import android.content.res.Configuration; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Paint; 26 import android.graphics.Rect; 27 import android.graphics.drawable.Icon; 28 import android.util.AttributeSet; 29 import android.util.MathUtils; 30 import android.util.Property; 31 import android.view.ContextThemeWrapper; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.animation.Interpolator; 35 36 import androidx.annotation.Nullable; 37 import androidx.annotation.VisibleForTesting; 38 import androidx.collection.ArrayMap; 39 40 import com.android.app.animation.Interpolators; 41 import com.android.internal.statusbar.StatusBarIcon; 42 import com.android.settingslib.Utils; 43 import com.android.systemui.res.R; 44 import com.android.systemui.statusbar.StatusBarIconView; 45 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; 46 import com.android.systemui.statusbar.notification.stack.AnimationFilter; 47 import com.android.systemui.statusbar.notification.stack.AnimationProperties; 48 import com.android.systemui.statusbar.notification.stack.ViewState; 49 50 import java.util.ArrayList; 51 import java.util.HashMap; 52 import java.util.function.Consumer; 53 54 /** 55 * A container for notification icons. It handles overflowing icons properly and positions them 56 * correctly on the screen. 57 */ 58 public class NotificationIconContainer extends ViewGroup { 59 private static final int NO_VALUE = Integer.MIN_VALUE; 60 private static final String TAG = "NotificationIconContainer"; 61 private static final boolean DEBUG = false; 62 private static final boolean DEBUG_OVERFLOW = false; 63 private static final int CANNED_ANIMATION_DURATION = 100; 64 private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() { 65 private final AnimationFilter mAnimationFilter = new AnimationFilter().animateX(); 66 67 @Override 68 public AnimationFilter getAnimationFilter() { 69 return mAnimationFilter; 70 } 71 }.setDuration(200); 72 73 private static final AnimationProperties ICON_ANIMATION_PROPERTIES = new AnimationProperties() { 74 private final AnimationFilter mAnimationFilter = new AnimationFilter() 75 .animateX() 76 .animateY() 77 .animateAlpha() 78 .animateScale(); 79 80 @Override 81 public AnimationFilter getAnimationFilter() { 82 return mAnimationFilter; 83 } 84 85 }.setDuration(CANNED_ANIMATION_DURATION); 86 87 /** 88 * Temporary AnimationProperties to avoid unnecessary allocations. 89 */ 90 private static final AnimationProperties sTempProperties = new AnimationProperties() { 91 private final AnimationFilter mAnimationFilter = new AnimationFilter(); 92 93 @Override 94 public AnimationFilter getAnimationFilter() { 95 return mAnimationFilter; 96 } 97 }; 98 99 private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() { 100 private final AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha(); 101 102 @Override 103 public AnimationFilter getAnimationFilter() { 104 return mAnimationFilter; 105 } 106 }.setDuration(200).setDelay(50); 107 108 /** 109 * The animation property used for all icons that were not isolated, when the isolation ends. 110 * This just fades the alpha and doesn't affect the movement and has a delay. 111 */ 112 private static final AnimationProperties UNISOLATION_PROPERTY_OTHERS 113 = new AnimationProperties() { 114 private final AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha(); 115 116 @Override 117 public AnimationFilter getAnimationFilter() { 118 return mAnimationFilter; 119 } 120 }.setDuration(CONTENT_FADE_DURATION); 121 122 /** 123 * The animation property used for the icon when its isolation ends. 124 * This animates the translation back to the right position. 125 */ 126 private static final AnimationProperties UNISOLATION_PROPERTY = new AnimationProperties() { 127 private final AnimationFilter mAnimationFilter = new AnimationFilter().animateX(); 128 129 @Override 130 public AnimationFilter getAnimationFilter() { 131 return mAnimationFilter; 132 } 133 }.setDuration(CONTENT_FADE_DURATION); 134 135 // TODO(b/278765923): Replace these with domain-agnostic state 136 /* Maximum number of icons on AOD when also showing overflow dot. */ 137 private int mMaxIconsOnAod; 138 /* Maximum number of icons in short shelf on lockscreen when also showing overflow dot. */ 139 private int mMaxIconsOnLockscreen; 140 /* Maximum number of icons in the status bar when also showing overflow dot. */ 141 private int mMaxStaticIcons; 142 private boolean mDozing; 143 private boolean mOnLockScreen; 144 private int mSpeedBumpIndex = -1; 145 146 private int mMaxIcons = Integer.MAX_VALUE; 147 private boolean mOverrideIconColor; 148 private boolean mIsStaticLayout = true; 149 private final HashMap<View, IconState> mIconStates = new HashMap<>(); 150 private int mDotPadding; 151 private int mStaticDotDiameter; 152 private int mActualLayoutWidth = NO_VALUE; 153 private float mActualPaddingEnd = NO_VALUE; 154 private float mActualPaddingStart = NO_VALUE; 155 private boolean mChangingViewPositions; 156 private int mAddAnimationStartIndex = -1; 157 private int mCannedAnimationStartIndex = -1; 158 private int mIconSize; 159 private boolean mDisallowNextAnimation; 160 private boolean mAnimationsEnabled = true; 161 private ArrayMap<String, StatusBarIcon> mReplacingIcons; 162 private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIconsLegacy; 163 // Keep track of the last visible icon so collapsed container can report on its location 164 private IconState mLastVisibleIconState; 165 private IconState mFirstVisibleIconState; 166 private float mVisualOverflowStart; 167 private boolean mIsShowingOverflowDot; 168 private StatusBarIconView mIsolatedIcon; 169 private Rect mIsolatedIconLocation; 170 private final int[] mAbsolutePosition = new int[2]; 171 private View mIsolatedIconForAnimation; 172 private int mThemedTextColorPrimary; 173 private Runnable mIsolatedIconAnimationEndRunnable; 174 private boolean mUseIncreasedIconScale; 175 NotificationIconContainer(Context context, AttributeSet attrs)176 public NotificationIconContainer(Context context, AttributeSet attrs) { 177 super(context, attrs); 178 initResources(); 179 setWillNotDraw(!(DEBUG || DEBUG_OVERFLOW)); 180 } 181 initResources()182 private void initResources() { 183 mMaxIconsOnAod = getResources().getInteger(R.integer.max_notif_icons_on_aod); 184 mMaxIconsOnLockscreen = getResources().getInteger(R.integer.max_notif_icons_on_lockscreen); 185 mMaxStaticIcons = getResources().getInteger(R.integer.max_notif_static_icons); 186 187 mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding); 188 int staticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius); 189 mStaticDotDiameter = 2 * staticDotRadius; 190 191 final Context themedContext = new ContextThemeWrapper(getContext(), 192 com.android.internal.R.style.Theme_DeviceDefault_DayNight); 193 mThemedTextColorPrimary = Utils.getColorAttr(themedContext, 194 com.android.internal.R.attr.textColorPrimary).getDefaultColor(); 195 } 196 197 @Override onDraw(Canvas canvas)198 protected void onDraw(Canvas canvas) { 199 super.onDraw(canvas); 200 Paint paint = new Paint(); 201 paint.setColor(Color.RED); 202 paint.setStyle(Paint.Style.STROKE); 203 canvas.drawRect(getActualPaddingStart(), 0, getLayoutEnd(), getHeight(), paint); 204 205 if (DEBUG_OVERFLOW) { 206 if (mLastVisibleIconState == null) { 207 return; 208 } 209 210 int height = getHeight(); 211 int end = getFinalTranslationX(); 212 213 // Visualize the "end" of the layout 214 paint.setColor(Color.BLUE); 215 canvas.drawLine(end, 0, end, height, paint); 216 217 paint.setColor(Color.GREEN); 218 int lastIcon = (int) mLastVisibleIconState.getXTranslation(); 219 canvas.drawLine(lastIcon, 0, lastIcon, height, paint); 220 221 if (mFirstVisibleIconState != null) { 222 int firstIcon = (int) mFirstVisibleIconState.getXTranslation(); 223 canvas.drawLine(firstIcon, 0, firstIcon, height, paint); 224 } 225 226 paint.setColor(Color.RED); 227 canvas.drawLine(mVisualOverflowStart, 0, mVisualOverflowStart, height, paint); 228 } 229 } 230 231 @Override onConfigurationChanged(Configuration newConfig)232 protected void onConfigurationChanged(Configuration newConfig) { 233 super.onConfigurationChanged(newConfig); 234 initResources(); 235 } 236 237 @Override hasOverlappingRendering()238 public boolean hasOverlappingRendering() { 239 // Does the same as "AlphaOptimizedFrameLayout". 240 return false; 241 } 242 243 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)244 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 245 final int childCount = getChildCount(); 246 final int maxVisibleIcons = getMaxVisibleIcons(childCount); 247 final int width = MeasureSpec.getSize(widthMeasureSpec); 248 final int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED); 249 int totalWidth = (int) (getActualPaddingStart() + getActualPaddingEnd()); 250 for (int i = 0; i < childCount; i++) { 251 View child = getChildAt(i); 252 measureChild(child, childWidthSpec, heightMeasureSpec); 253 if (i <= maxVisibleIcons) { 254 totalWidth += child.getMeasuredWidth(); 255 } 256 } 257 final int measuredWidth = resolveSize(totalWidth, widthMeasureSpec); 258 final int measuredHeight = MeasureSpec.getSize(heightMeasureSpec); 259 setMeasuredDimension(measuredWidth, measuredHeight); 260 } 261 262 @Override onLayout(boolean changed, int l, int t, int r, int b)263 protected void onLayout(boolean changed, int l, int t, int r, int b) { 264 float centerY = getHeight() / 2.0f; 265 // we layout all our children on the left at the top 266 mIconSize = 0; 267 for (int i = 0; i < getChildCount(); i++) { 268 View child = getChildAt(i); 269 // We need to layout all children even the GONE ones, such that the heights are 270 // calculated correctly as they are used to calculate how many we can fit on the screen 271 int width = child.getMeasuredWidth(); 272 int height = child.getMeasuredHeight(); 273 int top = (int) (centerY - height / 2.0f); 274 child.layout(0, top, width, top + height); 275 if (i == 0) { 276 setIconSize(child.getWidth()); 277 } 278 } 279 getLocationOnScreen(mAbsolutePosition); 280 if (mIsStaticLayout) { 281 updateState(); 282 } 283 } 284 285 @Override toString()286 public String toString() { 287 if (NotificationIconContainerRefactor.isEnabled()) { 288 return super.toString() 289 + " {" 290 + " overrideIconColor=" + mOverrideIconColor 291 + ", maxIcons=" + mMaxIcons 292 + ", isStaticLayout=" + mIsStaticLayout 293 + ", themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary) 294 + " }"; 295 } else { 296 return "NotificationIconContainer(" 297 + "dozing=" + mDozing + " onLockScreen=" + mOnLockScreen 298 + " overrideIconColor=" + mOverrideIconColor 299 + " speedBumpIndex=" + mSpeedBumpIndex 300 + " themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary) 301 + ')'; 302 } 303 } 304 305 @VisibleForTesting setIconSize(int size)306 public void setIconSize(int size) { 307 mIconSize = size; 308 } 309 updateState()310 private void updateState() { 311 resetViewStates(); 312 calculateIconXTranslations(); 313 applyIconStates(); 314 } 315 applyIconStates()316 public void applyIconStates() { 317 for (int i = 0; i < getChildCount(); i++) { 318 View child = getChildAt(i); 319 ViewState childState = mIconStates.get(child); 320 if (childState != null) { 321 childState.applyToView(child); 322 } 323 } 324 mAddAnimationStartIndex = -1; 325 mCannedAnimationStartIndex = -1; 326 mDisallowNextAnimation = false; 327 mIsolatedIconForAnimation = null; 328 } 329 330 @Override onViewAdded(View child)331 public void onViewAdded(View child) { 332 super.onViewAdded(child); 333 boolean isReplacingIcon = isReplacingIcon(child); 334 if (!mChangingViewPositions) { 335 IconState v = new IconState(child); 336 if (isReplacingIcon) { 337 v.justAdded = false; 338 v.justReplaced = true; 339 } 340 mIconStates.put(child, v); 341 } 342 int childIndex = indexOfChild(child); 343 if (childIndex < getChildCount() - 1 && !isReplacingIcon 344 && mIconStates.get(getChildAt(childIndex + 1)).iconAppearAmount > 0.0f) { 345 if (mAddAnimationStartIndex < 0) { 346 mAddAnimationStartIndex = childIndex; 347 } else { 348 mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, childIndex); 349 } 350 } 351 if (child instanceof StatusBarIconView) { 352 if (NotificationIconContainerRefactor.isEnabled()) { 353 if (!mChangingViewPositions) { 354 ((StatusBarIconView) child).updateIconDimens(); 355 } 356 } else { 357 ((StatusBarIconView) child).updateIconDimens(); 358 ((StatusBarIconView) child).setDozing(mDozing, false, 0); 359 } 360 } 361 } 362 isReplacingIcon(View child)363 private boolean isReplacingIcon(View child) { 364 if (!(child instanceof StatusBarIconView)) { 365 return false; 366 } 367 StatusBarIconView iconView = (StatusBarIconView) child; 368 Icon sourceIcon = iconView.getSourceIcon(); 369 String groupKey = iconView.getNotification().getGroupKey(); 370 if (NotificationIconContainerRefactor.isEnabled()) { 371 if (mReplacingIcons == null) { 372 return false; 373 } 374 StatusBarIcon replacedIcon = mReplacingIcons.get(groupKey); 375 return replacedIcon != null && sourceIcon.sameAs(replacedIcon.icon); 376 } else { 377 if (mReplacingIconsLegacy == null) { 378 return false; 379 } 380 ArrayList<StatusBarIcon> statusBarIcons = mReplacingIconsLegacy.get(groupKey); 381 if (statusBarIcons != null) { 382 StatusBarIcon replacedIcon = statusBarIcons.get(0); 383 return sourceIcon.sameAs(replacedIcon.icon); 384 } 385 return false; 386 } 387 } 388 389 @Override onViewRemoved(View child)390 public void onViewRemoved(View child) { 391 super.onViewRemoved(child); 392 393 if (child instanceof StatusBarIconView) { 394 boolean isReplacingIcon = isReplacingIcon(child); 395 final StatusBarIconView icon = (StatusBarIconView) child; 396 if (areAnimationsEnabled(icon) && icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN 397 && child.getVisibility() == VISIBLE && isReplacingIcon) { 398 int animationStartIndex = findFirstViewIndexAfter(icon.getTranslationX()); 399 if (mAddAnimationStartIndex < 0) { 400 mAddAnimationStartIndex = animationStartIndex; 401 } else { 402 mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, animationStartIndex); 403 } 404 } 405 if (!mChangingViewPositions) { 406 mIconStates.remove(child); 407 if (areAnimationsEnabled(icon) && !isReplacingIcon) { 408 addTransientView(icon, 0); 409 boolean isIsolatedIcon = child == mIsolatedIcon; 410 icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, true /* animate */, 411 () -> removeTransientView(icon), 412 isIsolatedIcon ? CONTENT_FADE_DURATION : 0); 413 } 414 } 415 } 416 } 417 418 /** 419 * Removes all child {@link StatusBarIconView} instances from this container, immediately and 420 * without animation. This should be called when tearing down this container so that external 421 * icon views are not holding onto a reference thru {@link View#getParent()}. 422 */ detachAllIcons()423 public void detachAllIcons() { 424 boolean animsWereEnabled = mAnimationsEnabled; 425 boolean wasChangingPositions = mChangingViewPositions; 426 mAnimationsEnabled = false; 427 mChangingViewPositions = true; 428 removeAllViews(); 429 mChangingViewPositions = wasChangingPositions; 430 mAnimationsEnabled = animsWereEnabled; 431 } 432 areIconsOverflowing()433 public boolean areIconsOverflowing() { 434 return mIsShowingOverflowDot; 435 } 436 areAnimationsEnabled(StatusBarIconView icon)437 private boolean areAnimationsEnabled(StatusBarIconView icon) { 438 return mAnimationsEnabled || icon == mIsolatedIcon; 439 } 440 441 /** 442 * Finds the first view with a translation bigger then a given value 443 */ findFirstViewIndexAfter(float translationX)444 private int findFirstViewIndexAfter(float translationX) { 445 for (int i = 0; i < getChildCount(); i++) { 446 View view = getChildAt(i); 447 if (view.getTranslationX() > translationX) { 448 return i; 449 } 450 } 451 return getChildCount(); 452 } 453 resetViewStates()454 public void resetViewStates() { 455 for (int i = 0; i < getChildCount(); i++) { 456 View view = getChildAt(i); 457 ViewState iconState = mIconStates.get(view); 458 iconState.initFrom(view); 459 iconState.setAlpha(mIsolatedIcon == null || view == mIsolatedIcon ? 1.0f : 0.0f); 460 iconState.hidden = false; 461 } 462 } 463 464 /** 465 * @return Width of shelf for the given number of icons 466 */ calculateWidthFor(float numIcons)467 public float calculateWidthFor(float numIcons) { 468 if (numIcons == 0) { 469 return 0f; 470 } 471 final float contentWidth; 472 if (NotificationIconContainerRefactor.isEnabled()) { 473 contentWidth = mIconSize * numIcons; 474 } else { 475 contentWidth = mIconSize * MathUtils.min(numIcons, mMaxIconsOnLockscreen + 1); 476 } 477 return getActualPaddingStart() + contentWidth + getActualPaddingEnd(); 478 } 479 480 @VisibleForTesting shouldForceOverflow(int i, int speedBumpIndex, float iconAppearAmount, int maxVisibleIcons)481 boolean shouldForceOverflow(int i, int speedBumpIndex, float iconAppearAmount, 482 int maxVisibleIcons) { 483 if (NotificationIconContainerRefactor.isEnabled()) { 484 return i >= maxVisibleIcons && iconAppearAmount > 0.0f; 485 } else { 486 return speedBumpIndex != -1 && i >= speedBumpIndex 487 && iconAppearAmount > 0.0f || i >= maxVisibleIcons; 488 } 489 } 490 491 @VisibleForTesting isOverflowing(boolean isLastChild, float translationX, float layoutEnd, float iconSize)492 boolean isOverflowing(boolean isLastChild, float translationX, float layoutEnd, 493 float iconSize) { 494 if (isLastChild) { 495 return translationX + iconSize > layoutEnd; 496 } else { 497 // If the child is not the last child, we need to ensure that we have room for the next 498 // icon and the dot. The dot could be as large as an icon, so verify that we have room 499 // for 2 icons. 500 return translationX + iconSize * 2f > layoutEnd; 501 } 502 } 503 504 /** 505 * Calculate the horizontal translations for each notification based on how much the icons 506 * are inserted into the notification container. 507 * If this is not a whole number, the fraction means by how much the icon is appearing. 508 */ calculateIconXTranslations()509 public void calculateIconXTranslations() { 510 float translationX = getActualPaddingStart(); 511 int firstOverflowIndex = -1; 512 int childCount = getChildCount(); 513 int maxVisibleIcons = getMaxVisibleIcons(childCount); 514 float layoutEnd = getLayoutEnd(); 515 mVisualOverflowStart = 0; 516 mFirstVisibleIconState = null; 517 for (int i = 0; i < childCount; i++) { 518 View view = getChildAt(i); 519 IconState iconState = mIconStates.get(view); 520 if (iconState.iconAppearAmount == 1.0f) { 521 // We only modify the xTranslation if it's fully inside of the container 522 // since during the transition to the shelf, the translations are controlled 523 // from the outside 524 iconState.setXTranslation(translationX); 525 } 526 if (mFirstVisibleIconState == null) { 527 mFirstVisibleIconState = iconState; 528 } 529 iconState.visibleState = iconState.hidden 530 ? StatusBarIconView.STATE_HIDDEN 531 : StatusBarIconView.STATE_ICON; 532 533 final boolean forceOverflow = shouldForceOverflow(i, mSpeedBumpIndex, 534 iconState.iconAppearAmount, maxVisibleIcons); 535 final boolean isOverflowing = forceOverflow || isOverflowing( 536 /* isLastChild= */ i == childCount - 1, translationX, layoutEnd, mIconSize); 537 538 // First icon to overflow. 539 if (firstOverflowIndex == -1 && isOverflowing) { 540 firstOverflowIndex = i; 541 mVisualOverflowStart = translationX; 542 } 543 544 final float drawingScale = getDrawingScale(view); 545 translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale; 546 } 547 mIsShowingOverflowDot = false; 548 if (firstOverflowIndex != -1) { 549 translationX = mVisualOverflowStart; 550 for (int i = firstOverflowIndex; i < childCount; i++) { 551 View view = getChildAt(i); 552 IconState iconState = mIconStates.get(view); 553 int dotWidth = mStaticDotDiameter + mDotPadding; 554 iconState.setXTranslation(translationX); 555 if (!mIsShowingOverflowDot) { 556 if (iconState.iconAppearAmount < 0.8f) { 557 iconState.visibleState = StatusBarIconView.STATE_ICON; 558 } else { 559 iconState.visibleState = StatusBarIconView.STATE_DOT; 560 mIsShowingOverflowDot = true; 561 } 562 translationX += dotWidth * iconState.iconAppearAmount; 563 mLastVisibleIconState = iconState; 564 } else { 565 iconState.visibleState = StatusBarIconView.STATE_HIDDEN; 566 } 567 } 568 } else if (childCount > 0) { 569 View lastChild = getChildAt(childCount - 1); 570 mLastVisibleIconState = mIconStates.get(lastChild); 571 mFirstVisibleIconState = mIconStates.get(getChildAt(0)); 572 } 573 if (isLayoutRtl()) { 574 for (int i = 0; i < childCount; i++) { 575 View view = getChildAt(i); 576 IconState iconState = mIconStates.get(view); 577 iconState.setXTranslation( 578 getWidth() - iconState.getXTranslation() - view.getWidth()); 579 } 580 } 581 if (mIsolatedIcon != null) { 582 IconState iconState = mIconStates.get(mIsolatedIcon); 583 if (iconState != null) { 584 // Most of the time the icon isn't yet added when this is called but only happening 585 // later. The isolated icon position left should equal to the mIsolatedIconLocation 586 // to ensure the icon be put at the center of the HUN icon placeholder, 587 // {@See HeadsUpAppearanceController#updateIsolatedIconLocation}. 588 iconState.setXTranslation(mIsolatedIconLocation.left - mAbsolutePosition[0]); 589 iconState.visibleState = StatusBarIconView.STATE_ICON; 590 } 591 } 592 } 593 getDrawingScale(View view)594 private float getDrawingScale(View view) { 595 final boolean useIncreasedScale = NotificationIconContainerRefactor.isEnabled() 596 ? mUseIncreasedIconScale 597 : mOnLockScreen; 598 return useIncreasedScale && view instanceof StatusBarIconView 599 ? ((StatusBarIconView) view).getIconScaleIncreased() 600 : 1f; 601 } 602 setUseIncreasedIconScale(boolean useIncreasedIconScale)603 public void setUseIncreasedIconScale(boolean useIncreasedIconScale) { 604 if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; 605 mUseIncreasedIconScale = useIncreasedIconScale; 606 } 607 getMaxVisibleIcons(int childCount)608 private int getMaxVisibleIcons(int childCount) { 609 if (NotificationIconContainerRefactor.isEnabled()) { 610 return mMaxIcons; 611 } else { 612 return mOnLockScreen ? mMaxIconsOnAod : mIsStaticLayout ? mMaxStaticIcons : childCount; 613 } 614 } 615 getLayoutEnd()616 private float getLayoutEnd() { 617 return getActualWidth() - getActualPaddingEnd(); 618 } 619 getActualPaddingEnd()620 private float getActualPaddingEnd() { 621 if (mActualPaddingEnd == NO_VALUE) { 622 return getPaddingEnd(); 623 } 624 return mActualPaddingEnd; 625 } 626 627 /** 628 * @return the actual startPadding of this view 629 */ getActualPaddingStart()630 public float getActualPaddingStart() { 631 if (mActualPaddingStart == NO_VALUE) { 632 return getPaddingStart(); 633 } 634 return mActualPaddingStart; 635 } 636 637 /** 638 * Sets whether the layout should always show the same number of icons. 639 * If this is true, the icon positions will be updated on layout. 640 * If this if false, the layout is managed from the outside and layouting won't trigger a 641 * repositioning of the icons. 642 */ setIsStaticLayout(boolean isStaticLayout)643 public void setIsStaticLayout(boolean isStaticLayout) { 644 mIsStaticLayout = isStaticLayout; 645 } 646 setActualLayoutWidth(int actualLayoutWidth)647 public void setActualLayoutWidth(int actualLayoutWidth) { 648 mActualLayoutWidth = actualLayoutWidth; 649 if (DEBUG) { 650 invalidate(); 651 } 652 } 653 setActualPaddingEnd(float paddingEnd)654 public void setActualPaddingEnd(float paddingEnd) { 655 mActualPaddingEnd = paddingEnd; 656 if (DEBUG) { 657 invalidate(); 658 } 659 } 660 setActualPaddingStart(float paddingStart)661 public void setActualPaddingStart(float paddingStart) { 662 mActualPaddingStart = paddingStart; 663 if (DEBUG) { 664 invalidate(); 665 } 666 } 667 getActualWidth()668 public int getActualWidth() { 669 if (mActualLayoutWidth == NO_VALUE) { 670 return getWidth(); 671 } 672 return mActualLayoutWidth; 673 } 674 getFinalTranslationX()675 public int getFinalTranslationX() { 676 if (mLastVisibleIconState == null) { 677 return 0; 678 } 679 680 int translation = (int) (isLayoutRtl() 681 ? getWidth() - mLastVisibleIconState.getXTranslation() 682 : mLastVisibleIconState.getXTranslation() + mIconSize); 683 684 // There's a chance that last translation goes beyond the edge maybe 685 return Math.min(getWidth(), translation); 686 } 687 setChangingViewPositions(boolean changingViewPositions)688 public void setChangingViewPositions(boolean changingViewPositions) { 689 mChangingViewPositions = changingViewPositions; 690 } 691 setDozing(boolean dozing, boolean animate, long delay)692 public void setDozing(boolean dozing, boolean animate, long delay) { 693 NotificationIconContainerRefactor.assertInLegacyMode(); 694 setDozing(dozing, animate, delay, /* endRunnable= */ null); 695 } 696 setDozing(boolean dozing, boolean animate, long delay, @Nullable Runnable endRunnable)697 private void setDozing(boolean dozing, boolean animate, long delay, 698 @Nullable Runnable endRunnable) { 699 NotificationIconContainerRefactor.assertInLegacyMode(); 700 mDozing = dozing; 701 mDisallowNextAnimation |= !animate; 702 final int childCount = getChildCount(); 703 // Track all the child invocations of setDozing, invoking the top-level endRunnable once 704 // they have all completed. 705 final Runnable onChildCompleted = endRunnable == null ? null : new Runnable() { 706 private int mPendingCallbacks = childCount; 707 708 @Override 709 public void run() { 710 if (--mPendingCallbacks == 0) { 711 endRunnable.run(); 712 } 713 } 714 }; 715 for (int i = 0; i < childCount; i++) { 716 View view = getChildAt(i); 717 if (view instanceof StatusBarIconView) { 718 ((StatusBarIconView) view).setDozing(dozing, animate, delay, onChildCompleted); 719 } else if (onChildCompleted != null) { 720 onChildCompleted.run(); 721 } 722 } 723 } 724 getIconState(StatusBarIconView icon)725 public IconState getIconState(StatusBarIconView icon) { 726 return mIconStates.get(icon); 727 } 728 setSpeedBumpIndex(int speedBumpIndex)729 public void setSpeedBumpIndex(int speedBumpIndex) { 730 NotificationIconContainerRefactor.assertInLegacyMode(); 731 mSpeedBumpIndex = speedBumpIndex; 732 } 733 setMaxIconsAmount(int maxIcons)734 public void setMaxIconsAmount(int maxIcons) { 735 if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; 736 mMaxIcons = maxIcons; 737 } 738 getIconSize()739 public int getIconSize() { 740 return mIconSize; 741 } 742 setAnimationsEnabled(boolean enabled)743 public void setAnimationsEnabled(boolean enabled) { 744 if (!enabled && mAnimationsEnabled) { 745 for (int i = 0; i < getChildCount(); i++) { 746 View child = getChildAt(i); 747 ViewState childState = mIconStates.get(child); 748 if (childState != null) { 749 childState.cancelAnimations(child); 750 childState.applyToView(child); 751 } 752 } 753 } 754 mAnimationsEnabled = enabled; 755 } 756 setReplacingIconsLegacy(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons)757 public void setReplacingIconsLegacy(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) { 758 NotificationIconContainerRefactor.assertInLegacyMode(); 759 mReplacingIconsLegacy = replacingIcons; 760 } 761 setReplacingIcons(ArrayMap<String, StatusBarIcon> replacingIcons)762 public void setReplacingIcons(ArrayMap<String, StatusBarIcon> replacingIcons) { 763 if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; 764 mReplacingIcons = replacingIcons; 765 } 766 767 @Deprecated showIconIsolatedLegacy(StatusBarIconView icon, boolean animated)768 public void showIconIsolatedLegacy(StatusBarIconView icon, boolean animated) { 769 NotificationIconContainerRefactor.assertInLegacyMode(); 770 if (animated) { 771 mIsolatedIconForAnimation = icon != null ? icon : mIsolatedIcon; 772 } 773 mIsolatedIcon = icon; 774 updateState(); 775 } 776 showIconIsolatedAnimated(StatusBarIconView icon, @Nullable Runnable onAnimationEnd)777 public void showIconIsolatedAnimated(StatusBarIconView icon, 778 @Nullable Runnable onAnimationEnd) { 779 if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; 780 mIsolatedIconForAnimation = icon != null ? icon : mIsolatedIcon; 781 mIsolatedIconAnimationEndRunnable = onAnimationEnd; 782 showIconIsolated(icon); 783 } 784 showIconIsolated(StatusBarIconView icon)785 public void showIconIsolated(StatusBarIconView icon) { 786 if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; 787 mIsolatedIcon = icon; 788 updateState(); 789 } 790 setIsolatedIconLocation(Rect isolatedIconLocation, boolean requireUpdate)791 public void setIsolatedIconLocation(Rect isolatedIconLocation, boolean requireUpdate) { 792 mIsolatedIconLocation = isolatedIconLocation; 793 if (requireUpdate) { 794 updateState(); 795 } 796 } 797 798 /** 799 * Set whether the device is on the lockscreen and which lockscreen mode the device is 800 * configured to. Depending on these values, the layout of the AOD icons change. 801 */ setOnLockScreen(boolean onLockScreen)802 public void setOnLockScreen(boolean onLockScreen) { 803 NotificationIconContainerRefactor.assertInLegacyMode(); 804 mOnLockScreen = onLockScreen; 805 } 806 807 @Deprecated setInNotificationIconShelf(boolean inShelf)808 public void setInNotificationIconShelf(boolean inShelf) { 809 NotificationIconContainerRefactor.assertInLegacyMode(); 810 mOverrideIconColor = inShelf; 811 } 812 setOverrideIconColor(boolean override)813 public void setOverrideIconColor(boolean override) { 814 if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return; 815 mOverrideIconColor = override; 816 } 817 818 public class IconState extends ViewState { 819 public float iconAppearAmount = 1.0f; 820 public float clampedAppearAmount = 1.0f; 821 public int visibleState; 822 public boolean justAdded = true; 823 private boolean justReplaced; 824 public boolean needsCannedAnimation; 825 public int iconColor = StatusBarIconView.NO_COLOR; 826 public boolean noAnimations; 827 private final View mView; 828 829 private final Consumer<Property> mCannedAnimationEndListener; 830 IconState(View child)831 public IconState(View child) { 832 mView = child; 833 mCannedAnimationEndListener = (property) -> { 834 // If we finished animating out of the shelf 835 if (property == View.TRANSLATION_Y && iconAppearAmount == 0.0f 836 && mView.getVisibility() == VISIBLE) { 837 mView.setVisibility(INVISIBLE); 838 } 839 }; 840 } 841 842 @Override applyToView(View view)843 public void applyToView(View view) { 844 if (view instanceof StatusBarIconView) { 845 StatusBarIconView icon = (StatusBarIconView) view; 846 boolean animate = false; 847 AnimationProperties animationProperties = null; 848 final boolean animationsAllowed = animationsAllowed(icon); 849 if (animationsAllowed) { 850 if (justAdded || justReplaced) { 851 super.applyToView(icon); 852 if (justAdded && iconAppearAmount != 0.0f) { 853 icon.setAlpha(0.0f); 854 icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, 855 false /* animate */); 856 animationProperties = ADD_ICON_PROPERTIES; 857 animate = true; 858 } 859 } else if (visibleState != icon.getVisibleState()) { 860 animationProperties = DOT_ANIMATION_PROPERTIES; 861 animate = true; 862 } 863 if (!animate && mAddAnimationStartIndex >= 0 864 && indexOfChild(view) >= mAddAnimationStartIndex 865 && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN 866 || visibleState != StatusBarIconView.STATE_HIDDEN)) { 867 animationProperties = DOT_ANIMATION_PROPERTIES; 868 animate = true; 869 } 870 if (needsCannedAnimation) { 871 AnimationFilter animationFilter = sTempProperties.getAnimationFilter(); 872 animationFilter.reset(); 873 animationFilter.combineFilter( 874 ICON_ANIMATION_PROPERTIES.getAnimationFilter()); 875 sTempProperties.resetCustomInterpolators(); 876 sTempProperties.combineCustomInterpolators(ICON_ANIMATION_PROPERTIES); 877 Interpolator interpolator; 878 if (icon.showsConversation()) { 879 interpolator = Interpolators.ICON_OVERSHOT_LESS; 880 } else { 881 interpolator = Interpolators.ICON_OVERSHOT; 882 } 883 sTempProperties.setCustomInterpolator(View.TRANSLATION_Y, interpolator); 884 sTempProperties.setAnimationEndAction(mCannedAnimationEndListener); 885 if (animationProperties != null) { 886 animationFilter.combineFilter(animationProperties.getAnimationFilter()); 887 sTempProperties.combineCustomInterpolators(animationProperties); 888 } 889 animationProperties = sTempProperties; 890 animationProperties.setDuration(CANNED_ANIMATION_DURATION); 891 animate = true; 892 mCannedAnimationStartIndex = indexOfChild(view); 893 } 894 if (!animate && mCannedAnimationStartIndex >= 0 895 && indexOfChild(view) > mCannedAnimationStartIndex 896 && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN 897 || visibleState != StatusBarIconView.STATE_HIDDEN)) { 898 AnimationFilter animationFilter = sTempProperties.getAnimationFilter(); 899 animationFilter.reset(); 900 animationFilter.animateX(); 901 sTempProperties.resetCustomInterpolators(); 902 animationProperties = sTempProperties; 903 animationProperties.setDuration(CANNED_ANIMATION_DURATION); 904 animate = true; 905 } 906 if (mIsolatedIconForAnimation != null) { 907 if (view == mIsolatedIconForAnimation) { 908 animationProperties = UNISOLATION_PROPERTY; 909 animationProperties.setDelay( 910 mIsolatedIcon != null ? CONTENT_FADE_DELAY : 0); 911 Consumer<Property> endAction = getEndAction(); 912 if (endAction != null) { 913 animationProperties.setAnimationEndAction(endAction); 914 animationProperties.setAnimationCancelAction(endAction); 915 } 916 } else { 917 animationProperties = UNISOLATION_PROPERTY_OTHERS; 918 animationProperties.setDelay( 919 mIsolatedIcon == null ? CONTENT_FADE_DELAY : 0); 920 } 921 animate = true; 922 } 923 } 924 icon.setVisibleState(visibleState, animationsAllowed); 925 if (NotificationIconContainerRefactor.isEnabled()) { 926 if (mOverrideIconColor) { 927 icon.setIconColor(mThemedTextColorPrimary, 928 /* animate= */ needsCannedAnimation && animationsAllowed); 929 } 930 } else { 931 icon.setIconColor(mOverrideIconColor ? mThemedTextColorPrimary : iconColor, 932 needsCannedAnimation && animationsAllowed); 933 } 934 if (animate) { 935 animateTo(icon, animationProperties); 936 } else { 937 super.applyToView(view); 938 } 939 sTempProperties.setAnimationEndAction(null); 940 } 941 justAdded = false; 942 justReplaced = false; 943 needsCannedAnimation = false; 944 } 945 animationsAllowed(StatusBarIconView icon)946 private boolean animationsAllowed(StatusBarIconView icon) { 947 final boolean isLowPriorityIconChange = 948 (visibleState == StatusBarIconView.STATE_HIDDEN 949 && icon.getVisibleState() == StatusBarIconView.STATE_DOT) 950 || (visibleState == StatusBarIconView.STATE_DOT 951 && icon.getVisibleState() == StatusBarIconView.STATE_HIDDEN); 952 return areAnimationsEnabled(icon) 953 && !mDisallowNextAnimation 954 && !noAnimations 955 && !isLowPriorityIconChange; 956 } 957 958 @Nullable getEndAction()959 private Consumer<Property> getEndAction() { 960 if (mIsolatedIconAnimationEndRunnable == null) return null; 961 final Runnable endRunnable = mIsolatedIconAnimationEndRunnable; 962 return prop -> { 963 endRunnable.run(); 964 if (mIsolatedIconAnimationEndRunnable == endRunnable) { 965 mIsolatedIconAnimationEndRunnable = null; 966 } 967 }; 968 } 969 970 @Override initFrom(View view)971 public void initFrom(View view) { 972 super.initFrom(view); 973 if (view instanceof StatusBarIconView) { 974 iconColor = ((StatusBarIconView) view).getStaticDrawableColor(); 975 } 976 } 977 } 978 } 979