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