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