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