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