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