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