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