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