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 androidx.annotation.VisibleForTesting; 28 29 import com.android.internal.policy.SystemBarUtils; 30 import com.android.systemui.R; 31 import com.android.systemui.animation.ShadeInterpolation; 32 import com.android.systemui.statusbar.EmptyShadeView; 33 import com.android.systemui.statusbar.NotificationShelf; 34 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; 35 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 36 import com.android.systemui.statusbar.notification.row.ExpandableView; 37 import com.android.systemui.statusbar.notification.row.FooterView; 38 39 import java.util.ArrayList; 40 import java.util.List; 41 42 /** 43 * The Algorithm of the 44 * {@link com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout} which can 45 * be queried for {@link StackScrollAlgorithmState} 46 */ 47 public class StackScrollAlgorithm { 48 49 public static final float START_FRACTION = 0.3f; 50 51 private static final String LOG_TAG = "StackScrollAlgorithm"; 52 private final ViewGroup mHostView; 53 54 private int mPaddingBetweenElements; 55 private int mGapHeight; 56 private int mCollapsedSize; 57 58 private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); 59 private boolean mIsExpanded; 60 private boolean mClipNotificationScrollToTop; 61 @VisibleForTesting float mHeadsUpInset; 62 private int mPinnedZTranslationExtra; 63 private float mNotificationScrimPadding; 64 private int mCloseHandleUnderlapHeight; 65 StackScrollAlgorithm( Context context, ViewGroup hostView)66 public StackScrollAlgorithm( 67 Context context, 68 ViewGroup hostView) { 69 mHostView = hostView; 70 initView(context); 71 } 72 initView(Context context)73 public void initView(Context context) { 74 initConstants(context); 75 } 76 initConstants(Context context)77 private void initConstants(Context context) { 78 Resources res = context.getResources(); 79 mPaddingBetweenElements = res.getDimensionPixelSize( 80 R.dimen.notification_divider_height); 81 mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height); 82 mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop); 83 int statusBarHeight = SystemBarUtils.getStatusBarHeight(context); 84 mHeadsUpInset = statusBarHeight + res.getDimensionPixelSize( 85 R.dimen.heads_up_status_bar_padding); 86 mPinnedZTranslationExtra = res.getDimensionPixelSize( 87 R.dimen.heads_up_pinned_elevation); 88 mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height); 89 mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings); 90 mCloseHandleUnderlapHeight = res.getDimensionPixelSize(R.dimen.close_handle_underlap); 91 } 92 93 /** 94 * Updates the state of all children in the hostview based on this algorithm. 95 */ resetViewStates(AmbientState ambientState, int speedBumpIndex)96 public void resetViewStates(AmbientState ambientState, int speedBumpIndex) { 97 // The state of the local variables are saved in an algorithmState to easily subdivide it 98 // into multiple phases. 99 StackScrollAlgorithmState algorithmState = mTempAlgorithmState; 100 101 // First we reset the view states to their default values. 102 resetChildViewStates(); 103 initAlgorithmState(algorithmState, ambientState); 104 updatePositionsForState(algorithmState, ambientState); 105 updateZValuesForState(algorithmState, ambientState); 106 updateHeadsUpStates(algorithmState, ambientState); 107 updatePulsingStates(algorithmState, ambientState); 108 109 updateDimmedActivatedHideSensitive(ambientState, algorithmState); 110 updateClipping(algorithmState, ambientState); 111 updateSpeedBumpState(algorithmState, speedBumpIndex); 112 updateShelfState(algorithmState, ambientState); 113 getNotificationChildrenStates(algorithmState, ambientState); 114 } 115 116 /** 117 * How expanded or collapsed notifications are when pulling down the shade. 118 * @param ambientState Current ambient state. 119 * @return 0 when fully collapsed, 1 when expanded. 120 */ getNotificationSquishinessFraction(AmbientState ambientState)121 public float getNotificationSquishinessFraction(AmbientState ambientState) { 122 return getExpansionFractionWithoutShelf(mTempAlgorithmState, ambientState); 123 } 124 resetChildViewStates()125 private void resetChildViewStates() { 126 int numChildren = mHostView.getChildCount(); 127 for (int i = 0; i < numChildren; i++) { 128 ExpandableView child = (ExpandableView) mHostView.getChildAt(i); 129 child.resetViewState(); 130 } 131 } 132 getNotificationChildrenStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)133 private void getNotificationChildrenStates(StackScrollAlgorithmState algorithmState, 134 AmbientState ambientState) { 135 int childCount = algorithmState.visibleChildren.size(); 136 for (int i = 0; i < childCount; i++) { 137 ExpandableView v = algorithmState.visibleChildren.get(i); 138 if (v instanceof ExpandableNotificationRow) { 139 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 140 row.updateChildrenStates(ambientState); 141 } 142 } 143 } 144 updateSpeedBumpState(StackScrollAlgorithmState algorithmState, int speedBumpIndex)145 private void updateSpeedBumpState(StackScrollAlgorithmState algorithmState, 146 int speedBumpIndex) { 147 int childCount = algorithmState.visibleChildren.size(); 148 int belowSpeedBump = speedBumpIndex; 149 for (int i = 0; i < childCount; i++) { 150 ExpandableView child = algorithmState.visibleChildren.get(i); 151 ExpandableViewState childViewState = child.getViewState(); 152 153 // The speed bump can also be gone, so equality needs to be taken when comparing 154 // indices. 155 childViewState.belowSpeedBump = i >= belowSpeedBump; 156 } 157 158 } 159 updateShelfState( StackScrollAlgorithmState algorithmState, AmbientState ambientState)160 private void updateShelfState( 161 StackScrollAlgorithmState algorithmState, 162 AmbientState ambientState) { 163 164 NotificationShelf shelf = ambientState.getShelf(); 165 if (shelf == null) { 166 return; 167 } 168 169 shelf.updateState(algorithmState, ambientState); 170 171 // After the shelf has updated its yTranslation, explicitly set alpha=0 for view below shelf 172 // to skip rendering them in the hardware layer. We do not set them invisible because that 173 // runs invalidate & onDraw when these views return onscreen, which is more expensive. 174 if (shelf.getViewState().hidden) { 175 // When the shelf is hidden, it won't clip views, so we don't hide rows 176 return; 177 } 178 final float shelfTop = shelf.getViewState().yTranslation; 179 180 for (ExpandableView view : algorithmState.visibleChildren) { 181 final float viewTop = view.getViewState().yTranslation; 182 if (viewTop >= shelfTop) { 183 view.getViewState().alpha = 0; 184 } 185 } 186 } 187 updateClipping(StackScrollAlgorithmState algorithmState, AmbientState ambientState)188 private void updateClipping(StackScrollAlgorithmState algorithmState, 189 AmbientState ambientState) { 190 float drawStart = ambientState.isOnKeyguard() ? 0 191 : ambientState.getStackY() - ambientState.getScrollY(); 192 float clipStart = 0; 193 int childCount = algorithmState.visibleChildren.size(); 194 boolean firstHeadsUp = true; 195 for (int i = 0; i < childCount; i++) { 196 ExpandableView child = algorithmState.visibleChildren.get(i); 197 ExpandableViewState state = child.getViewState(); 198 if (!child.mustStayOnScreen() || state.headsUpIsVisible) { 199 clipStart = Math.max(drawStart, clipStart); 200 } 201 float newYTranslation = state.yTranslation; 202 float newHeight = state.height; 203 float newNotificationEnd = newYTranslation + newHeight; 204 boolean isHeadsUp = (child instanceof ExpandableNotificationRow) && child.isPinned(); 205 if (mClipNotificationScrollToTop 206 && (!state.inShelf || (isHeadsUp && !firstHeadsUp)) 207 && newYTranslation < clipStart 208 && !ambientState.isShadeOpening()) { 209 // The previous view is overlapping on top, clip! 210 float overlapAmount = clipStart - newYTranslation; 211 state.clipTopAmount = (int) overlapAmount; 212 } else { 213 state.clipTopAmount = 0; 214 } 215 if (isHeadsUp) { 216 firstHeadsUp = false; 217 } 218 if (!child.isTransparent()) { 219 // Only update the previous values if we are not transparent, 220 // otherwise we would clip to a transparent view. 221 clipStart = Math.max(clipStart, isHeadsUp ? newYTranslation : newNotificationEnd); 222 } 223 } 224 } 225 226 /** 227 * Updates the dimmed, activated and hiding sensitive states of the children. 228 */ updateDimmedActivatedHideSensitive(AmbientState ambientState, StackScrollAlgorithmState algorithmState)229 private void updateDimmedActivatedHideSensitive(AmbientState ambientState, 230 StackScrollAlgorithmState algorithmState) { 231 boolean dimmed = ambientState.isDimmed(); 232 boolean hideSensitive = ambientState.isHideSensitive(); 233 View activatedChild = ambientState.getActivatedChild(); 234 int childCount = algorithmState.visibleChildren.size(); 235 for (int i = 0; i < childCount; i++) { 236 ExpandableView child = algorithmState.visibleChildren.get(i); 237 ExpandableViewState childViewState = child.getViewState(); 238 childViewState.dimmed = dimmed; 239 childViewState.hideSensitive = hideSensitive; 240 boolean isActivatedChild = activatedChild == child; 241 if (dimmed && isActivatedChild) { 242 childViewState.zTranslation += 2.0f * ambientState.getZDistanceBetweenElements(); 243 } 244 } 245 } 246 247 /** 248 * Initialize the algorithm state like updating the visible children. 249 */ initAlgorithmState(StackScrollAlgorithmState state, AmbientState ambientState)250 private void initAlgorithmState(StackScrollAlgorithmState state, AmbientState ambientState) { 251 state.scrollY = ambientState.getScrollY(); 252 state.mCurrentYPosition = -state.scrollY; 253 state.mCurrentExpandedYPosition = -state.scrollY; 254 255 //now init the visible children and update paddings 256 int childCount = mHostView.getChildCount(); 257 state.visibleChildren.clear(); 258 state.visibleChildren.ensureCapacity(childCount); 259 int notGoneIndex = 0; 260 for (int i = 0; i < childCount; i++) { 261 ExpandableView v = (ExpandableView) mHostView.getChildAt(i); 262 if (v.getVisibility() != View.GONE) { 263 if (v == ambientState.getShelf()) { 264 continue; 265 } 266 notGoneIndex = updateNotGoneIndex(state, notGoneIndex, v); 267 if (v instanceof ExpandableNotificationRow) { 268 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 269 270 // handle the notGoneIndex for the children as well 271 List<ExpandableNotificationRow> children = row.getAttachedChildren(); 272 if (row.isSummaryWithChildren() && children != null) { 273 for (ExpandableNotificationRow childRow : children) { 274 if (childRow.getVisibility() != View.GONE) { 275 ExpandableViewState childState = childRow.getViewState(); 276 childState.notGoneIndex = notGoneIndex; 277 notGoneIndex++; 278 } 279 } 280 } 281 } 282 } 283 } 284 285 // Save the index of first view in shelf from when shade is fully 286 // expanded. Consider updating these states in updateContentView instead so that we don't 287 // have to recalculate in every frame. 288 float currentY = -ambientState.getScrollY(); 289 if (!ambientState.isOnKeyguard() 290 || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) { 291 // add top padding at the start as long as we're not on the lock screen 292 currentY += mNotificationScrimPadding; 293 } 294 state.firstViewInShelf = null; 295 for (int i = 0; i < state.visibleChildren.size(); i++) { 296 final ExpandableView view = state.visibleChildren.get(i); 297 298 final boolean applyGapHeight = childNeedsGapHeight( 299 ambientState.getSectionProvider(), i, 300 view, getPreviousView(i, state)); 301 if (applyGapHeight) { 302 currentY += mGapHeight; 303 } 304 305 if (ambientState.getShelf() != null) { 306 final float shelfStart = ambientState.getStackEndHeight() 307 - ambientState.getShelf().getIntrinsicHeight(); 308 if (currentY >= shelfStart 309 && !(view instanceof FooterView) 310 && state.firstViewInShelf == null) { 311 state.firstViewInShelf = view; 312 } 313 } 314 currentY = currentY 315 + getMaxAllowedChildHeight(view) 316 + mPaddingBetweenElements; 317 } 318 } 319 updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, ExpandableView v)320 private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, 321 ExpandableView v) { 322 ExpandableViewState viewState = v.getViewState(); 323 viewState.notGoneIndex = notGoneIndex; 324 state.visibleChildren.add(v); 325 notGoneIndex++; 326 return notGoneIndex; 327 } 328 getPreviousView(int i, StackScrollAlgorithmState algorithmState)329 private ExpandableView getPreviousView(int i, StackScrollAlgorithmState algorithmState) { 330 return i > 0 ? algorithmState.visibleChildren.get(i - 1) : null; 331 } 332 333 /** 334 * Determine the positions for the views. This is the main part of the algorithm. 335 * 336 * @param algorithmState The state in which the current pass of the algorithm is currently in 337 * @param ambientState The current ambient state 338 */ updatePositionsForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)339 private void updatePositionsForState(StackScrollAlgorithmState algorithmState, 340 AmbientState ambientState) { 341 if (!ambientState.isOnKeyguard() 342 || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) { 343 algorithmState.mCurrentYPosition += mNotificationScrimPadding; 344 algorithmState.mCurrentExpandedYPosition += mNotificationScrimPadding; 345 } 346 347 int childCount = algorithmState.visibleChildren.size(); 348 for (int i = 0; i < childCount; i++) { 349 updateChild(i, algorithmState, ambientState); 350 } 351 } 352 setLocation(ExpandableViewState expandableViewState, float currentYPosition, int i)353 private void setLocation(ExpandableViewState expandableViewState, float currentYPosition, 354 int i) { 355 expandableViewState.location = ExpandableViewState.LOCATION_MAIN_AREA; 356 if (currentYPosition <= 0) { 357 expandableViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP; 358 } 359 } 360 361 /** 362 * @return Fraction to apply to view height and gap between views. 363 * Does not include shelf height even if shelf is showing. 364 */ getExpansionFractionWithoutShelf( StackScrollAlgorithmState algorithmState, AmbientState ambientState)365 private float getExpansionFractionWithoutShelf( 366 StackScrollAlgorithmState algorithmState, 367 AmbientState ambientState) { 368 369 final boolean showingShelf = ambientState.getShelf() != null 370 && algorithmState.firstViewInShelf != null; 371 372 final float shelfHeight = showingShelf ? ambientState.getShelf().getIntrinsicHeight() : 0f; 373 final float scrimPadding = ambientState.isOnKeyguard() 374 && (!ambientState.isBypassEnabled() || !ambientState.isPulseExpanding()) 375 ? 0 : mNotificationScrimPadding; 376 377 final float stackHeight = ambientState.getStackHeight() - shelfHeight - scrimPadding; 378 final float stackEndHeight = ambientState.getStackEndHeight() - shelfHeight - scrimPadding; 379 if (stackEndHeight == 0f) { 380 // This should not happen, since even when the shade is empty we show EmptyShadeView 381 // but check just in case, so we don't return infinity or NaN. 382 return 0f; 383 } 384 return stackHeight / stackEndHeight; 385 } 386 hasOngoingNotifs(StackScrollAlgorithmState algorithmState)387 public boolean hasOngoingNotifs(StackScrollAlgorithmState algorithmState) { 388 for (int i = 0; i < algorithmState.visibleChildren.size(); i++) { 389 View child = algorithmState.visibleChildren.get(i); 390 if (!(child instanceof ExpandableNotificationRow)) { 391 continue; 392 } 393 final ExpandableNotificationRow row = (ExpandableNotificationRow) child; 394 if (!row.canViewBeDismissed()) { 395 return true; 396 } 397 } 398 return false; 399 } 400 401 // TODO(b/172289889) polish shade open from HUN 402 /** 403 * Populates the {@link ExpandableViewState} for a single child. 404 * 405 * @param i The index of the child in 406 * {@link StackScrollAlgorithmState#visibleChildren}. 407 * @param algorithmState The overall output state of the algorithm. 408 * @param ambientState The input state provided to the algorithm. 409 */ updateChild( int i, StackScrollAlgorithmState algorithmState, AmbientState ambientState)410 protected void updateChild( 411 int i, 412 StackScrollAlgorithmState algorithmState, 413 AmbientState ambientState) { 414 415 ExpandableView view = algorithmState.visibleChildren.get(i); 416 ExpandableViewState viewState = view.getViewState(); 417 viewState.location = ExpandableViewState.LOCATION_UNKNOWN; 418 419 final boolean isHunGoingToShade = ambientState.isShadeExpanded() 420 && view == ambientState.getTrackedHeadsUpRow(); 421 if (isHunGoingToShade) { 422 // Keep 100% opacity for heads up notification going to shade. 423 } else if (ambientState.isOnKeyguard()) { 424 // Adjust alpha for wakeup to lockscreen. 425 viewState.alpha = 1f - ambientState.getHideAmount(); 426 } else if (ambientState.isExpansionChanging()) { 427 // Adjust alpha for shade open & close. 428 float expansion = ambientState.getExpansionFraction(); 429 viewState.alpha = ShadeInterpolation.getContentAlpha(expansion); 430 } 431 432 if (ambientState.isShadeExpanded() && view.mustStayOnScreen() 433 && viewState.yTranslation >= 0) { 434 // Even if we're not scrolled away we're in view and we're also not in the 435 // shelf. We can relax the constraints and let us scroll off the top! 436 float end = viewState.yTranslation + viewState.height + ambientState.getStackY(); 437 viewState.headsUpIsVisible = end < ambientState.getMaxHeadsUpTranslation(); 438 } 439 440 final float expansionFraction = getExpansionFractionWithoutShelf( 441 algorithmState, ambientState); 442 443 // Add gap between sections. 444 final boolean applyGapHeight = 445 childNeedsGapHeight( 446 ambientState.getSectionProvider(), i, 447 view, getPreviousView(i, algorithmState)); 448 if (applyGapHeight) { 449 algorithmState.mCurrentYPosition += expansionFraction * mGapHeight; 450 algorithmState.mCurrentExpandedYPosition += mGapHeight; 451 } 452 453 viewState.yTranslation = algorithmState.mCurrentYPosition; 454 455 if (view instanceof FooterView) { 456 final boolean shadeClosed = !ambientState.isShadeExpanded(); 457 final boolean isShelfShowing = algorithmState.firstViewInShelf != null; 458 if (shadeClosed) { 459 viewState.hidden = true; 460 } else { 461 final float footerEnd = algorithmState.mCurrentExpandedYPosition 462 + view.getIntrinsicHeight(); 463 final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight(); 464 ((FooterView.FooterViewState) viewState).hideContent = 465 isShelfShowing || noSpaceForFooter 466 || (ambientState.isDismissAllInProgress() 467 && !hasOngoingNotifs(algorithmState)); 468 } 469 } else { 470 if (view instanceof EmptyShadeView) { 471 float fullHeight = ambientState.getLayoutMaxHeight() + mCloseHandleUnderlapHeight 472 - ambientState.getStackY(); 473 viewState.yTranslation = (fullHeight - getMaxAllowedChildHeight(view)) / 2f; 474 } else if (view != ambientState.getTrackedHeadsUpRow()) { 475 if (ambientState.isExpansionChanging()) { 476 // We later update shelf state, then hide views below the shelf. 477 viewState.hidden = false; 478 viewState.inShelf = algorithmState.firstViewInShelf != null 479 && i >= algorithmState.visibleChildren.indexOf( 480 algorithmState.firstViewInShelf); 481 } else if (ambientState.getShelf() != null) { 482 // When pulsing (incoming notification on AOD), innerHeight is 0; clamp all 483 // to shelf start, thereby hiding all notifications (except the first one, which 484 // we later unhide in updatePulsingState) 485 // TODO(b/192348384): merge InnerHeight with StackHeight 486 // Note: Bypass pulse looks different, but when it is not expanding, we need 487 // to use the innerHeight which doesn't update continuously, otherwise we show 488 // more notifications than we should during this special transitional states. 489 boolean bypassPulseNotExpanding = ambientState.isBypassEnabled() 490 && ambientState.isOnKeyguard() && !ambientState.isPulseExpanding(); 491 final int stackBottom = 492 !ambientState.isShadeExpanded() || ambientState.isDozing() 493 || bypassPulseNotExpanding 494 ? ambientState.getInnerHeight() 495 : (int) ambientState.getStackHeight(); 496 final int shelfStart = 497 stackBottom - ambientState.getShelf().getIntrinsicHeight(); 498 viewState.yTranslation = Math.min(viewState.yTranslation, shelfStart); 499 if (viewState.yTranslation >= shelfStart) { 500 viewState.hidden = !view.isExpandAnimationRunning() 501 && !view.hasExpandingChild(); 502 viewState.inShelf = true; 503 // Notifications in the shelf cannot be visible HUNs. 504 viewState.headsUpIsVisible = false; 505 } 506 } 507 } 508 // Clip height of view right before shelf. 509 viewState.height = (int) (getMaxAllowedChildHeight(view) * expansionFraction); 510 } 511 512 algorithmState.mCurrentYPosition += viewState.height 513 + expansionFraction * mPaddingBetweenElements; 514 algorithmState.mCurrentExpandedYPosition += view.getIntrinsicHeight() 515 + mPaddingBetweenElements; 516 517 setLocation(view.getViewState(), algorithmState.mCurrentYPosition, i); 518 viewState.yTranslation += ambientState.getStackY(); 519 } 520 521 /** 522 * Get the gap height needed for before a view 523 * 524 * @param sectionProvider the sectionProvider used to understand the sections 525 * @param visibleIndex the visible index of this view in the list 526 * @param child the child asked about 527 * @param previousChild the child right before it or null if none 528 * @return the size of the gap needed or 0 if none is needed 529 */ getGapHeightForChild( SectionProvider sectionProvider, int visibleIndex, View child, View previousChild)530 public float getGapHeightForChild( 531 SectionProvider sectionProvider, 532 int visibleIndex, 533 View child, 534 View previousChild) { 535 536 if (childNeedsGapHeight(sectionProvider, visibleIndex, child, 537 previousChild)) { 538 return mGapHeight; 539 } else { 540 return 0; 541 } 542 } 543 544 /** 545 * Does a given child need a gap, i.e spacing before a view? 546 * 547 * @param sectionProvider the sectionProvider used to understand the sections 548 * @param visibleIndex the visible index of this view in the list 549 * @param child the child asked about 550 * @param previousChild the child right before it or null if none 551 * @return if the child needs a gap height 552 */ childNeedsGapHeight( SectionProvider sectionProvider, int visibleIndex, View child, View previousChild)553 private boolean childNeedsGapHeight( 554 SectionProvider sectionProvider, 555 int visibleIndex, 556 View child, 557 View previousChild) { 558 return sectionProvider.beginsSection(child, previousChild) 559 && visibleIndex > 0 560 && !(previousChild instanceof SectionHeaderView) 561 && !(child instanceof FooterView); 562 } 563 updatePulsingStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)564 private void updatePulsingStates(StackScrollAlgorithmState algorithmState, 565 AmbientState ambientState) { 566 int childCount = algorithmState.visibleChildren.size(); 567 for (int i = 0; i < childCount; i++) { 568 View child = algorithmState.visibleChildren.get(i); 569 if (!(child instanceof ExpandableNotificationRow)) { 570 continue; 571 } 572 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 573 if (!row.showingPulsing() || (i == 0 && ambientState.isPulseExpanding())) { 574 continue; 575 } 576 ExpandableViewState viewState = row.getViewState(); 577 viewState.hidden = false; 578 } 579 } 580 updateHeadsUpStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)581 private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState, 582 AmbientState ambientState) { 583 int childCount = algorithmState.visibleChildren.size(); 584 585 // Move the tracked heads up into position during the appear animation, by interpolating 586 // between the HUN inset (where it will appear as a HUN) and the end position in the shade 587 float headsUpTranslation = mHeadsUpInset - ambientState.getStackTopMargin(); 588 ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow(); 589 if (trackedHeadsUpRow != null) { 590 ExpandableViewState childState = trackedHeadsUpRow.getViewState(); 591 if (childState != null) { 592 float endPosition = childState.yTranslation - ambientState.getStackTranslation(); 593 childState.yTranslation = MathUtils.lerp( 594 headsUpTranslation, endPosition, ambientState.getAppearFraction()); 595 } 596 } 597 598 ExpandableNotificationRow topHeadsUpEntry = null; 599 for (int i = 0; i < childCount; i++) { 600 View child = algorithmState.visibleChildren.get(i); 601 if (!(child instanceof ExpandableNotificationRow)) { 602 continue; 603 } 604 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 605 if (!(row.isHeadsUp() || row.isHeadsUpAnimatingAway())) { 606 continue; 607 } 608 ExpandableViewState childState = row.getViewState(); 609 if (topHeadsUpEntry == null && row.mustStayOnScreen() && !childState.headsUpIsVisible) { 610 topHeadsUpEntry = row; 611 childState.location = ExpandableViewState.LOCATION_FIRST_HUN; 612 } 613 boolean isTopEntry = topHeadsUpEntry == row; 614 float unmodifiedEndLocation = childState.yTranslation + childState.height; 615 if (mIsExpanded) { 616 if (row.mustStayOnScreen() && !childState.headsUpIsVisible 617 && !row.showingPulsing()) { 618 // Ensure that the heads up is always visible even when scrolled off 619 clampHunToTop(ambientState, row, childState); 620 if (isTopEntry && row.isAboveShelf()) { 621 // the first hun can't get off screen. 622 clampHunToMaxTranslation(ambientState, row, childState); 623 childState.hidden = false; 624 } 625 } 626 } 627 if (row.isPinned()) { 628 childState.yTranslation = Math.max(childState.yTranslation, headsUpTranslation); 629 childState.height = Math.max(row.getIntrinsicHeight(), childState.height); 630 childState.hidden = false; 631 ExpandableViewState topState = 632 topHeadsUpEntry == null ? null : topHeadsUpEntry.getViewState(); 633 if (topState != null && !isTopEntry && (!mIsExpanded 634 || unmodifiedEndLocation > topState.yTranslation + topState.height)) { 635 // Ensure that a headsUp doesn't vertically extend further than the heads-up at 636 // the top most z-position 637 childState.height = row.getIntrinsicHeight(); 638 childState.yTranslation = Math.min(topState.yTranslation + topState.height 639 - childState.height, childState.yTranslation); 640 } 641 642 // heads up notification show and this row is the top entry of heads up 643 // notifications. i.e. this row should be the only one row that has input field 644 // To check if the row need to do translation according to scroll Y 645 // heads up show full of row's content and any scroll y indicate that the 646 // translationY need to move up the HUN. 647 if (!mIsExpanded && isTopEntry && ambientState.getScrollY() > 0) { 648 childState.yTranslation -= ambientState.getScrollY(); 649 } 650 } 651 if (row.isHeadsUpAnimatingAway()) { 652 childState.yTranslation = Math.max(childState.yTranslation, mHeadsUpInset); 653 childState.hidden = false; 654 } 655 } 656 } 657 clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row, ExpandableViewState childState)658 private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row, 659 ExpandableViewState childState) { 660 float newTranslation = Math.max(ambientState.getTopPadding() 661 + ambientState.getStackTranslation(), childState.yTranslation); 662 childState.height = (int) Math.max(childState.height - (newTranslation 663 - childState.yTranslation), row.getCollapsedHeight()); 664 childState.yTranslation = newTranslation; 665 } 666 clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, ExpandableViewState childState)667 private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, 668 ExpandableViewState childState) { 669 float newTranslation; 670 float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation(); 671 float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding() 672 + ambientState.getStackTranslation(); 673 maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition); 674 float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight(); 675 newTranslation = Math.min(childState.yTranslation, bottomPosition); 676 childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation 677 - newTranslation); 678 childState.yTranslation = newTranslation; 679 } 680 getMaxAllowedChildHeight(View child)681 protected int getMaxAllowedChildHeight(View child) { 682 if (child instanceof ExpandableView) { 683 ExpandableView expandableView = (ExpandableView) child; 684 return expandableView.getIntrinsicHeight(); 685 } 686 return child == null ? mCollapsedSize : child.getHeight(); 687 } 688 689 /** 690 * Calculate the Z positions for all children based on the number of items in both stacks and 691 * save it in the resultState 692 * 693 * @param algorithmState The state in which the current pass of the algorithm is currently in 694 * @param ambientState The ambient state of the algorithm 695 */ updateZValuesForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)696 private void updateZValuesForState(StackScrollAlgorithmState algorithmState, 697 AmbientState ambientState) { 698 int childCount = algorithmState.visibleChildren.size(); 699 float childrenOnTop = 0.0f; 700 701 int topHunIndex = -1; 702 for (int i = 0; i < childCount; i++) { 703 ExpandableView child = algorithmState.visibleChildren.get(i); 704 if (child instanceof ActivatableNotificationView 705 && (child.isAboveShelf() || child.showingPulsing())) { 706 topHunIndex = i; 707 break; 708 } 709 } 710 711 for (int i = childCount - 1; i >= 0; i--) { 712 childrenOnTop = updateChildZValue(i, childrenOnTop, 713 algorithmState, ambientState, i == topHunIndex); 714 } 715 } 716 updateChildZValue(int i, float childrenOnTop, StackScrollAlgorithmState algorithmState, AmbientState ambientState, boolean shouldElevateHun)717 protected float updateChildZValue(int i, float childrenOnTop, 718 StackScrollAlgorithmState algorithmState, 719 AmbientState ambientState, 720 boolean shouldElevateHun) { 721 ExpandableView child = algorithmState.visibleChildren.get(i); 722 ExpandableViewState childViewState = child.getViewState(); 723 int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements(); 724 float baseZ = ambientState.getBaseZHeight(); 725 if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible 726 && !ambientState.isDozingAndNotPulsing(child) 727 && childViewState.yTranslation < ambientState.getTopPadding() 728 + ambientState.getStackTranslation()) { 729 if (childrenOnTop != 0.0f) { 730 childrenOnTop++; 731 } else { 732 float overlap = ambientState.getTopPadding() 733 + ambientState.getStackTranslation() - childViewState.yTranslation; 734 childrenOnTop += Math.min(1.0f, overlap / childViewState.height); 735 } 736 childViewState.zTranslation = baseZ 737 + childrenOnTop * zDistanceBetweenElements; 738 } else if (shouldElevateHun) { 739 // In case this is a new view that has never been measured before, we don't want to 740 // elevate if we are currently expanded more then the notification 741 int shelfHeight = ambientState.getShelf() == null ? 0 : 742 ambientState.getShelf().getIntrinsicHeight(); 743 float shelfStart = ambientState.getInnerHeight() 744 - shelfHeight + ambientState.getTopPadding() 745 + ambientState.getStackTranslation(); 746 float notificationEnd = childViewState.yTranslation + child.getIntrinsicHeight() 747 + mPaddingBetweenElements; 748 if (shelfStart > notificationEnd) { 749 childViewState.zTranslation = baseZ; 750 } else { 751 float factor = (notificationEnd - shelfStart) / shelfHeight; 752 factor = Math.min(factor, 1.0f); 753 childViewState.zTranslation = baseZ + factor * zDistanceBetweenElements; 754 } 755 } else { 756 childViewState.zTranslation = baseZ; 757 } 758 759 // We need to scrim the notification more from its surrounding content when we are pinned, 760 // and we therefore elevate it higher. 761 // We can use the headerVisibleAmount for this, since the value nicely goes from 0 to 1 when 762 // expanding after which we have a normal elevation again. 763 childViewState.zTranslation += (1.0f - child.getHeaderVisibleAmount()) 764 * mPinnedZTranslationExtra; 765 return childrenOnTop; 766 } 767 setIsExpanded(boolean isExpanded)768 public void setIsExpanded(boolean isExpanded) { 769 this.mIsExpanded = isExpanded; 770 } 771 772 public static class StackScrollAlgorithmState { 773 774 /** 775 * The scroll position of the algorithm (absolute scrolling). 776 */ 777 public int scrollY; 778 779 /** 780 * First view in shelf. 781 */ 782 public ExpandableView firstViewInShelf; 783 784 /** 785 * The children from the host view which are not gone. 786 */ 787 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<>(); 788 789 /** 790 * Y position of the current view during updating children 791 * with expansion factor applied. 792 */ 793 private int mCurrentYPosition; 794 795 /** 796 * Y position of the current view during updating children 797 * without applying the expansion factor. 798 */ 799 private int mCurrentExpandedYPosition; 800 } 801 802 /** 803 * Interface for telling the SSA when a new notification section begins (so it can add in 804 * appropriate margins). 805 */ 806 public interface SectionProvider { 807 /** 808 * True if this view starts a new "section" of notifications, such as the gentle 809 * notifications section. False if sections are not enabled. 810 */ beginsSection(@onNull View view, @Nullable View previous)811 boolean beginsSection(@NonNull View view, @Nullable View previous); 812 } 813 814 /** 815 * Interface for telling the StackScrollAlgorithm information about the bypass state 816 */ 817 public interface BypassController { 818 /** 819 * True if bypass is enabled. Note that this is always false if face auth is not enabled. 820 */ isBypassEnabled()821 boolean isBypassEnabled(); 822 } 823 } 824