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