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