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