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