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