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