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 17 package com.android.systemui.statusbar; 18 19 import static com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress; 20 import static com.android.systemui.util.ColorUtilKt.hexColorString; 21 22 import android.content.Context; 23 import android.content.res.Configuration; 24 import android.content.res.Resources; 25 import android.graphics.Rect; 26 import android.os.Bundle; 27 import android.util.AttributeSet; 28 import android.util.IndentingPrintWriter; 29 import android.util.MathUtils; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.ViewTreeObserver; 33 import android.view.accessibility.AccessibilityNodeInfo; 34 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 35 import android.view.animation.Interpolator; 36 import android.view.animation.PathInterpolator; 37 38 import androidx.annotation.NonNull; 39 40 import com.android.app.animation.Interpolators; 41 import com.android.internal.annotations.VisibleForTesting; 42 import com.android.internal.policy.SystemBarUtils; 43 import com.android.systemui.animation.ShadeInterpolation; 44 import com.android.systemui.res.R; 45 import com.android.systemui.scene.shared.flag.SceneContainerFlag; 46 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; 47 import com.android.systemui.statusbar.notification.ColorUpdateLogger; 48 import com.android.systemui.statusbar.notification.NotificationUtils; 49 import com.android.systemui.statusbar.notification.SourceType; 50 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; 51 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 52 import com.android.systemui.statusbar.notification.row.ExpandableView; 53 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; 54 import com.android.systemui.statusbar.notification.shared.NotificationMinimalism; 55 import com.android.systemui.statusbar.notification.shelf.NotificationShelfBackgroundView; 56 import com.android.systemui.statusbar.notification.shelf.NotificationShelfIconContainer; 57 import com.android.systemui.statusbar.notification.stack.AmbientState; 58 import com.android.systemui.statusbar.notification.stack.AnimationProperties; 59 import com.android.systemui.statusbar.notification.stack.ExpandableViewState; 60 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager; 61 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; 62 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm; 63 import com.android.systemui.statusbar.notification.stack.ViewState; 64 import com.android.systemui.statusbar.phone.NotificationIconContainer; 65 import com.android.systemui.util.DumpUtilsKt; 66 67 import java.io.PrintWriter; 68 69 /** 70 * A notification shelf view that is placed inside the notification scroller. It manages the 71 * overflow icons that don't fit into the regular list anymore. 72 */ 73 public class NotificationShelf extends ActivatableNotificationView { 74 75 private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag; 76 private static final String TAG = "NotificationShelf"; 77 78 // More extreme version of SLOW_OUT_LINEAR_IN which keeps the icon nearly invisible until after 79 // the next icon has translated out of the way, to avoid overlapping. 80 private static final Interpolator ICON_ALPHA_INTERPOLATOR = 81 new PathInterpolator(0.6f, 0f, 0.6f, 0f); 82 private static final SourceType BASE_VALUE = SourceType.from("BaseValue"); 83 private static final SourceType SHELF_SCROLL = SourceType.from("ShelfScroll"); 84 85 @VisibleForTesting 86 public NotificationShelfIconContainer mShelfIcons; 87 // This field hides mBackgroundNormal from super class for short-shelf alignment 88 @VisibleForTesting 89 public NotificationShelfBackgroundView mBackgroundNormal; 90 private boolean mHideBackground; 91 private int mStatusBarHeight; 92 private boolean mEnableNotificationClipping; 93 private AmbientState mAmbientState; 94 private int mPaddingBetweenElements; 95 private int mNotGoneIndex; 96 private boolean mHasItemsInStableShelf; 97 private boolean mAlignedToEnd; 98 private int mScrollFastThreshold; 99 private boolean mInteractive; 100 private boolean mAnimationsEnabled = true; 101 private boolean mShowNotificationShelf; 102 private final Rect mClipRect = new Rect(); 103 private int mIndexOfFirstViewInShelf = -1; 104 private float mCornerAnimationDistance; 105 private float mActualWidth = -1; 106 private int mMaxIconsOnLockscreen; 107 private boolean mCanModifyColorOfNotifications; 108 private boolean mCanInteract; 109 private NotificationStackScrollLayout mHostLayout; 110 private NotificationRoundnessManager mRoundnessManager; 111 NotificationShelf(Context context, AttributeSet attrs)112 public NotificationShelf(Context context, AttributeSet attrs) { 113 super(context, attrs); 114 } 115 116 @VisibleForTesting NotificationShelf(Context context, AttributeSet attrs, boolean showNotificationShelf)117 public NotificationShelf(Context context, AttributeSet attrs, boolean showNotificationShelf) { 118 super(context, attrs); 119 mShowNotificationShelf = showNotificationShelf; 120 } 121 122 @Override 123 @VisibleForTesting onFinishInflate()124 public void onFinishInflate() { 125 super.onFinishInflate(); 126 mShelfIcons = findViewById(R.id.content); 127 mShelfIcons.setClipChildren(false); 128 mShelfIcons.setClipToPadding(false); 129 130 mBackgroundNormal = (NotificationShelfBackgroundView) super.mBackgroundNormal; 131 132 setClipToActualHeight(false); 133 setClipChildren(false); 134 setClipToPadding(false); 135 mShelfIcons.setIsStaticLayout(false); 136 requestRoundness(/* top = */ 1f, /* bottom = */ 1f, BASE_VALUE, /* animate = */ false); 137 updateResources(); 138 } 139 bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout, NotificationRoundnessManager roundnessManager)140 public void bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout, 141 NotificationRoundnessManager roundnessManager) { 142 mAmbientState = ambientState; 143 mHostLayout = hostLayout; 144 mRoundnessManager = roundnessManager; 145 } 146 updateResources()147 private void updateResources() { 148 Resources res = getResources(); 149 mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext); 150 mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height); 151 mMaxIconsOnLockscreen = res.getInteger(R.integer.max_notif_icons_on_lockscreen); 152 153 ViewGroup.LayoutParams layoutParams = getLayoutParams(); 154 final int newShelfHeight = res.getDimensionPixelOffset(R.dimen.notification_shelf_height); 155 if (newShelfHeight != layoutParams.height) { 156 layoutParams.height = newShelfHeight; 157 setLayoutParams(layoutParams); 158 } 159 160 final int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding); 161 mShelfIcons.setPadding(padding, 0, padding, 0); 162 mScrollFastThreshold = res.getDimensionPixelOffset(R.dimen.scroll_fast_threshold); 163 mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf); 164 mCornerAnimationDistance = res.getDimensionPixelSize( 165 R.dimen.notification_corner_animation_distance); 166 mEnableNotificationClipping = res.getBoolean(R.bool.notification_enable_clipping); 167 168 mShelfIcons.setOverrideIconColor(true); 169 if (!mShowNotificationShelf) { 170 setVisibility(GONE); 171 } 172 } 173 174 @Override onConfigurationChanged(Configuration newConfig)175 protected void onConfigurationChanged(Configuration newConfig) { 176 super.onConfigurationChanged(newConfig); 177 updateResources(); 178 } 179 180 @Override getContentView()181 protected View getContentView() { 182 return mShelfIcons; 183 } 184 getShelfIcons()185 public NotificationIconContainer getShelfIcons() { 186 return mShelfIcons; 187 } 188 189 @Override 190 @NonNull createExpandableViewState()191 public ExpandableViewState createExpandableViewState() { 192 return new ShelfState(); 193 } 194 195 @Override toString()196 public String toString() { 197 return super.toString() 198 + " (hideBackground=" + mHideBackground 199 + " notGoneIndex=" + mNotGoneIndex 200 + " hasItemsInStableShelf=" + mHasItemsInStableShelf 201 + " interactive=" + mInteractive 202 + " animationsEnabled=" + mAnimationsEnabled 203 + " showNotificationShelf=" + mShowNotificationShelf 204 + " indexOfFirstViewInShelf=" + mIndexOfFirstViewInShelf 205 + ')'; 206 } 207 208 /** 209 * Update the state of the shelf. 210 */ updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState, AmbientState ambientState)211 public void updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState, 212 AmbientState ambientState) { 213 ExpandableView lastView = ambientState.getLastVisibleBackgroundChild(); 214 ShelfState viewState = (ShelfState) getViewState(); 215 if (mShowNotificationShelf && lastView != null) { 216 ExpandableViewState lastViewState = lastView.getViewState(); 217 viewState.copyFrom(lastViewState); 218 219 viewState.height = getIntrinsicHeight(); 220 viewState.setZTranslation(ambientState.getBaseZHeight()); 221 viewState.clipTopAmount = 0; 222 223 if (ambientState.isExpansionChanging() && !ambientState.isOnKeyguard()) { 224 float expansion = ambientState.getExpansionFraction(); 225 if (ambientState.isBouncerInTransit()) { 226 viewState.setAlpha(aboutToShowBouncerProgress(expansion)); 227 } else { 228 if (ambientState.isSmallScreen()) { 229 viewState.setAlpha(ShadeInterpolation.getContentAlpha(expansion)); 230 } else { 231 LargeScreenShadeInterpolator interpolator = 232 ambientState.getLargeScreenShadeInterpolator(); 233 viewState.setAlpha(interpolator.getNotificationContentAlpha(expansion)); 234 } 235 } 236 } else { 237 viewState.setAlpha(1f - ambientState.getHideAmount()); 238 } 239 viewState.hideSensitive = false; 240 viewState.setXTranslation(getTranslationX()); 241 viewState.hasItemsInStableShelf = lastViewState.inShelf; 242 viewState.firstViewInShelf = algorithmState.firstViewInShelf; 243 if (mNotGoneIndex != -1) { 244 viewState.notGoneIndex = Math.min(viewState.notGoneIndex, mNotGoneIndex); 245 } 246 247 viewState.hidden = !mAmbientState.isShadeExpanded() 248 || algorithmState.firstViewInShelf == null; 249 250 final int indexOfFirstViewInShelf = algorithmState.visibleChildren.indexOf( 251 algorithmState.firstViewInShelf); 252 253 if (mAmbientState.isExpansionChanging() 254 && algorithmState.firstViewInShelf != null 255 && indexOfFirstViewInShelf > 0) { 256 257 // Show shelf if section before it is showing. 258 final ExpandableView viewBeforeShelf = algorithmState.visibleChildren.get( 259 indexOfFirstViewInShelf - 1); 260 if (viewBeforeShelf.getViewState().hidden) { 261 viewState.hidden = true; 262 } 263 } 264 } else { 265 viewState.hidden = true; 266 viewState.location = ExpandableViewState.LOCATION_GONE; 267 viewState.hasItemsInStableShelf = false; 268 } 269 270 final float stackBottom = SceneContainerFlag.isEnabled() 271 ? ambientState.getStackTop() + ambientState.getInterpolatedStackHeight() 272 : ambientState.getStackY() + ambientState.getInterpolatedStackHeight(); 273 274 if (viewState.hidden) { 275 // if the shelf is hidden, position it at the end of the stack (plus the clip 276 // padding), such that when it appears animated, it will smoothly move in from the 277 // bottom, without jump cutting any notifications 278 viewState.setYTranslation(stackBottom + mPaddingBetweenElements); 279 } else { 280 viewState.setYTranslation(stackBottom - viewState.height); 281 } 282 } 283 284 /** 285 * Set the actual width of the shelf, this will only differ from width for short shelves. 286 */ 287 @VisibleForTesting setActualWidth(float actualWidth)288 public void setActualWidth(float actualWidth) { 289 setBackgroundWidth((int) actualWidth); 290 if (mShelfIcons != null) { 291 mShelfIcons.setAlignToEnd(isAlignedToEnd()); 292 mShelfIcons.setActualLayoutWidth((int) actualWidth); 293 } 294 mActualWidth = actualWidth; 295 } 296 297 @Override setBackgroundWidth(int width)298 public void setBackgroundWidth(int width) { 299 super.setBackgroundWidth(width); 300 if (!NotificationMinimalism.isEnabled()) { 301 return; 302 } 303 if (mBackgroundNormal != null) { 304 mBackgroundNormal.setAlignToEnd(isAlignedToEnd()); 305 } 306 } 307 308 @Override getBoundsOnScreen(Rect outRect, boolean clipToParent)309 public void getBoundsOnScreen(Rect outRect, boolean clipToParent) { 310 super.getBoundsOnScreen(outRect, clipToParent); 311 final int actualWidth = getActualWidth(); 312 final boolean alignedToRight = NotificationMinimalism.isEnabled() ? isAlignedToRight() : 313 isLayoutRtl(); 314 if (alignedToRight) { 315 outRect.left = outRect.right - actualWidth; 316 } else { 317 outRect.right = outRect.left + actualWidth; 318 } 319 } 320 321 /** 322 * @return Actual width of shelf, accounting for possible ongoing width animation 323 */ getActualWidth()324 public int getActualWidth() { 325 return mActualWidth > -1 ? (int) mActualWidth : getWidth(); 326 } 327 328 /** 329 * @param localX Click x from left of screen 330 * @param slop Margin of error within which we count x for valid click 331 * @param left Left of shelf, from left of screen 332 * @param right Right of shelf, from left of screen 333 * @return Whether click x was in view 334 */ 335 @VisibleForTesting isXInView(float localX, float slop, float left, float right)336 public boolean isXInView(float localX, float slop, float left, float right) { 337 return (left - slop) <= localX && localX < (right + slop); 338 } 339 340 /** 341 * @param localY Click y from top of shelf 342 * @param slop Margin of error within which we count y for valid click 343 * @param top Top of shelf 344 * @param bottom Height of shelf 345 * @return Whether click y was in view 346 */ 347 @VisibleForTesting isYInView(float localY, float slop, float top, float bottom)348 public boolean isYInView(float localY, float slop, float top, float bottom) { 349 return (top - slop) <= localY && localY < (bottom + slop); 350 } 351 352 /** 353 * @param localX Click x 354 * @param localY Click y 355 * @param slop Margin of error for valid click 356 * @return Whether this click was on the visible (non-clipped) part of the shelf 357 */ 358 @Override pointInView(float localX, float localY, float slop)359 public boolean pointInView(float localX, float localY, float slop) { 360 final float left, right; 361 362 if (NotificationMinimalism.isEnabled()) { 363 left = getShelfLeftBound(); 364 right = getShelfRightBound(); 365 } else { 366 final float containerWidth = getWidth(); 367 final float shelfWidth = getActualWidth(); 368 left = isLayoutRtl() ? containerWidth - shelfWidth : 0; 369 right = isLayoutRtl() ? containerWidth : shelfWidth; 370 } 371 372 final float top = mClipTopAmount; 373 final float bottom = getActualHeight(); 374 375 return isXInView(localX, slop, left, right) 376 && isYInView(localY, slop, top, bottom); 377 } 378 379 /** 380 * @return The left boundary of the shelf. 381 */ 382 @VisibleForTesting getShelfLeftBound()383 public float getShelfLeftBound() { 384 if (isAlignedToRight()) { 385 return getWidth() - getActualWidth(); 386 } else { 387 return 0; 388 } 389 } 390 391 /** 392 * @return The right boundary of the shelf. 393 */ 394 @VisibleForTesting getShelfRightBound()395 public float getShelfRightBound() { 396 if (isAlignedToRight()) { 397 return getWidth(); 398 } else { 399 return getActualWidth(); 400 } 401 } 402 403 @VisibleForTesting isAlignedToRight()404 public boolean isAlignedToRight() { 405 return isAlignedToEnd() ^ isLayoutRtl(); 406 } 407 408 /** 409 * When notification minimalism is on, on split shade, we want the notification shelf to align 410 * to the layout end (right for LTR; left for RTL). 411 * @return whether to align with the minimalism split shade style 412 */ 413 @VisibleForTesting isAlignedToEnd()414 public boolean isAlignedToEnd() { 415 if (!NotificationMinimalism.isEnabled()) { 416 return false; 417 } else if (SceneContainerFlag.isEnabled()) { 418 return mAlignedToEnd; 419 } else { 420 return mAmbientState.getUseSplitShade(); 421 } 422 } 423 424 /** @see #isAlignedToEnd() */ setAlignedToEnd(boolean alignedToEnd)425 public void setAlignedToEnd(boolean alignedToEnd) { 426 if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { 427 return; 428 } 429 if (mAlignedToEnd != alignedToEnd) { 430 mAlignedToEnd = alignedToEnd; 431 requestLayout(); 432 } 433 } 434 435 @Override updateBackgroundColors()436 public void updateBackgroundColors() { 437 super.updateBackgroundColors(); 438 ColorUpdateLogger colorUpdateLogger = ColorUpdateLogger.getInstance(); 439 440 if (colorUpdateLogger != null) { 441 colorUpdateLogger.logEvent("Shelf.updateBackgroundColors()", 442 "normalBgColor=" + hexColorString(getNormalBgColor()) 443 + " background=" + mBackgroundNormal.toDumpString()); 444 } 445 } 446 447 /** 448 * Update the shelf appearance based on the other notifications around it. This transforms 449 * the icons from the notification area into the shelf. 450 */ updateAppearance()451 public void updateAppearance() { 452 // If the shelf should not be shown, then there is no need to update anything. 453 if (!mShowNotificationShelf) { 454 return; 455 } 456 mShelfIcons.resetViewStates(); 457 float shelfStart = getTranslationY(); 458 float numViewsInShelf = 0.0f; 459 View lastChild = mAmbientState.getLastVisibleBackgroundChild(); 460 mNotGoneIndex = -1; 461 // find the first view that doesn't overlap with the shelf 462 int notGoneIndex = 0; 463 int colorOfViewBeforeLast = NO_COLOR; 464 int colorTwoBefore = NO_COLOR; 465 int previousColor = NO_COLOR; 466 float transitionAmount = 0.0f; 467 float currentScrollVelocity = mAmbientState.getCurrentScrollVelocity(); 468 boolean scrollingFast = currentScrollVelocity > mScrollFastThreshold 469 || (mAmbientState.isExpansionChanging() 470 && Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold); 471 boolean expandingAnimated = mAmbientState.isExpansionChanging() 472 && !mAmbientState.isPanelTracking(); 473 int baseZHeight = mAmbientState.getBaseZHeight(); 474 int clipTopAmount = 0; 475 476 for (int i = 0; i < getHostLayoutChildCount(); i++) { 477 ExpandableView child = getHostLayoutChildAt(i); 478 if (!child.needsClippingToShelf() || child.getVisibility() == GONE) { 479 continue; 480 } 481 float notificationClipEnd; 482 boolean aboveShelf = ViewState.getFinalTranslationZ(child) > baseZHeight 483 || child.isPinned(); 484 boolean isLastChild = child == lastChild; 485 final float viewStart = child.getTranslationY(); 486 final float shelfClipStart = getTranslationY() - mPaddingBetweenElements; 487 final float inShelfAmount = getAmountInShelf(i, child, scrollingFast, 488 expandingAnimated, isLastChild, shelfClipStart); 489 490 // TODO(b/172289889) scale mPaddingBetweenElements with expansion amount 491 if (aboveShelf) { 492 notificationClipEnd = shelfStart + getIntrinsicHeight(); 493 } else { 494 notificationClipEnd = shelfStart - mPaddingBetweenElements; 495 } 496 int clipTop = updateNotificationClipHeight(child, notificationClipEnd, notGoneIndex); 497 clipTopAmount = Math.max(clipTop, clipTopAmount); 498 499 // If the current row is an ExpandableNotificationRow, update its color, roundedness, 500 // and icon state. 501 if (child instanceof ExpandableNotificationRow expandableRow) { 502 numViewsInShelf += inShelfAmount; 503 int ownColorUntinted = expandableRow.getBackgroundColorWithoutTint(); 504 if (viewStart >= shelfStart && mNotGoneIndex == -1) { 505 mNotGoneIndex = notGoneIndex; 506 setTintColor(previousColor); 507 setOverrideTintColor(colorTwoBefore, transitionAmount); 508 509 } else if (mNotGoneIndex == -1) { 510 colorTwoBefore = previousColor; 511 transitionAmount = inShelfAmount; 512 } 513 // We don't want to modify the color if the notification is hun'd 514 if (isLastChild && canModifyColorOfNotifications()) { 515 if (colorOfViewBeforeLast == NO_COLOR) { 516 colorOfViewBeforeLast = ownColorUntinted; 517 } 518 expandableRow.setOverrideTintColor(colorOfViewBeforeLast, inShelfAmount); 519 } else { 520 colorOfViewBeforeLast = ownColorUntinted; 521 expandableRow.setOverrideTintColor(NO_COLOR, 0 /* overrideAmount */); 522 } 523 if (notGoneIndex != 0 || !aboveShelf) { 524 expandableRow.setAboveShelf(false); 525 } 526 527 previousColor = ownColorUntinted; 528 notGoneIndex++; 529 } 530 531 if (child instanceof ActivatableNotificationView anv) { 532 updateCornerRoundnessOnScroll(anv, viewStart, shelfStart); 533 } 534 } 535 536 clipTransientViews(); 537 538 setClipTopAmount(clipTopAmount); 539 540 boolean isHidden = getViewState().hidden 541 || clipTopAmount >= getIntrinsicHeight() 542 || !mShowNotificationShelf 543 || numViewsInShelf < 1f; 544 545 final float fractionToShade = Interpolators.STANDARD.getInterpolation( 546 mAmbientState.getFractionToShade()); 547 548 if (mAmbientState.isOnKeyguard()) { 549 float numViews = MathUtils.min(numViewsInShelf, mMaxIconsOnLockscreen + 1); 550 float shortestWidth = mShelfIcons.calculateWidthFor(numViews); 551 float actualWidth = MathUtils.lerp(shortestWidth, getWidth(), fractionToShade); 552 setActualWidth(actualWidth); 553 } else { 554 setActualWidth(getWidth()); 555 } 556 557 // TODO(b/172289889) transition last icon in shelf to notification icon and vice versa. 558 setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE); 559 mShelfIcons.calculateIconXTranslations(); 560 mShelfIcons.applyIconStates(); 561 for (int i = 0; i < getHostLayoutChildCount(); i++) { 562 View child = getHostLayoutChildAt(i); 563 if (!(child instanceof ExpandableNotificationRow row) 564 || child.getVisibility() == GONE) { 565 continue; 566 } 567 updateContinuousClipping(row); 568 } 569 boolean hideBackground = isHidden; 570 setHideBackground(hideBackground); 571 if (mNotGoneIndex == -1) { 572 mNotGoneIndex = notGoneIndex; 573 } 574 } 575 576 private ExpandableView getHostLayoutChildAt(int index) { 577 return (ExpandableView) mHostLayout.getChildAt(index); 578 } 579 580 private int getHostLayoutChildCount() { 581 return mHostLayout.getChildCount(); 582 } 583 584 private boolean canModifyColorOfNotifications() { 585 return mCanModifyColorOfNotifications && mAmbientState.isShadeExpanded(); 586 } 587 588 private void updateCornerRoundnessOnScroll( 589 ActivatableNotificationView anv, 590 float viewStart, 591 float shelfStart) { 592 593 final boolean isUnlockedHeadsUp = !mAmbientState.isOnKeyguard() 594 && !mAmbientState.isShadeExpanded() 595 && anv instanceof ExpandableNotificationRow 596 && anv.isHeadsUp(); 597 598 final boolean isHunGoingToShade = mAmbientState.isShadeExpanded() 599 && anv == mAmbientState.getTrackedHeadsUpRow(); 600 601 final boolean shouldUpdateCornerRoundness = viewStart < shelfStart 602 && !isViewAffectedBySwipe(anv) 603 && !isUnlockedHeadsUp 604 && !isHunGoingToShade 605 && !anv.isAboveShelf() 606 && !mAmbientState.isPulsing() 607 && !mAmbientState.isDozing(); 608 609 if (!shouldUpdateCornerRoundness) { 610 return; 611 } 612 613 final float viewEnd = viewStart + anv.getActualHeight(); 614 final float cornerAnimationDistance = mCornerAnimationDistance 615 * mAmbientState.getExpansionFraction(); 616 final float cornerAnimationTop = shelfStart - cornerAnimationDistance; 617 618 final float topValue; 619 if (viewStart >= cornerAnimationTop) { 620 // Round top corners within animation bounds 621 topValue = MathUtils.saturate( 622 (viewStart - cornerAnimationTop) / cornerAnimationDistance); 623 } else { 624 // Fast scroll skips frames and leaves corners with unfinished rounding. 625 // Reset top and bottom corners outside of animation bounds. 626 topValue = 0f; 627 } 628 anv.requestTopRoundness(topValue, SHELF_SCROLL, /* animate = */ false); 629 630 final float bottomValue; 631 if (viewEnd >= cornerAnimationTop) { 632 // Round bottom corners within animation bounds 633 bottomValue = MathUtils.saturate( 634 (viewEnd - cornerAnimationTop) / cornerAnimationDistance); 635 } else { 636 // Fast scroll skips frames and leaves corners with unfinished rounding. 637 // Reset top and bottom corners outside of animation bounds. 638 bottomValue = 0f; 639 } 640 anv.requestBottomRoundness(bottomValue, SHELF_SCROLL, /* animate = */ false); 641 } 642 643 private boolean isViewAffectedBySwipe(ExpandableView expandableView) { 644 return mRoundnessManager.isViewAffectedBySwipe(expandableView); 645 } 646 647 /** 648 * Clips transient views to the top of the shelf - Transient views are only used for 649 * disappearing views/animations and need to be clipped correctly by the shelf to ensure they 650 * don't show underneath the notification stack when something is animating and the user 651 * swipes quickly. 652 */ 653 private void clipTransientViews() { 654 for (int i = 0; i < getHostLayoutTransientViewCount(); i++) { 655 View transientView = getHostLayoutTransientView(i); 656 if (transientView instanceof ExpandableView transientExpandableView) { 657 updateNotificationClipHeight(transientExpandableView, getTranslationY(), -1); 658 } 659 } 660 } 661 662 private View getHostLayoutTransientView(int index) { 663 return mHostLayout.getTransientView(index); 664 } 665 666 private int getHostLayoutTransientViewCount() { 667 return mHostLayout.getTransientViewCount(); 668 } 669 670 private void updateIconClipAmount(ExpandableNotificationRow row) { 671 float maxTop = row.getTranslationY(); 672 if (getClipTopAmount() != 0) { 673 // if the shelf is clipped, lets make sure we also clip the icon 674 maxTop = Math.max(maxTop, getTranslationY() + getClipTopAmount()); 675 } 676 StatusBarIconView icon = NotificationBundleUi.isEnabled() 677 ? row.getEntryAdapter().getIcons().getShelfIcon() 678 : row.getEntryLegacy().getIcons().getShelfIcon(); 679 float shelfIconPosition = getTranslationY() + icon.getTop() + icon.getTranslationY(); 680 if (shelfIconPosition < maxTop && !mAmbientState.isFullyHidden()) { 681 int top = (int) (maxTop - shelfIconPosition); 682 Rect clipRect = new Rect(0, top, icon.getWidth(), Math.max(top, icon.getHeight())); 683 icon.setClipBounds(clipRect); 684 } else { 685 icon.setClipBounds(null); 686 } 687 } 688 689 private void updateContinuousClipping(final ExpandableNotificationRow row) { 690 StatusBarIconView icon = NotificationBundleUi.isEnabled() 691 ? row.getEntryAdapter().getIcons().getShelfIcon() 692 : row.getEntryLegacy().getIcons().getShelfIcon(); 693 boolean needsContinuousClipping = ViewState.isAnimatingY(icon) && !mAmbientState.isDozing(); 694 boolean isContinuousClipping = icon.getTag(TAG_CONTINUOUS_CLIPPING) != null; 695 if (needsContinuousClipping && !isContinuousClipping) { 696 final ViewTreeObserver observer = icon.getViewTreeObserver(); 697 ViewTreeObserver.OnPreDrawListener predrawListener = 698 new ViewTreeObserver.OnPreDrawListener() { 699 @Override 700 public boolean onPreDraw() { 701 boolean animatingY = ViewState.isAnimatingY(icon); 702 if (!animatingY) { 703 if (observer.isAlive()) { 704 observer.removeOnPreDrawListener(this); 705 } 706 icon.setTag(TAG_CONTINUOUS_CLIPPING, null); 707 return true; 708 } 709 updateIconClipAmount(row); 710 return true; 711 } 712 }; 713 observer.addOnPreDrawListener(predrawListener); 714 icon.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { 715 @Override 716 public void onViewAttachedToWindow(View v) { 717 } 718 719 @Override 720 public void onViewDetachedFromWindow(View v) { 721 if (v == icon) { 722 if (observer.isAlive()) { 723 observer.removeOnPreDrawListener(predrawListener); 724 } 725 icon.setTag(TAG_CONTINUOUS_CLIPPING, null); 726 } 727 } 728 }); 729 icon.setTag(TAG_CONTINUOUS_CLIPPING, predrawListener); 730 } 731 } 732 733 /** 734 * Update the clipping of this view. 735 * 736 * @return the amount that our own top should be clipped 737 */ 738 private int updateNotificationClipHeight(ExpandableView view, 739 float notificationClipEnd, int childIndex) { 740 float viewEnd = view.getTranslationY() + view.getActualHeight(); 741 boolean isPinned = (view.isPinned() || view.isHeadsUpAnimatingAway()) 742 && !mAmbientState.isDozingAndNotPulsing(view); 743 boolean shouldClipOwnTop; 744 if (mAmbientState.isPulseExpanding()) { 745 shouldClipOwnTop = childIndex == 0; 746 } else { 747 shouldClipOwnTop = view.showingPulsing(); 748 } 749 if (!isPinned) { 750 if (viewEnd > notificationClipEnd && !shouldClipOwnTop) { 751 int clipBottomAmount = 752 mEnableNotificationClipping ? (int) (viewEnd - notificationClipEnd) : 0; 753 view.setClipBottomAmount(clipBottomAmount); 754 } else { 755 view.setClipBottomAmount(0); 756 } 757 } 758 if (shouldClipOwnTop) { 759 return (int) (viewEnd - getTranslationY()); 760 } else { 761 return 0; 762 } 763 } 764 765 @Override 766 public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, 767 int outlineTranslation) { 768 if (!mHasItemsInStableShelf) { 769 shadowIntensity = 0.0f; 770 } 771 super.setFakeShadowIntensity(shadowIntensity, outlineAlpha, shadowYEnd, outlineTranslation); 772 } 773 774 /** 775 * @param i Index of the view in the host layout. 776 * @param view The current ExpandableView. 777 * @param scrollingFast Whether we are scrolling fast. 778 * @param expandingAnimated Whether we are expanding a notification. 779 * @param isLastChild Whether this is the last view. 780 * @param shelfClipStart The point at which notifications start getting clipped by the shelf. 781 * @return The amount how much this notification is in the shelf. 782 * 0f is not in shelf. 1f is completely in shelf. 783 */ 784 @VisibleForTesting 785 public float getAmountInShelf( 786 int i, 787 ExpandableView view, 788 boolean scrollingFast, 789 boolean expandingAnimated, 790 boolean isLastChild, 791 float shelfClipStart 792 ) { 793 794 // Let's calculate how much the view is in the shelf 795 float viewStart = view.getTranslationY(); 796 int fullHeight = view.getActualHeight() + mPaddingBetweenElements; 797 float iconTransformStart = calculateIconTransformationStart(view); 798 799 // Let's make sure the transform distance is 800 // at most to the icon (relevant for conversations) 801 float transformDistance = Math.min( 802 viewStart + fullHeight - iconTransformStart, 803 getIntrinsicHeight()); 804 805 if (isLastChild) { 806 fullHeight = Math.min(fullHeight, view.getMinHeight() - getIntrinsicHeight()); 807 transformDistance = Math.min( 808 transformDistance, 809 view.getMinHeight() - getIntrinsicHeight()); 810 } 811 812 float viewEnd = viewStart + fullHeight; 813 float fullTransitionAmount = 0.0f; 814 float iconTransitionAmount = 0.0f; 815 816 // Don't animate shelf icons during shade expansion. 817 if (mAmbientState.isExpansionChanging() && !mAmbientState.isOnKeyguard()) { 818 // TODO(b/172289889) handle icon placement for notification that is clipped by the shelf 819 if (mIndexOfFirstViewInShelf != -1 && i >= mIndexOfFirstViewInShelf) { 820 fullTransitionAmount = 1f; 821 iconTransitionAmount = 1f; 822 } 823 824 } else if (viewEnd >= shelfClipStart 825 && (mAmbientState.isShadeExpanded() 826 || (!view.isPinned() && !view.isHeadsUpAnimatingAway()))) { 827 828 if (viewStart < shelfClipStart && Math.abs(viewStart - shelfClipStart) > 0.001f) { 829 // Partially clipped by shelf. 830 float fullAmount = (shelfClipStart - viewStart) / fullHeight; 831 fullAmount = Math.min(1.0f, fullAmount); 832 fullTransitionAmount = 1.0f - fullAmount; 833 if (isLastChild) { 834 // Reduce icon transform distance to completely fade in shelf icon 835 // by the time the notification icon fades out, and vice versa 836 iconTransitionAmount = (shelfClipStart - viewStart) 837 / (iconTransformStart - viewStart); 838 } else { 839 iconTransitionAmount = (shelfClipStart - iconTransformStart) 840 / transformDistance; 841 } 842 iconTransitionAmount = MathUtils.constrain(iconTransitionAmount, 0.0f, 1.0f); 843 iconTransitionAmount = 1.0f - iconTransitionAmount; 844 } else { 845 // Fully in shelf. 846 fullTransitionAmount = 1.0f; 847 iconTransitionAmount = 1.0f; 848 } 849 } 850 updateIconPositioning(view, iconTransitionAmount, 851 scrollingFast, expandingAnimated, isLastChild); 852 return fullTransitionAmount; 853 } 854 855 /** 856 * @return the location where the transformation into the shelf should start. 857 */ calculateIconTransformationStart(ExpandableView view)858 private float calculateIconTransformationStart(ExpandableView view) { 859 View target = view.getShelfTransformationTarget(); 860 if (target == null) { 861 return view.getTranslationY(); 862 } 863 float start = view.getTranslationY() + view.getRelativeTopPadding(target); 864 865 // Let's not start the transformation right at the icon but by the padding before it. 866 start -= view.getShelfIcon().getTop(); 867 return start; 868 } 869 updateIconPositioning( ExpandableView view, float iconTransitionAmount, boolean scrollingFast, boolean expandingAnimated, boolean isLastChild )870 private void updateIconPositioning( 871 ExpandableView view, 872 float iconTransitionAmount, 873 boolean scrollingFast, 874 boolean expandingAnimated, 875 boolean isLastChild 876 ) { 877 StatusBarIconView icon = view.getShelfIcon(); 878 NotificationIconContainer.IconState iconState = getIconState(icon); 879 if (iconState == null) { 880 return; 881 } 882 boolean clampInShelf = iconTransitionAmount > 0.5f || isTargetClipped(view); 883 float clampedAmount = clampInShelf ? 1.0f : 0.0f; 884 if (iconTransitionAmount == clampedAmount) { 885 iconState.noAnimations = (scrollingFast || expandingAnimated) && !isLastChild; 886 } 887 if (!isLastChild 888 && (scrollingFast || (expandingAnimated && !ViewState.isAnimatingY(icon)))) { 889 iconState.cancelAnimations(icon); 890 iconState.noAnimations = true; 891 } 892 float transitionAmount; 893 if (mAmbientState.isHiddenAtAll() && !view.isInShelf()) { 894 transitionAmount = mAmbientState.isFullyHidden() ? 1 : 0; 895 } else { 896 transitionAmount = iconTransitionAmount; 897 iconState.needsCannedAnimation = iconState.clampedAppearAmount != clampedAmount; 898 } 899 iconState.clampedAppearAmount = clampedAmount; 900 setIconTransformationAmount(view, transitionAmount); 901 } 902 isTargetClipped(ExpandableView view)903 private boolean isTargetClipped(ExpandableView view) { 904 View target = view.getShelfTransformationTarget(); 905 if (target == null) { 906 return false; 907 } 908 // We should never clip the target, let's instead put it into the shelf! 909 float endOfTarget = view.getTranslationY() 910 + view.getContentTranslation() 911 + view.getRelativeTopPadding(target) 912 + target.getHeight(); 913 return endOfTarget >= getTranslationY() - mPaddingBetweenElements; 914 } 915 setIconTransformationAmount(ExpandableView view, float transitionAmount)916 private void setIconTransformationAmount(ExpandableView view, float transitionAmount) { 917 if (!(view instanceof ExpandableNotificationRow row)) { 918 return; 919 } 920 StatusBarIconView icon = row.getShelfIcon(); 921 NotificationIconContainer.IconState iconState = getIconState(icon); 922 if (iconState == null) { 923 return; 924 } 925 iconState.setAlpha(ICON_ALPHA_INTERPOLATOR.getInterpolation(transitionAmount)); 926 boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf(); 927 iconState.hidden = isAppearing 928 || (view instanceof ExpandableNotificationRow 929 && ((ExpandableNotificationRow) view).isMinimized() 930 && mShelfIcons.areIconsOverflowing()) 931 || (transitionAmount == 0.0f && !iconState.isAnimating(icon)) 932 || row.isAboveShelf() 933 || row.showingPulsing() 934 || row.getTranslationZ() > mAmbientState.getBaseZHeight(); 935 936 iconState.iconAppearAmount = iconState.hidden ? 0f : transitionAmount; 937 938 // Fade in icons at shelf start 939 // This is important for conversation icons, which are badged and need x reset 940 iconState.setXTranslation(mShelfIcons.getActualPaddingStart()); 941 942 final boolean stayingInShelf = row.isInShelf() && !row.isTransformingIntoShelf(); 943 if (stayingInShelf) { 944 iconState.iconAppearAmount = 1.0f; 945 iconState.setAlpha(1.0f); 946 iconState.hidden = false; 947 } 948 int backgroundColor = getBackgroundColorWithoutTint(); 949 int shelfColor = icon.getContrastedStaticDrawableColor(backgroundColor); 950 if (row.isShowingIcon() && shelfColor != StatusBarIconView.NO_COLOR) { 951 int iconColor = row.getOriginalIconColor(); 952 shelfColor = NotificationUtils.interpolateColors(iconColor, shelfColor, 953 iconState.iconAppearAmount); 954 } 955 iconState.iconColor = shelfColor; 956 } 957 getIconState(StatusBarIconView icon)958 private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) { 959 if (mShelfIcons == null) { 960 return null; 961 } 962 return mShelfIcons.getIconState(icon); 963 } 964 965 @Override hasNoContentHeight()966 public boolean hasNoContentHeight() { 967 return true; 968 } 969 setHideBackground(boolean hideBackground)970 private void setHideBackground(boolean hideBackground) { 971 if (mHideBackground != hideBackground) { 972 mHideBackground = hideBackground; 973 updateOutline(); 974 } 975 } 976 977 @Override needsOutline()978 protected boolean needsOutline() { 979 return !mHideBackground && super.needsOutline(); 980 } 981 982 983 @Override onLayout(boolean changed, int left, int top, int right, int bottom)984 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 985 super.onLayout(changed, left, top, right, bottom); 986 987 // we always want to clip to our sides, such that nothing can draw outside of these bounds 988 int height = getResources().getDisplayMetrics().heightPixels; 989 mClipRect.set(0, -height, getWidth(), height); 990 if (mShelfIcons != null) { 991 mShelfIcons.setClipBounds(mClipRect); 992 } 993 } 994 995 /** 996 * @return the index of the notification at which the shelf visually resides 997 */ getNotGoneIndex()998 public int getNotGoneIndex() { 999 return mNotGoneIndex; 1000 } 1001 setHasItemsInStableShelf(boolean hasItemsInStableShelf)1002 private void setHasItemsInStableShelf(boolean hasItemsInStableShelf) { 1003 if (mHasItemsInStableShelf != hasItemsInStableShelf) { 1004 mHasItemsInStableShelf = hasItemsInStableShelf; 1005 updateInteractiveness(); 1006 } 1007 } 1008 updateInteractiveness()1009 private void updateInteractiveness() { 1010 mInteractive = mCanInteract && mHasItemsInStableShelf; 1011 setClickable(mInteractive); 1012 setFocusable(mInteractive); 1013 setImportantForAccessibility(mInteractive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES 1014 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 1015 } 1016 1017 @Override isInteractive()1018 protected boolean isInteractive() { 1019 return mInteractive; 1020 } 1021 setAnimationsEnabled(boolean enabled)1022 public void setAnimationsEnabled(boolean enabled) { 1023 mAnimationsEnabled = enabled; 1024 if (!enabled) { 1025 // we need to wait with enabling the animations until the first frame has passed 1026 mShelfIcons.setAnimationsEnabled(false); 1027 } 1028 } 1029 1030 @Override hasOverlappingRendering()1031 public boolean hasOverlappingRendering() { 1032 return false; // Shelf only uses alpha for transitions where the difference can't be seen. 1033 } 1034 1035 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)1036 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1037 super.onInitializeAccessibilityNodeInfo(info); 1038 if (mInteractive) { 1039 // Add two accessibility actions that both performs expanding the notification shade 1040 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); 1041 1042 AccessibilityAction seeAll = new AccessibilityAction( 1043 AccessibilityNodeInfo.ACTION_CLICK, 1044 getContext().getString(R.string.accessibility_overflow_action) 1045 ); 1046 info.addAction(seeAll); 1047 } 1048 } 1049 1050 @Override performAccessibilityAction(int action, Bundle args)1051 public boolean performAccessibilityAction(int action, Bundle args) { 1052 // override ACTION_EXPAND with ACTION_CLICK 1053 if (action == AccessibilityNodeInfo.ACTION_EXPAND) { 1054 return super.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, args); 1055 } else { 1056 return super.performAccessibilityAction(action, args); 1057 } 1058 } 1059 1060 @Override needsClippingToShelf()1061 public boolean needsClippingToShelf() { 1062 return false; 1063 } 1064 setCanModifyColorOfNotifications(boolean canModifyColorOfNotifications)1065 public void setCanModifyColorOfNotifications(boolean canModifyColorOfNotifications) { 1066 mCanModifyColorOfNotifications = canModifyColorOfNotifications; 1067 } 1068 setCanInteract(boolean canInteract)1069 public void setCanInteract(boolean canInteract) { 1070 mCanInteract = canInteract; 1071 updateInteractiveness(); 1072 } 1073 setIndexOfFirstViewInShelf(ExpandableView firstViewInShelf)1074 public void setIndexOfFirstViewInShelf(ExpandableView firstViewInShelf) { 1075 mIndexOfFirstViewInShelf = getIndexOfViewInHostLayout(firstViewInShelf); 1076 } 1077 getIndexOfViewInHostLayout(ExpandableView child)1078 private int getIndexOfViewInHostLayout(ExpandableView child) { 1079 return mHostLayout.indexOfChild(child); 1080 } 1081 requestRoundnessResetFor(ExpandableView child)1082 public void requestRoundnessResetFor(ExpandableView child) { 1083 child.requestRoundnessReset(SHELF_SCROLL); 1084 } 1085 1086 @Override dump(PrintWriter pwOriginal, String[] args)1087 public void dump(PrintWriter pwOriginal, String[] args) { 1088 IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal); 1089 super.dump(pw, args); 1090 if (DUMP_VERBOSE) { 1091 DumpUtilsKt.withIncreasedIndent(pw, () -> { 1092 pw.println("mActualWidth: " + mActualWidth); 1093 pw.println("mStatusBarHeight: " + mStatusBarHeight); 1094 }); 1095 } 1096 } 1097 1098 public class ShelfState extends ExpandableViewState { 1099 private boolean hasItemsInStableShelf; 1100 private ExpandableView firstViewInShelf; 1101 1102 @Override applyToView(View view)1103 public void applyToView(View view) { 1104 if (!mShowNotificationShelf) { 1105 return; 1106 } 1107 super.applyToView(view); 1108 setIndexOfFirstViewInShelf(firstViewInShelf); 1109 updateAppearance(); 1110 setHasItemsInStableShelf(hasItemsInStableShelf); 1111 mShelfIcons.setAnimationsEnabled(mAnimationsEnabled); 1112 } 1113 1114 @Override animateTo(View view, AnimationProperties properties)1115 public void animateTo(View view, AnimationProperties properties) { 1116 if (!mShowNotificationShelf) { 1117 return; 1118 } 1119 super.animateTo(view, properties); 1120 setIndexOfFirstViewInShelf(firstViewInShelf); 1121 updateAppearance(); 1122 setHasItemsInStableShelf(hasItemsInStableShelf); 1123 mShelfIcons.setAnimationsEnabled(mAnimationsEnabled); 1124 } 1125 } 1126 } 1127