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