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