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