StackScrollAlgorithm.java revision 42357e030c095867b95e2e8a718649587c5ebf52
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 17package com.android.systemui.statusbar.stack; 18 19import android.content.Context; 20import android.util.Log; 21import android.view.View; 22import android.view.ViewGroup; 23 24import com.android.systemui.R; 25import com.android.systemui.statusbar.ExpandableNotificationRow; 26import com.android.systemui.statusbar.ExpandableView; 27import com.android.systemui.statusbar.notification.FakeShadowView; 28import com.android.systemui.statusbar.notification.NotificationUtils; 29 30import java.util.ArrayList; 31import java.util.HashMap; 32import java.util.List; 33 34/** 35 * The Algorithm of the {@link com.android.systemui.statusbar.stack 36 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar 37 * .stack.StackScrollState} 38 */ 39public class StackScrollAlgorithm { 40 41 private static final String LOG_TAG = "StackScrollAlgorithm"; 42 43 private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3; 44 45 private int mPaddingBetweenElements; 46 private int mIncreasedPaddingBetweenElements; 47 private int mCollapsedSize; 48 private int mBottomStackPeekSize; 49 private int mZDistanceBetweenElements; 50 private int mZBasicHeight; 51 52 private StackIndentationFunctor mBottomStackIndentationFunctor; 53 54 private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); 55 private boolean mIsExpansionChanging; 56 private int mFirstChildMaxHeight; 57 private boolean mIsExpanded; 58 private ExpandableView mFirstChildWhileExpanding; 59 private boolean mExpandedOnStart; 60 private int mBottomStackSlowDownLength; 61 private int mCollapseSecondCardPadding; 62 63 public StackScrollAlgorithm(Context context) { 64 initView(context); 65 } 66 67 public void initView(Context context) { 68 initConstants(context); 69 } 70 71 public int getBottomStackSlowDownLength() { 72 return mBottomStackSlowDownLength + mPaddingBetweenElements; 73 } 74 75 private void initConstants(Context context) { 76 mPaddingBetweenElements = Math.max(1, context.getResources() 77 .getDimensionPixelSize(R.dimen.notification_divider_height)); 78 mIncreasedPaddingBetweenElements = context.getResources() 79 .getDimensionPixelSize(R.dimen.notification_divider_height_increased); 80 mCollapsedSize = context.getResources() 81 .getDimensionPixelSize(R.dimen.notification_min_height); 82 mBottomStackPeekSize = context.getResources() 83 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); 84 mZDistanceBetweenElements = Math.max(1, context.getResources() 85 .getDimensionPixelSize(R.dimen.z_distance_between_notifications)); 86 mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements; 87 mBottomStackSlowDownLength = context.getResources() 88 .getDimensionPixelSize(R.dimen.bottom_stack_slow_down_length); 89 mCollapseSecondCardPadding = context.getResources().getDimensionPixelSize( 90 R.dimen.notification_collapse_second_card_padding); 91 mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( 92 MAX_ITEMS_IN_BOTTOM_STACK, 93 mBottomStackPeekSize, 94 getBottomStackSlowDownLength(), 95 0.5f); 96 } 97 98 public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) { 99 // The state of the local variables are saved in an algorithmState to easily subdivide it 100 // into multiple phases. 101 StackScrollAlgorithmState algorithmState = mTempAlgorithmState; 102 103 // First we reset the view states to their default values. 104 resultState.resetViewStates(); 105 106 initAlgorithmState(resultState, algorithmState, ambientState); 107 108 updatePositionsForState(resultState, algorithmState, ambientState); 109 110 updateZValuesForState(resultState, algorithmState, ambientState); 111 112 updateHeadsUpStates(resultState, algorithmState, ambientState); 113 114 handleDraggedViews(ambientState, resultState, algorithmState); 115 updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState); 116 updateClipping(resultState, algorithmState, ambientState); 117 updateSpeedBumpState(resultState, algorithmState, ambientState.getSpeedBumpIndex()); 118 getNotificationChildrenStates(resultState, algorithmState); 119 } 120 121 private void getNotificationChildrenStates(StackScrollState resultState, 122 StackScrollAlgorithmState algorithmState) { 123 int childCount = algorithmState.visibleChildren.size(); 124 for (int i = 0; i < childCount; i++) { 125 ExpandableView v = algorithmState.visibleChildren.get(i); 126 if (v instanceof ExpandableNotificationRow) { 127 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 128 row.getChildrenStates(resultState); 129 } 130 } 131 } 132 133 private void updateSpeedBumpState(StackScrollState resultState, 134 StackScrollAlgorithmState algorithmState, int speedBumpIndex) { 135 int childCount = algorithmState.visibleChildren.size(); 136 for (int i = 0; i < childCount; i++) { 137 View child = algorithmState.visibleChildren.get(i); 138 StackViewState childViewState = resultState.getViewStateForView(child); 139 140 // The speed bump can also be gone, so equality needs to be taken when comparing 141 // indices. 142 childViewState.belowSpeedBump = speedBumpIndex != -1 && i >= speedBumpIndex; 143 } 144 } 145 146 private void updateClipping(StackScrollState resultState, 147 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 148 boolean dismissAllInProgress = ambientState.isDismissAllInProgress(); 149 float drawStart = ambientState.getTopPadding() + ambientState.getStackTranslation(); 150 float previousNotificationEnd = 0; 151 float previousNotificationStart = 0; 152 boolean previousNotificationIsSwiped = false; 153 int childCount = algorithmState.visibleChildren.size(); 154 for (int i = 0; i < childCount; i++) { 155 ExpandableView child = algorithmState.visibleChildren.get(i); 156 StackViewState state = resultState.getViewStateForView(child); 157 if (!child.mustStayOnScreen()) { 158 previousNotificationEnd = Math.max(drawStart, previousNotificationEnd); 159 previousNotificationStart = Math.max(drawStart, previousNotificationStart); 160 } 161 float newYTranslation = state.yTranslation; 162 float newHeight = state.height; 163 // apply clipping and shadow 164 float newNotificationEnd = newYTranslation + newHeight; 165 166 float clipHeight; 167 if (previousNotificationIsSwiped) { 168 // When the previous notification is swiped, we don't clip the content to the 169 // bottom of it. 170 clipHeight = newHeight; 171 } else { 172 clipHeight = newNotificationEnd - previousNotificationEnd; 173 clipHeight = Math.max(0.0f, clipHeight); 174 } 175 176 updateChildClippingAndBackground(state, newHeight, clipHeight, 177 newHeight - (previousNotificationStart - newYTranslation)); 178 179 if (dismissAllInProgress) { 180 state.clipTopAmount = Math.max(child.getMinClipTopAmount(), state.clipTopAmount); 181 } 182 183 if (!child.isTransparent()) { 184 // Only update the previous values if we are not transparent, 185 // otherwise we would clip to a transparent view. 186 if ((dismissAllInProgress && canChildBeDismissed(child))) { 187 previousNotificationIsSwiped = true; 188 } else { 189 previousNotificationIsSwiped = ambientState.getDraggedViews().contains(child); 190 previousNotificationEnd = newNotificationEnd; 191 previousNotificationStart =newYTranslation + state.clipTopAmount; 192 } 193 } 194 } 195 } 196 197 public static boolean canChildBeDismissed(View v) { 198 final View veto = v.findViewById(R.id.veto); 199 return (veto != null && veto.getVisibility() != View.GONE); 200 } 201 202 /** 203 * Updates the shadow outline and the clipping for a view. 204 * 205 * @param state the viewState to update 206 * @param realHeight the currently applied height of the view 207 * @param clipHeight the desired clip height, the rest of the view will be clipped from the top 208 * @param backgroundHeight the desired background height. The shadows of the view will be 209 * based on this height and the content will be clipped from the top 210 */ 211 private void updateChildClippingAndBackground(StackViewState state, float realHeight, 212 float clipHeight, float backgroundHeight) { 213 if (realHeight > clipHeight) { 214 // Rather overlap than create a hole. 215 state.topOverLap = (int) Math.floor(realHeight - clipHeight); 216 } else { 217 state.topOverLap = 0; 218 } 219 if (realHeight > backgroundHeight) { 220 // Rather overlap than create a hole. 221 state.clipTopAmount = (int) Math.floor(realHeight - backgroundHeight); 222 } else { 223 state.clipTopAmount = 0; 224 } 225 } 226 227 /** 228 * Updates the dimmed, activated and hiding sensitive states of the children. 229 */ 230 private void updateDimmedActivatedHideSensitive(AmbientState ambientState, 231 StackScrollState resultState, StackScrollAlgorithmState algorithmState) { 232 boolean dimmed = ambientState.isDimmed(); 233 boolean dark = ambientState.isDark(); 234 boolean hideSensitive = ambientState.isHideSensitive(); 235 View activatedChild = ambientState.getActivatedChild(); 236 int childCount = algorithmState.visibleChildren.size(); 237 for (int i = 0; i < childCount; i++) { 238 View child = algorithmState.visibleChildren.get(i); 239 StackViewState childViewState = resultState.getViewStateForView(child); 240 childViewState.dimmed = dimmed; 241 childViewState.dark = dark; 242 childViewState.hideSensitive = hideSensitive; 243 boolean isActivatedChild = activatedChild == child; 244 if (dimmed && isActivatedChild) { 245 childViewState.zTranslation += 2.0f * mZDistanceBetweenElements; 246 } 247 } 248 } 249 250 /** 251 * Handle the special state when views are being dragged 252 */ 253 private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState, 254 StackScrollAlgorithmState algorithmState) { 255 ArrayList<View> draggedViews = ambientState.getDraggedViews(); 256 for (View draggedView : draggedViews) { 257 int childIndex = algorithmState.visibleChildren.indexOf(draggedView); 258 if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) { 259 View nextChild = algorithmState.visibleChildren.get(childIndex + 1); 260 if (!draggedViews.contains(nextChild)) { 261 // only if the view is not dragged itself we modify its state to be fully 262 // visible 263 StackViewState viewState = resultState.getViewStateForView( 264 nextChild); 265 // The child below the dragged one must be fully visible 266 if (ambientState.isShadeExpanded()) { 267 viewState.shadowAlpha = 1; 268 viewState.hidden = false; 269 } 270 } 271 272 // Lets set the alpha to the one it currently has, as its currently being dragged 273 StackViewState viewState = resultState.getViewStateForView(draggedView); 274 // The dragged child should keep the set alpha 275 viewState.alpha = draggedView.getAlpha(); 276 } 277 } 278 } 279 280 /** 281 * Initialize the algorithm state like updating the visible children. 282 */ 283 private void initAlgorithmState(StackScrollState resultState, StackScrollAlgorithmState state, 284 AmbientState ambientState) { 285 state.itemsInBottomStack = 0.0f; 286 state.partialInBottom = 0.0f; 287 float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */); 288 289 int scrollY = ambientState.getScrollY(); 290 291 // Due to the overScroller, the stackscroller can have negative scroll state. This is 292 // already accounted for by the top padding and doesn't need an additional adaption 293 scrollY = Math.max(0, scrollY); 294 state.scrollY = (int) (scrollY + bottomOverScroll); 295 296 //now init the visible children and update paddings 297 ViewGroup hostView = resultState.getHostView(); 298 int childCount = hostView.getChildCount(); 299 state.visibleChildren.clear(); 300 state.visibleChildren.ensureCapacity(childCount); 301 state.increasedPaddingMap.clear(); 302 int notGoneIndex = 0; 303 ExpandableView lastView = null; 304 for (int i = 0; i < childCount; i++) { 305 ExpandableView v = (ExpandableView) hostView.getChildAt(i); 306 if (v.getVisibility() != View.GONE) { 307 notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v); 308 float increasedPadding = v.getIncreasedPaddingAmount(); 309 if (increasedPadding != 0.0f) { 310 state.increasedPaddingMap.put(v, increasedPadding); 311 if (lastView != null) { 312 Float prevValue = state.increasedPaddingMap.get(lastView); 313 float newValue = prevValue != null 314 ? Math.max(prevValue, increasedPadding) 315 : increasedPadding; 316 state.increasedPaddingMap.put(lastView, newValue); 317 } 318 } 319 if (v instanceof ExpandableNotificationRow) { 320 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 321 322 // handle the notgoneIndex for the children as well 323 List<ExpandableNotificationRow> children = 324 row.getNotificationChildren(); 325 if (row.isSummaryWithChildren() && children != null) { 326 for (ExpandableNotificationRow childRow : children) { 327 if (childRow.getVisibility() != View.GONE) { 328 StackViewState childState 329 = resultState.getViewStateForView(childRow); 330 childState.notGoneIndex = notGoneIndex; 331 notGoneIndex++; 332 } 333 } 334 } 335 } 336 lastView = v; 337 } 338 } 339 } 340 341 private int updateNotGoneIndex(StackScrollState resultState, 342 StackScrollAlgorithmState state, int notGoneIndex, 343 ExpandableView v) { 344 StackViewState viewState = resultState.getViewStateForView(v); 345 viewState.notGoneIndex = notGoneIndex; 346 state.visibleChildren.add(v); 347 notGoneIndex++; 348 return notGoneIndex; 349 } 350 351 /** 352 * Determine the positions for the views. This is the main part of the algorithm. 353 * 354 * @param resultState The result state to update if a change to the properties of a child occurs 355 * @param algorithmState The state in which the current pass of the algorithm is currently in 356 * @param ambientState The current ambient state 357 */ 358 private void updatePositionsForState(StackScrollState resultState, 359 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 360 361 // The starting position of the bottom stack peek 362 float bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize; 363 364 // The position where the bottom stack starts. 365 float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength; 366 367 // The y coordinate of the current child. 368 float currentYPosition = -algorithmState.scrollY; 369 370 int childCount = algorithmState.visibleChildren.size(); 371 int paddingAfterChild; 372 for (int i = 0; i < childCount; i++) { 373 ExpandableView child = algorithmState.visibleChildren.get(i); 374 StackViewState childViewState = resultState.getViewStateForView(child); 375 childViewState.location = StackViewState.LOCATION_UNKNOWN; 376 paddingAfterChild = getPaddingAfterChild(algorithmState, child); 377 int childHeight = getMaxAllowedChildHeight(child); 378 int minHeight = child.getMinHeight(); 379 childViewState.yTranslation = currentYPosition; 380 if (i == 0) { 381 updateFirstChildHeight(child, childViewState, childHeight, ambientState); 382 } 383 384 // The y position after this element 385 float nextYPosition = currentYPosition + childHeight + 386 paddingAfterChild; 387 if (nextYPosition >= bottomStackStart) { 388 // Case 1: 389 // We are in the bottom stack. 390 if (currentYPosition >= bottomStackStart) { 391 // According to the regular scroll view we are fully translated out of the 392 // bottom of the screen so we are fully in the bottom stack 393 updateStateForChildFullyInBottomStack(algorithmState, 394 bottomStackStart, childViewState, minHeight, ambientState, child); 395 } else { 396 // According to the regular scroll view we are currently translating out of / 397 // into the bottom of the screen 398 updateStateForChildTransitioningInBottom(algorithmState, 399 bottomStackStart, child, currentYPosition, 400 childViewState, childHeight); 401 } 402 } else { 403 // Case 2: 404 // We are in the regular scroll area. 405 childViewState.location = StackViewState.LOCATION_MAIN_AREA; 406 clampPositionToBottomStackStart(childViewState, childViewState.height, childHeight, 407 ambientState); 408 } 409 410 if (i == 0 && ambientState.getScrollY() <= 0) { 411 // The first card can get into the bottom stack if it's the only one 412 // on the lockscreen which pushes it up. Let's make sure that doesn't happen and 413 // it stays at the top 414 childViewState.yTranslation = Math.max(0, childViewState.yTranslation); 415 } 416 currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild; 417 if (currentYPosition <= 0) { 418 childViewState.location = StackViewState.LOCATION_HIDDEN_TOP; 419 } 420 if (childViewState.location == StackViewState.LOCATION_UNKNOWN) { 421 Log.wtf(LOG_TAG, "Failed to assign location for child " + i); 422 } 423 424 childViewState.yTranslation += ambientState.getTopPadding() 425 + ambientState.getStackTranslation(); 426 } 427 } 428 429 private int getPaddingAfterChild(StackScrollAlgorithmState algorithmState, 430 ExpandableView child) { 431 Float paddingValue = algorithmState.increasedPaddingMap.get(child); 432 return paddingValue == null 433 ? mPaddingBetweenElements 434 : (int) NotificationUtils.interpolate(mPaddingBetweenElements, 435 mIncreasedPaddingBetweenElements, 436 paddingValue); 437 } 438 439 private void updateHeadsUpStates(StackScrollState resultState, 440 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 441 int childCount = algorithmState.visibleChildren.size(); 442 ExpandableNotificationRow topHeadsUpEntry = null; 443 for (int i = 0; i < childCount; i++) { 444 View child = algorithmState.visibleChildren.get(i); 445 if (!(child instanceof ExpandableNotificationRow)) { 446 break; 447 } 448 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 449 if (!row.isHeadsUp()) { 450 break; 451 } 452 StackViewState childState = resultState.getViewStateForView(row); 453 if (topHeadsUpEntry == null) { 454 topHeadsUpEntry = row; 455 childState.location = StackViewState.LOCATION_FIRST_HUN; 456 } 457 boolean isTopEntry = topHeadsUpEntry == row; 458 float unmodifiedEndLocation = childState.yTranslation + childState.height; 459 if (mIsExpanded) { 460 // Ensure that the heads up is always visible even when scrolled off 461 clampHunToTop(ambientState, row, childState); 462 clampHunToMaxTranslation(ambientState, row, childState); 463 } 464 if (row.isPinned()) { 465 childState.yTranslation = Math.max(childState.yTranslation, 0); 466 childState.height = Math.max(row.getIntrinsicHeight(), childState.height); 467 StackViewState topState = resultState.getViewStateForView(topHeadsUpEntry); 468 if (!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 } 478 } 479 480 private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row, 481 StackViewState childState) { 482 float newTranslation = Math.max(ambientState.getTopPadding() 483 + ambientState.getStackTranslation(), childState.yTranslation); 484 childState.height = (int) Math.max(childState.height - (newTranslation 485 - childState.yTranslation), row.getMinHeight()); 486 childState.yTranslation = newTranslation; 487 } 488 489 private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, 490 StackViewState childState) { 491 float newTranslation; 492 float bottomPosition = ambientState.getMaxHeadsUpTranslation() - row.getMinHeight(); 493 newTranslation = Math.min(childState.yTranslation, bottomPosition); 494 childState.height = (int) Math.max(childState.height 495 - (childState.yTranslation - newTranslation), row.getMinHeight()); 496 childState.yTranslation = newTranslation; 497 } 498 499 /** 500 * Clamp the yTranslation of the child down such that its end is at most on the beginning of 501 * the bottom stack. 502 * 503 * @param childViewState the view state of the child 504 * @param childHeight the height of this child 505 * @param minHeight the minumum Height of the View 506 */ 507 private void clampPositionToBottomStackStart(StackViewState childViewState, 508 int childHeight, int minHeight, AmbientState ambientState) { 509 510 int bottomStackStart = ambientState.getInnerHeight() 511 - mBottomStackPeekSize - mCollapseSecondCardPadding; 512 int childStart = bottomStackStart - childHeight; 513 if (childStart < childViewState.yTranslation) { 514 float newHeight = bottomStackStart - childViewState.yTranslation; 515 if (newHeight < minHeight) { 516 newHeight = minHeight; 517 childViewState.yTranslation = bottomStackStart - minHeight; 518 } 519 childViewState.height = (int) newHeight; 520 } 521 } 522 523 private int getMaxAllowedChildHeight(View child) { 524 if (child instanceof ExpandableView) { 525 ExpandableView expandableView = (ExpandableView) child; 526 return expandableView.getIntrinsicHeight(); 527 } 528 return child == null? mCollapsedSize : child.getHeight(); 529 } 530 531 private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, 532 float transitioningPositionStart, ExpandableView child, float currentYPosition, 533 StackViewState childViewState, int childHeight) { 534 535 // This is the transitioning element on top of bottom stack, calculate how far we are in. 536 algorithmState.partialInBottom = 1.0f - ( 537 (transitioningPositionStart - currentYPosition) / (childHeight + 538 getPaddingAfterChild(algorithmState, child))); 539 540 // the offset starting at the transitionPosition of the bottom stack 541 float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom); 542 algorithmState.itemsInBottomStack += algorithmState.partialInBottom; 543 int newHeight = childHeight; 544 if (childHeight > child.getMinHeight()) { 545 newHeight = (int) Math.max(Math.min(transitioningPositionStart + offset - 546 getPaddingAfterChild(algorithmState, child) - currentYPosition, childHeight), 547 child.getMinHeight()); 548 childViewState.height = newHeight; 549 } 550 childViewState.yTranslation = transitioningPositionStart + offset - newHeight 551 - getPaddingAfterChild(algorithmState, child); 552 childViewState.location = StackViewState.LOCATION_MAIN_AREA; 553 } 554 555 private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, 556 float transitioningPositionStart, StackViewState childViewState, 557 int minHeight, AmbientState ambientState, ExpandableView child) { 558 float currentYPosition; 559 algorithmState.itemsInBottomStack += 1.0f; 560 if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) { 561 // We are visually entering the bottom stack 562 currentYPosition = transitioningPositionStart 563 + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack) 564 - getPaddingAfterChild(algorithmState, child); 565 childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_PEEKING; 566 } else { 567 // we are fully inside the stack 568 if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) { 569 childViewState.hidden = true; 570 childViewState.shadowAlpha = 0.0f; 571 } else if (algorithmState.itemsInBottomStack 572 > MAX_ITEMS_IN_BOTTOM_STACK + 1) { 573 childViewState.shadowAlpha = 1.0f - algorithmState.partialInBottom; 574 } 575 childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_HIDDEN; 576 currentYPosition = ambientState.getInnerHeight(); 577 } 578 childViewState.height = minHeight; 579 childViewState.yTranslation = currentYPosition - minHeight; 580 } 581 582 583 /** 584 * Update the height of the first child i.e clamp it to the bottom stack 585 * 586 * 587 588 * @param child the child to update 589 * @param childViewState the viewstate of the child 590 * @param childHeight the height of the child 591 * @param ambientState The ambient state of the algorithm 592 */ 593 private void updateFirstChildHeight(ExpandableView child, StackViewState childViewState, 594 int childHeight, AmbientState ambientState) { 595 596 // The starting position of the bottom stack peek 597 int bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize - 598 mCollapseSecondCardPadding + ambientState.getScrollY(); 599 // Collapse and expand the first child while the shade is being expanded 600 float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding 601 ? mFirstChildMaxHeight 602 : childHeight; 603 childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight), 604 child.getMinHeight()); 605 } 606 607 /** 608 * Calculate the Z positions for all children based on the number of items in both stacks and 609 * save it in the resultState 610 * @param resultState The result state to update the zTranslation values 611 * @param algorithmState The state in which the current pass of the algorithm is currently in 612 * @param ambientState The ambient state of the algorithm 613 */ 614 private void updateZValuesForState(StackScrollState resultState, 615 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 616 int childCount = algorithmState.visibleChildren.size(); 617 float childrenOnTop = 0.0f; 618 for (int i = childCount - 1; i >= 0; i--) { 619 ExpandableView child = algorithmState.visibleChildren.get(i); 620 StackViewState childViewState = resultState.getViewStateForView(child); 621 if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) { 622 // We are in the bottom stack 623 float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack); 624 float zSubtraction; 625 if (numItemsAbove <= 1.0f) { 626 float factor = 0.2f; 627 // Lets fade in slower to the threshold to make the shadow fade in look nicer 628 if (numItemsAbove <= factor) { 629 zSubtraction = FakeShadowView.SHADOW_SIBLING_TRESHOLD 630 * numItemsAbove * (1.0f / factor); 631 } else { 632 zSubtraction = FakeShadowView.SHADOW_SIBLING_TRESHOLD 633 + (numItemsAbove - factor) * (1.0f / (1.0f - factor)) 634 * (mZDistanceBetweenElements 635 - FakeShadowView.SHADOW_SIBLING_TRESHOLD); 636 } 637 } else { 638 zSubtraction = numItemsAbove * mZDistanceBetweenElements; 639 } 640 childViewState.zTranslation = mZBasicHeight - zSubtraction; 641 } else if (child.mustStayOnScreen() 642 && childViewState.yTranslation < ambientState.getTopPadding() 643 + ambientState.getStackTranslation()) { 644 if (childrenOnTop != 0.0f) { 645 childrenOnTop++; 646 } else { 647 float overlap = ambientState.getTopPadding() 648 + ambientState.getStackTranslation() - childViewState.yTranslation; 649 childrenOnTop += Math.min(1.0f, overlap / childViewState.height); 650 } 651 childViewState.zTranslation = mZBasicHeight 652 + childrenOnTop * mZDistanceBetweenElements; 653 } else { 654 childViewState.zTranslation = mZBasicHeight; 655 } 656 } 657 } 658 659 public void onExpansionStarted(StackScrollState currentState) { 660 mIsExpansionChanging = true; 661 mExpandedOnStart = mIsExpanded; 662 ViewGroup hostView = currentState.getHostView(); 663 updateFirstChildHeightWhileExpanding(hostView); 664 } 665 666 private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) { 667 mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView); 668 if (mFirstChildWhileExpanding != null) { 669 if (mExpandedOnStart) { 670 671 // We are collapsing the shade, so the first child can get as most as high as the 672 // current height or the end value of the animation. 673 mFirstChildMaxHeight = StackStateAnimator.getFinalActualHeight( 674 mFirstChildWhileExpanding); 675 } else { 676 updateFirstChildMaxSizeToMaxHeight(); 677 } 678 } else { 679 mFirstChildMaxHeight = 0; 680 } 681 } 682 683 private void updateFirstChildMaxSizeToMaxHeight() { 684 // We are expanding the shade, expand it to its full height. 685 if (!isMaxSizeInitialized(mFirstChildWhileExpanding)) { 686 687 // This child was not layouted yet, wait for a layout pass 688 mFirstChildWhileExpanding 689 .addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 690 @Override 691 public void onLayoutChange(View v, int left, int top, int right, 692 int bottom, int oldLeft, int oldTop, int oldRight, 693 int oldBottom) { 694 if (mFirstChildWhileExpanding != null) { 695 mFirstChildMaxHeight = getMaxAllowedChildHeight( 696 mFirstChildWhileExpanding); 697 } else { 698 mFirstChildMaxHeight = 0; 699 } 700 v.removeOnLayoutChangeListener(this); 701 } 702 }); 703 } else { 704 mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding); 705 } 706 } 707 708 private boolean isMaxSizeInitialized(ExpandableView child) { 709 if (child instanceof ExpandableNotificationRow) { 710 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 711 return row.isMaxExpandHeightInitialized(); 712 } 713 return child == null || child.getWidth() != 0; 714 } 715 716 private View findFirstVisibleChild(ViewGroup container) { 717 int childCount = container.getChildCount(); 718 for (int i = 0; i < childCount; i++) { 719 View child = container.getChildAt(i); 720 if (child.getVisibility() != View.GONE) { 721 return child; 722 } 723 } 724 return null; 725 } 726 727 public void onExpansionStopped() { 728 mIsExpansionChanging = false; 729 mFirstChildWhileExpanding = null; 730 } 731 732 public void setIsExpanded(boolean isExpanded) { 733 this.mIsExpanded = isExpanded; 734 } 735 736 public void notifyChildrenChanged(final NotificationStackScrollLayout hostView) { 737 if (mIsExpansionChanging) { 738 hostView.post(new Runnable() { 739 @Override 740 public void run() { 741 updateFirstChildHeightWhileExpanding(hostView); 742 } 743 }); 744 } 745 } 746 747 public void onReset(ExpandableView view) { 748 if (view.equals(mFirstChildWhileExpanding)) { 749 updateFirstChildMaxSizeToMaxHeight(); 750 } 751 } 752 753 class StackScrollAlgorithmState { 754 755 /** 756 * The scroll position of the algorithm 757 */ 758 public int scrollY; 759 760 /** 761 * The quantity of items which are in the bottom stack. 762 */ 763 public float itemsInBottomStack; 764 765 /** 766 * how far in is the element currently transitioning into the bottom stack 767 */ 768 public float partialInBottom; 769 770 /** 771 * The children from the host view which are not gone. 772 */ 773 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>(); 774 775 /** 776 * The children from the host that need an increased padding after them. A value of 0 means 777 * no increased padding, a value of 1 means full padding. 778 */ 779 public final HashMap<ExpandableView, Float> increasedPaddingMap = new HashMap<>(); 780 } 781 782} 783