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