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.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 import com.android.systemui.R; 25 import com.android.systemui.statusbar.EmptyShadeView; 26 import com.android.systemui.statusbar.ExpandableNotificationRow; 27 import com.android.systemui.statusbar.ExpandableView; 28 import com.android.systemui.statusbar.FooterView; 29 import com.android.systemui.statusbar.NotificationShelf; 30 import com.android.systemui.statusbar.notification.NotificationUtils; 31 32 import java.util.ArrayList; 33 import java.util.HashMap; 34 import java.util.List; 35 36 /** 37 * The Algorithm of the {@link com.android.systemui.statusbar.stack 38 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar 39 * .stack.StackScrollState} 40 */ 41 public class StackScrollAlgorithm { 42 43 private static final String LOG_TAG = "StackScrollAlgorithm"; 44 45 private int mPaddingBetweenElements; 46 private int mIncreasedPaddingBetweenElements; 47 private int mCollapsedSize; 48 49 private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); 50 private boolean mIsExpanded; 51 private boolean mClipNotificationScrollToTop; 52 private int mStatusBarHeight; 53 private float mHeadsUpInset; 54 private int mPinnedZTranslationExtra; 55 StackScrollAlgorithm(Context context)56 public StackScrollAlgorithm(Context context) { 57 initView(context); 58 } 59 initView(Context context)60 public void initView(Context context) { 61 initConstants(context); 62 } 63 initConstants(Context context)64 private void initConstants(Context context) { 65 Resources res = context.getResources(); 66 mPaddingBetweenElements = res.getDimensionPixelSize( 67 R.dimen.notification_divider_height); 68 mIncreasedPaddingBetweenElements = 69 res.getDimensionPixelSize(R.dimen.notification_divider_height_increased); 70 mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height); 71 mStatusBarHeight = res.getDimensionPixelSize(R.dimen.status_bar_height); 72 mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop); 73 mHeadsUpInset = mStatusBarHeight + res.getDimensionPixelSize( 74 R.dimen.heads_up_status_bar_padding); 75 mPinnedZTranslationExtra = res.getDimensionPixelSize( 76 R.dimen.heads_up_pinned_elevation); 77 } 78 getStackScrollState(AmbientState ambientState, StackScrollState resultState)79 public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) { 80 // The state of the local variables are saved in an algorithmState to easily subdivide it 81 // into multiple phases. 82 StackScrollAlgorithmState algorithmState = mTempAlgorithmState; 83 84 // First we reset the view states to their default values. 85 resultState.resetViewStates(); 86 87 initAlgorithmState(resultState, algorithmState, ambientState); 88 89 updatePositionsForState(resultState, algorithmState, ambientState); 90 91 updateZValuesForState(resultState, algorithmState, ambientState); 92 93 updateHeadsUpStates(resultState, algorithmState, ambientState); 94 95 handleDraggedViews(ambientState, resultState, algorithmState); 96 updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState); 97 updateClipping(resultState, algorithmState, ambientState); 98 updateSpeedBumpState(resultState, algorithmState, ambientState); 99 updateShelfState(resultState, ambientState); 100 getNotificationChildrenStates(resultState, algorithmState, ambientState); 101 } 102 getNotificationChildrenStates(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)103 private void getNotificationChildrenStates(StackScrollState resultState, 104 StackScrollAlgorithmState algorithmState, 105 AmbientState ambientState) { 106 int childCount = algorithmState.visibleChildren.size(); 107 for (int i = 0; i < childCount; i++) { 108 ExpandableView v = algorithmState.visibleChildren.get(i); 109 if (v instanceof ExpandableNotificationRow) { 110 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 111 row.getChildrenStates(resultState, ambientState); 112 } 113 } 114 } 115 updateSpeedBumpState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)116 private void updateSpeedBumpState(StackScrollState resultState, 117 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 118 int childCount = algorithmState.visibleChildren.size(); 119 int belowSpeedBump = ambientState.getSpeedBumpIndex(); 120 for (int i = 0; i < childCount; i++) { 121 View child = algorithmState.visibleChildren.get(i); 122 ExpandableViewState childViewState = resultState.getViewStateForView(child); 123 124 // The speed bump can also be gone, so equality needs to be taken when comparing 125 // indices. 126 childViewState.belowSpeedBump = i >= belowSpeedBump; 127 } 128 129 } updateShelfState(StackScrollState resultState, AmbientState ambientState)130 private void updateShelfState(StackScrollState resultState, AmbientState ambientState) { 131 NotificationShelf shelf = ambientState.getShelf(); 132 if (shelf != null) { 133 shelf.updateState(resultState, ambientState); 134 } 135 } 136 updateClipping(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)137 private void updateClipping(StackScrollState resultState, 138 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 139 float drawStart = !ambientState.isOnKeyguard() ? ambientState.getTopPadding() 140 + ambientState.getStackTranslation() + ambientState.getExpandAnimationTopChange() 141 : 0; 142 float previousNotificationEnd = 0; 143 float previousNotificationStart = 0; 144 int childCount = algorithmState.visibleChildren.size(); 145 for (int i = 0; i < childCount; i++) { 146 ExpandableView child = algorithmState.visibleChildren.get(i); 147 ExpandableViewState state = resultState.getViewStateForView(child); 148 if (!child.mustStayOnScreen() || state.headsUpIsVisible) { 149 previousNotificationEnd = Math.max(drawStart, previousNotificationEnd); 150 previousNotificationStart = Math.max(drawStart, previousNotificationStart); 151 } 152 float newYTranslation = state.yTranslation; 153 float newHeight = state.height; 154 float newNotificationEnd = newYTranslation + newHeight; 155 boolean isHeadsUp = (child instanceof ExpandableNotificationRow) 156 && ((ExpandableNotificationRow) child).isPinned(); 157 if (mClipNotificationScrollToTop 158 && !state.inShelf && newYTranslation < previousNotificationEnd 159 && (!isHeadsUp || ambientState.isShadeExpanded())) { 160 // The previous view is overlapping on top, clip! 161 float overlapAmount = previousNotificationEnd - newYTranslation; 162 state.clipTopAmount = (int) overlapAmount; 163 } else { 164 state.clipTopAmount = 0; 165 } 166 167 if (!child.isTransparent()) { 168 // Only update the previous values if we are not transparent, 169 // otherwise we would clip to a transparent view. 170 previousNotificationEnd = newNotificationEnd; 171 previousNotificationStart = newYTranslation; 172 } 173 } 174 } 175 canChildBeDismissed(View v)176 public static boolean canChildBeDismissed(View v) { 177 if (!(v instanceof ExpandableNotificationRow)) { 178 return false; 179 } 180 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 181 if (row.areGutsExposed()) { 182 return false; 183 } 184 return row.canViewBeDismissed(); 185 } 186 187 /** 188 * Updates the dimmed, activated and hiding sensitive states of the children. 189 */ updateDimmedActivatedHideSensitive(AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState)190 private void updateDimmedActivatedHideSensitive(AmbientState ambientState, 191 StackScrollState resultState, StackScrollAlgorithmState algorithmState) { 192 boolean dimmed = ambientState.isDimmed(); 193 boolean dark = ambientState.isFullyDark(); 194 boolean hideSensitive = ambientState.isHideSensitive(); 195 View activatedChild = ambientState.getActivatedChild(); 196 int childCount = algorithmState.visibleChildren.size(); 197 for (int i = 0; i < childCount; i++) { 198 View child = algorithmState.visibleChildren.get(i); 199 ExpandableViewState childViewState = resultState.getViewStateForView(child); 200 childViewState.dimmed = dimmed; 201 childViewState.dark = dark; 202 childViewState.hideSensitive = hideSensitive; 203 boolean isActivatedChild = activatedChild == child; 204 if (dimmed && isActivatedChild) { 205 childViewState.zTranslation += 2.0f * ambientState.getZDistanceBetweenElements(); 206 } 207 } 208 } 209 210 /** 211 * Handle the special state when views are being dragged 212 */ handleDraggedViews(AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState)213 private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState, 214 StackScrollAlgorithmState algorithmState) { 215 ArrayList<View> draggedViews = ambientState.getDraggedViews(); 216 for (View draggedView : draggedViews) { 217 int childIndex = algorithmState.visibleChildren.indexOf(draggedView); 218 if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) { 219 View nextChild = algorithmState.visibleChildren.get(childIndex + 1); 220 if (!draggedViews.contains(nextChild)) { 221 // only if the view is not dragged itself we modify its state to be fully 222 // visible 223 ExpandableViewState viewState = resultState.getViewStateForView( 224 nextChild); 225 // The child below the dragged one must be fully visible 226 if (ambientState.isShadeExpanded()) { 227 viewState.shadowAlpha = 1; 228 viewState.hidden = false; 229 } 230 } 231 232 // Lets set the alpha to the one it currently has, as its currently being dragged 233 ExpandableViewState viewState = resultState.getViewStateForView(draggedView); 234 // The dragged child should keep the set alpha 235 viewState.alpha = draggedView.getAlpha(); 236 } 237 } 238 } 239 240 /** 241 * Initialize the algorithm state like updating the visible children. 242 */ initAlgorithmState(StackScrollState resultState, StackScrollAlgorithmState state, AmbientState ambientState)243 private void initAlgorithmState(StackScrollState resultState, StackScrollAlgorithmState state, 244 AmbientState ambientState) { 245 float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */); 246 247 int scrollY = ambientState.getScrollY(); 248 249 // Due to the overScroller, the stackscroller can have negative scroll state. This is 250 // already accounted for by the top padding and doesn't need an additional adaption 251 scrollY = Math.max(0, scrollY); 252 state.scrollY = (int) (scrollY + bottomOverScroll); 253 254 //now init the visible children and update paddings 255 ViewGroup hostView = resultState.getHostView(); 256 int childCount = hostView.getChildCount(); 257 state.visibleChildren.clear(); 258 state.visibleChildren.ensureCapacity(childCount); 259 state.paddingMap.clear(); 260 int notGoneIndex = 0; 261 ExpandableView lastView = null; 262 int firstHiddenIndex = ambientState.isDark() 263 ? (ambientState.hasPulsingNotifications() ? 1 : 0) 264 : childCount; 265 266 // The goal here is to fill the padding map, by iterating over how much padding each child 267 // needs. The map is thereby reused, by first filling it with the padding amount and when 268 // iterating over it again, it's filled with the actual resolved value. 269 270 for (int i = 0; i < childCount; i++) { 271 ExpandableView v = (ExpandableView) hostView.getChildAt(i); 272 if (v.getVisibility() != View.GONE) { 273 if (v == ambientState.getShelf()) { 274 continue; 275 } 276 if (i >= firstHiddenIndex) { 277 // we need normal padding now, to be in sync with what the stack calculates 278 lastView = null; 279 } 280 notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v); 281 float increasedPadding = v.getIncreasedPaddingAmount(); 282 if (increasedPadding != 0.0f) { 283 state.paddingMap.put(v, increasedPadding); 284 if (lastView != null) { 285 Float prevValue = state.paddingMap.get(lastView); 286 float newValue = getPaddingForValue(increasedPadding); 287 if (prevValue != null) { 288 float prevPadding = getPaddingForValue(prevValue); 289 if (increasedPadding > 0) { 290 newValue = NotificationUtils.interpolate( 291 prevPadding, 292 newValue, 293 increasedPadding); 294 } else if (prevValue > 0) { 295 newValue = NotificationUtils.interpolate( 296 newValue, 297 prevPadding, 298 prevValue); 299 } 300 } 301 state.paddingMap.put(lastView, newValue); 302 } 303 } else if (lastView != null) { 304 305 // Let's now resolve the value to an actual padding 306 float newValue = getPaddingForValue(state.paddingMap.get(lastView)); 307 state.paddingMap.put(lastView, newValue); 308 } 309 if (v instanceof ExpandableNotificationRow) { 310 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 311 312 // handle the notgoneIndex for the children as well 313 List<ExpandableNotificationRow> children = 314 row.getNotificationChildren(); 315 if (row.isSummaryWithChildren() && children != null) { 316 for (ExpandableNotificationRow childRow : children) { 317 if (childRow.getVisibility() != View.GONE) { 318 ExpandableViewState childState 319 = resultState.getViewStateForView(childRow); 320 childState.notGoneIndex = notGoneIndex; 321 notGoneIndex++; 322 } 323 } 324 } 325 } 326 lastView = v; 327 } 328 } 329 ExpandableNotificationRow expandingNotification = ambientState.getExpandingNotification(); 330 state.indexOfExpandingNotification = expandingNotification != null 331 ? expandingNotification.isChildInGroup() 332 ? state.visibleChildren.indexOf(expandingNotification.getNotificationParent()) 333 : state.visibleChildren.indexOf(expandingNotification) 334 : -1; 335 } 336 getPaddingForValue(Float increasedPadding)337 private float getPaddingForValue(Float increasedPadding) { 338 if (increasedPadding == null) { 339 return mPaddingBetweenElements; 340 } else if (increasedPadding >= 0.0f) { 341 return NotificationUtils.interpolate( 342 mPaddingBetweenElements, 343 mIncreasedPaddingBetweenElements, 344 increasedPadding); 345 } else { 346 return NotificationUtils.interpolate( 347 0, 348 mPaddingBetweenElements, 349 1.0f + increasedPadding); 350 } 351 } 352 updateNotGoneIndex(StackScrollState resultState, StackScrollAlgorithmState state, int notGoneIndex, ExpandableView v)353 private int updateNotGoneIndex(StackScrollState resultState, 354 StackScrollAlgorithmState state, int notGoneIndex, 355 ExpandableView v) { 356 ExpandableViewState viewState = resultState.getViewStateForView(v); 357 viewState.notGoneIndex = notGoneIndex; 358 state.visibleChildren.add(v); 359 notGoneIndex++; 360 return notGoneIndex; 361 } 362 363 /** 364 * Determine the positions for the views. This is the main part of the algorithm. 365 * 366 * @param resultState The result state to update if a change to the properties of a child occurs 367 * @param algorithmState The state in which the current pass of the algorithm is currently in 368 * @param ambientState The current ambient state 369 */ updatePositionsForState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)370 private void updatePositionsForState(StackScrollState resultState, 371 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 372 373 // The y coordinate of the current child. 374 float currentYPosition = -algorithmState.scrollY; 375 int childCount = algorithmState.visibleChildren.size(); 376 for (int i = 0; i < childCount; i++) { 377 currentYPosition = updateChild(i, resultState, algorithmState, ambientState, 378 currentYPosition); 379 } 380 } 381 updateChild(int i, StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState, float currentYPosition)382 protected float updateChild(int i, StackScrollState resultState, 383 StackScrollAlgorithmState algorithmState, AmbientState ambientState, 384 float currentYPosition) { 385 ExpandableView child = algorithmState.visibleChildren.get(i); 386 ExpandableViewState childViewState = resultState.getViewStateForView(child); 387 childViewState.location = ExpandableViewState.LOCATION_UNKNOWN; 388 int paddingAfterChild = getPaddingAfterChild(algorithmState, child); 389 int childHeight = getMaxAllowedChildHeight(child); 390 childViewState.yTranslation = currentYPosition; 391 boolean isFooterView = child instanceof FooterView; 392 boolean isEmptyShadeView = child instanceof EmptyShadeView; 393 394 childViewState.location = ExpandableViewState.LOCATION_MAIN_AREA; 395 float inset = ambientState.getTopPadding() + ambientState.getStackTranslation(); 396 if (i <= algorithmState.getIndexOfExpandingNotification()) { 397 inset += ambientState.getExpandAnimationTopChange(); 398 } 399 if (child.mustStayOnScreen() && childViewState.yTranslation >= 0) { 400 // Even if we're not scrolled away we're in view and we're also not in the 401 // shelf. We can relax the constraints and let us scroll off the top! 402 float end = childViewState.yTranslation + childViewState.height + inset; 403 childViewState.headsUpIsVisible = end < ambientState.getMaxHeadsUpTranslation(); 404 } 405 if (isFooterView) { 406 childViewState.yTranslation = Math.min(childViewState.yTranslation, 407 ambientState.getInnerHeight() - childHeight); 408 } else if (isEmptyShadeView) { 409 childViewState.yTranslation = ambientState.getInnerHeight() - childHeight 410 + ambientState.getStackTranslation() * 0.25f; 411 } else { 412 clampPositionToShelf(child, childViewState, ambientState); 413 } 414 415 currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild; 416 if (currentYPosition <= 0) { 417 childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP; 418 } 419 if (childViewState.location == ExpandableViewState.LOCATION_UNKNOWN) { 420 Log.wtf(LOG_TAG, "Failed to assign location for child " + i); 421 } 422 423 childViewState.yTranslation += inset; 424 return currentYPosition; 425 } 426 427 protected int getPaddingAfterChild(StackScrollAlgorithmState algorithmState, 428 ExpandableView child) { 429 return algorithmState.getPaddingAfterChild(child); 430 } 431 432 private void updateHeadsUpStates(StackScrollState resultState, 433 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 434 int childCount = algorithmState.visibleChildren.size(); 435 ExpandableNotificationRow topHeadsUpEntry = null; 436 for (int i = 0; i < childCount; i++) { 437 View child = algorithmState.visibleChildren.get(i); 438 if (!(child instanceof ExpandableNotificationRow)) { 439 break; 440 } 441 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 442 if (!row.isHeadsUp()) { 443 break; 444 } 445 ExpandableViewState childState = resultState.getViewStateForView(row); 446 if (topHeadsUpEntry == null && row.mustStayOnScreen() && !childState.headsUpIsVisible) { 447 topHeadsUpEntry = row; 448 childState.location = ExpandableViewState.LOCATION_FIRST_HUN; 449 } 450 boolean isTopEntry = topHeadsUpEntry == row; 451 float unmodifiedEndLocation = childState.yTranslation + childState.height; 452 if (mIsExpanded) { 453 if (row.mustStayOnScreen() && !childState.headsUpIsVisible) { 454 // Ensure that the heads up is always visible even when scrolled off 455 clampHunToTop(ambientState, row, childState); 456 if (i == 0 && ambientState.isAboveShelf(row)) { 457 // the first hun can't get off screen. 458 clampHunToMaxTranslation(ambientState, row, childState); 459 childState.hidden = false; 460 } 461 } 462 } 463 if (row.isPinned()) { 464 childState.yTranslation = Math.max(childState.yTranslation, mHeadsUpInset); 465 childState.height = Math.max(row.getIntrinsicHeight(), childState.height); 466 childState.hidden = false; 467 ExpandableViewState topState = resultState.getViewStateForView(topHeadsUpEntry); 468 if (topState != null && !isTopEntry && (!mIsExpanded 469 || unmodifiedEndLocation < topState.yTranslation + topState.height)) { 470 // Ensure that a headsUp doesn't vertically extend further than the heads-up at 471 // the top most z-position 472 childState.height = row.getIntrinsicHeight(); 473 childState.yTranslation = topState.yTranslation + topState.height 474 - childState.height; 475 } 476 } 477 if (row.isHeadsUpAnimatingAway()) { 478 childState.hidden = false; 479 } 480 } 481 } 482 483 private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row, 484 ExpandableViewState childState) { 485 float newTranslation = Math.max(ambientState.getTopPadding() 486 + ambientState.getStackTranslation(), childState.yTranslation); 487 childState.height = (int) Math.max(childState.height - (newTranslation 488 - childState.yTranslation), row.getCollapsedHeight()); 489 childState.yTranslation = newTranslation; 490 } 491 492 private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, 493 ExpandableViewState childState) { 494 float newTranslation; 495 float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation(); 496 float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding() 497 + ambientState.getStackTranslation(); 498 maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition); 499 float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight(); 500 newTranslation = Math.min(childState.yTranslation, bottomPosition); 501 childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation 502 - newTranslation); 503 childState.yTranslation = newTranslation; 504 } 505 506 /** 507 * Clamp the height of the child down such that its end is at most on the beginning of 508 * the shelf. 509 * 510 * @param child 511 * @param childViewState the view state of the child 512 * @param ambientState the ambient state 513 */ 514 private void clampPositionToShelf(ExpandableView child, 515 ExpandableViewState childViewState, 516 AmbientState ambientState) { 517 if (ambientState.getShelf() == null) { 518 return; 519 } 520 521 int shelfStart = ambientState.getInnerHeight() 522 - ambientState.getShelf().getIntrinsicHeight(); 523 if (ambientState.isAppearing() && !child.isAboveShelf()) { 524 // Don't show none heads-up notifications while in appearing phase. 525 childViewState.yTranslation = Math.max(childViewState.yTranslation, shelfStart); 526 } 527 childViewState.yTranslation = Math.min(childViewState.yTranslation, shelfStart); 528 if (childViewState.yTranslation >= shelfStart) { 529 childViewState.hidden = !child.isExpandAnimationRunning() && !child.hasExpandingChild(); 530 childViewState.inShelf = true; 531 childViewState.headsUpIsVisible = false; 532 } 533 } 534 535 protected int getMaxAllowedChildHeight(View child) { 536 if (child instanceof ExpandableView) { 537 ExpandableView expandableView = (ExpandableView) child; 538 return expandableView.getIntrinsicHeight(); 539 } 540 return child == null? mCollapsedSize : child.getHeight(); 541 } 542 543 /** 544 * Calculate the Z positions for all children based on the number of items in both stacks and 545 * save it in the resultState 546 * @param resultState The result state to update the zTranslation values 547 * @param algorithmState The state in which the current pass of the algorithm is currently in 548 * @param ambientState The ambient state of the algorithm 549 */ 550 private void updateZValuesForState(StackScrollState resultState, 551 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 552 int childCount = algorithmState.visibleChildren.size(); 553 float childrenOnTop = 0.0f; 554 for (int i = childCount - 1; i >= 0; i--) { 555 childrenOnTop = updateChildZValue(i, childrenOnTop, 556 resultState, algorithmState, ambientState); 557 } 558 } 559 560 protected float updateChildZValue(int i, float childrenOnTop, 561 StackScrollState resultState, StackScrollAlgorithmState algorithmState, 562 AmbientState ambientState) { 563 ExpandableView child = algorithmState.visibleChildren.get(i); 564 ExpandableViewState childViewState = resultState.getViewStateForView(child); 565 int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements(); 566 float baseZ = ambientState.getBaseZHeight(); 567 if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible 568 && !ambientState.isDozingAndNotPulsing(child) 569 && childViewState.yTranslation < ambientState.getTopPadding() 570 + ambientState.getStackTranslation()) { 571 if (childrenOnTop != 0.0f) { 572 childrenOnTop++; 573 } else { 574 float overlap = ambientState.getTopPadding() 575 + ambientState.getStackTranslation() - childViewState.yTranslation; 576 childrenOnTop += Math.min(1.0f, overlap / childViewState.height); 577 } 578 childViewState.zTranslation = baseZ 579 + childrenOnTop * zDistanceBetweenElements; 580 } else if (i == 0 && ambientState.isAboveShelf(child)) { 581 // In case this is a new view that has never been measured before, we don't want to 582 // elevate if we are currently expanded more then the notification 583 int shelfHeight = ambientState.getShelf() == null ? 0 : 584 ambientState.getShelf().getIntrinsicHeight(); 585 float shelfStart = ambientState.getInnerHeight() 586 - shelfHeight + ambientState.getTopPadding() 587 + ambientState.getStackTranslation(); 588 float notificationEnd = childViewState.yTranslation + child.getPinnedHeadsUpHeight() 589 + mPaddingBetweenElements; 590 if (shelfStart > notificationEnd) { 591 childViewState.zTranslation = baseZ; 592 } else { 593 float factor = (notificationEnd - shelfStart) / shelfHeight; 594 factor = Math.min(factor, 1.0f); 595 childViewState.zTranslation = baseZ + factor * zDistanceBetweenElements; 596 } 597 } else { 598 childViewState.zTranslation = baseZ; 599 } 600 601 // We need to scrim the notification more from its surrounding content when we are pinned, 602 // and we therefore elevate it higher. 603 // We can use the headerVisibleAmount for this, since the value nicely goes from 0 to 1 when 604 // expanding after which we have a normal elevation again. 605 childViewState.zTranslation += (1.0f - child.getHeaderVisibleAmount()) 606 * mPinnedZTranslationExtra; 607 return childrenOnTop; 608 } 609 610 public void setIsExpanded(boolean isExpanded) { 611 this.mIsExpanded = isExpanded; 612 } 613 614 public class StackScrollAlgorithmState { 615 616 /** 617 * The scroll position of the algorithm 618 */ 619 public int scrollY; 620 621 /** 622 * The children from the host view which are not gone. 623 */ 624 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>(); 625 626 /** 627 * The padding after each child measured in pixels. 628 */ 629 public final HashMap<ExpandableView, Float> paddingMap = new HashMap<>(); 630 private int indexOfExpandingNotification; 631 632 public int getPaddingAfterChild(ExpandableView child) { 633 Float padding = paddingMap.get(child); 634 if (padding == null) { 635 // Should only happen for the last view 636 return mPaddingBetweenElements; 637 } 638 return (int) padding.floatValue(); 639 } 640 641 public int getIndexOfExpandingNotification() { 642 return indexOfExpandingNotification; 643 } 644 } 645 646 } 647