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