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