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