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