StackScrollAlgorithm.java revision 684a442b812a5e95d813700ffa2fd17ca72048a7
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 (!NotificationStackScrollLayout.isPinnedHeadsUp(draggedView) 315 || NotificationStackScrollLayout.isPinnedHeadsUp(nextChild)) { 316 viewState.alpha = 1; 317 } 318 } 319 320 // Lets set the alpha to the one it currently has, as its currently being dragged 321 StackViewState viewState = resultState.getViewStateForView(draggedView); 322 // The dragged child should keep the set alpha 323 viewState.alpha = draggedView.getAlpha(); 324 } 325 } 326 } 327 328 /** 329 * Update the visible children on the state. 330 */ 331 private void updateVisibleChildren(StackScrollState resultState, 332 StackScrollAlgorithmState state) { 333 ViewGroup hostView = resultState.getHostView(); 334 int childCount = hostView.getChildCount(); 335 state.visibleChildren.clear(); 336 state.visibleChildren.ensureCapacity(childCount); 337 int notGoneIndex = 0; 338 for (int i = 0; i < childCount; i++) { 339 ExpandableView v = (ExpandableView) hostView.getChildAt(i); 340 if (v.getVisibility() != View.GONE) { 341 notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v); 342 if (v instanceof ExpandableNotificationRow) { 343 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 344 345 // handle the notgoneIndex for the children as well 346 List<ExpandableNotificationRow> children = 347 row.getNotificationChildren(); 348 if (row.areChildrenExpanded() && children != null) { 349 for (ExpandableNotificationRow childRow : children) { 350 if (childRow.getVisibility() != View.GONE) { 351 StackViewState childState 352 = resultState.getViewStateForView(childRow); 353 childState.notGoneIndex = notGoneIndex; 354 notGoneIndex++; 355 } 356 } 357 } 358 } 359 } 360 } 361 } 362 363 private int updateNotGoneIndex(StackScrollState resultState, 364 StackScrollAlgorithmState state, int notGoneIndex, 365 ExpandableView v) { 366 StackViewState viewState = resultState.getViewStateForView(v); 367 viewState.notGoneIndex = notGoneIndex; 368 state.visibleChildren.add(v); 369 notGoneIndex++; 370 return notGoneIndex; 371 } 372 373 /** 374 * Determine the positions for the views. This is the main part of the algorithm. 375 * 376 * @param resultState The result state to update if a change to the properties of a child occurs 377 * @param algorithmState The state in which the current pass of the algorithm is currently in 378 * @param ambientState The current ambient state 379 */ 380 private void updatePositionsForState(StackScrollState resultState, 381 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 382 383 // The starting position of the bottom stack peek 384 float bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize; 385 386 // The position where the bottom stack starts. 387 float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength; 388 389 // The y coordinate of the current child. 390 float currentYPosition = 0.0f; 391 392 // How far in is the element currently transitioning into the bottom stack. 393 float yPositionInScrollView = 0.0f; 394 395 // If we have a heads-up higher than the collapsed height we need to add the difference to 396 // the padding of all other elements, i.e push in the top stack slightly. 397 ExpandableNotificationRow topHeadsUpEntry = ambientState.getTopHeadsUpEntry(); 398 399 int childCount = algorithmState.visibleChildren.size(); 400 int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack; 401 for (int i = 0; i < childCount; i++) { 402 ExpandableView child = algorithmState.visibleChildren.get(i); 403 StackViewState childViewState = resultState.getViewStateForView(child); 404 childViewState.location = StackViewState.LOCATION_UNKNOWN; 405 int childHeight = getMaxAllowedChildHeight(child, ambientState); 406 float yPositionInScrollViewAfterElement = yPositionInScrollView 407 + childHeight 408 + mPaddingBetweenElements; 409 float scrollOffset = yPositionInScrollView - algorithmState.scrollY + mCollapsedSize; 410 411 if (i == algorithmState.lastTopStackIndex + 1) { 412 // Normally the position of this child is the position in the regular scrollview, 413 // but if the two stacks are very close to each other, 414 // then have have to push it even more upwards to the position of the bottom 415 // stack start. 416 currentYPosition = Math.min(scrollOffset, bottomStackStart); 417 } 418 childViewState.yTranslation = currentYPosition; 419 420 // The y position after this element 421 float nextYPosition = currentYPosition + childHeight + 422 mPaddingBetweenElements; 423 424 if (i <= algorithmState.lastTopStackIndex) { 425 // Case 1: 426 // We are in the top Stack 427 updateStateForTopStackChild(algorithmState, 428 numberOfElementsCompletelyIn, i, childHeight, childViewState, scrollOffset); 429 clampPositionToTopStackEnd(childViewState, childHeight); 430 431 // check if we are overlapping with the bottom stack 432 if (childViewState.yTranslation + childHeight + mPaddingBetweenElements 433 >= bottomStackStart && !mIsExpansionChanging && i != 0 && mIsSmallScreen) { 434 // we just collapse this element slightly 435 int newSize = (int) Math.max(bottomStackStart - mPaddingBetweenElements - 436 childViewState.yTranslation, mCollapsedSize); 437 childViewState.height = newSize; 438 updateStateForChildTransitioningInBottom(algorithmState, bottomStackStart, 439 bottomPeekStart, childViewState.yTranslation, childViewState, 440 childHeight); 441 } 442 clampPositionToBottomStackStart(childViewState, childViewState.height, 443 ambientState); 444 } else if (nextYPosition >= bottomStackStart) { 445 // Case 2: 446 // We are in the bottom stack. 447 if (currentYPosition >= bottomStackStart) { 448 // According to the regular scroll view we are fully translated out of the 449 // bottom of the screen so we are fully in the bottom stack 450 updateStateForChildFullyInBottomStack(algorithmState, 451 bottomStackStart, childViewState, childHeight, ambientState); 452 } else { 453 // According to the regular scroll view we are currently translating out of / 454 // into the bottom of the screen 455 updateStateForChildTransitioningInBottom(algorithmState, 456 bottomStackStart, bottomPeekStart, currentYPosition, 457 childViewState, childHeight); 458 } 459 } else { 460 // Case 3: 461 // We are in the regular scroll area. 462 childViewState.location = StackViewState.LOCATION_MAIN_AREA; 463 clampYTranslation(childViewState, childHeight, ambientState); 464 } 465 466 // The first card is always rendered. 467 if (i == 0) { 468 childViewState.alpha = 1.0f; 469 childViewState.yTranslation = Math.max(mCollapsedSize - algorithmState.scrollY, 0); 470 if (childViewState.yTranslation + childViewState.height 471 > bottomPeekStart - mCollapseSecondCardPadding) { 472 childViewState.height = (int) Math.max( 473 bottomPeekStart - mCollapseSecondCardPadding 474 - childViewState.yTranslation, mCollapsedSize); 475 } 476 childViewState.location = StackViewState.LOCATION_FIRST_CARD; 477 } 478 if (childViewState.location == StackViewState.LOCATION_UNKNOWN) { 479 Log.wtf(LOG_TAG, "Failed to assign location for child " + i); 480 } 481 currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements; 482 yPositionInScrollView = yPositionInScrollViewAfterElement; 483 484 if (ambientState.isShadeExpanded() && topHeadsUpEntry != null 485 && child != topHeadsUpEntry) { 486 childViewState.yTranslation += topHeadsUpEntry.getHeadsUpHeight() - mCollapsedSize; 487 } 488 childViewState.yTranslation += ambientState.getTopPadding() 489 + ambientState.getStackTranslation(); 490 } 491 updateHeadsUpStates(resultState, algorithmState, ambientState); 492 } 493 494 private void updateHeadsUpStates(StackScrollState resultState, 495 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 496 int childCount = algorithmState.visibleChildren.size(); 497 ExpandableNotificationRow topHeadsUpEntry = null; 498 for (int i = 0; i < childCount; i++) { 499 View child = algorithmState.visibleChildren.get(i); 500 if (!(child instanceof ExpandableNotificationRow)) { 501 break; 502 } 503 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 504 if (!row.isHeadsUp()) { 505 break; 506 } else if (topHeadsUpEntry == null) { 507 topHeadsUpEntry = row; 508 } 509 StackViewState childState = resultState.getViewStateForView(row); 510 boolean isTopEntry = topHeadsUpEntry == row; 511 if (row.isPinned()) { 512 childState.yTranslation = 0; 513 childState.height = row.getHeadsUpHeight(); 514 if (!isTopEntry) { 515 // Ensure that a headsUp doesn't vertically extend further than the heads-up at 516 // the top most z-position 517 StackViewState topState = resultState.getViewStateForView(topHeadsUpEntry); 518 childState.height = row.getHeadsUpHeight(); 519 childState.yTranslation = topState.yTranslation + topState.height 520 - childState.height; 521 } 522 } else if (mIsExpanded) { 523 if (isTopEntry) { 524 childState.height += row.getHeadsUpHeight() - mCollapsedSize; 525 } 526 childState.height = Math.max(childState.height, row.getHeadsUpHeight()); 527 // Ensure that the heads up is always visible even when scrolled of from the bottom 528 float bottomPosition = ambientState.getMaxHeadsUpTranslation() - childState.height; 529 childState.yTranslation = Math.min(childState.yTranslation, 530 bottomPosition); 531 } 532 } 533 } 534 535 /** 536 * Clamp the yTranslation both up and down to valid positions. 537 * 538 * @param childViewState the view state of the child 539 * @param childHeight the height of this child 540 */ 541 private void clampYTranslation(StackViewState childViewState, int childHeight, 542 AmbientState ambientState) { 543 clampPositionToBottomStackStart(childViewState, childHeight, ambientState); 544 clampPositionToTopStackEnd(childViewState, childHeight); 545 } 546 547 /** 548 * Clamp the yTranslation of the child down such that its end is at most on the beginning of 549 * the bottom stack. 550 * 551 * @param childViewState the view state of the child 552 * @param childHeight the height of this child 553 */ 554 private void clampPositionToBottomStackStart(StackViewState childViewState, 555 int childHeight, AmbientState ambientState) { 556 childViewState.yTranslation = Math.min(childViewState.yTranslation, 557 ambientState.getInnerHeight() - mBottomStackPeekSize - mCollapseSecondCardPadding 558 - childHeight); 559 } 560 561 /** 562 * Clamp the yTranslation of the child up such that its end is at lest on the end of the top 563 * stack. 564 * 565 * @param childViewState the view state of the child 566 * @param childHeight the height of this child 567 */ 568 private void clampPositionToTopStackEnd(StackViewState childViewState, 569 int childHeight) { 570 childViewState.yTranslation = Math.max(childViewState.yTranslation, 571 mCollapsedSize - childHeight); 572 } 573 574 private int getMaxAllowedChildHeight(View child, AmbientState ambientState) { 575 if (child instanceof ExpandableNotificationRow) { 576 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 577 if (ambientState == null && row.isHeadsUp() 578 || ambientState != null && ambientState.getTopHeadsUpEntry() == child) { 579 int extraSize = row.getIntrinsicHeight() - row.getHeadsUpHeight(); 580 return mCollapsedSize + extraSize; 581 } 582 return row.getIntrinsicHeight(); 583 } else if (child instanceof ExpandableView) { 584 ExpandableView expandableView = (ExpandableView) child; 585 return expandableView.getActualHeight(); 586 } 587 return child == null? mCollapsedSize : child.getHeight(); 588 } 589 590 private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, 591 float transitioningPositionStart, float bottomPeakStart, float currentYPosition, 592 StackViewState childViewState, int childHeight) { 593 594 // This is the transitioning element on top of bottom stack, calculate how far we are in. 595 algorithmState.partialInBottom = 1.0f - ( 596 (transitioningPositionStart - currentYPosition) / (childHeight + 597 mPaddingBetweenElements)); 598 599 // the offset starting at the transitionPosition of the bottom stack 600 float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom); 601 algorithmState.itemsInBottomStack += algorithmState.partialInBottom; 602 int newHeight = childHeight; 603 if (childHeight > mCollapsedSize && mIsSmallScreen) { 604 newHeight = (int) Math.max(Math.min(transitioningPositionStart + offset - 605 mPaddingBetweenElements - currentYPosition, childHeight), mCollapsedSize); 606 childViewState.height = newHeight; 607 } 608 childViewState.yTranslation = transitioningPositionStart + offset - newHeight 609 - mPaddingBetweenElements; 610 611 // We want at least to be at the end of the top stack when collapsing 612 clampPositionToTopStackEnd(childViewState, newHeight); 613 childViewState.location = StackViewState.LOCATION_MAIN_AREA; 614 } 615 616 private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, 617 float transitioningPositionStart, StackViewState childViewState, 618 int childHeight, AmbientState ambientState) { 619 float currentYPosition; 620 algorithmState.itemsInBottomStack += 1.0f; 621 if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) { 622 // We are visually entering the bottom stack 623 currentYPosition = transitioningPositionStart 624 + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack) 625 - mPaddingBetweenElements; 626 childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_PEEKING; 627 } else { 628 // we are fully inside the stack 629 if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) { 630 childViewState.alpha = 0.0f; 631 } else if (algorithmState.itemsInBottomStack 632 > MAX_ITEMS_IN_BOTTOM_STACK + 1) { 633 childViewState.alpha = 1.0f - algorithmState.partialInBottom; 634 } 635 childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_HIDDEN; 636 currentYPosition = ambientState.getInnerHeight(); 637 } 638 childViewState.yTranslation = currentYPosition - childHeight; 639 clampPositionToTopStackEnd(childViewState, childHeight); 640 } 641 642 private void updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, 643 int numberOfElementsCompletelyIn, int i, int childHeight, 644 StackViewState childViewState, float scrollOffset) { 645 646 647 // First we calculate the index relative to the current stack window of size at most 648 // {@link #MAX_ITEMS_IN_TOP_STACK} 649 int paddedIndex = i - 1 650 - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0); 651 if (paddedIndex >= 0) { 652 653 // We are currently visually entering the top stack 654 float distanceToStack = (childHeight + mPaddingBetweenElements) 655 - algorithmState.scrolledPixelsTop; 656 if (i == algorithmState.lastTopStackIndex 657 && distanceToStack > (mTopStackTotalSize + mPaddingBetweenElements)) { 658 659 // Child is currently translating into stack but not yet inside slow down zone. 660 // Handle it like the regular scrollview. 661 childViewState.yTranslation = scrollOffset; 662 } else { 663 // Apply stacking logic. 664 float numItemsBefore; 665 if (i == algorithmState.lastTopStackIndex) { 666 numItemsBefore = 1.0f 667 - (distanceToStack / (mTopStackTotalSize + mPaddingBetweenElements)); 668 } else { 669 numItemsBefore = algorithmState.itemsInTopStack - i; 670 } 671 // The end position of the current child 672 float currentChildEndY = mCollapsedSize + mTopStackTotalSize 673 - mTopStackIndentationFunctor.getValue(numItemsBefore); 674 childViewState.yTranslation = currentChildEndY - childHeight; 675 } 676 childViewState.location = StackViewState.LOCATION_TOP_STACK_PEEKING; 677 } else { 678 if (paddedIndex == -1) { 679 childViewState.alpha = 1.0f - algorithmState.partialInTop; 680 } else { 681 // We are hidden behind the top card and faded out, so we can hide ourselves. 682 childViewState.alpha = 0.0f; 683 } 684 childViewState.yTranslation = mCollapsedSize - childHeight; 685 childViewState.location = StackViewState.LOCATION_TOP_STACK_HIDDEN; 686 } 687 688 689 } 690 691 /** 692 * Find the number of items in the top stack and update the result state if needed. 693 * 694 * @param resultState The result state to update if a height change of an child occurs 695 * @param algorithmState The state in which the current pass of the algorithm is currently in 696 */ 697 private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState, 698 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 699 700 // The y Position if the element would be in a regular scrollView 701 float yPositionInScrollView = 0.0f; 702 int childCount = algorithmState.visibleChildren.size(); 703 704 // find the number of elements in the top stack. 705 for (int i = 0; i < childCount; i++) { 706 ExpandableView child = algorithmState.visibleChildren.get(i); 707 StackViewState childViewState = resultState.getViewStateForView(child); 708 int childHeight = getMaxAllowedChildHeight(child, ambientState); 709 float yPositionInScrollViewAfterElement = yPositionInScrollView 710 + childHeight 711 + mPaddingBetweenElements; 712 if (yPositionInScrollView < algorithmState.scrollY) { 713 if (i == 0 && algorithmState.scrollY <= mCollapsedSize) { 714 715 // The starting position of the bottom stack peek 716 int bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize - 717 mCollapseSecondCardPadding; 718 // Collapse and expand the first child while the shade is being expanded 719 float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding 720 ? mFirstChildMaxHeight 721 : childHeight; 722 childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight), 723 mCollapsedSize); 724 algorithmState.itemsInTopStack = 1.0f; 725 726 } else if (yPositionInScrollViewAfterElement < algorithmState.scrollY) { 727 // According to the regular scroll view we are fully off screen 728 algorithmState.itemsInTopStack += 1.0f; 729 if (i == 0) { 730 childViewState.height = mCollapsedSize; 731 } 732 } else { 733 // According to the regular scroll view we are partially off screen 734 735 // How much did we scroll into this child 736 algorithmState.scrolledPixelsTop = algorithmState.scrollY 737 - yPositionInScrollView; 738 algorithmState.partialInTop = (algorithmState.scrolledPixelsTop) / (childHeight 739 + mPaddingBetweenElements); 740 741 // Our element can be expanded, so this can get negative 742 algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop); 743 algorithmState.itemsInTopStack += algorithmState.partialInTop; 744 745 if (i == 0) { 746 // If it is expanded we have to collapse it to a new size 747 float newSize = yPositionInScrollViewAfterElement 748 - mPaddingBetweenElements 749 - algorithmState.scrollY + mCollapsedSize; 750 newSize = Math.max(mCollapsedSize, newSize); 751 algorithmState.itemsInTopStack = 1.0f; 752 childViewState.height = (int) newSize; 753 } 754 algorithmState.lastTopStackIndex = i; 755 break; 756 } 757 } else { 758 algorithmState.lastTopStackIndex = i - 1; 759 // We are already past the stack so we can end the loop 760 break; 761 } 762 yPositionInScrollView = yPositionInScrollViewAfterElement; 763 } 764 } 765 766 /** 767 * Calculate the Z positions for all children based on the number of items in both stacks and 768 * save it in the resultState 769 * 770 * @param resultState The result state to update the zTranslation values 771 * @param algorithmState The state in which the current pass of the algorithm is currently in 772 */ 773 private void updateZValuesForState(StackScrollState resultState, 774 StackScrollAlgorithmState algorithmState) { 775 int childCount = algorithmState.visibleChildren.size(); 776 for (int i = 0; i < childCount; i++) { 777 View child = algorithmState.visibleChildren.get(i); 778 StackViewState childViewState = resultState.getViewStateForView(child); 779 if (i < algorithmState.itemsInTopStack) { 780 float stackIndex = algorithmState.itemsInTopStack - i; 781 782 // Ensure that the topmost item is a little bit higher than the rest when fully 783 // scrolled, to avoid drawing errors when swiping it out 784 float max = MAX_ITEMS_IN_TOP_STACK + (i == 0 ? 2.5f : 2); 785 stackIndex = Math.min(stackIndex, max); 786 if (i == 0 && algorithmState.itemsInTopStack < 2.0f) { 787 788 // We only have the top item and an additional item in the top stack, 789 // Interpolate the index from 0 to 2 while the second item is 790 // translating in. 791 stackIndex -= 1.0f; 792 if (algorithmState.scrollY > mCollapsedSize) { 793 794 // Since there is a shadow treshhold, we cant just interpolate from 0 to 795 // 2 but we interpolate from 0.1f to 2.0f when scrolled in. The jump in 796 // height will not be noticable since we have padding in between. 797 stackIndex = 0.1f + stackIndex * 1.9f; 798 } 799 } 800 childViewState.zTranslation = mZBasicHeight 801 + stackIndex * mZDistanceBetweenElements; 802 } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) { 803 float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack); 804 float translationZ = mZBasicHeight 805 - numItemsAbove * mZDistanceBetweenElements; 806 childViewState.zTranslation = translationZ; 807 } else { 808 childViewState.zTranslation = mZBasicHeight; 809 } 810 } 811 } 812 813 /** 814 * Update whether the device is very small, i.e. Notifications can be in both the top and the 815 * bottom stack at the same time 816 * 817 * @param panelHeight The normal height of the panel when it's open 818 */ 819 public void updateIsSmallScreen(int panelHeight) { 820 mIsSmallScreen = panelHeight < 821 mCollapsedSize /* top stack */ 822 + mBottomStackSlowDownLength + mBottomStackPeekSize /* bottom stack */ 823 + mMaxNotificationHeight; /* max notification height */ 824 } 825 826 public void onExpansionStarted(StackScrollState currentState) { 827 mIsExpansionChanging = true; 828 mExpandedOnStart = mIsExpanded; 829 ViewGroup hostView = currentState.getHostView(); 830 updateFirstChildHeightWhileExpanding(hostView); 831 } 832 833 private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) { 834 mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView); 835 if (mFirstChildWhileExpanding != null) { 836 if (mExpandedOnStart) { 837 838 // We are collapsing the shade, so the first child can get as most as high as the 839 // current height or the end value of the animation. 840 mFirstChildMaxHeight = StackStateAnimator.getFinalActualHeight( 841 mFirstChildWhileExpanding); 842 if (mFirstChildWhileExpanding instanceof ExpandableNotificationRow) { 843 ExpandableNotificationRow row = 844 (ExpandableNotificationRow) mFirstChildWhileExpanding; 845 if (row.isHeadsUp()) { 846 mFirstChildMaxHeight += mCollapsedSize - row.getHeadsUpHeight(); 847 } 848 } 849 } else { 850 updateFirstChildMaxSizeToMaxHeight(); 851 } 852 } else { 853 mFirstChildMaxHeight = 0; 854 } 855 } 856 857 private void updateFirstChildMaxSizeToMaxHeight() { 858 // We are expanding the shade, expand it to its full height. 859 if (!isMaxSizeInitialized(mFirstChildWhileExpanding)) { 860 861 // This child was not layouted yet, wait for a layout pass 862 mFirstChildWhileExpanding 863 .addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 864 @Override 865 public void onLayoutChange(View v, int left, int top, int right, 866 int bottom, int oldLeft, int oldTop, int oldRight, 867 int oldBottom) { 868 if (mFirstChildWhileExpanding != null) { 869 mFirstChildMaxHeight = getMaxAllowedChildHeight( 870 mFirstChildWhileExpanding, null); 871 } else { 872 mFirstChildMaxHeight = 0; 873 } 874 v.removeOnLayoutChangeListener(this); 875 } 876 }); 877 } else { 878 mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding, null); 879 } 880 } 881 882 private boolean isMaxSizeInitialized(ExpandableView child) { 883 if (child instanceof ExpandableNotificationRow) { 884 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 885 return row.isMaxExpandHeightInitialized(); 886 } 887 return child == null || child.getWidth() != 0; 888 } 889 890 private View findFirstVisibleChild(ViewGroup container) { 891 int childCount = container.getChildCount(); 892 for (int i = 0; i < childCount; i++) { 893 View child = container.getChildAt(i); 894 if (child.getVisibility() != View.GONE) { 895 return child; 896 } 897 } 898 return null; 899 } 900 901 public void onExpansionStopped() { 902 mIsExpansionChanging = false; 903 mFirstChildWhileExpanding = null; 904 } 905 906 public void setIsExpanded(boolean isExpanded) { 907 this.mIsExpanded = isExpanded; 908 } 909 910 public void notifyChildrenChanged(final ViewGroup hostView) { 911 if (mIsExpansionChanging) { 912 hostView.post(new Runnable() { 913 @Override 914 public void run() { 915 updateFirstChildHeightWhileExpanding(hostView); 916 } 917 }); 918 } 919 } 920 921 public void setDimmed(boolean dimmed) { 922 updatePadding(dimmed); 923 } 924 925 public void onReset(ExpandableView view) { 926 if (view.equals(mFirstChildWhileExpanding)) { 927 updateFirstChildMaxSizeToMaxHeight(); 928 } 929 } 930 931 public void setHeadsUpManager(HeadsUpManager headsUpManager) { 932 mHeadsUpManager = headsUpManager; 933 } 934 935 class StackScrollAlgorithmState { 936 937 /** 938 * The scroll position of the algorithm 939 */ 940 public int scrollY; 941 942 /** 943 * The quantity of items which are in the top stack. 944 */ 945 public float itemsInTopStack; 946 947 /** 948 * how far in is the element currently transitioning into the top stack 949 */ 950 public float partialInTop; 951 952 /** 953 * The number of pixels the last child in the top stack has scrolled in to the stack 954 */ 955 public float scrolledPixelsTop; 956 957 /** 958 * The last item index which is in the top stack. 959 */ 960 public int lastTopStackIndex; 961 962 /** 963 * The quantity of items which are in the bottom stack. 964 */ 965 public float itemsInBottomStack; 966 967 /** 968 * how far in is the element currently transitioning into the bottom stack 969 */ 970 public float partialInBottom; 971 972 /** 973 * The children from the host view which are not gone. 974 */ 975 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>(); 976 } 977 978} 979