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