StackScrollAlgorithm.java revision 61633a8db8dfdefa84b4444a7ae668d0e24f53e9
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 private static final int MAX_ITEMS_IN_TOP_STACK = 3; 43 44 private int mPaddingBetweenElements; 45 private int mIncreasedPaddingBetweenElements; 46 private int mCollapsedSize; 47 private int mTopStackPeekSize; 48 private int mBottomStackPeekSize; 49 private int mZDistanceBetweenElements; 50 private int mZBasicHeight; 51 52 private StackIndentationFunctor mTopStackIndentationFunctor; 53 private StackIndentationFunctor mBottomStackIndentationFunctor; 54 55 private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); 56 private boolean mIsExpansionChanging; 57 private int mFirstChildMaxHeight; 58 private boolean mIsExpanded; 59 private ExpandableView mFirstChildWhileExpanding; 60 private boolean mExpandedOnStart; 61 private int mTopStackTotalSize; 62 private int mBottomStackSlowDownLength; 63 private int mTopStackSlowDownLength; 64 private int mCollapseSecondCardPadding; 65 private ExpandableView mFirstChild; 66 private int mFirstChildMinHeight; 67 68 public StackScrollAlgorithm(Context context) { 69 initView(context); 70 } 71 72 public void initView(Context context) { 73 initConstants(context); 74 updatePadding(); 75 } 76 77 private void updatePadding() { 78 mTopStackTotalSize = mTopStackSlowDownLength + mPaddingBetweenElements 79 + mTopStackPeekSize; 80 mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( 81 MAX_ITEMS_IN_TOP_STACK, 82 mTopStackPeekSize, 83 mTopStackTotalSize - mTopStackPeekSize, 84 0.5f); 85 mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( 86 MAX_ITEMS_IN_BOTTOM_STACK, 87 mBottomStackPeekSize, 88 getBottomStackSlowDownLength(), 89 0.5f); 90 } 91 92 public int getBottomStackSlowDownLength() { 93 return mBottomStackSlowDownLength + mPaddingBetweenElements; 94 } 95 96 private void initConstants(Context context) { 97 mPaddingBetweenElements = Math.max(1, context.getResources() 98 .getDimensionPixelSize(R.dimen.notification_divider_height)); 99 mIncreasedPaddingBetweenElements = context.getResources() 100 .getDimensionPixelSize(R.dimen.notification_divider_height_increased); 101 mCollapsedSize = context.getResources() 102 .getDimensionPixelSize(R.dimen.notification_min_height); 103 mTopStackPeekSize = context.getResources() 104 .getDimensionPixelSize(R.dimen.top_stack_peek_amount); 105 mBottomStackPeekSize = context.getResources() 106 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); 107 mZDistanceBetweenElements = Math.max(1, context.getResources() 108 .getDimensionPixelSize(R.dimen.z_distance_between_notifications)); 109 mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements; 110 mBottomStackSlowDownLength = context.getResources() 111 .getDimensionPixelSize(R.dimen.bottom_stack_slow_down_length); 112 mTopStackSlowDownLength = context.getResources() 113 .getDimensionPixelSize(R.dimen.top_stack_slow_down_length); 114 mCollapseSecondCardPadding = context.getResources().getDimensionPixelSize( 115 R.dimen.notification_collapse_second_card_padding); 116 } 117 118 public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) { 119 // The state of the local variables are saved in an algorithmState to easily subdivide it 120 // into multiple phases. 121 StackScrollAlgorithmState algorithmState = mTempAlgorithmState; 122 123 // First we reset the view states to their default values. 124 resultState.resetViewStates(); 125 126 algorithmState.itemsInTopStack = 0.0f; 127 algorithmState.partialInTop = 0.0f; 128 algorithmState.lastTopStackIndex = 0; 129 algorithmState.scrolledPixelsTop = 0; 130 algorithmState.itemsInBottomStack = 0.0f; 131 algorithmState.partialInBottom = 0.0f; 132 mFirstChildMinHeight = mFirstChild == null ? 0 : mFirstChild.getMinHeight(); 133 float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */); 134 135 int scrollY = ambientState.getScrollY(); 136 137 // Due to the overScroller, the stackscroller can have negative scroll state. This is 138 // already accounted for by the top padding and doesn't need an additional adaption 139 scrollY = Math.max(0, scrollY); 140 algorithmState.scrollY = (int) (scrollY + mFirstChildMinHeight + bottomOverScroll); 141 142 initAlgorithmState(resultState, algorithmState); 143 144 // Phase 1: 145 findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState, ambientState); 146 147 // Phase 2: 148 updatePositionsForState(resultState, algorithmState, ambientState); 149 150 // Phase 3: 151 updateZValuesForState(resultState, algorithmState); 152 153 handleDraggedViews(ambientState, resultState, algorithmState); 154 updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState); 155 updateClipping(resultState, algorithmState, ambientState); 156 updateSpeedBumpState(resultState, algorithmState, ambientState.getSpeedBumpIndex()); 157 getNotificationChildrenStates(resultState, algorithmState); 158 } 159 160 private void getNotificationChildrenStates(StackScrollState resultState, 161 StackScrollAlgorithmState algorithmState) { 162 int childCount = algorithmState.visibleChildren.size(); 163 for (int i = 0; i < childCount; i++) { 164 ExpandableView v = algorithmState.visibleChildren.get(i); 165 if (v instanceof ExpandableNotificationRow) { 166 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 167 row.getChildrenStates(resultState); 168 } 169 } 170 } 171 172 private void updateSpeedBumpState(StackScrollState resultState, 173 StackScrollAlgorithmState algorithmState, int speedBumpIndex) { 174 int childCount = algorithmState.visibleChildren.size(); 175 for (int i = 0; i < childCount; i++) { 176 View child = algorithmState.visibleChildren.get(i); 177 StackViewState childViewState = resultState.getViewStateForView(child); 178 179 // The speed bump can also be gone, so equality needs to be taken when comparing 180 // indices. 181 childViewState.belowSpeedBump = speedBumpIndex != -1 && i >= speedBumpIndex; 182 } 183 } 184 185 private void updateClipping(StackScrollState resultState, 186 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 187 boolean dismissAllInProgress = ambientState.isDismissAllInProgress(); 188 float previousNotificationEnd = 0; 189 float previousNotificationStart = 0; 190 boolean previousNotificationIsSwiped = false; 191 int childCount = algorithmState.visibleChildren.size(); 192 for (int i = 0; i < childCount; i++) { 193 ExpandableView child = algorithmState.visibleChildren.get(i); 194 StackViewState state = resultState.getViewStateForView(child); 195 float newYTranslation = state.yTranslation; 196 float newHeight = state.height; 197 // apply clipping and shadow 198 float newNotificationEnd = newYTranslation + newHeight; 199 200 float clipHeight; 201 if (previousNotificationIsSwiped) { 202 // When the previous notification is swiped, we don't clip the content to the 203 // bottom of it. 204 clipHeight = newHeight; 205 } else { 206 clipHeight = newNotificationEnd - previousNotificationEnd; 207 clipHeight = Math.max(0.0f, clipHeight); 208 } 209 210 updateChildClippingAndBackground(state, newHeight, clipHeight, 211 newHeight - (previousNotificationStart - newYTranslation)); 212 213 if (dismissAllInProgress) { 214 state.clipTopAmount = Math.max(child.getMinClipTopAmount(), state.clipTopAmount); 215 } 216 217 if (!child.isTransparent()) { 218 // Only update the previous values if we are not transparent, 219 // otherwise we would clip to a transparent view. 220 if ((dismissAllInProgress && canChildBeDismissed(child))) { 221 previousNotificationIsSwiped = true; 222 } else { 223 previousNotificationIsSwiped = ambientState.getDraggedViews().contains(child); 224 previousNotificationEnd = newNotificationEnd; 225 previousNotificationStart = newYTranslation + state.clipTopAmount; 226 } 227 } 228 } 229 } 230 231 public static boolean canChildBeDismissed(View v) { 232 final View veto = v.findViewById(R.id.veto); 233 return (veto != null && veto.getVisibility() != View.GONE); 234 } 235 236 /** 237 * Updates the shadow outline and the clipping for a view. 238 * 239 * @param state the viewState to update 240 * @param realHeight the currently applied height of the view 241 * @param clipHeight the desired clip height, the rest of the view will be clipped from the top 242 * @param backgroundHeight the desired background height. The shadows of the view will be 243 * based on this height and the content will be clipped from the top 244 */ 245 private void updateChildClippingAndBackground(StackViewState state, float realHeight, 246 float clipHeight, float backgroundHeight) { 247 if (realHeight > clipHeight) { 248 // Rather overlap than create a hole. 249 state.topOverLap = (int) Math.floor(realHeight - clipHeight); 250 } else { 251 state.topOverLap = 0; 252 } 253 if (realHeight > backgroundHeight) { 254 // Rather overlap than create a hole. 255 state.clipTopAmount = (int) Math.floor(realHeight - backgroundHeight); 256 } else { 257 state.clipTopAmount = 0; 258 } 259 } 260 261 /** 262 * Updates the dimmed, activated and hiding sensitive states of the children. 263 */ 264 private void updateDimmedActivatedHideSensitive(AmbientState ambientState, 265 StackScrollState resultState, StackScrollAlgorithmState algorithmState) { 266 boolean dimmed = ambientState.isDimmed(); 267 boolean dark = ambientState.isDark(); 268 boolean hideSensitive = ambientState.isHideSensitive(); 269 View activatedChild = ambientState.getActivatedChild(); 270 int childCount = algorithmState.visibleChildren.size(); 271 for (int i = 0; i < childCount; i++) { 272 View child = algorithmState.visibleChildren.get(i); 273 StackViewState childViewState = resultState.getViewStateForView(child); 274 childViewState.dimmed = dimmed; 275 childViewState.dark = dark; 276 childViewState.hideSensitive = hideSensitive; 277 boolean isActivatedChild = activatedChild == child; 278 if (dimmed && isActivatedChild) { 279 childViewState.zTranslation += 2.0f * mZDistanceBetweenElements; 280 } 281 } 282 } 283 284 /** 285 * Handle the special state when views are being dragged 286 */ 287 private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState, 288 StackScrollAlgorithmState algorithmState) { 289 ArrayList<View> draggedViews = ambientState.getDraggedViews(); 290 for (View draggedView : draggedViews) { 291 int childIndex = algorithmState.visibleChildren.indexOf(draggedView); 292 if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) { 293 View nextChild = algorithmState.visibleChildren.get(childIndex + 1); 294 if (!draggedViews.contains(nextChild)) { 295 // only if the view is not dragged itself we modify its state to be fully 296 // visible 297 StackViewState viewState = resultState.getViewStateForView( 298 nextChild); 299 // The child below the dragged one must be fully visible 300 if (ambientState.isShadeExpanded()) { 301 viewState.shadowAlpha = 1; 302 viewState.hidden = false; 303 } 304 } 305 306 // Lets set the alpha to the one it currently has, as its currently being dragged 307 StackViewState viewState = resultState.getViewStateForView(draggedView); 308 // The dragged child should keep the set alpha 309 viewState.alpha = draggedView.getAlpha(); 310 } 311 } 312 } 313 314 /** 315 * Initialize the algorithm state like updating the visible children. 316 */ 317 private void initAlgorithmState(StackScrollState resultState, 318 StackScrollAlgorithmState state) { 319 ViewGroup hostView = resultState.getHostView(); 320 int childCount = hostView.getChildCount(); 321 state.visibleChildren.clear(); 322 state.visibleChildren.ensureCapacity(childCount); 323 state.increasedPaddingSet.clear(); 324 int notGoneIndex = 0; 325 ExpandableView lastView = null; 326 for (int i = 0; i < childCount; i++) { 327 ExpandableView v = (ExpandableView) hostView.getChildAt(i); 328 if (v.getVisibility() != View.GONE) { 329 notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v); 330 boolean needsIncreasedPadding = v.needsIncreasedPadding(); 331 if (needsIncreasedPadding) { 332 state.increasedPaddingSet.add(v); 333 if (lastView != null) { 334 state.increasedPaddingSet.add(lastView); 335 } 336 } 337 if (v instanceof ExpandableNotificationRow) { 338 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 339 340 // handle the notgoneIndex for the children as well 341 List<ExpandableNotificationRow> children = 342 row.getNotificationChildren(); 343 if (row.isSummaryWithChildren() && children != null) { 344 for (ExpandableNotificationRow childRow : children) { 345 if (childRow.getVisibility() != View.GONE) { 346 StackViewState childState 347 = resultState.getViewStateForView(childRow); 348 childState.notGoneIndex = notGoneIndex; 349 notGoneIndex++; 350 } 351 } 352 } 353 } 354 lastView = v; 355 } 356 } 357 } 358 359 private int updateNotGoneIndex(StackScrollState resultState, 360 StackScrollAlgorithmState state, int notGoneIndex, 361 ExpandableView v) { 362 StackViewState viewState = resultState.getViewStateForView(v); 363 viewState.notGoneIndex = notGoneIndex; 364 state.visibleChildren.add(v); 365 notGoneIndex++; 366 return notGoneIndex; 367 } 368 369 /** 370 * Determine the positions for the views. This is the main part of the algorithm. 371 * 372 * @param resultState The result state to update if a change to the properties of a child occurs 373 * @param algorithmState The state in which the current pass of the algorithm is currently in 374 * @param ambientState The current ambient state 375 */ 376 private void updatePositionsForState(StackScrollState resultState, 377 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 378 379 // The starting position of the bottom stack peek 380 float bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize; 381 382 // The position where the bottom stack starts. 383 float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength; 384 385 // The y coordinate of the current child. 386 float currentYPosition = 0.0f; 387 388 // How far in is the element currently transitioning into the bottom stack. 389 float yPositionInScrollView = 0.0f; 390 391 int childCount = algorithmState.visibleChildren.size(); 392 int numberOfElementsCompletelyIn = algorithmState.partialInTop == 1.0f 393 ? algorithmState.lastTopStackIndex 394 : (int) algorithmState.itemsInTopStack; 395 int paddingAfterChild; 396 for (int i = 0; i < childCount; i++) { 397 ExpandableView child = algorithmState.visibleChildren.get(i); 398 StackViewState childViewState = resultState.getViewStateForView(child); 399 childViewState.location = StackViewState.LOCATION_UNKNOWN; 400 paddingAfterChild = getPaddingAfterChild(algorithmState, child); 401 int childHeight = getMaxAllowedChildHeight(child); 402 int minHeight = child.getMinHeight(); 403 float yPositionInScrollViewAfterElement = yPositionInScrollView 404 + childHeight 405 + paddingAfterChild; 406 float scrollOffset = yPositionInScrollView - algorithmState.scrollY + 407 mFirstChildMinHeight; 408 409 if (i == algorithmState.lastTopStackIndex + 1) { 410 // Normally the position of this child is the position in the regular scrollview, 411 // but if the two stacks are very close to each other, 412 // then have have to push it even more upwards to the position of the bottom 413 // stack start. 414 currentYPosition = Math.min(scrollOffset, bottomStackStart); 415 } 416 childViewState.yTranslation = currentYPosition; 417 418 // The y position after this element 419 float nextYPosition = currentYPosition + childHeight + 420 paddingAfterChild; 421 422 if (i <= algorithmState.lastTopStackIndex) { 423 // Case 1: 424 // We are in the top Stack 425 updateStateForTopStackChild(algorithmState, child, 426 numberOfElementsCompletelyIn, i, childHeight, childViewState, scrollOffset); 427 clampPositionToTopStackEnd(childViewState, childHeight); 428 429 // check if we are overlapping with the bottom stack 430 if (childViewState.yTranslation + childHeight + paddingAfterChild 431 >= bottomStackStart && !mIsExpansionChanging && i != 0) { 432 // we just collapse this element slightly 433 int newSize = (int) Math.max(bottomStackStart - paddingAfterChild - 434 childViewState.yTranslation, minHeight); 435 childViewState.height = newSize; 436 updateStateForChildTransitioningInBottom(algorithmState, bottomStackStart, 437 child, childViewState.yTranslation, childViewState, 438 childHeight); 439 } 440 clampPositionToBottomStackStart(childViewState, childViewState.height, 441 minHeight, ambientState); 442 } else if (nextYPosition >= bottomStackStart) { 443 // Case 2: 444 // We are in the bottom stack. 445 if (currentYPosition >= bottomStackStart) { 446 // According to the regular scroll view we are fully translated out of the 447 // bottom of the screen so we are fully in the bottom stack 448 updateStateForChildFullyInBottomStack(algorithmState, 449 bottomStackStart, childViewState, minHeight, ambientState, child); 450 } else { 451 // According to the regular scroll view we are currently translating out of / 452 // into the bottom of the screen 453 updateStateForChildTransitioningInBottom(algorithmState, 454 bottomStackStart, child, currentYPosition, 455 childViewState, childHeight); 456 } 457 } else { 458 // Case 3: 459 // We are in the regular scroll area. 460 childViewState.location = StackViewState.LOCATION_MAIN_AREA; 461 clampYTranslation(childViewState, childHeight, ambientState); 462 } 463 464 // The first card is always rendered. 465 if (i == 0) { 466 childViewState.hidden = false; 467 childViewState.shadowAlpha = 1.0f; 468 childViewState.yTranslation = Math.max( 469 mFirstChildMinHeight - algorithmState.scrollY, 0); 470 if (childViewState.yTranslation + childViewState.height 471 > bottomPeekStart - mCollapseSecondCardPadding) { 472 childViewState.height = (int) Math.max( 473 bottomPeekStart - mCollapseSecondCardPadding 474 - childViewState.yTranslation, mFirstChildMinHeight); 475 } 476 childViewState.location = StackViewState.LOCATION_FIRST_CARD; 477 } 478 if (childViewState.location == StackViewState.LOCATION_UNKNOWN) { 479 Log.wtf(LOG_TAG, "Failed to assign location for child " + i); 480 } 481 currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild; 482 yPositionInScrollView = yPositionInScrollViewAfterElement; 483 484 childViewState.yTranslation += ambientState.getTopPadding() 485 + ambientState.getStackTranslation(); 486 } 487 updateHeadsUpStates(resultState, algorithmState, ambientState); 488 } 489 490 private int getPaddingAfterChild(StackScrollAlgorithmState algorithmState, 491 ExpandableView child) { 492 return algorithmState.increasedPaddingSet.contains(child) 493 ? mIncreasedPaddingBetweenElements 494 : mPaddingBetweenElements; 495 } 496 497 private void updateHeadsUpStates(StackScrollState resultState, 498 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 499 int childCount = algorithmState.visibleChildren.size(); 500 ExpandableNotificationRow topHeadsUpEntry = null; 501 for (int i = 0; i < childCount; i++) { 502 View child = algorithmState.visibleChildren.get(i); 503 if (!(child instanceof ExpandableNotificationRow)) { 504 break; 505 } 506 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 507 if (!row.isHeadsUp()) { 508 break; 509 } else if (topHeadsUpEntry == null) { 510 topHeadsUpEntry = row; 511 } 512 StackViewState childState = resultState.getViewStateForView(row); 513 boolean isTopEntry = topHeadsUpEntry == row; 514 if (mIsExpanded) { 515 // Ensure that the heads up is always visible even when scrolled off from the bottom 516 float bottomPosition = ambientState.getMaxHeadsUpTranslation() - childState.height; 517 childState.yTranslation = Math.min(childState.yTranslation, 518 bottomPosition); 519 } 520 if (row.isPinned()) { 521 childState.yTranslation = Math.max(childState.yTranslation, 0); 522 childState.height = Math.max(row.getIntrinsicHeight(), childState.height); 523 if (!isTopEntry) { 524 // Ensure that a headsUp doesn't vertically extend further than the heads-up at 525 // the top most z-position 526 StackViewState topState = resultState.getViewStateForView(topHeadsUpEntry); 527 childState.height = row.getIntrinsicHeight(); 528 childState.yTranslation = topState.yTranslation + topState.height 529 - childState.height; 530 } 531 } 532 } 533 } 534 535 /** 536 * Clamp the yTranslation both up and down to valid positions. 537 * 538 * @param childViewState the view state of the child 539 * @param minHeight the minimum height of this child 540 */ 541 private void clampYTranslation(StackViewState childViewState, int minHeight, 542 AmbientState ambientState) { 543 clampPositionToBottomStackStart(childViewState, childViewState.height, minHeight, 544 ambientState); 545 clampPositionToTopStackEnd(childViewState, childViewState.height); 546 } 547 548 /** 549 * Clamp the yTranslation of the child down such that its end is at most on the beginning of 550 * the bottom stack. 551 * 552 * @param childViewState the view state of the child 553 * @param childHeight the height of this child 554 * @param minHeight the minumum Height of the View 555 */ 556 private void clampPositionToBottomStackStart(StackViewState childViewState, 557 int childHeight, int minHeight, AmbientState ambientState) { 558 559 int bottomStackStart = ambientState.getInnerHeight() 560 - mBottomStackPeekSize - mCollapseSecondCardPadding; 561 int childStart = bottomStackStart - childHeight; 562 if (childStart < childViewState.yTranslation) { 563 float newHeight = bottomStackStart - childViewState.yTranslation; 564 if (newHeight < minHeight) { 565 newHeight = minHeight; 566 childViewState.yTranslation = bottomStackStart - minHeight; 567 } 568 childViewState.height = (int) newHeight; 569 } 570 } 571 572 /** 573 * Clamp the yTranslation of the child up such that its end is at lest on the end of the top 574 * stack. 575 * 576 * @param childViewState the view state of the child 577 * @param childHeight the height of this child 578 */ 579 private void clampPositionToTopStackEnd(StackViewState childViewState, 580 int childHeight) { 581 childViewState.yTranslation = Math.max(childViewState.yTranslation, 582 mFirstChildMinHeight - childHeight); 583 } 584 585 private int getMaxAllowedChildHeight(View child) { 586 if (child instanceof ExpandableView) { 587 ExpandableView expandableView = (ExpandableView) child; 588 return expandableView.getIntrinsicHeight(); 589 } 590 return child == null? mCollapsedSize : child.getHeight(); 591 } 592 593 private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, 594 float transitioningPositionStart, ExpandableView child, float currentYPosition, 595 StackViewState childViewState, int childHeight) { 596 597 // This is the transitioning element on top of bottom stack, calculate how far we are in. 598 algorithmState.partialInBottom = 1.0f - ( 599 (transitioningPositionStart - currentYPosition) / (childHeight + 600 getPaddingAfterChild(algorithmState, child))); 601 602 // the offset starting at the transitionPosition of the bottom stack 603 float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom); 604 algorithmState.itemsInBottomStack += algorithmState.partialInBottom; 605 int newHeight = childHeight; 606 if (childHeight > child.getMinHeight()) { 607 newHeight = (int) Math.max(Math.min(transitioningPositionStart + offset - 608 getPaddingAfterChild(algorithmState, child) - currentYPosition, childHeight), 609 child.getMinHeight()); 610 childViewState.height = newHeight; 611 } 612 childViewState.yTranslation = transitioningPositionStart + offset - newHeight 613 - getPaddingAfterChild(algorithmState, child); 614 615 // We want at least to be at the end of the top stack when collapsing 616 clampPositionToTopStackEnd(childViewState, newHeight); 617 childViewState.location = StackViewState.LOCATION_MAIN_AREA; 618 } 619 620 private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, 621 float transitioningPositionStart, StackViewState childViewState, 622 int minHeight, AmbientState ambientState, ExpandableView child) { 623 float currentYPosition; 624 algorithmState.itemsInBottomStack += 1.0f; 625 if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) { 626 // We are visually entering the bottom stack 627 currentYPosition = transitioningPositionStart 628 + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack) 629 - getPaddingAfterChild(algorithmState, child); 630 childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_PEEKING; 631 } else { 632 // we are fully inside the stack 633 if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) { 634 childViewState.hidden = true; 635 childViewState.shadowAlpha = 0.0f; 636 } else if (algorithmState.itemsInBottomStack 637 > MAX_ITEMS_IN_BOTTOM_STACK + 1) { 638 childViewState.shadowAlpha = 1.0f - algorithmState.partialInBottom; 639 } 640 childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_HIDDEN; 641 currentYPosition = ambientState.getInnerHeight(); 642 } 643 childViewState.height = minHeight; 644 childViewState.yTranslation = currentYPosition - minHeight; 645 clampPositionToTopStackEnd(childViewState, minHeight); 646 } 647 648 private void updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, 649 ExpandableView child, int numberOfElementsCompletelyIn, int i, int childHeight, 650 StackViewState childViewState, float scrollOffset) { 651 652 653 // First we calculate the index relative to the current stack window of size at most 654 // {@link #MAX_ITEMS_IN_TOP_STACK} 655 int paddedIndex = i - 1 656 - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0); 657 if (paddedIndex >= 0) { 658 659 // We are currently visually entering the top stack 660 float distanceToStack = (childHeight + getPaddingAfterChild(algorithmState, child)) 661 - algorithmState.scrolledPixelsTop; 662 if (i == algorithmState.lastTopStackIndex 663 && distanceToStack > (mTopStackTotalSize 664 + getPaddingAfterChild(algorithmState, child))) { 665 666 // Child is currently translating into stack but not yet inside slow down zone. 667 // Handle it like the regular scrollview. 668 childViewState.yTranslation = scrollOffset; 669 } else { 670 // Apply stacking logic. 671 float numItemsBefore; 672 if (i == algorithmState.lastTopStackIndex) { 673 numItemsBefore = 1.0f 674 - (distanceToStack / (mTopStackTotalSize 675 + getPaddingAfterChild(algorithmState, child))); 676 } else { 677 numItemsBefore = algorithmState.itemsInTopStack - i; 678 } 679 // The end position of the current child 680 float currentChildEndY = mFirstChildMinHeight + mTopStackTotalSize 681 - mTopStackIndentationFunctor.getValue(numItemsBefore); 682 childViewState.yTranslation = currentChildEndY - childHeight; 683 } 684 childViewState.location = StackViewState.LOCATION_TOP_STACK_PEEKING; 685 } else { 686 if (paddedIndex == -1) { 687 childViewState.shadowAlpha = 1.0f - algorithmState.partialInTop; 688 } else { 689 // We are hidden behind the top card and faded out, so we can hide ourselves. 690 childViewState.hidden = true; 691 childViewState.shadowAlpha = 0.0f; 692 } 693 childViewState.yTranslation = mFirstChildMinHeight - childHeight; 694 childViewState.location = StackViewState.LOCATION_TOP_STACK_HIDDEN; 695 } 696 697 698 } 699 700 /** 701 * Find the number of items in the top stack and update the result state if needed. 702 * 703 * @param resultState The result state to update if a height change of an child occurs 704 * @param algorithmState The state in which the current pass of the algorithm is currently in 705 */ 706 private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState, 707 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 708 709 // The y Position if the element would be in a regular scrollView 710 float yPositionInScrollView = 0.0f; 711 int childCount = algorithmState.visibleChildren.size(); 712 // find the number of elements in the top stack. 713 for (int i = 0; i < childCount; i++) { 714 ExpandableView child = algorithmState.visibleChildren.get(i); 715 StackViewState childViewState = resultState.getViewStateForView(child); 716 int childHeight = getMaxAllowedChildHeight(child); 717 int paddingAfterChild = getPaddingAfterChild(algorithmState, child); 718 float yPositionInScrollViewAfterElement = yPositionInScrollView 719 + childHeight 720 + paddingAfterChild; 721 if (yPositionInScrollView < algorithmState.scrollY) { 722 if (i == 0 && algorithmState.scrollY <= mFirstChildMinHeight) { 723 724 // The starting position of the bottom stack peek 725 int bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize - 726 mCollapseSecondCardPadding; 727 // Collapse and expand the first child while the shade is being expanded 728 float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding 729 ? mFirstChildMaxHeight 730 : childHeight; 731 childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight), 732 mFirstChildMinHeight); 733 algorithmState.itemsInTopStack = 1.0f; 734 735 } else if (yPositionInScrollViewAfterElement < algorithmState.scrollY) { 736 // According to the regular scroll view we are fully off screen 737 algorithmState.itemsInTopStack += 1.0f; 738 if (i == 0) { 739 childViewState.height = child.getMinHeight(); 740 } 741 } else { 742 // According to the regular scroll view we are partially off screen 743 744 // How much did we scroll into this child 745 algorithmState.scrolledPixelsTop = algorithmState.scrollY 746 - yPositionInScrollView; 747 algorithmState.partialInTop = (algorithmState.scrolledPixelsTop) / (childHeight 748 + paddingAfterChild); 749 750 // Our element can be expanded, so this can get negative 751 algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop); 752 algorithmState.itemsInTopStack += algorithmState.partialInTop; 753 754 if (i == 0) { 755 // If it is expanded we have to collapse it to a new size 756 float newSize = yPositionInScrollViewAfterElement 757 - paddingAfterChild 758 - algorithmState.scrollY + mFirstChildMinHeight; 759 newSize = Math.max(mFirstChildMinHeight, newSize); 760 algorithmState.itemsInTopStack = 1.0f; 761 childViewState.height = (int) newSize; 762 } 763 algorithmState.lastTopStackIndex = i; 764 break; 765 } 766 } else { 767 algorithmState.lastTopStackIndex = i - 1; 768 // We are already past the stack so we can end the loop 769 break; 770 } 771 yPositionInScrollView = yPositionInScrollViewAfterElement; 772 } 773 } 774 775 /** 776 * Calculate the Z positions for all children based on the number of items in both stacks and 777 * save it in the resultState 778 * 779 * @param resultState The result state to update the zTranslation values 780 * @param algorithmState The state in which the current pass of the algorithm is currently in 781 */ 782 private void updateZValuesForState(StackScrollState resultState, 783 StackScrollAlgorithmState algorithmState) { 784 int childCount = algorithmState.visibleChildren.size(); 785 for (int i = 0; i < childCount; i++) { 786 View child = algorithmState.visibleChildren.get(i); 787 StackViewState childViewState = resultState.getViewStateForView(child); 788 if (i < algorithmState.itemsInTopStack) { 789 float stackIndex = algorithmState.itemsInTopStack - i; 790 791 // Ensure that the topmost item is a little bit higher than the rest when fully 792 // scrolled, to avoid drawing errors when swiping it out 793 float max = MAX_ITEMS_IN_TOP_STACK + (i == 0 ? 2.5f : 2); 794 stackIndex = Math.min(stackIndex, max); 795 if (i == 0 && algorithmState.itemsInTopStack < 2.0f) { 796 797 // We only have the top item and an additional item in the top stack, 798 // Interpolate the index from 0 to 2 while the second item is 799 // translating in. 800 stackIndex -= 1.0f; 801 if (algorithmState.scrollY > mFirstChildMinHeight) { 802 803 // Since there is a shadow treshhold, we cant just interpolate from 0 to 804 // 2 but we interpolate from 0.1f to 2.0f when scrolled in. The jump in 805 // height will not be noticable since we have padding in between. 806 stackIndex = 0.1f + stackIndex * 1.9f; 807 } 808 } 809 childViewState.zTranslation = mZBasicHeight 810 + stackIndex * mZDistanceBetweenElements; 811 } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) { 812 float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack); 813 float translationZ = mZBasicHeight 814 - numItemsAbove * mZDistanceBetweenElements; 815 childViewState.zTranslation = translationZ; 816 } else { 817 childViewState.zTranslation = mZBasicHeight; 818 } 819 } 820 } 821 822 public void onExpansionStarted(StackScrollState currentState) { 823 mIsExpansionChanging = true; 824 mExpandedOnStart = mIsExpanded; 825 ViewGroup hostView = currentState.getHostView(); 826 updateFirstChildHeightWhileExpanding(hostView); 827 } 828 829 private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) { 830 mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView); 831 if (mFirstChildWhileExpanding != null) { 832 if (mExpandedOnStart) { 833 834 // We are collapsing the shade, so the first child can get as most as high as the 835 // current height or the end value of the animation. 836 mFirstChildMaxHeight = StackStateAnimator.getFinalActualHeight( 837 mFirstChildWhileExpanding); 838 } else { 839 updateFirstChildMaxSizeToMaxHeight(); 840 } 841 } else { 842 mFirstChildMaxHeight = 0; 843 } 844 } 845 846 private void updateFirstChildMaxSizeToMaxHeight() { 847 // We are expanding the shade, expand it to its full height. 848 if (!isMaxSizeInitialized(mFirstChildWhileExpanding)) { 849 850 // This child was not layouted yet, wait for a layout pass 851 mFirstChildWhileExpanding 852 .addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 853 @Override 854 public void onLayoutChange(View v, int left, int top, int right, 855 int bottom, int oldLeft, int oldTop, int oldRight, 856 int oldBottom) { 857 if (mFirstChildWhileExpanding != null) { 858 mFirstChildMaxHeight = getMaxAllowedChildHeight( 859 mFirstChildWhileExpanding); 860 } else { 861 mFirstChildMaxHeight = 0; 862 } 863 v.removeOnLayoutChangeListener(this); 864 } 865 }); 866 } else { 867 mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding); 868 } 869 } 870 871 private boolean isMaxSizeInitialized(ExpandableView child) { 872 if (child instanceof ExpandableNotificationRow) { 873 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 874 return row.isMaxExpandHeightInitialized(); 875 } 876 return child == null || child.getWidth() != 0; 877 } 878 879 private View findFirstVisibleChild(ViewGroup container) { 880 int childCount = container.getChildCount(); 881 for (int i = 0; i < childCount; i++) { 882 View child = container.getChildAt(i); 883 if (child.getVisibility() != View.GONE) { 884 return child; 885 } 886 } 887 return null; 888 } 889 890 public void onExpansionStopped() { 891 mIsExpansionChanging = false; 892 mFirstChildWhileExpanding = null; 893 } 894 895 public void setIsExpanded(boolean isExpanded) { 896 this.mIsExpanded = isExpanded; 897 } 898 899 public void notifyChildrenChanged(final NotificationStackScrollLayout hostView) { 900 mFirstChild = hostView.getFirstChildNotGone(); 901 if (mIsExpansionChanging) { 902 hostView.post(new Runnable() { 903 @Override 904 public void run() { 905 updateFirstChildHeightWhileExpanding(hostView); 906 } 907 }); 908 } 909 } 910 911 public void onReset(ExpandableView view) { 912 if (view.equals(mFirstChildWhileExpanding)) { 913 updateFirstChildMaxSizeToMaxHeight(); 914 } 915 } 916 917 class StackScrollAlgorithmState { 918 919 /** 920 * The scroll position of the algorithm 921 */ 922 public int scrollY; 923 924 /** 925 * The quantity of items which are in the top stack. 926 */ 927 public float itemsInTopStack; 928 929 /** 930 * how far in is the element currently transitioning into the top stack 931 */ 932 public float partialInTop; 933 934 /** 935 * The number of pixels the last child in the top stack has scrolled in to the stack 936 */ 937 public float scrolledPixelsTop; 938 939 /** 940 * The last item index which is in the top stack. 941 */ 942 public int lastTopStackIndex; 943 944 /** 945 * The quantity of items which are in the bottom stack. 946 */ 947 public float itemsInBottomStack; 948 949 /** 950 * how far in is the element currently transitioning into the bottom stack 951 */ 952 public float partialInBottom; 953 954 /** 955 * The children from the host view which are not gone. 956 */ 957 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>(); 958 959 /** 960 * The children from the host that need an increased padding after them. 961 */ 962 public final HashSet<ExpandableView> increasedPaddingSet = new HashSet<>(); 963 } 964 965} 966