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