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.content.Context; 20 import android.content.res.Resources; 21 import android.util.Log; 22 import android.view.View; 23 import android.view.ViewGroup; 24 25 import com.android.systemui.R; 26 import com.android.systemui.statusbar.EmptyShadeView; 27 import com.android.systemui.statusbar.NotificationShelf; 28 import com.android.systemui.statusbar.notification.NotificationUtils; 29 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 30 import com.android.systemui.statusbar.notification.row.ExpandableView; 31 import com.android.systemui.statusbar.notification.row.FooterView; 32 33 import java.util.ArrayList; 34 import java.util.HashMap; 35 import java.util.List; 36 37 /** 38 * The Algorithm of the {@link com.android.systemui.statusbar.notification.stack 39 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar 40 * .stack.StackScrollState} 41 */ 42 public class StackScrollAlgorithm { 43 44 static final boolean ANCHOR_SCROLLING = false; 45 46 private static final String LOG_TAG = "StackScrollAlgorithm"; 47 private final ViewGroup mHostView; 48 49 private int mPaddingBetweenElements; 50 private int mIncreasedPaddingBetweenElements; 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 StackScrollAlgorithm( Context context, ViewGroup hostView)61 public StackScrollAlgorithm( 62 Context context, 63 ViewGroup hostView) { 64 mHostView = hostView; 65 initView(context); 66 } 67 initView(Context context)68 public void initView(Context context) { 69 initConstants(context); 70 } 71 initConstants(Context context)72 private void initConstants(Context context) { 73 Resources res = context.getResources(); 74 mPaddingBetweenElements = res.getDimensionPixelSize( 75 R.dimen.notification_divider_height); 76 mIncreasedPaddingBetweenElements = 77 res.getDimensionPixelSize(R.dimen.notification_divider_height_increased); 78 mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height); 79 mStatusBarHeight = res.getDimensionPixelSize(R.dimen.status_bar_height); 80 mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop); 81 mHeadsUpInset = mStatusBarHeight + res.getDimensionPixelSize( 82 R.dimen.heads_up_status_bar_padding); 83 mPinnedZTranslationExtra = res.getDimensionPixelSize( 84 R.dimen.heads_up_pinned_elevation); 85 mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height); 86 } 87 88 /** 89 * Updates the state of all children in the hostview based on this algorithm. 90 */ resetViewStates(AmbientState ambientState)91 public void resetViewStates(AmbientState ambientState) { 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 99 initAlgorithmState(mHostView, algorithmState, ambientState); 100 101 updatePositionsForState(algorithmState, ambientState); 102 103 updateZValuesForState(algorithmState, ambientState); 104 105 updateHeadsUpStates(algorithmState, ambientState); 106 updatePulsingStates(algorithmState, ambientState); 107 108 updateDimmedActivatedHideSensitive(ambientState, algorithmState); 109 updateClipping(algorithmState, ambientState); 110 updateSpeedBumpState(algorithmState, ambientState); 111 updateShelfState(ambientState); 112 getNotificationChildrenStates(algorithmState, ambientState); 113 } 114 resetChildViewStates()115 private void resetChildViewStates() { 116 int numChildren = mHostView.getChildCount(); 117 for (int i = 0; i < numChildren; i++) { 118 ExpandableView child = (ExpandableView) mHostView.getChildAt(i); 119 child.resetViewState(); 120 } 121 } 122 getNotificationChildrenStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)123 private void getNotificationChildrenStates(StackScrollAlgorithmState algorithmState, 124 AmbientState ambientState) { 125 int childCount = algorithmState.visibleChildren.size(); 126 for (int i = 0; i < childCount; i++) { 127 ExpandableView v = algorithmState.visibleChildren.get(i); 128 if (v instanceof ExpandableNotificationRow) { 129 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 130 row.updateChildrenStates(ambientState); 131 } 132 } 133 } 134 updateSpeedBumpState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)135 private void updateSpeedBumpState(StackScrollAlgorithmState algorithmState, 136 AmbientState ambientState) { 137 int childCount = algorithmState.visibleChildren.size(); 138 int belowSpeedBump = ambientState.getSpeedBumpIndex(); 139 for (int i = 0; i < childCount; i++) { 140 ExpandableView child = algorithmState.visibleChildren.get(i); 141 ExpandableViewState childViewState = child.getViewState(); 142 143 // The speed bump can also be gone, so equality needs to be taken when comparing 144 // indices. 145 childViewState.belowSpeedBump = i >= belowSpeedBump; 146 } 147 148 } 149 updateShelfState(AmbientState ambientState)150 private void updateShelfState(AmbientState ambientState) { 151 NotificationShelf shelf = ambientState.getShelf(); 152 if (shelf != null) { 153 shelf.updateState(ambientState); 154 } 155 } 156 updateClipping(StackScrollAlgorithmState algorithmState, AmbientState ambientState)157 private void updateClipping(StackScrollAlgorithmState algorithmState, 158 AmbientState ambientState) { 159 float drawStart = !ambientState.isOnKeyguard() ? ambientState.getTopPadding() 160 + ambientState.getStackTranslation() + ambientState.getExpandAnimationTopChange() 161 : 0; 162 float previousNotificationEnd = 0; 163 float previousNotificationStart = 0; 164 int childCount = algorithmState.visibleChildren.size(); 165 for (int i = 0; i < childCount; i++) { 166 ExpandableView child = algorithmState.visibleChildren.get(i); 167 ExpandableViewState state = child.getViewState(); 168 if (!child.mustStayOnScreen() || state.headsUpIsVisible) { 169 previousNotificationEnd = Math.max(drawStart, previousNotificationEnd); 170 previousNotificationStart = Math.max(drawStart, previousNotificationStart); 171 } 172 float newYTranslation = state.yTranslation; 173 float newHeight = state.height; 174 float newNotificationEnd = newYTranslation + newHeight; 175 boolean isHeadsUp = (child instanceof ExpandableNotificationRow) 176 && ((ExpandableNotificationRow) child).isPinned(); 177 if (mClipNotificationScrollToTop 178 && !state.inShelf && newYTranslation < previousNotificationEnd 179 && (!isHeadsUp || ambientState.isShadeExpanded())) { 180 // The previous view is overlapping on top, clip! 181 float overlapAmount = previousNotificationEnd - newYTranslation; 182 state.clipTopAmount = (int) overlapAmount; 183 } else { 184 state.clipTopAmount = 0; 185 } 186 187 if (!child.isTransparent()) { 188 // Only update the previous values if we are not transparent, 189 // otherwise we would clip to a transparent view. 190 previousNotificationEnd = newNotificationEnd; 191 previousNotificationStart = newYTranslation; 192 } 193 } 194 } 195 canChildBeDismissed(View v)196 public static boolean canChildBeDismissed(View v) { 197 if (!(v instanceof ExpandableNotificationRow)) { 198 return false; 199 } 200 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 201 if (row.isBlockingHelperShowingAndTranslationFinished()) { 202 return true; 203 } 204 if (row.areGutsExposed() || !row.getEntry().hasFinishedInitialization()) { 205 return false; 206 } 207 return row.canViewBeDismissed(); 208 } 209 210 /** 211 * Updates the dimmed, activated and hiding sensitive states of the children. 212 */ updateDimmedActivatedHideSensitive(AmbientState ambientState, StackScrollAlgorithmState algorithmState)213 private void updateDimmedActivatedHideSensitive(AmbientState ambientState, 214 StackScrollAlgorithmState algorithmState) { 215 boolean dimmed = ambientState.isDimmed(); 216 boolean dark = ambientState.isFullyDark(); 217 boolean hideSensitive = ambientState.isHideSensitive(); 218 View activatedChild = ambientState.getActivatedChild(); 219 int childCount = algorithmState.visibleChildren.size(); 220 for (int i = 0; i < childCount; i++) { 221 ExpandableView child = algorithmState.visibleChildren.get(i); 222 ExpandableViewState childViewState = child.getViewState(); 223 childViewState.dimmed = dimmed; 224 childViewState.dark = dark; 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(ViewGroup hostView, StackScrollAlgorithmState state, AmbientState ambientState)236 private void initAlgorithmState(ViewGroup hostView, StackScrollAlgorithmState state, 237 AmbientState ambientState) { 238 float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */); 239 240 int scrollY = ambientState.getScrollY(); 241 242 // Due to the overScroller, the stackscroller can have negative scroll state. This is 243 // already accounted for by the top padding and doesn't need an additional adaption 244 scrollY = Math.max(0, scrollY); 245 state.scrollY = (int) (scrollY + bottomOverScroll); 246 247 if (ANCHOR_SCROLLING) { 248 state.anchorViewY = (int) (ambientState.getAnchorViewY() - bottomOverScroll); 249 } 250 251 //now init the visible children and update paddings 252 int childCount = hostView.getChildCount(); 253 state.visibleChildren.clear(); 254 state.visibleChildren.ensureCapacity(childCount); 255 state.paddingMap.clear(); 256 int notGoneIndex = 0; 257 ExpandableView lastView = null; 258 int firstHiddenIndex = ambientState.isDark() 259 ? (ambientState.hasPulsingNotifications() ? 1 : 0) 260 : childCount; 261 262 // The goal here is to fill the padding map, by iterating over how much padding each child 263 // needs. The map is thereby reused, by first filling it with the padding amount and when 264 // iterating over it again, it's filled with the actual resolved value. 265 266 for (int i = 0; i < childCount; i++) { 267 if (ANCHOR_SCROLLING) { 268 if (i == ambientState.getAnchorViewIndex()) { 269 state.anchorViewIndex = state.visibleChildren.size(); 270 } 271 } 272 ExpandableView v = (ExpandableView) hostView.getChildAt(i); 273 if (v.getVisibility() != View.GONE) { 274 if (v == ambientState.getShelf()) { 275 continue; 276 } 277 if (i >= firstHiddenIndex) { 278 // we need normal padding now, to be in sync with what the stack calculates 279 lastView = null; 280 } 281 notGoneIndex = updateNotGoneIndex(state, notGoneIndex, v); 282 float increasedPadding = v.getIncreasedPaddingAmount(); 283 if (increasedPadding != 0.0f) { 284 state.paddingMap.put(v, increasedPadding); 285 if (lastView != null) { 286 Float prevValue = state.paddingMap.get(lastView); 287 float newValue = getPaddingForValue(increasedPadding); 288 if (prevValue != null) { 289 float prevPadding = getPaddingForValue(prevValue); 290 if (increasedPadding > 0) { 291 newValue = NotificationUtils.interpolate( 292 prevPadding, 293 newValue, 294 increasedPadding); 295 } else if (prevValue > 0) { 296 newValue = NotificationUtils.interpolate( 297 newValue, 298 prevPadding, 299 prevValue); 300 } 301 } 302 state.paddingMap.put(lastView, newValue); 303 } 304 } else if (lastView != null) { 305 306 // Let's now resolve the value to an actual padding 307 float newValue = getPaddingForValue(state.paddingMap.get(lastView)); 308 state.paddingMap.put(lastView, newValue); 309 } 310 if (v instanceof ExpandableNotificationRow) { 311 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 312 313 // handle the notgoneIndex for the children as well 314 List<ExpandableNotificationRow> children = row.getNotificationChildren(); 315 if (row.isSummaryWithChildren() && children != null) { 316 for (ExpandableNotificationRow childRow : children) { 317 if (childRow.getVisibility() != View.GONE) { 318 ExpandableViewState childState = childRow.getViewState(); 319 childState.notGoneIndex = notGoneIndex; 320 notGoneIndex++; 321 } 322 } 323 } 324 } 325 lastView = v; 326 } 327 } 328 ExpandableNotificationRow expandingNotification = ambientState.getExpandingNotification(); 329 state.indexOfExpandingNotification = expandingNotification != null 330 ? expandingNotification.isChildInGroup() 331 ? state.visibleChildren.indexOf(expandingNotification.getNotificationParent()) 332 : state.visibleChildren.indexOf(expandingNotification) 333 : -1; 334 } 335 getPaddingForValue(Float increasedPadding)336 private float getPaddingForValue(Float increasedPadding) { 337 if (increasedPadding == null) { 338 return mPaddingBetweenElements; 339 } else if (increasedPadding >= 0.0f) { 340 return NotificationUtils.interpolate( 341 mPaddingBetweenElements, 342 mIncreasedPaddingBetweenElements, 343 increasedPadding); 344 } else { 345 return NotificationUtils.interpolate( 346 0, 347 mPaddingBetweenElements, 348 1.0f + increasedPadding); 349 } 350 } 351 updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, ExpandableView v)352 private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, 353 ExpandableView v) { 354 ExpandableViewState viewState = v.getViewState(); 355 viewState.notGoneIndex = notGoneIndex; 356 state.visibleChildren.add(v); 357 notGoneIndex++; 358 return notGoneIndex; 359 } 360 361 /** 362 * Determine the positions for the views. This is the main part of the algorithm. 363 * 364 * @param algorithmState The state in which the current pass of the algorithm is currently in 365 * @param ambientState The current ambient state 366 */ updatePositionsForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)367 private void updatePositionsForState(StackScrollAlgorithmState algorithmState, 368 AmbientState ambientState) { 369 if (ANCHOR_SCROLLING) { 370 float currentYPosition = algorithmState.anchorViewY; 371 int childCount = algorithmState.visibleChildren.size(); 372 for (int i = algorithmState.anchorViewIndex; i < childCount; i++) { 373 currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition, 374 false /* reverse */); 375 } 376 currentYPosition = algorithmState.anchorViewY; 377 for (int i = algorithmState.anchorViewIndex - 1; i >= 0; i--) { 378 currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition, 379 true /* reverse */); 380 } 381 } else { 382 // The y coordinate of the current child. 383 float currentYPosition = -algorithmState.scrollY; 384 int childCount = algorithmState.visibleChildren.size(); 385 for (int i = 0; i < childCount; i++) { 386 currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition, 387 false /* reverse */); 388 } 389 } 390 } 391 392 /** 393 * Populates the {@link ExpandableViewState} for a single child. 394 * 395 * @param i The index of the child in 396 * {@link StackScrollAlgorithmState#visibleChildren}. 397 * @param algorithmState The overall output state of the algorithm. 398 * @param ambientState The input state provided to the algorithm. 399 * @param currentYPosition The Y position of the current pass of the algorithm. For a forward 400 * pass, this should be the top of the child; for a reverse pass, the 401 * bottom of the child. 402 * @param reverse Whether we're laying out children in the reverse direction (Y 403 * positions 404 * decreasing) instead of the forward direction (Y positions 405 * increasing). 406 * @return The Y position after laying out the child. This will be the {@code currentYPosition} 407 * for the next call to this method, after adjusting for any gaps between children. 408 */ updateChild( int i, StackScrollAlgorithmState algorithmState, AmbientState ambientState, float currentYPosition, boolean reverse)409 protected float updateChild( 410 int i, 411 StackScrollAlgorithmState algorithmState, 412 AmbientState ambientState, 413 float currentYPosition, 414 boolean reverse) { 415 ExpandableView child = algorithmState.visibleChildren.get(i); 416 final boolean applyGapHeight = 417 childNeedsGapHeight(ambientState.getSectionProvider(), algorithmState, i, child); 418 ExpandableViewState childViewState = child.getViewState(); 419 childViewState.location = ExpandableViewState.LOCATION_UNKNOWN; 420 421 if (applyGapHeight && !reverse) { 422 currentYPosition += mGapHeight; 423 } 424 425 int paddingAfterChild = getPaddingAfterChild(algorithmState, child); 426 int childHeight = getMaxAllowedChildHeight(child); 427 if (reverse) { 428 childViewState.yTranslation = currentYPosition - (childHeight + paddingAfterChild); 429 if (currentYPosition <= 0) { 430 childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP; 431 } 432 } else { 433 childViewState.yTranslation = currentYPosition; 434 } 435 boolean isFooterView = child instanceof FooterView; 436 boolean isEmptyShadeView = child instanceof EmptyShadeView; 437 438 childViewState.location = ExpandableViewState.LOCATION_MAIN_AREA; 439 float inset = ambientState.getTopPadding() + ambientState.getStackTranslation(); 440 if (i <= algorithmState.getIndexOfExpandingNotification()) { 441 inset += ambientState.getExpandAnimationTopChange(); 442 } 443 if (child.mustStayOnScreen() && childViewState.yTranslation >= 0) { 444 // Even if we're not scrolled away we're in view and we're also not in the 445 // shelf. We can relax the constraints and let us scroll off the top! 446 float end = childViewState.yTranslation + childViewState.height + inset; 447 childViewState.headsUpIsVisible = end < ambientState.getMaxHeadsUpTranslation(); 448 } 449 if (isFooterView) { 450 childViewState.yTranslation = Math.min(childViewState.yTranslation, 451 ambientState.getInnerHeight() - childHeight); 452 } else if (isEmptyShadeView) { 453 childViewState.yTranslation = ambientState.getInnerHeight() - childHeight 454 + ambientState.getStackTranslation() * 0.25f; 455 } else { 456 clampPositionToShelf(child, childViewState, ambientState); 457 } 458 459 if (reverse) { 460 currentYPosition = childViewState.yTranslation; 461 if (applyGapHeight) { 462 currentYPosition -= mGapHeight; 463 } 464 } else { 465 currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild; 466 if (currentYPosition <= 0) { 467 childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP; 468 } 469 } 470 if (childViewState.location == ExpandableViewState.LOCATION_UNKNOWN) { 471 Log.wtf(LOG_TAG, "Failed to assign location for child " + i); 472 } 473 474 childViewState.yTranslation += inset; 475 return currentYPosition; 476 } 477 478 private boolean childNeedsGapHeight( 479 SectionProvider sectionProvider, 480 StackScrollAlgorithmState algorithmState, 481 int visibleIndex, 482 View child) { 483 boolean needsGapHeight = sectionProvider.beginsSection(child) && visibleIndex > 0; 484 if (ANCHOR_SCROLLING) { 485 needsGapHeight &= visibleIndex != algorithmState.anchorViewIndex; 486 } 487 return needsGapHeight; 488 } 489 490 protected int getPaddingAfterChild(StackScrollAlgorithmState algorithmState, 491 ExpandableView child) { 492 return algorithmState.getPaddingAfterChild(child); 493 } 494 495 private void updatePulsingStates(StackScrollAlgorithmState algorithmState, 496 AmbientState ambientState) { 497 int childCount = algorithmState.visibleChildren.size(); 498 for (int i = 0; i < childCount; i++) { 499 View child = algorithmState.visibleChildren.get(i); 500 if (!(child instanceof ExpandableNotificationRow)) { 501 continue; 502 } 503 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 504 if (!row.showingAmbientPulsing() || ambientState.isFullyDark() 505 || (i == 0 && ambientState.isPulseExpanding())) { 506 continue; 507 } 508 ExpandableViewState viewState = row.getViewState(); 509 viewState.hidden = false; 510 } 511 } 512 513 private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState, 514 AmbientState ambientState) { 515 int childCount = algorithmState.visibleChildren.size(); 516 ExpandableNotificationRow topHeadsUpEntry = null; 517 for (int i = 0; i < childCount; i++) { 518 View child = algorithmState.visibleChildren.get(i); 519 if (!(child instanceof ExpandableNotificationRow)) { 520 break; 521 } 522 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 523 if (!row.isHeadsUp()) { 524 break; 525 } 526 ExpandableViewState childState = row.getViewState(); 527 if (topHeadsUpEntry == null && row.mustStayOnScreen() && !childState.headsUpIsVisible) { 528 topHeadsUpEntry = row; 529 childState.location = ExpandableViewState.LOCATION_FIRST_HUN; 530 } 531 boolean isTopEntry = topHeadsUpEntry == row; 532 float unmodifiedEndLocation = childState.yTranslation + childState.height; 533 if (mIsExpanded) { 534 if (row.mustStayOnScreen() && !childState.headsUpIsVisible) { 535 // Ensure that the heads up is always visible even when scrolled off 536 clampHunToTop(ambientState, row, childState); 537 if (i == 0 && row.isAboveShelf()) { 538 // the first hun can't get off screen. 539 clampHunToMaxTranslation(ambientState, row, childState); 540 childState.hidden = false; 541 } 542 } 543 } 544 if (row.isPinned()) { 545 childState.yTranslation = Math.max(childState.yTranslation, mHeadsUpInset); 546 childState.height = Math.max(row.getIntrinsicHeight(), childState.height); 547 childState.hidden = false; 548 ExpandableViewState topState = 549 topHeadsUpEntry == null ? null : topHeadsUpEntry.getViewState(); 550 if (topState != null && !isTopEntry && (!mIsExpanded 551 || unmodifiedEndLocation < topState.yTranslation + topState.height)) { 552 // Ensure that a headsUp doesn't vertically extend further than the heads-up at 553 // the top most z-position 554 childState.height = row.getIntrinsicHeight(); 555 childState.yTranslation = topState.yTranslation + topState.height 556 - childState.height; 557 } 558 559 // heads up notification show and this row is the top entry of heads up 560 // notifications. i.e. this row should be the only one row that has input field 561 // To check if the row need to do translation according to scroll Y 562 // heads up show full of row's content and any scroll y indicate that the 563 // translationY need to move up the HUN. 564 // TODO: fix this check for anchor scrolling. 565 if (!mIsExpanded && isTopEntry && ambientState.getScrollY() > 0) { 566 childState.yTranslation -= ambientState.getScrollY(); 567 } 568 } 569 if (row.isHeadsUpAnimatingAway()) { 570 childState.hidden = false; 571 } 572 } 573 } 574 575 private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row, 576 ExpandableViewState childState) { 577 float newTranslation = Math.max(ambientState.getTopPadding() 578 + ambientState.getStackTranslation(), childState.yTranslation); 579 childState.height = (int) Math.max(childState.height - (newTranslation 580 - childState.yTranslation), row.getCollapsedHeight()); 581 childState.yTranslation = newTranslation; 582 } 583 584 private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, 585 ExpandableViewState childState) { 586 float newTranslation; 587 float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation(); 588 float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding() 589 + ambientState.getStackTranslation(); 590 maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition); 591 float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight(); 592 newTranslation = Math.min(childState.yTranslation, bottomPosition); 593 childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation 594 - newTranslation); 595 childState.yTranslation = newTranslation; 596 } 597 598 /** 599 * Clamp the height of the child down such that its end is at most on the beginning of 600 * the shelf. 601 * 602 * @param childViewState the view state of the child 603 * @param ambientState the ambient state 604 */ 605 private void clampPositionToShelf(ExpandableView child, 606 ExpandableViewState childViewState, 607 AmbientState ambientState) { 608 if (ambientState.getShelf() == null) { 609 return; 610 } 611 612 int shelfStart = ambientState.getInnerHeight() 613 - ambientState.getShelf().getIntrinsicHeight(); 614 if (ambientState.isAppearing() && !child.isAboveShelf()) { 615 // Don't show none heads-up notifications while in appearing phase. 616 childViewState.yTranslation = Math.max(childViewState.yTranslation, shelfStart); 617 } 618 childViewState.yTranslation = Math.min(childViewState.yTranslation, shelfStart); 619 if (childViewState.yTranslation >= shelfStart) { 620 childViewState.hidden = !child.isExpandAnimationRunning() && !child.hasExpandingChild(); 621 childViewState.inShelf = true; 622 childViewState.headsUpIsVisible = false; 623 } 624 } 625 626 protected int getMaxAllowedChildHeight(View child) { 627 if (child instanceof ExpandableView) { 628 ExpandableView expandableView = (ExpandableView) child; 629 return expandableView.getIntrinsicHeight(); 630 } 631 return child == null ? mCollapsedSize : child.getHeight(); 632 } 633 634 /** 635 * Calculate the Z positions for all children based on the number of items in both stacks and 636 * save it in the resultState 637 * 638 * @param algorithmState The state in which the current pass of the algorithm is currently in 639 * @param ambientState The ambient state of the algorithm 640 */ 641 private void updateZValuesForState(StackScrollAlgorithmState algorithmState, 642 AmbientState ambientState) { 643 int childCount = algorithmState.visibleChildren.size(); 644 float childrenOnTop = 0.0f; 645 for (int i = childCount - 1; i >= 0; i--) { 646 childrenOnTop = updateChildZValue(i, childrenOnTop, 647 algorithmState, ambientState); 648 } 649 } 650 651 protected float updateChildZValue(int i, float childrenOnTop, 652 StackScrollAlgorithmState algorithmState, 653 AmbientState ambientState) { 654 ExpandableView child = algorithmState.visibleChildren.get(i); 655 ExpandableViewState childViewState = child.getViewState(); 656 int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements(); 657 float baseZ = ambientState.getBaseZHeight(); 658 if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible 659 && !ambientState.isDozingAndNotPulsing(child) 660 && childViewState.yTranslation < ambientState.getTopPadding() 661 + ambientState.getStackTranslation()) { 662 if (childrenOnTop != 0.0f) { 663 childrenOnTop++; 664 } else { 665 float overlap = ambientState.getTopPadding() 666 + ambientState.getStackTranslation() - childViewState.yTranslation; 667 childrenOnTop += Math.min(1.0f, overlap / childViewState.height); 668 } 669 childViewState.zTranslation = baseZ 670 + childrenOnTop * zDistanceBetweenElements; 671 } else if (i == 0 && child.isAboveShelf()) { 672 // In case this is a new view that has never been measured before, we don't want to 673 // elevate if we are currently expanded more then the notification 674 int shelfHeight = ambientState.getShelf() == null ? 0 : 675 ambientState.getShelf().getIntrinsicHeight(); 676 float shelfStart = ambientState.getInnerHeight() 677 - shelfHeight + ambientState.getTopPadding() 678 + ambientState.getStackTranslation(); 679 float notificationEnd = childViewState.yTranslation + child.getPinnedHeadsUpHeight() 680 + mPaddingBetweenElements; 681 if (shelfStart > notificationEnd) { 682 childViewState.zTranslation = baseZ; 683 } else { 684 float factor = (notificationEnd - shelfStart) / shelfHeight; 685 factor = Math.min(factor, 1.0f); 686 childViewState.zTranslation = baseZ + factor * zDistanceBetweenElements; 687 } 688 } else { 689 childViewState.zTranslation = baseZ; 690 } 691 692 // We need to scrim the notification more from its surrounding content when we are pinned, 693 // and we therefore elevate it higher. 694 // We can use the headerVisibleAmount for this, since the value nicely goes from 0 to 1 when 695 // expanding after which we have a normal elevation again. 696 childViewState.zTranslation += (1.0f - child.getHeaderVisibleAmount()) 697 * mPinnedZTranslationExtra; 698 return childrenOnTop; 699 } 700 701 public void setIsExpanded(boolean isExpanded) { 702 this.mIsExpanded = isExpanded; 703 } 704 705 public class StackScrollAlgorithmState { 706 707 /** 708 * The scroll position of the algorithm (absolute scrolling). 709 */ 710 public int scrollY; 711 712 /** The index of the anchor view (anchor scrolling). */ 713 public int anchorViewIndex; 714 715 /** 716 * The Y position, relative to the top of the screen, of the anchor view (anchor scrolling). 717 */ 718 public int anchorViewY; 719 720 /** 721 * The children from the host view which are not gone. 722 */ 723 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>(); 724 725 /** 726 * The padding after each child measured in pixels. 727 */ 728 public final HashMap<ExpandableView, Float> paddingMap = new HashMap<>(); 729 private int indexOfExpandingNotification; 730 731 public int getPaddingAfterChild(ExpandableView child) { 732 Float padding = paddingMap.get(child); 733 if (padding == null) { 734 // Should only happen for the last view 735 return mPaddingBetweenElements; 736 } 737 return (int) padding.floatValue(); 738 } 739 740 public int getIndexOfExpandingNotification() { 741 return indexOfExpandingNotification; 742 } 743 } 744 745 /** 746 * Interface for telling the SSA when a new notification section begins (so it can add in 747 * appropriate margins). 748 */ 749 public interface SectionProvider { 750 /** 751 * True if this view starts a new "section" of notifications, such as the gentle 752 * notifications section. False if sections are not enabled. 753 */ 754 boolean beginsSection(View view); 755 } 756 } 757