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