1 /* 2 * Copyright (C) 2014 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.notification.stack; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.util.MathUtils; 24 import android.view.View; 25 import android.view.ViewGroup; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 import com.android.internal.policy.SystemBarUtils; 29 import com.android.keyguard.BouncerPanelExpansionCalculator; 30 import com.android.systemui.Flags; 31 import com.android.systemui.animation.ShadeInterpolation; 32 import com.android.systemui.res.R; 33 import com.android.systemui.scene.shared.flag.SceneContainerFlag; 34 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; 35 import com.android.systemui.statusbar.NotificationShelf; 36 import com.android.systemui.statusbar.notification.SourceType; 37 import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView; 38 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; 39 import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimator; 40 import com.android.systemui.statusbar.notification.headsup.NotificationsHunSharedAnimationValues; 41 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; 42 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 43 import com.android.systemui.statusbar.notification.row.ExpandableView; 44 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; 45 import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling; 46 47 import java.util.ArrayList; 48 import java.util.List; 49 50 /** 51 * The Algorithm of the 52 * {@link com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout} which can 53 * be queried for {@link StackScrollAlgorithmState} 54 */ 55 public class StackScrollAlgorithm { 56 57 public static final float START_FRACTION = 0.5f; 58 59 private static final String TAG = "StackScrollAlgorithm"; 60 private static final SourceType STACK_SCROLL_ALGO = SourceType.from("StackScrollAlgorithm"); 61 private final ViewGroup mHostView; 62 @Nullable 63 private final HeadsUpAnimator mHeadsUpAnimator; 64 65 private float mPaddingBetweenElements; 66 private float mGapHeight; 67 private float mGapHeightOnLockscreen; 68 private int mCollapsedSize; 69 private boolean mEnableNotificationClipping; 70 71 private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); 72 private boolean mIsExpanded; 73 private boolean mClipNotificationScrollToTop; 74 @VisibleForTesting 75 float mHeadsUpInset; 76 @VisibleForTesting 77 float mHeadsUpAppearStartAboveScreen; 78 private int mPinnedZTranslationExtra; 79 private float mNotificationScrimPadding; 80 private int mMarginBottom; 81 private float mQuickQsOffsetHeight; 82 private float mSmallCornerRadius; 83 private float mLargeCornerRadius; 84 private int mHeadsUpAppearHeightBottom; 85 private int mHeadsUpCyclingPadding; 86 StackScrollAlgorithm( Context context, ViewGroup hostView, @Nullable HeadsUpAnimator headsUpAnimator)87 public StackScrollAlgorithm( 88 Context context, 89 ViewGroup hostView, 90 @Nullable HeadsUpAnimator headsUpAnimator) { 91 mHostView = hostView; 92 mHeadsUpAnimator = headsUpAnimator; 93 initView(context); 94 } 95 initView(Context context)96 public void initView(Context context) { 97 updateResources(context); 98 } 99 updateResources(Context context)100 private void updateResources(Context context) { 101 Resources res = context.getResources(); 102 mPaddingBetweenElements = res.getDimensionPixelSize( 103 R.dimen.notification_divider_height); 104 mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height); 105 mEnableNotificationClipping = res.getBoolean(R.bool.notification_enable_clipping); 106 mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop); 107 int statusBarHeight = SystemBarUtils.getStatusBarHeight(context); 108 mHeadsUpInset = statusBarHeight + res.getDimensionPixelSize( 109 R.dimen.heads_up_status_bar_padding); 110 mHeadsUpAppearStartAboveScreen = res.getDimensionPixelSize( 111 R.dimen.heads_up_appear_y_above_screen); 112 mHeadsUpCyclingPadding = context.getResources() 113 .getDimensionPixelSize(R.dimen.heads_up_cycling_padding); 114 mPinnedZTranslationExtra = res.getDimensionPixelSize( 115 R.dimen.heads_up_pinned_elevation); 116 mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height); 117 mGapHeightOnLockscreen = res.getDimensionPixelSize( 118 R.dimen.notification_section_divider_height_lockscreen); 119 mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings); 120 mMarginBottom = res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom); 121 mQuickQsOffsetHeight = SystemBarUtils.getQuickQsOffsetHeight(context); 122 mSmallCornerRadius = res.getDimension(R.dimen.notification_corner_radius_small); 123 mLargeCornerRadius = res.getDimension(R.dimen.notification_corner_radius); 124 if (NotificationsHunSharedAnimationValues.isEnabled()) { 125 mHeadsUpAnimator.updateResources(context); 126 } 127 } 128 129 /** 130 * Updates the state of all children in the hostview based on this algorithm. 131 */ resetViewStates(AmbientState ambientState, int speedBumpIndex)132 public void resetViewStates(AmbientState ambientState, int speedBumpIndex) { 133 // The state of the local variables are saved in an algorithmState to easily subdivide it 134 // into multiple phases. 135 StackScrollAlgorithmState algorithmState = mTempAlgorithmState; 136 137 // First we reset the view states to their default values. 138 resetChildViewStates(); 139 initAlgorithmState(algorithmState, ambientState); 140 updatePositionsForState(algorithmState, ambientState); 141 updateZValuesForState(algorithmState, ambientState); 142 updateHeadsUpStates(algorithmState, ambientState); 143 updatePulsingStates(algorithmState, ambientState); 144 145 updateDimmedAndHideSensitive(ambientState, algorithmState); 146 updateClipping(algorithmState, ambientState); 147 updateSpeedBumpState(algorithmState, speedBumpIndex); 148 updateShelfState(algorithmState, ambientState); 149 updateAlphaState(algorithmState, ambientState); 150 getNotificationChildrenStates(algorithmState); 151 } 152 updateAlphaState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)153 private void updateAlphaState(StackScrollAlgorithmState algorithmState, 154 AmbientState ambientState) { 155 for (ExpandableView view : algorithmState.visibleChildren) { 156 final ViewState viewState = view.getViewState(); 157 final boolean isHunGoingToShade = ambientState.isShadeExpanded() 158 && view == ambientState.getTrackedHeadsUpRow(); 159 160 if (isHunGoingToShade) { 161 // Keep 100% opacity for heads up notification going to shade. 162 viewState.setAlpha(1f); 163 } else if (!SceneContainerFlag.isEnabled() && ambientState.isOnKeyguard()) { 164 // Adjust alpha for wakeup to lockscreen. 165 if (view.isHeadsUpState()) { 166 // Pulsing HUN should be visible on AOD and stay visible during 167 // AOD=>lockscreen transition 168 viewState.setAlpha(1f - ambientState.getHideAmount()); 169 } else { 170 // Normal notifications are hidden on AOD and should fade in during 171 // AOD=>lockscreen transition 172 viewState.setAlpha(1f - ambientState.getDozeAmount()); 173 } 174 } else if (SceneContainerFlag.isEnabled() 175 && ambientState.isShowingStackOnLockscreen()) { 176 // Adjust alpha for wakeup to lockscreen. 177 if (view.isHeadsUpState()) { 178 // Pulsing HUN should be visible on AOD and stay visible during 179 // AOD=>lockscreen transition 180 viewState.setAlpha(1f - ambientState.getHideAmount()); 181 } else { 182 // Take into account scene container-specific Lockscreen fade-in progress 183 float fadeAlpha = ambientState.getLockscreenStackFadeInProgress(); 184 float dozeAlpha = 1f - ambientState.getDozeAmount(); 185 viewState.setAlpha(Math.min(dozeAlpha, fadeAlpha)); 186 } 187 } else if (ambientState.isExpansionChanging()) { 188 // Adjust alpha for shade open & close. 189 float expansion = ambientState.getExpansionFraction(); 190 if (ambientState.isBouncerInTransit()) { 191 viewState.setAlpha( 192 BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(expansion)); 193 } else if (view instanceof FooterView) { 194 viewState.setAlpha(interpolateFooterAlpha(ambientState)); 195 } else { 196 viewState.setAlpha(interpolateNotificationContentAlpha(ambientState)); 197 } 198 } 199 200 // On the final call to {@link #resetViewState}, the alpha is set back to 1f but 201 // ambientState.isExpansionChanging() is now false. This causes a flicker on the 202 // EmptyShadeView after the shade is collapsed. Make sure the empty shade view 203 // isn't visible unless the shade is expanded. 204 if (view instanceof EmptyShadeView && ambientState.getExpansionFraction() == 0f) { 205 viewState.setAlpha(0f); 206 } 207 208 // For EmptyShadeView if on keyguard, we need to control the alpha to create 209 // a nice transition when the user is dragging down the notification panel. 210 if (view instanceof EmptyShadeView && ambientState.isOnKeyguard()) { 211 final float fractionToShade = ambientState.getFractionToShade(); 212 viewState.setAlpha(ShadeInterpolation.getContentAlpha(fractionToShade)); 213 } 214 215 NotificationShelf shelf = ambientState.getShelf(); 216 if (shelf != null) { 217 final ViewState shelfState = shelf.getViewState(); 218 219 // After the shelf has updated its yTranslation, explicitly set alpha=0 for view 220 // below shelf to skip rendering them in the hardware layer. We do not set them 221 // invisible because that runs invalidate & onDraw when these views return onscreen, 222 // which is more expensive. 223 if (shelfState.hidden) { 224 // When the shelf is hidden, it won't clip views, so we don't hide rows 225 continue; 226 } 227 228 final float shelfTop = shelfState.getYTranslation(); 229 final float viewTop = viewState.getYTranslation(); 230 if (viewTop >= shelfTop) { 231 viewState.setAlpha(0); 232 } 233 } 234 } 235 } 236 interpolateFooterAlpha(AmbientState ambientState)237 private float interpolateFooterAlpha(AmbientState ambientState) { 238 float expansion = ambientState.getExpansionFraction(); 239 if (ambientState.isSmallScreen()) { 240 return ShadeInterpolation.getContentAlpha(expansion); 241 } 242 LargeScreenShadeInterpolator interpolator = ambientState.getLargeScreenShadeInterpolator(); 243 return interpolator.getNotificationFooterAlpha(expansion); 244 } 245 interpolateNotificationContentAlpha(AmbientState ambientState)246 private float interpolateNotificationContentAlpha(AmbientState ambientState) { 247 float expansion = ambientState.getExpansionFraction(); 248 if (ambientState.isSmallScreen()) { 249 return ShadeInterpolation.getContentAlpha(expansion); 250 } 251 LargeScreenShadeInterpolator interpolator = ambientState.getLargeScreenShadeInterpolator(); 252 return interpolator.getNotificationContentAlpha(expansion); 253 } 254 255 /** 256 * How expanded or collapsed notifications are when pulling down the shade. 257 * 258 * @param ambientState Current ambient state. 259 * @return 0 when fully collapsed, 1 when expanded. 260 */ getNotificationSquishinessFraction(AmbientState ambientState)261 public float getNotificationSquishinessFraction(AmbientState ambientState) { 262 return getExpansionFractionWithoutShelf(mTempAlgorithmState, ambientState); 263 } 264 setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom)265 public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) { 266 NotificationsHunSharedAnimationValues.assertInLegacyMode(); 267 mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom; 268 } 269 270 /** 271 * If the QuickSettings is showing full screen, we want to animate the HeadsUp Notifications 272 * from the bottom of the screen. 273 * 274 * @param ambientState Current ambient state. 275 * @param viewState The state of the HUN that is being queried to appear from the bottom. 276 * 277 * @return true if the HeadsUp Notifications should appear from the bottom 278 */ shouldHunAppearFromBottom(AmbientState ambientState, ExpandableViewState viewState)279 public boolean shouldHunAppearFromBottom(AmbientState ambientState, 280 ExpandableViewState viewState) { 281 return viewState.getYTranslation() + viewState.height 282 >= ambientState.getMaxHeadsUpTranslation(); 283 } 284 debugLog(String s)285 public static void debugLog(String s) { 286 android.util.Log.i(TAG, s); 287 } 288 debugLogView(View view, String s)289 public static void debugLogView(View view, String s) { 290 String viewString = ""; 291 if (view instanceof ExpandableNotificationRow row) { 292 viewString = row.getKey(); 293 } else if (view == null) { 294 viewString = "View is null"; 295 } else if (view instanceof SectionHeaderView) { 296 viewString = "SectionHeaderView"; 297 } else if (view instanceof FooterView) { 298 viewString = "FooterView"; 299 } else if (view instanceof MediaContainerView) { 300 viewString = "MediaContainerView"; 301 } else if (view instanceof EmptyShadeView) { 302 viewString = "EmptyShadeView"; 303 } else { 304 viewString = view.toString(); 305 } 306 debugLog(viewString + " " + s); 307 } 308 resetChildViewStates()309 private void resetChildViewStates() { 310 int numChildren = mHostView.getChildCount(); 311 for (int i = 0; i < numChildren; i++) { 312 ExpandableView child = (ExpandableView) mHostView.getChildAt(i); 313 child.resetViewState(); 314 } 315 } 316 getNotificationChildrenStates(StackScrollAlgorithmState algorithmState)317 private void getNotificationChildrenStates(StackScrollAlgorithmState algorithmState) { 318 int childCount = algorithmState.visibleChildren.size(); 319 for (int i = 0; i < childCount; i++) { 320 ExpandableView v = algorithmState.visibleChildren.get(i); 321 if (v instanceof ExpandableNotificationRow row) { 322 row.updateChildrenStates(); 323 } 324 } 325 } 326 updateSpeedBumpState(StackScrollAlgorithmState algorithmState, int speedBumpIndex)327 private void updateSpeedBumpState(StackScrollAlgorithmState algorithmState, 328 int speedBumpIndex) { 329 int childCount = algorithmState.visibleChildren.size(); 330 int belowSpeedBump = speedBumpIndex; 331 for (int i = 0; i < childCount; i++) { 332 ExpandableView child = algorithmState.visibleChildren.get(i); 333 ExpandableViewState childViewState = child.getViewState(); 334 335 // The speed bump can also be gone, so equality needs to be taken when comparing 336 // indices. 337 childViewState.belowSpeedBump = i >= belowSpeedBump; 338 } 339 340 } 341 updateShelfState( StackScrollAlgorithmState algorithmState, AmbientState ambientState)342 private void updateShelfState( 343 StackScrollAlgorithmState algorithmState, 344 AmbientState ambientState) { 345 346 NotificationShelf shelf = ambientState.getShelf(); 347 if (shelf == null) { 348 return; 349 } 350 351 shelf.updateState(algorithmState, ambientState); 352 } 353 updateClipping(StackScrollAlgorithmState algorithmState, AmbientState ambientState)354 private void updateClipping(StackScrollAlgorithmState algorithmState, 355 AmbientState ambientState) { 356 float stackTop = SceneContainerFlag.isEnabled() ? ambientState.getStackTop() 357 : ambientState.getStackY() - ambientState.getScrollY(); 358 float drawStart = ambientState.isOnKeyguard() ? 0 359 : stackTop; 360 float clipStart = 0; 361 int childCount = algorithmState.visibleChildren.size(); 362 boolean firstHeadsUp = true; 363 float firstHeadsUpEnd = 0; 364 for (int i = 0; i < childCount; i++) { 365 ExpandableView child = algorithmState.visibleChildren.get(i); 366 ExpandableViewState state = child.getViewState(); 367 if (!child.mustStayOnScreen() || state.headsUpIsVisible) { 368 clipStart = Math.max(drawStart, clipStart); 369 } 370 float newYTranslation = state.getYTranslation(); 371 float newHeight = state.height; 372 float newNotificationEnd = newYTranslation + newHeight; 373 boolean isHeadsUp = (child instanceof ExpandableNotificationRow) && child.isPinned(); 374 if (mClipNotificationScrollToTop 375 && !firstHeadsUp 376 && (isHeadsUp || child.isHeadsUpAnimatingAway()) 377 && newNotificationEnd > firstHeadsUpEnd 378 && !ambientState.isShadeExpanded() 379 && !skipClipBottomForCycling(child, ambientState)) { 380 // The bottom of this view is peeking out from under the previous view. 381 // Clip the part that is peeking out. 382 float overlapAmount = newNotificationEnd - firstHeadsUpEnd; 383 state.clipBottomAmount = mEnableNotificationClipping ? (int) overlapAmount : 0; 384 } else { 385 state.clipBottomAmount = 0; 386 } 387 if (firstHeadsUp) { 388 firstHeadsUpEnd = newNotificationEnd; 389 } 390 if (isHeadsUp) { 391 firstHeadsUp = false; 392 } 393 if (!child.isTransparent()) { 394 // Only update the previous values if we are not transparent, 395 // otherwise we would clip to a transparent view. 396 clipStart = Math.max(clipStart, isHeadsUp ? newYTranslation : newNotificationEnd); 397 } 398 } 399 } 400 401 /** 402 * @return Should we skip clipping the bottom clipping when new hun has lower bottom line for 403 * the hun cycling animation. 404 */ skipClipBottomForCycling(ExpandableView view, AmbientState ambientState)405 private boolean skipClipBottomForCycling(ExpandableView view, AmbientState ambientState) { 406 if (!NotificationHeadsUpCycling.isEnabled()) return false; 407 if (!isCyclingOut(view, ambientState)) return false; 408 // skip bottom clipping if we animate the bottom line 409 return NotificationHeadsUpCycling.getAnimateTallToShort(); 410 } 411 412 /** 413 * Whether the view is the hun that is cycling out by the notification avalanche. 414 */ isCyclingOut(ExpandableView view, AmbientState ambientState)415 public boolean isCyclingOut(ExpandableView view, AmbientState ambientState) { 416 if (!NotificationHeadsUpCycling.isEnabled()) return false; 417 if (!(view instanceof ExpandableNotificationRow)) return false; 418 return isCyclingOut((ExpandableNotificationRow) view, ambientState); 419 } 420 421 /** 422 * Whether the row is the hun that is cycling out by the notification avalanche. 423 */ isCyclingOut(ExpandableNotificationRow row, AmbientState ambientState)424 public boolean isCyclingOut(ExpandableNotificationRow row, AmbientState ambientState) { 425 if (!NotificationHeadsUpCycling.isEnabled()) return false; 426 String cyclingOutKey = ambientState.getAvalanchePreviousHunKey(); 427 return row.getKey().equals(cyclingOutKey); 428 } 429 430 /** 431 * Whether the row is the hun that is cycling in by the notification avalanche. 432 */ isCyclingIn(ExpandableNotificationRow row, AmbientState ambientState)433 public boolean isCyclingIn(ExpandableNotificationRow row, AmbientState ambientState) { 434 if (!NotificationHeadsUpCycling.isEnabled()) return false; 435 String cyclingInKey = ambientState.getAvalancheShowingHunKey(); 436 return row.getKey().equals(cyclingInKey); 437 } 438 439 /** Updates the dimmed and hiding sensitive states of the children. */ updateDimmedAndHideSensitive(AmbientState ambientState, StackScrollAlgorithmState algorithmState)440 private void updateDimmedAndHideSensitive(AmbientState ambientState, 441 StackScrollAlgorithmState algorithmState) { 442 boolean hideSensitive = ambientState.isHideSensitive(); 443 int childCount = algorithmState.visibleChildren.size(); 444 for (int i = 0; i < childCount; i++) { 445 ExpandableView child = algorithmState.visibleChildren.get(i); 446 ExpandableViewState childViewState = child.getViewState(); 447 childViewState.hideSensitive = hideSensitive; 448 } 449 } 450 451 /** 452 * Initialize the algorithm state like updating the visible children. 453 */ initAlgorithmState(StackScrollAlgorithmState state, AmbientState ambientState)454 private void initAlgorithmState(StackScrollAlgorithmState state, AmbientState ambientState) { 455 state.scrollY = ambientState.getScrollY(); 456 state.mCurrentYPosition = -state.scrollY; 457 state.mCurrentExpandedYPosition = -state.scrollY; 458 459 //now init the visible children and update paddings 460 int childCount = mHostView.getChildCount(); 461 state.visibleChildren.clear(); 462 state.visibleChildren.ensureCapacity(childCount); 463 int notGoneIndex = 0; 464 boolean emptyShadeVisible = false; 465 for (int i = 0; i < childCount; i++) { 466 ExpandableView v = (ExpandableView) mHostView.getChildAt(i); 467 if (v.getVisibility() != View.GONE) { 468 if (v == ambientState.getShelf()) { 469 continue; 470 } 471 if (v instanceof EmptyShadeView) { 472 emptyShadeVisible = true; 473 } 474 if (v instanceof FooterView footerView) { 475 if (emptyShadeVisible || notGoneIndex == 0) { 476 // if the empty shade is visible or the footer is the first visible 477 // view, we're in a transitory state so let's leave the footer alone. 478 if (Flags.notificationsFooterVisibilityFix() 479 && !SceneContainerFlag.isEnabled()) { 480 // ...except for the hidden state, to prevent it from flashing on 481 // the screen (this piece is copied from updateChild, and is not 482 // necessary in flexiglass). 483 if (footerView.shouldBeHidden() || !ambientState.isShadeExpanded()) { 484 footerView.getViewState().hidden = true; 485 } 486 } 487 continue; 488 } 489 } 490 491 notGoneIndex = updateNotGoneIndex(state, notGoneIndex, v); 492 if (v instanceof ExpandableNotificationRow row) { 493 494 // handle the notGoneIndex for the children as well 495 List<ExpandableNotificationRow> children = row.getAttachedChildren(); 496 if (row.isSummaryWithChildren() && children != null) { 497 for (ExpandableNotificationRow childRow : children) { 498 if (childRow.getVisibility() != View.GONE) { 499 ExpandableViewState childState = childRow.getViewState(); 500 childState.notGoneIndex = notGoneIndex; 501 notGoneIndex++; 502 } 503 } 504 } 505 } 506 } 507 } 508 509 // Save the index of first view in shelf from when shade is fully 510 // expanded. Consider updating these states in updateContentView instead so that we don't 511 // have to recalculate in every frame. 512 float currentY = -ambientState.getScrollY(); 513 // add top padding at the start as long as we're not on the lock screen 514 currentY += getScrimTopPaddingOrZero(ambientState); 515 state.firstViewInShelf = null; 516 for (int i = 0; i < state.visibleChildren.size(); i++) { 517 final ExpandableView view = state.visibleChildren.get(i); 518 519 final boolean applyGapHeight = childNeedsGapHeight( 520 ambientState.getSectionProvider(), i, 521 view, getPreviousView(i, state)); 522 if (applyGapHeight) { 523 currentY += getGapForLocation( 524 ambientState.getFractionToShade(), ambientState.isOnKeyguard()); 525 } 526 527 if (ambientState.getShelf() != null) { 528 final float shelfStart = ambientState.getStackEndHeight() 529 - ambientState.getShelf().getIntrinsicHeight() 530 - mPaddingBetweenElements; 531 if (currentY >= shelfStart 532 && !(view instanceof FooterView) 533 && state.firstViewInShelf == null) { 534 state.firstViewInShelf = view; 535 } 536 } 537 currentY = currentY 538 + getMaxAllowedChildHeight(view) 539 + mPaddingBetweenElements; 540 } 541 } 542 updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, ExpandableView v)543 private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, 544 ExpandableView v) { 545 ExpandableViewState viewState = v.getViewState(); 546 viewState.notGoneIndex = notGoneIndex; 547 state.visibleChildren.add(v); 548 notGoneIndex++; 549 return notGoneIndex; 550 } 551 getPreviousView(int i, StackScrollAlgorithmState algorithmState)552 private ExpandableView getPreviousView(int i, StackScrollAlgorithmState algorithmState) { 553 return i > 0 ? algorithmState.visibleChildren.get(i - 1) : null; 554 } 555 556 /** 557 * Update the position of QS Frame. 558 */ updateQSFrameTop(int qsHeight)559 public void updateQSFrameTop(int qsHeight) { 560 // Intentionally empty for sub-classes in other device form factors to override 561 } 562 563 /** 564 * Determine the positions for the views. This is the main part of the algorithm. 565 * 566 * @param algorithmState The state in which the current pass of the algorithm is currently in 567 * @param ambientState The current ambient state 568 */ updatePositionsForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)569 protected void updatePositionsForState(StackScrollAlgorithmState algorithmState, 570 AmbientState ambientState) { 571 float scrimTopPadding = getScrimTopPaddingOrZero(ambientState); 572 algorithmState.mCurrentYPosition += scrimTopPadding; 573 algorithmState.mCurrentExpandedYPosition += scrimTopPadding; 574 575 int childCount = algorithmState.visibleChildren.size(); 576 for (int i = 0; i < childCount; i++) { 577 updateChild(i, algorithmState, ambientState); 578 } 579 } 580 setLocation(ExpandableViewState expandableViewState, float currentYPosition, int i)581 private void setLocation(ExpandableViewState expandableViewState, float currentYPosition, 582 int i) { 583 expandableViewState.location = ExpandableViewState.LOCATION_MAIN_AREA; 584 if (currentYPosition <= 0) { 585 expandableViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP; 586 } 587 } 588 589 /** 590 * @return Fraction to apply to view height and gap between views. 591 * Does not include shelf height even if shelf is showing. 592 */ getExpansionFractionWithoutShelf( StackScrollAlgorithmState algorithmState, AmbientState ambientState)593 protected float getExpansionFractionWithoutShelf( 594 StackScrollAlgorithmState algorithmState, 595 AmbientState ambientState) { 596 597 final boolean showingShelf = ambientState.getShelf() != null 598 && algorithmState.firstViewInShelf != null; 599 600 final float shelfHeight = showingShelf ? ambientState.getShelf().getIntrinsicHeight() : 0f; 601 final float scrimPadding = getScrimTopPaddingOrZero(ambientState); 602 603 final float stackHeight = 604 ambientState.getInterpolatedStackHeight() - shelfHeight - scrimPadding; 605 final float stackEndHeight = ambientState.getStackEndHeight() - shelfHeight - scrimPadding; 606 if (stackEndHeight == 0f) { 607 // This should not happen, since even when the shade is empty we show EmptyShadeView 608 // but check just in case, so we don't return infinity or NaN. 609 return 0f; 610 } 611 return stackHeight / stackEndHeight; 612 } 613 614 /** 615 * Returns the top scrim padding, or zero if the SceneContainer flag is enabled. 616 */ getScrimTopPaddingOrZero(AmbientState ambientState)617 private float getScrimTopPaddingOrZero(AmbientState ambientState) { 618 if (SceneContainerFlag.isEnabled()) { 619 // the scrim padding is set on the notification placeholder 620 return 0f; 621 } 622 623 boolean shouldUsePadding = 624 !ambientState.isOnKeyguard() 625 || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding()); 626 return shouldUsePadding ? mNotificationScrimPadding : 0f; 627 } 628 hasNonClearableNotifs(StackScrollAlgorithmState algorithmState)629 private boolean hasNonClearableNotifs(StackScrollAlgorithmState algorithmState) { 630 for (int i = 0; i < algorithmState.visibleChildren.size(); i++) { 631 View child = algorithmState.visibleChildren.get(i); 632 if (!(child instanceof ExpandableNotificationRow row)) { 633 continue; 634 } 635 if (!row.canViewBeCleared()) { 636 return true; 637 } 638 } 639 return false; 640 } 641 642 @VisibleForTesting maybeUpdateHeadsUpIsVisible( ExpandableViewState viewState, boolean isShadeExpanded, boolean mustStayOnScreen, boolean topVisible, float viewEnd, float hunMax)643 void maybeUpdateHeadsUpIsVisible( 644 ExpandableViewState viewState, 645 boolean isShadeExpanded, 646 boolean mustStayOnScreen, 647 boolean topVisible, 648 float viewEnd, 649 float hunMax) { 650 if (isShadeExpanded && mustStayOnScreen && topVisible) { 651 viewState.headsUpIsVisible = viewEnd < hunMax; 652 } 653 } 654 655 // TODO(b/172289889) polish shade open from HUN 656 657 /** 658 * Populates the {@link ExpandableViewState} for a single child. 659 * 660 * @param i The index of the child in 661 * {@link StackScrollAlgorithmState#visibleChildren}. 662 * @param algorithmState The overall output state of the algorithm. 663 * @param ambientState The input state provided to the algorithm. 664 */ 665 protected void updateChild( 666 int i, 667 StackScrollAlgorithmState algorithmState, 668 AmbientState ambientState) { 669 670 ExpandableView view = algorithmState.visibleChildren.get(i); 671 ExpandableViewState viewState = view.getViewState(); 672 viewState.location = ExpandableViewState.LOCATION_UNKNOWN; 673 674 float expansionFraction = getExpansionFractionWithoutShelf( 675 algorithmState, ambientState); 676 677 // Add gap between sections. 678 final boolean applyGapHeight = 679 childNeedsGapHeight( 680 ambientState.getSectionProvider(), i, 681 view, getPreviousView(i, algorithmState)); 682 if (applyGapHeight) { 683 final float gap = getGapForLocation( 684 ambientState.getFractionToShade(), ambientState.isOnKeyguard()); 685 algorithmState.mCurrentYPosition += expansionFraction * gap; 686 algorithmState.mCurrentExpandedYPosition += gap; 687 } 688 689 // Must set viewState.yTranslation _before_ use. 690 // Incoming views have yTranslation=0 by default. 691 viewState.setYTranslation(algorithmState.mCurrentYPosition); 692 693 float stackTop = SceneContainerFlag.isEnabled() 694 ? ambientState.getStackTop() 695 : ambientState.getStackY(); 696 float viewEnd = stackTop + viewState.getYTranslation() + viewState.height; 697 maybeUpdateHeadsUpIsVisible(viewState, ambientState.isShadeExpanded(), 698 view.mustStayOnScreen(), 699 // TODO(b/332574413) use the position from the HeadsUpNotificationPlaceholder 700 /* topVisible= */ viewState.getYTranslation() >= mNotificationScrimPadding, 701 viewEnd, /* hunMax */ ambientState.getMaxHeadsUpTranslation() 702 ); 703 if (view instanceof FooterView) { 704 if (SceneContainerFlag.isEnabled()) { 705 final float footerEnd = 706 stackTop + viewState.getYTranslation() + view.getIntrinsicHeight(); 707 final boolean noSpaceForFooter = footerEnd > ambientState.getStackCutoff(); 708 ((FooterView.FooterViewState) viewState).hideContent = 709 noSpaceForFooter || (ambientState.isClearAllInProgress() 710 && !hasNonClearableNotifs(algorithmState)); 711 } else { 712 // TODO(b/333445519): shouldBeHidden should reflect whether the shade is closed 713 // already, so we shouldn't need to use ambientState here. However, 714 // currently it doesn't get updated quickly enough and can cause the footer to 715 // flash when closing the shade. As such, we temporarily also check the 716 // ambientState directly. 717 if (((FooterView) view).shouldBeHidden() || !ambientState.isShadeExpanded()) { 718 viewState.hidden = true; 719 } else { 720 final float footerEnd = algorithmState.mCurrentExpandedYPosition 721 + view.getIntrinsicHeight(); 722 final boolean noSpaceForFooter = 723 footerEnd > ambientState.getStackEndHeight(); 724 ((FooterView.FooterViewState) viewState).hideContent = 725 noSpaceForFooter || (ambientState.isClearAllInProgress() 726 && !hasNonClearableNotifs(algorithmState)); 727 } 728 } 729 } else { 730 if (view instanceof EmptyShadeView) { 731 float fullHeight = SceneContainerFlag.isEnabled() 732 ? ambientState.getStackCutoff() - ambientState.getStackTop() 733 : ambientState.getLayoutMaxHeight() + mMarginBottom 734 - ambientState.getStackY(); 735 viewState.setYTranslation((fullHeight - getMaxAllowedChildHeight(view)) / 2f); 736 } else if (view != ambientState.getTrackedHeadsUpRow()) { 737 if (ambientState.isExpansionChanging()) { 738 // We later update shelf state, then hide views below the shelf. 739 viewState.hidden = false; 740 viewState.inShelf = algorithmState.firstViewInShelf != null 741 && i >= algorithmState.visibleChildren.indexOf( 742 algorithmState.firstViewInShelf); 743 } else if (ambientState.getShelf() != null) { 744 // When pulsing (incoming notification on AOD), innerHeight is 0; clamp all 745 // to shelf start, thereby hiding all notifications (except the first one, which 746 // we later unhide in updatePulsingState) 747 // TODO(b/192348384): merge InnerHeight with StackHeight 748 // Note: Bypass pulse looks different, but when it is not expanding, we need 749 // to use the innerHeight which doesn't update continuously, otherwise we show 750 // more notifications than we should during this special transitional states. 751 boolean bypassPulseNotExpanding = ambientState.isBypassEnabled() 752 && ambientState.isOnKeyguard() && !ambientState.isPulseExpanding(); 753 final float stackBottom = !ambientState.isShadeExpanded() 754 || ambientState.getDozeAmount() == 1f 755 || bypassPulseNotExpanding 756 ? ambientState.getInnerHeight() 757 : ambientState.getInterpolatedStackHeight(); 758 final float shelfStart = stackBottom 759 - ambientState.getShelf().getIntrinsicHeight() 760 - mPaddingBetweenElements; 761 updateViewWithShelf(view, viewState, shelfStart); 762 } 763 } 764 viewState.height = getMaxAllowedChildHeight(view); 765 if (!view.isPinned() && !view.isHeadsUpAnimatingAway() 766 && !ambientState.isPulsingRow(view)) { 767 // The expansion fraction should not affect HUNs or pulsing notifications. 768 viewState.height *= expansionFraction; 769 } 770 } 771 772 algorithmState.mCurrentYPosition += 773 expansionFraction * (getMaxAllowedChildHeight(view) + mPaddingBetweenElements); 774 algorithmState.mCurrentExpandedYPosition += view.getIntrinsicHeight() 775 + mPaddingBetweenElements; 776 777 setLocation(view.getViewState(), algorithmState.mCurrentYPosition, i); 778 viewState.setYTranslation(viewState.getYTranslation() + stackTop); 779 } 780 781 @VisibleForTesting updateViewWithShelf(ExpandableView view, ExpandableViewState viewState, float shelfStart)782 void updateViewWithShelf(ExpandableView view, ExpandableViewState viewState, float shelfStart) { 783 viewState.setYTranslation(Math.min(viewState.getYTranslation(), shelfStart)); 784 if (viewState.getYTranslation() >= shelfStart) { 785 viewState.hidden = !view.isExpandAnimationRunning() 786 && !view.hasExpandingChild(); 787 viewState.inShelf = true; 788 // Notifications in the shelf cannot be visible HUNs. 789 viewState.headsUpIsVisible = false; 790 } 791 } 792 793 /** 794 * Get the gap height needed for before a view 795 * 796 * @param sectionProvider the sectionProvider used to understand the sections 797 * @param visibleIndex the visible index of this view in the list 798 * @param child the child asked about 799 * @param previousChild the child right before it or null if none 800 * @return the size of the gap needed or 0 if none is needed 801 */ getGapHeightForChild( SectionProvider sectionProvider, int visibleIndex, View child, View previousChild, float fractionToShade, boolean onKeyguard)802 public float getGapHeightForChild( 803 SectionProvider sectionProvider, 804 int visibleIndex, 805 View child, 806 View previousChild, 807 float fractionToShade, 808 boolean onKeyguard) { 809 810 if (childNeedsGapHeight(sectionProvider, visibleIndex, child, 811 previousChild)) { 812 return getGapForLocation(fractionToShade, onKeyguard); 813 } else { 814 return 0; 815 } 816 } 817 818 @VisibleForTesting getGapForLocation(float fractionToShade, boolean onKeyguard)819 float getGapForLocation(float fractionToShade, boolean onKeyguard) { 820 if (fractionToShade > 0f) { 821 return MathUtils.lerp(mGapHeightOnLockscreen, mGapHeight, fractionToShade); 822 } 823 if (onKeyguard) { 824 return mGapHeightOnLockscreen; 825 } 826 return mGapHeight; 827 } 828 829 /** 830 * Does a given child need a gap, i.e spacing before a view? 831 * 832 * @param sectionProvider the sectionProvider used to understand the sections 833 * @param visibleIndex the visible index of this view in the list 834 * @param child the child asked about 835 * @param previousChild the child right before it or null if none 836 * @return if the child needs a gap height 837 */ childNeedsGapHeight( SectionProvider sectionProvider, int visibleIndex, View child, View previousChild)838 private boolean childNeedsGapHeight( 839 SectionProvider sectionProvider, 840 int visibleIndex, 841 View child, 842 View previousChild) { 843 return sectionProvider.beginsSection(child, previousChild) 844 && visibleIndex > 0 845 && !(previousChild instanceof SectionHeaderView) 846 && !(child instanceof FooterView); 847 } 848 849 @VisibleForTesting updatePulsingStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)850 void updatePulsingStates(StackScrollAlgorithmState algorithmState, 851 AmbientState ambientState) { 852 int childCount = algorithmState.visibleChildren.size(); 853 ExpandableNotificationRow pulsingRow = null; 854 for (int i = 0; i < childCount; i++) { 855 View child = algorithmState.visibleChildren.get(i); 856 if (!(child instanceof ExpandableNotificationRow row)) { 857 continue; 858 } 859 if (!row.showingPulsing() || (i == 0 && ambientState.isPulseExpanding())) { 860 continue; 861 } 862 ExpandableViewState viewState = row.getViewState(); 863 viewState.hidden = false; 864 pulsingRow = row; 865 } 866 867 // Set AmbientState#pulsingRow to the current pulsing row when on AOD. 868 // Set AmbientState#pulsingRow=null when on lockscreen, since AmbientState#pulsingRow 869 // is only used for skipping the unfurl animation for (the notification that was already 870 // showing at full height on AOD) during the AOD=>lockscreen transition, where 871 // dozeAmount=[1f, 0f). We also need to reset the pulsingRow once it is no longer used 872 // because it will interfere with future unfurling animations - for example, during the 873 // LS=>AOD animation, the pulsingRow may stay at full height when it should squish with the 874 // rest of the stack. 875 if (ambientState.getDozeAmount() == 0.0f || ambientState.getDozeAmount() == 1.0f) { 876 ambientState.setPulsingRow(pulsingRow); 877 } 878 } 879 updateHeadsUpStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)880 private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState, 881 AmbientState ambientState) { 882 int childCount = algorithmState.visibleChildren.size(); 883 884 // Move the tracked heads up into position during the appear animation, by interpolating 885 // between the HUN inset (where it will appear as a HUN) and the end position in the shade 886 float headsUpTranslation = 887 SceneContainerFlag.isEnabled() 888 ? ambientState.getHeadsUpTop() 889 : mHeadsUpInset - ambientState.getStackTopMargin(); 890 ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow(); 891 if (trackedHeadsUpRow != null) { 892 ExpandableViewState childState = trackedHeadsUpRow.getViewState(); 893 if (childState != null) { 894 float endPos = childState.getYTranslation() - ambientState.getStackTranslation(); 895 childState.setYTranslation(MathUtils.lerp( 896 headsUpTranslation, endPos, ambientState.getAppearFraction())); 897 } 898 } 899 900 ExpandableNotificationRow topHeadsUpEntry = null; 901 int cyclingInHunHeight = -1; 902 for (int i = 0; i < childCount; i++) { 903 View child = algorithmState.visibleChildren.get(i); 904 if (!(child instanceof ExpandableNotificationRow row)) { 905 continue; 906 } 907 if (!(row.isHeadsUp() || row.isHeadsUpAnimatingAway())) { 908 continue; 909 } 910 ExpandableViewState childState = row.getViewState(); 911 boolean shouldSetTopHeadsUpEntry; 912 if (SceneContainerFlag.isEnabled()) { 913 shouldSetTopHeadsUpEntry = row.isHeadsUp(); 914 } else { 915 shouldSetTopHeadsUpEntry = row.mustStayOnScreen(); 916 } 917 if (topHeadsUpEntry == null && shouldSetTopHeadsUpEntry 918 && !childState.headsUpIsVisible) { 919 topHeadsUpEntry = row; 920 childState.location = ExpandableViewState.LOCATION_FIRST_HUN; 921 } 922 boolean isTopEntry = topHeadsUpEntry == row; 923 float unmodifiedEndLocation = childState.getYTranslation() + childState.height; 924 if (mIsExpanded) { 925 if (SceneContainerFlag.isEnabled()) { 926 if (shouldHunBeVisibleWhenScrolled(row.isHeadsUp(), 927 childState.headsUpIsVisible, row.showingPulsing(), 928 ambientState.isOnKeyguard(), NotificationBundleUi.isEnabled() 929 ? row.getEntryAdapter().canPeek() 930 : row.getEntryLegacy().isStickyAndNotDemoted())) { 931 // the height of this child before clamping it to the top 932 float unmodifiedChildHeight = childState.height; 933 clampHunToTop( 934 /* headsUpTop = */ headsUpTranslation, 935 /* collapsedHeight = */ row.getCollapsedHeight(), 936 /* viewState = */ childState 937 ); 938 float baseZ = ambientState.getBaseZHeight(); 939 if (headsUpTranslation > ambientState.getStackTop() 940 && row.isAboveShelf()) { 941 // HUN displayed outside of the stack during transition from Gone/LS; 942 // add a shadow that corresponds to the transition progress. 943 float fraction = 1 - ambientState.getExpansionFraction(); 944 childState.setZTranslation(baseZ + fraction * mPinnedZTranslationExtra); 945 } else if (headsUpTranslation < ambientState.getStackTop() 946 && row.isAboveShelf()) { 947 // HUN displayed outside of the stack during transition from QS; 948 // add a shadow that corresponds to the transition progress. 949 float fraction = ambientState.getQsExpansionFraction(); 950 childState.setZTranslation(baseZ + fraction * mPinnedZTranslationExtra); 951 } else if (headsUpTranslation > ambientState.getStackTop()) { 952 // HUN displayed within the stack, add a shadow if it overlaps with 953 // other elements. 954 // 955 // Views stack vertically from the top. Add the HUN's original height 956 // (before clamping) to the stack top, to determine the starting 957 // point for the remaining content. 958 float scrollingContentTop = 959 ambientState.getStackTop() + unmodifiedChildHeight; 960 updateZTranslationForHunInStack( 961 /* scrollingContentTop = */ scrollingContentTop, 962 /* scrollingContentTopPadding = */ mGapHeight, 963 /* baseZ = */ baseZ, 964 /* viewState = */ childState 965 ); 966 } else { 967 childState.setZTranslation(baseZ); 968 } 969 if (isTopEntry && row.isAboveShelf()) { 970 float headsUpBottom = NotificationsHunSharedAnimationValues.isEnabled() 971 ? mHeadsUpAnimator.getHeadsUpAppearHeightBottom() 972 : ambientState.getHeadsUpBottom(); 973 clampHunToMaxTranslation( 974 /* headsUpTop = */ headsUpTranslation, 975 /* headsUpBottom = */ headsUpBottom, 976 /* viewState = */ childState 977 ); 978 updateCornerRoundnessForPinnedHun(row, ambientState.getStackTop()); 979 childState.hidden = false; 980 } 981 } 982 } else { 983 if (shouldHunBeVisibleWhenScrolled(row.mustStayOnScreen(), 984 childState.headsUpIsVisible, row.showingPulsing(), 985 ambientState.isOnKeyguard(), NotificationBundleUi.isEnabled() 986 ? row.getEntryAdapter().canPeek() 987 : row.getEntryLegacy().isStickyAndNotDemoted())) { 988 // Ensure that the heads up is always visible even when scrolled off. 989 // NSSL y starts at top of screen in non-split-shade, but below the qs 990 // offset 991 // in split shade, so we only need to inset by the scrim padding in split 992 // shade. 993 final float clampInset = ambientState.getUseSplitShade() 994 ? mNotificationScrimPadding : mQuickQsOffsetHeight; 995 clampHunToTop(clampInset, ambientState.getStackTranslation(), 996 row.getCollapsedHeight(), childState); 997 if (isTopEntry && row.isAboveShelf()) { 998 // the first hun can't get off screen. 999 clampHunToMaxTranslation(ambientState, row, childState); 1000 updateCornerRoundnessForPinnedHun(row, ambientState.getStackY()); 1001 childState.hidden = false; 1002 } 1003 } 1004 } 1005 } 1006 if (row.isPinned()) { 1007 // Make sure row yTranslation is at at least the HUN yTranslation, 1008 // which accounts for AmbientState.stackTopMargin in split-shade. 1009 // Once we start opening the shade, we keep the previously calculated translation. 1010 childState.setYTranslation( 1011 Math.max(childState.getYTranslation(), headsUpTranslation)); 1012 childState.height = Math.max(row.getIntrinsicHeight(), childState.height); 1013 if (NotificationHeadsUpCycling.isEnabled()) { 1014 if (isCyclingIn(row, ambientState)) { 1015 if (cyclingInHunHeight == -1) { 1016 cyclingInHunHeight = childState.height; 1017 } 1018 } 1019 } 1020 childState.hidden = false; 1021 ExpandableViewState topState = 1022 topHeadsUpEntry == null ? null : topHeadsUpEntry.getViewState(); 1023 if (topState != null && !isTopEntry && (!mIsExpanded 1024 || unmodifiedEndLocation > topState.getYTranslation() + topState.height)) { 1025 // Ensure that a headsUp doesn't vertically extend further than the heads-up at 1026 // the top most z-position 1027 childState.height = row.getIntrinsicHeight(); 1028 } 1029 1030 // heads up notification show and this row is the top entry of heads up 1031 // notifications. i.e. this row should be the only one row that has input field 1032 // To check if the row need to do translation according to scroll Y 1033 // heads up show full of row's content and any scroll y indicate that the 1034 // translationY need to move up the HUN. 1035 if (!mIsExpanded && isTopEntry && ambientState.getScrollY() > 0) { 1036 childState.setYTranslation( 1037 childState.getYTranslation() - ambientState.getScrollY()); 1038 } 1039 } 1040 if (row.isHeadsUpAnimatingAway()) { 1041 if (NotificationHeadsUpCycling.isEnabled() && isCyclingOut(row, ambientState)) { 1042 // If the two HUNs in the cycling animation have different heights, we need 1043 // an extra y translation to align the animation. 1044 int extraTranslation; 1045 if (NotificationHeadsUpCycling.getAnimateTallToShort()) { 1046 if (cyclingInHunHeight > 0) { 1047 extraTranslation = cyclingInHunHeight - childState.height; 1048 } else { 1049 extraTranslation = 0; 1050 } 1051 } else { 1052 extraTranslation = cyclingInHunHeight >= childState.height 1053 ? cyclingInHunHeight - childState.height : 0; 1054 } 1055 extraTranslation += mHeadsUpCyclingPadding; 1056 float inSpaceTranslation = Math.max(childState.getYTranslation(), 1057 headsUpTranslation); 1058 childState.setYTranslation(inSpaceTranslation + extraTranslation); 1059 cyclingInHunHeight = -1; 1060 } else if (!ambientState.isDozing()) { 1061 boolean shouldHunAppearFromBottom = 1062 shouldHunAppearFromBottom(ambientState, childState); 1063 if (NotificationsHunSharedAnimationValues.isEnabled()) { 1064 int yTranslation = 1065 mHeadsUpAnimator.getHeadsUpYTranslation( 1066 shouldHunAppearFromBottom, 1067 row.hasStatusBarChipDuringHeadsUpAnimation()); 1068 childState.setYTranslation(yTranslation); 1069 } else { 1070 if (shouldHunAppearFromBottom) { 1071 // move to the bottom of the screen 1072 childState.setYTranslation( 1073 mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen); 1074 } else { 1075 // move to the top of the screen 1076 childState.setYTranslation(-ambientState.getStackTopMargin() 1077 - mHeadsUpAppearStartAboveScreen); 1078 } 1079 } 1080 } else { 1081 // Make sure row yTranslation is at maximum the HUN yTranslation, 1082 // which accounts for AmbientState.stackTopMargin in split-shade. 1083 childState.setYTranslation( 1084 Math.max(childState.getYTranslation(), headsUpTranslation)); 1085 } 1086 // keep it visible for the animation 1087 childState.hidden = false; 1088 } 1089 } 1090 } 1091 1092 @VisibleForTesting shouldHunBeVisibleWhenScrolled(boolean mustStayOnScreen, boolean headsUpIsVisible, boolean showingPulsing, boolean isOnKeyguard, boolean headsUpOnKeyguard)1093 boolean shouldHunBeVisibleWhenScrolled(boolean mustStayOnScreen, boolean headsUpIsVisible, 1094 boolean showingPulsing, boolean isOnKeyguard, boolean headsUpOnKeyguard) { 1095 return mustStayOnScreen && !headsUpIsVisible 1096 && !showingPulsing 1097 && (!isOnKeyguard || headsUpOnKeyguard); 1098 } 1099 1100 /** 1101 * When shade is open and we are scrolled to the bottom of notifications, 1102 * clamp incoming HUN in its collapsed form, right below qs offset. 1103 * Transition pinned collapsed HUN to full height when scrolling back up. 1104 */ 1105 @VisibleForTesting clampHunToTop(float clampInset, float stackTranslation, float collapsedHeight, ExpandableViewState viewState)1106 void clampHunToTop(float clampInset, float stackTranslation, float collapsedHeight, 1107 ExpandableViewState viewState) { 1108 SceneContainerFlag.assertInLegacyMode(); 1109 clampHunToTop(clampInset + stackTranslation, collapsedHeight, viewState); 1110 } 1111 1112 @VisibleForTesting clampHunToTop(float headsUpTop, float collapsedHeight, ExpandableViewState viewState)1113 void clampHunToTop(float headsUpTop, float collapsedHeight, ExpandableViewState viewState) { 1114 final float newTranslation = Math.max(headsUpTop, viewState.getYTranslation()); 1115 1116 // Transition from collapsed pinned state to fully expanded state 1117 // when the pinned HUN approaches its actual location (when scrolling back to top). 1118 final float distToRealY = newTranslation - viewState.getYTranslation(); 1119 final float availableHeight = viewState.height - distToRealY; 1120 1121 viewState.setYTranslation(newTranslation); 1122 viewState.height = (int) Math.max(availableHeight, collapsedHeight); 1123 } 1124 1125 @VisibleForTesting updateZTranslationForHunInStack(float scrollingContentTop, float scrollingContentTopPadding, float baseZ, ExpandableViewState viewState)1126 void updateZTranslationForHunInStack(float scrollingContentTop, 1127 float scrollingContentTopPadding, float baseZ, ExpandableViewState viewState) { 1128 if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; 1129 float hunBottom = viewState.getYTranslation() + viewState.height; 1130 float overlap = Math.max(0f, hunBottom - scrollingContentTop); 1131 1132 float shadowFraction = 1f; 1133 if (scrollingContentTopPadding > 0f) { 1134 // scrollingContentTopPadding makes a gap between the bottom of the HUN and the top 1135 // of the scrolling content. Use this to animate to the full shadow. 1136 shadowFraction = Math.clamp(overlap / scrollingContentTopPadding, 0f, 1f); 1137 } 1138 1139 if (overlap > 0.0f) { 1140 // add a shadow to this HUN, because it overlaps with the scrolling stack 1141 viewState.setZTranslation(baseZ + shadowFraction * mPinnedZTranslationExtra); 1142 } 1143 } 1144 1145 // Pin HUN to bottom of expanded QS 1146 // while the rest of notifications are scrolled offscreen. clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, ExpandableViewState childState)1147 private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, 1148 ExpandableViewState childState) { 1149 SceneContainerFlag.assertInLegacyMode(); 1150 float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation(); 1151 final float maxShelfPosition = 1152 ambientState.getInnerHeight() 1153 + ambientState.getTopPadding() 1154 + ambientState.getStackTranslation(); 1155 maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition); 1156 1157 final float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight(); 1158 final float newTranslation = Math.min(childState.getYTranslation(), bottomPosition); 1159 childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation 1160 - newTranslation); 1161 childState.setYTranslation(newTranslation); 1162 } 1163 clampHunToMaxTranslation(float headsUpTop, float headsUpBottom, ExpandableViewState viewState)1164 private void clampHunToMaxTranslation(float headsUpTop, float headsUpBottom, 1165 ExpandableViewState viewState) { 1166 if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; 1167 final float maxHeight = Math.max(0f, headsUpBottom - headsUpTop); 1168 viewState.setYTranslation(Math.min(headsUpTop, viewState.getYTranslation())); 1169 viewState.height = (int) Math.min(maxHeight, viewState.height); 1170 } 1171 updateCornerRoundnessForPinnedHun(ExpandableNotificationRow row, float stackTop)1172 private void updateCornerRoundnessForPinnedHun(ExpandableNotificationRow row, float stackTop) { 1173 // Animate pinned HUN bottom corners to and from original roundness. 1174 final float originalCornerRadius = 1175 row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius); 1176 final float bottomValue = computeCornerRoundnessForPinnedHun(mHostView.getHeight(), 1177 stackTop, getMaxAllowedChildHeight(row), originalCornerRadius); 1178 row.requestBottomRoundness(bottomValue, STACK_SCROLL_ALGO); 1179 row.addOnDetachResetRoundness(STACK_SCROLL_ALGO); 1180 } 1181 1182 @VisibleForTesting computeCornerRoundnessForPinnedHun(float hostViewHeight, float stackY, float viewMaxHeight, float originalCornerRadius)1183 float computeCornerRoundnessForPinnedHun(float hostViewHeight, float stackY, 1184 float viewMaxHeight, float originalCornerRadius) { 1185 1186 // Compute y where corner roundness should be in its original unpinned state. 1187 // We use view max height because the pinned collapsed HUN expands to max height 1188 // when it becomes unpinned. 1189 final float originalRoundnessY = hostViewHeight - viewMaxHeight; 1190 1191 final float distToOriginalRoundness = Math.max(0f, stackY - originalRoundnessY); 1192 final float progressToPinnedRoundness = Math.min(1f, 1193 distToOriginalRoundness / viewMaxHeight); 1194 1195 return MathUtils.lerp(originalCornerRadius, 1f, progressToPinnedRoundness); 1196 } 1197 getMaxAllowedChildHeight(View child)1198 protected int getMaxAllowedChildHeight(View child) { 1199 if (child instanceof ExpandableView expandableView) { 1200 return expandableView.getIntrinsicHeight(); 1201 } 1202 return child == null ? mCollapsedSize : child.getHeight(); 1203 } 1204 1205 /** 1206 * Calculate the Z positions for all children based on the number of items in both stacks and 1207 * save it in the resultState 1208 * 1209 * @param algorithmState The state in which the current pass of the algorithm is currently in 1210 * @param ambientState The ambient state of the algorithm 1211 */ updateZValuesForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)1212 private void updateZValuesForState(StackScrollAlgorithmState algorithmState, 1213 AmbientState ambientState) { 1214 int childCount = algorithmState.visibleChildren.size(); 1215 float childrenOnTop = 0.0f; 1216 1217 int topHunIndex = -1; 1218 for (int i = 0; i < childCount; i++) { 1219 ExpandableView child = algorithmState.visibleChildren.get(i); 1220 if (child instanceof ActivatableNotificationView 1221 && (child.isAboveShelf() || child.showingPulsing())) { 1222 topHunIndex = i; 1223 break; 1224 } 1225 } 1226 1227 for (int i = childCount - 1; i >= 0; i--) { 1228 childrenOnTop = updateChildZValue(i, childrenOnTop, 1229 algorithmState, ambientState, i == topHunIndex); 1230 } 1231 } 1232 1233 /** 1234 * Calculate and update the Z positions for a given child. We currently only give shadows to 1235 * HUNs to distinguish a HUN from its surroundings. 1236 * 1237 * @param isTopHun Whether the child is a top HUN. A top HUN means a HUN that shows on the 1238 * vertically top of screen. Top HUNs should have drop shadows 1239 * @param childrenOnTop It is greater than 0 when there's an existing HUN that is elevated 1240 * @return childrenOnTop The decimal part represents the fraction of the elevated HUN's height 1241 * that overlaps with QQS Panel. The integer part represents the count of 1242 * previous HUNs whose Z positions are greater than 0. 1243 */ updateChildZValue(int i, float childrenOnTop, StackScrollAlgorithmState algorithmState, AmbientState ambientState, boolean isTopHun)1244 protected float updateChildZValue(int i, float childrenOnTop, 1245 StackScrollAlgorithmState algorithmState, 1246 AmbientState ambientState, 1247 boolean isTopHun) { 1248 ExpandableView child = algorithmState.visibleChildren.get(i); 1249 ExpandableViewState childViewState = child.getViewState(); 1250 float baseZ = ambientState.getBaseZHeight(); 1251 1252 if (SceneContainerFlag.isEnabled()) { 1253 // SceneContainer simplifies this logic, because: 1254 // - there are no overlapping HUNs anymore, no need for multiplying their shadows 1255 // - shadows for HUNs overlapping with the stack are now set from updateHeadsUpStates 1256 if (child.isPinned() || ambientState.getTrackedHeadsUpRow() == child) { 1257 // set a default elevation on the HUN, which would be overridden 1258 // from updateHeadsUpStates if it is displayed in the shade 1259 childViewState.setZTranslation(baseZ + mPinnedZTranslationExtra); 1260 } else { 1261 // set baseZ for every notification 1262 childViewState.setZTranslation(baseZ); 1263 } 1264 } else { 1265 if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible 1266 && !ambientState.isDozingAndNotPulsing(child) 1267 && childViewState.getYTranslation() < ambientState.getTopPadding() 1268 + ambientState.getStackTranslation()) { 1269 1270 if (childrenOnTop != 0.0f) { 1271 // To elevate the later HUN over previous HUN when multiple HUNs exist 1272 childrenOnTop++; 1273 } else { 1274 // Handles HUN shadow when Shade is opened, and AmbientState.mScrollY > 0 1275 // Calculate the HUN's z-value based on its overlapping fraction with QQS Panel. 1276 // When scrolling down shade to make HUN back to in-position in Notif Panel, 1277 // The overlapping fraction goes to 0, and shadows hides gradually. 1278 float overlap = ambientState.getTopPadding() 1279 + ambientState.getStackTranslation() - childViewState.getYTranslation(); 1280 // To prevent over-shadow during HUN entry 1281 childrenOnTop += Math.min( 1282 1.0f, 1283 overlap / childViewState.height 1284 ); 1285 } 1286 childViewState.setZTranslation(baseZ 1287 + childrenOnTop * mPinnedZTranslationExtra); 1288 } else if (isTopHun) { 1289 // In case this is a new view that has never been measured before, we don't want to 1290 // elevate if we are currently expanded more than the notification 1291 int shelfHeight = ambientState.getShelf() == null ? 0 : 1292 ambientState.getShelf().getIntrinsicHeight(); 1293 float shelfStart = ambientState.getInnerHeight() 1294 - shelfHeight + ambientState.getTopPadding() 1295 + ambientState.getStackTranslation(); 1296 float notificationEnd = 1297 childViewState.getYTranslation() + child.getIntrinsicHeight() 1298 + mPaddingBetweenElements; 1299 if (shelfStart > notificationEnd) { 1300 // When the notification doesn't overlap with Notification Shelf, 1301 // there's no shadow 1302 childViewState.setZTranslation(baseZ); 1303 } else { 1304 // Give shadow to the notification if it overlaps with Notification Shelf 1305 float factor = (notificationEnd - shelfStart) / shelfHeight; 1306 if (Float.isNaN(factor)) { // Avoid problems when the above is 0/0. 1307 factor = 1.0f; 1308 } 1309 factor = Math.min(factor, 1.0f); 1310 childViewState.setZTranslation(baseZ + factor * mPinnedZTranslationExtra); 1311 } 1312 } else { 1313 childViewState.setZTranslation(baseZ); 1314 } 1315 } 1316 1317 // While HUN is showing and Shade is closed: headerVisibleAmount stays 0, shadow stays. 1318 // During HUN-to-Shade (eg. dragging down HUN to open Shade): headerVisibleAmount goes 1319 // gradually from 0 to 1, shadow hides gradually. 1320 // Header visibility is a deprecated concept, we are using headerVisibleAmount only because 1321 // this value nicely goes from 0 to 1 during the HUN-to-Shade process. 1322 1323 childViewState.setZTranslation(childViewState.getZTranslation() 1324 + (1.0f - child.getHeaderVisibleAmount()) * mPinnedZTranslationExtra); 1325 return childrenOnTop; 1326 } 1327 setIsExpanded(boolean isExpanded)1328 public void setIsExpanded(boolean isExpanded) { 1329 this.mIsExpanded = isExpanded; 1330 } 1331 1332 public static class StackScrollAlgorithmState { 1333 1334 /** 1335 * The scroll position of the algorithm (absolute scrolling). 1336 */ 1337 public int scrollY; 1338 1339 /** 1340 * First view in shelf. 1341 */ 1342 public ExpandableView firstViewInShelf; 1343 1344 /** 1345 * The children from the host view which are not gone. 1346 */ 1347 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<>(); 1348 1349 /** 1350 * Y position of the current view during updating children 1351 * with expansion factor applied. 1352 */ 1353 private float mCurrentYPosition; 1354 1355 /** 1356 * Y position of the current view during updating children 1357 * without applying the expansion factor. 1358 */ 1359 private float mCurrentExpandedYPosition; 1360 } 1361 1362 /** 1363 * Interface for telling the SSA when a new notification section begins (so it can add in 1364 * appropriate margins). 1365 */ 1366 public interface SectionProvider { 1367 /** 1368 * True if this view starts a new "section" of notifications, such as the gentle 1369 * notifications section. False if sections are not enabled. 1370 */ 1371 boolean beginsSection(@NonNull View view, @Nullable View previous); 1372 } 1373 1374 /** 1375 * Interface for telling the StackScrollAlgorithm information about the bypass state 1376 */ 1377 public interface BypassController { 1378 /** 1379 * True if bypass is enabled. Note that this is always false if face auth is not enabled. 1380 */ 1381 boolean isBypassEnabled(); 1382 } 1383 } 1384