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