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