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 17 package com.android.systemui.statusbar.stack; 18 19 import android.content.Context; 20 import android.util.DisplayMetrics; 21 import android.util.Log; 22 import android.view.View; 23 import android.view.ViewGroup; 24 25 import com.android.systemui.R; 26 import com.android.systemui.statusbar.ExpandableNotificationRow; 27 import com.android.systemui.statusbar.ExpandableView; 28 import com.android.systemui.statusbar.policy.HeadsUpManager; 29 30 import java.util.ArrayList; 31 import java.util.List; 32 33 /** 34 * The Algorithm of the {@link com.android.systemui.statusbar.stack 35 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar 36 * .stack.StackScrollState} 37 */ 38 public class StackScrollAlgorithm { 39 40 private static final String LOG_TAG = "StackScrollAlgorithm"; 41 42 private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3; 43 private static final int MAX_ITEMS_IN_TOP_STACK = 3; 44 45 public static final float DIMMED_SCALE = 0.95f; 46 47 private int mPaddingBetweenElements; 48 private int mCollapsedSize; 49 private int mTopStackPeekSize; 50 private int mBottomStackPeekSize; 51 private int mZDistanceBetweenElements; 52 private int mZBasicHeight; 53 private int mRoundedRectCornerRadius; 54 55 private StackIndentationFunctor mTopStackIndentationFunctor; 56 private StackIndentationFunctor mBottomStackIndentationFunctor; 57 58 private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); 59 private boolean mIsExpansionChanging; 60 private int mFirstChildMaxHeight; 61 private boolean mIsExpanded; 62 private ExpandableView mFirstChildWhileExpanding; 63 private boolean mExpandedOnStart; 64 private int mTopStackTotalSize; 65 private int mPaddingBetweenElementsDimmed; 66 private int mPaddingBetweenElementsNormal; 67 private int mNotificationsTopPadding; 68 private int mBottomStackSlowDownLength; 69 private int mTopStackSlowDownLength; 70 private int mCollapseSecondCardPadding; 71 private boolean mIsSmallScreen; 72 private int mMaxNotificationHeight; 73 private boolean mScaleDimmed; 74 private HeadsUpManager mHeadsUpManager; 75 StackScrollAlgorithm(Context context)76 public StackScrollAlgorithm(Context context) { 77 initConstants(context); 78 updatePadding(false); 79 } 80 updatePadding(boolean dimmed)81 private void updatePadding(boolean dimmed) { 82 mPaddingBetweenElements = dimmed && mScaleDimmed 83 ? mPaddingBetweenElementsDimmed 84 : mPaddingBetweenElementsNormal; 85 mTopStackTotalSize = mTopStackSlowDownLength + mPaddingBetweenElements 86 + mTopStackPeekSize; 87 mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( 88 MAX_ITEMS_IN_TOP_STACK, 89 mTopStackPeekSize, 90 mTopStackTotalSize - mTopStackPeekSize, 91 0.5f); 92 mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( 93 MAX_ITEMS_IN_BOTTOM_STACK, 94 mBottomStackPeekSize, 95 getBottomStackSlowDownLength(), 96 0.5f); 97 } 98 getBottomStackSlowDownLength()99 public int getBottomStackSlowDownLength() { 100 return mBottomStackSlowDownLength + mPaddingBetweenElements; 101 } 102 initConstants(Context context)103 private void initConstants(Context context) { 104 mPaddingBetweenElementsDimmed = context.getResources() 105 .getDimensionPixelSize(R.dimen.notification_padding_dimmed); 106 mPaddingBetweenElementsNormal = context.getResources() 107 .getDimensionPixelSize(R.dimen.notification_padding); 108 mNotificationsTopPadding = context.getResources() 109 .getDimensionPixelSize(R.dimen.notifications_top_padding); 110 mCollapsedSize = context.getResources() 111 .getDimensionPixelSize(R.dimen.notification_min_height); 112 mMaxNotificationHeight = context.getResources() 113 .getDimensionPixelSize(R.dimen.notification_max_height); 114 mTopStackPeekSize = context.getResources() 115 .getDimensionPixelSize(R.dimen.top_stack_peek_amount); 116 mBottomStackPeekSize = context.getResources() 117 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); 118 mZDistanceBetweenElements = context.getResources() 119 .getDimensionPixelSize(R.dimen.z_distance_between_notifications); 120 mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements; 121 mBottomStackSlowDownLength = context.getResources() 122 .getDimensionPixelSize(R.dimen.bottom_stack_slow_down_length); 123 mTopStackSlowDownLength = context.getResources() 124 .getDimensionPixelSize(R.dimen.top_stack_slow_down_length); 125 mRoundedRectCornerRadius = context.getResources().getDimensionPixelSize( 126 R.dimen.notification_material_rounded_rect_radius); 127 mCollapseSecondCardPadding = context.getResources().getDimensionPixelSize( 128 R.dimen.notification_collapse_second_card_padding); 129 mScaleDimmed = context.getResources().getDisplayMetrics().densityDpi 130 >= DisplayMetrics.DENSITY_XXHIGH; 131 } 132 shouldScaleDimmed()133 public boolean shouldScaleDimmed() { 134 return mScaleDimmed; 135 } 136 getStackScrollState(AmbientState ambientState, StackScrollState resultState)137 public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) { 138 // The state of the local variables are saved in an algorithmState to easily subdivide it 139 // into multiple phases. 140 StackScrollAlgorithmState algorithmState = mTempAlgorithmState; 141 142 // First we reset the view states to their default values. 143 resultState.resetViewStates(); 144 145 algorithmState.itemsInTopStack = 0.0f; 146 algorithmState.partialInTop = 0.0f; 147 algorithmState.lastTopStackIndex = 0; 148 algorithmState.scrolledPixelsTop = 0; 149 algorithmState.itemsInBottomStack = 0.0f; 150 algorithmState.partialInBottom = 0.0f; 151 float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */); 152 153 int scrollY = ambientState.getScrollY(); 154 155 // Due to the overScroller, the stackscroller can have negative scroll state. This is 156 // already accounted for by the top padding and doesn't need an additional adaption 157 scrollY = Math.max(0, scrollY); 158 algorithmState.scrollY = (int) (scrollY + mCollapsedSize + bottomOverScroll); 159 160 updateVisibleChildren(resultState, algorithmState); 161 162 // Phase 1: 163 findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState, ambientState); 164 165 // Phase 2: 166 updatePositionsForState(resultState, algorithmState, ambientState); 167 168 // Phase 3: 169 updateZValuesForState(resultState, algorithmState); 170 171 handleDraggedViews(ambientState, resultState, algorithmState); 172 updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState); 173 updateClipping(resultState, algorithmState, ambientState); 174 updateSpeedBumpState(resultState, algorithmState, ambientState.getSpeedBumpIndex()); 175 getNotificationChildrenStates(resultState, algorithmState); 176 } 177 getNotificationChildrenStates(StackScrollState resultState, StackScrollAlgorithmState algorithmState)178 private void getNotificationChildrenStates(StackScrollState resultState, 179 StackScrollAlgorithmState algorithmState) { 180 int childCount = algorithmState.visibleChildren.size(); 181 for (int i = 0; i < childCount; i++) { 182 ExpandableView v = algorithmState.visibleChildren.get(i); 183 if (v instanceof ExpandableNotificationRow) { 184 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 185 row.getChildrenStates(resultState); 186 } 187 } 188 } 189 updateSpeedBumpState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, int speedBumpIndex)190 private void updateSpeedBumpState(StackScrollState resultState, 191 StackScrollAlgorithmState algorithmState, int speedBumpIndex) { 192 int childCount = algorithmState.visibleChildren.size(); 193 for (int i = 0; i < childCount; i++) { 194 View child = algorithmState.visibleChildren.get(i); 195 StackViewState childViewState = resultState.getViewStateForView(child); 196 197 // The speed bump can also be gone, so equality needs to be taken when comparing 198 // indices. 199 childViewState.belowSpeedBump = speedBumpIndex != -1 && i >= speedBumpIndex; 200 } 201 } 202 updateClipping(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)203 private void updateClipping(StackScrollState resultState, 204 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 205 boolean dismissAllInProgress = ambientState.isDismissAllInProgress(); 206 float previousNotificationEnd = 0; 207 float previousNotificationStart = 0; 208 boolean previousNotificationIsSwiped = false; 209 int childCount = algorithmState.visibleChildren.size(); 210 for (int i = 0; i < childCount; i++) { 211 ExpandableView child = algorithmState.visibleChildren.get(i); 212 StackViewState state = resultState.getViewStateForView(child); 213 float newYTranslation = state.yTranslation + state.height * (1f - state.scale) / 2f; 214 float newHeight = state.height * state.scale; 215 // apply clipping and shadow 216 float newNotificationEnd = newYTranslation + newHeight; 217 218 float clipHeight; 219 if (previousNotificationIsSwiped) { 220 // When the previous notification is swiped, we don't clip the content to the 221 // bottom of it. 222 clipHeight = newHeight; 223 } else { 224 clipHeight = newNotificationEnd - previousNotificationEnd; 225 clipHeight = Math.max(0.0f, clipHeight); 226 if (clipHeight != 0.0f) { 227 228 // In the unlocked shade we have to clip a little bit higher because of the rounded 229 // corners of the notifications, but only if we are not fully overlapped by 230 // the top card. 231 float clippingCorrection = state.dimmed 232 ? 0 233 : mRoundedRectCornerRadius * state.scale; 234 clipHeight += clippingCorrection; 235 } 236 } 237 238 updateChildClippingAndBackground(state, newHeight, clipHeight, 239 newHeight - (previousNotificationStart - newYTranslation)); 240 241 if (dismissAllInProgress) { 242 state.clipTopAmount = Math.max(child.getMinClipTopAmount(), state.clipTopAmount); 243 } 244 245 if (!child.isTransparent()) { 246 // Only update the previous values if we are not transparent, 247 // otherwise we would clip to a transparent view. 248 if ((dismissAllInProgress && canChildBeDismissed(child))) { 249 previousNotificationIsSwiped = true; 250 } else { 251 previousNotificationIsSwiped = ambientState.getDraggedViews().contains(child); 252 previousNotificationEnd = newNotificationEnd; 253 previousNotificationStart = newYTranslation + state.clipTopAmount * state.scale; 254 } 255 } 256 } 257 } 258 canChildBeDismissed(View v)259 public static boolean canChildBeDismissed(View v) { 260 final View veto = v.findViewById(R.id.veto); 261 return (veto != null && veto.getVisibility() != View.GONE); 262 } 263 264 /** 265 * Updates the shadow outline and the clipping for a view. 266 * 267 * @param state the viewState to update 268 * @param realHeight the currently applied height of the view 269 * @param clipHeight the desired clip height, the rest of the view will be clipped from the top 270 * @param backgroundHeight the desired background height. The shadows of the view will be 271 * based on this height and the content will be clipped from the top 272 */ updateChildClippingAndBackground(StackViewState state, float realHeight, float clipHeight, float backgroundHeight)273 private void updateChildClippingAndBackground(StackViewState state, float realHeight, 274 float clipHeight, float backgroundHeight) { 275 if (realHeight > clipHeight) { 276 // Rather overlap than create a hole. 277 state.topOverLap = (int) Math.floor((realHeight - clipHeight) / state.scale); 278 } else { 279 state.topOverLap = 0; 280 } 281 if (realHeight > backgroundHeight) { 282 // Rather overlap than create a hole. 283 state.clipTopAmount = (int) Math.floor((realHeight - backgroundHeight) / state.scale); 284 } else { 285 state.clipTopAmount = 0; 286 } 287 } 288 289 /** 290 * Updates the dimmed, activated and hiding sensitive states of the children. 291 */ updateDimmedActivatedHideSensitive(AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState)292 private void updateDimmedActivatedHideSensitive(AmbientState ambientState, 293 StackScrollState resultState, StackScrollAlgorithmState algorithmState) { 294 boolean dimmed = ambientState.isDimmed(); 295 boolean dark = ambientState.isDark(); 296 boolean hideSensitive = ambientState.isHideSensitive(); 297 View activatedChild = ambientState.getActivatedChild(); 298 int childCount = algorithmState.visibleChildren.size(); 299 for (int i = 0; i < childCount; i++) { 300 View child = algorithmState.visibleChildren.get(i); 301 StackViewState childViewState = resultState.getViewStateForView(child); 302 childViewState.dimmed = dimmed; 303 childViewState.dark = dark; 304 childViewState.hideSensitive = hideSensitive; 305 boolean isActivatedChild = activatedChild == child; 306 childViewState.scale = !mScaleDimmed || !dimmed || isActivatedChild 307 ? 1.0f 308 : DIMMED_SCALE; 309 if (dimmed && isActivatedChild) { 310 childViewState.zTranslation += 2.0f * mZDistanceBetweenElements; 311 } 312 } 313 } 314 315 /** 316 * Handle the special state when views are being dragged 317 */ handleDraggedViews(AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState)318 private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState, 319 StackScrollAlgorithmState algorithmState) { 320 ArrayList<View> draggedViews = ambientState.getDraggedViews(); 321 for (View draggedView : draggedViews) { 322 int childIndex = algorithmState.visibleChildren.indexOf(draggedView); 323 if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) { 324 View nextChild = algorithmState.visibleChildren.get(childIndex + 1); 325 if (!draggedViews.contains(nextChild)) { 326 // only if the view is not dragged itself we modify its state to be fully 327 // visible 328 StackViewState viewState = resultState.getViewStateForView( 329 nextChild); 330 // The child below the dragged one must be fully visible 331 if (ambientState.isShadeExpanded()) { 332 viewState.alpha = 1; 333 } 334 } 335 336 // Lets set the alpha to the one it currently has, as its currently being dragged 337 StackViewState viewState = resultState.getViewStateForView(draggedView); 338 // The dragged child should keep the set alpha 339 viewState.alpha = draggedView.getAlpha(); 340 } 341 } 342 } 343 344 /** 345 * Update the visible children on the state. 346 */ updateVisibleChildren(StackScrollState resultState, StackScrollAlgorithmState state)347 private void updateVisibleChildren(StackScrollState resultState, 348 StackScrollAlgorithmState state) { 349 ViewGroup hostView = resultState.getHostView(); 350 int childCount = hostView.getChildCount(); 351 state.visibleChildren.clear(); 352 state.visibleChildren.ensureCapacity(childCount); 353 int notGoneIndex = 0; 354 for (int i = 0; i < childCount; i++) { 355 ExpandableView v = (ExpandableView) hostView.getChildAt(i); 356 if (v.getVisibility() != View.GONE) { 357 notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v); 358 if (v instanceof ExpandableNotificationRow) { 359 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 360 361 // handle the notgoneIndex for the children as well 362 List<ExpandableNotificationRow> children = 363 row.getNotificationChildren(); 364 if (row.areChildrenExpanded() && children != null) { 365 for (ExpandableNotificationRow childRow : children) { 366 if (childRow.getVisibility() != View.GONE) { 367 StackViewState childState 368 = resultState.getViewStateForView(childRow); 369 childState.notGoneIndex = notGoneIndex; 370 notGoneIndex++; 371 } 372 } 373 } 374 } 375 } 376 } 377 } 378 updateNotGoneIndex(StackScrollState resultState, StackScrollAlgorithmState state, int notGoneIndex, ExpandableView v)379 private int updateNotGoneIndex(StackScrollState resultState, 380 StackScrollAlgorithmState state, int notGoneIndex, 381 ExpandableView v) { 382 StackViewState viewState = resultState.getViewStateForView(v); 383 viewState.notGoneIndex = notGoneIndex; 384 state.visibleChildren.add(v); 385 notGoneIndex++; 386 return notGoneIndex; 387 } 388 389 /** 390 * Determine the positions for the views. This is the main part of the algorithm. 391 * 392 * @param resultState The result state to update if a change to the properties of a child occurs 393 * @param algorithmState The state in which the current pass of the algorithm is currently in 394 * @param ambientState The current ambient state 395 */ updatePositionsForState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)396 private void updatePositionsForState(StackScrollState resultState, 397 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 398 399 // The starting position of the bottom stack peek 400 float bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize; 401 402 // The position where the bottom stack starts. 403 float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength; 404 405 // The y coordinate of the current child. 406 float currentYPosition = 0.0f; 407 408 // How far in is the element currently transitioning into the bottom stack. 409 float yPositionInScrollView = 0.0f; 410 411 // If we have a heads-up higher than the collapsed height we need to add the difference to 412 // the padding of all other elements, i.e push in the top stack slightly. 413 ExpandableNotificationRow topHeadsUpEntry = ambientState.getTopHeadsUpEntry(); 414 415 int childCount = algorithmState.visibleChildren.size(); 416 int numberOfElementsCompletelyIn = algorithmState.partialInTop == 1.0f 417 ? algorithmState.lastTopStackIndex 418 : (int) algorithmState.itemsInTopStack; 419 for (int i = 0; i < childCount; i++) { 420 ExpandableView child = algorithmState.visibleChildren.get(i); 421 StackViewState childViewState = resultState.getViewStateForView(child); 422 childViewState.location = StackViewState.LOCATION_UNKNOWN; 423 int childHeight = getMaxAllowedChildHeight(child, ambientState); 424 float yPositionInScrollViewAfterElement = yPositionInScrollView 425 + childHeight 426 + mPaddingBetweenElements; 427 float scrollOffset = yPositionInScrollView - algorithmState.scrollY + mCollapsedSize; 428 429 if (i == algorithmState.lastTopStackIndex + 1) { 430 // Normally the position of this child is the position in the regular scrollview, 431 // but if the two stacks are very close to each other, 432 // then have have to push it even more upwards to the position of the bottom 433 // stack start. 434 currentYPosition = Math.min(scrollOffset, bottomStackStart); 435 } 436 childViewState.yTranslation = currentYPosition; 437 438 // The y position after this element 439 float nextYPosition = currentYPosition + childHeight + 440 mPaddingBetweenElements; 441 442 if (i <= algorithmState.lastTopStackIndex) { 443 // Case 1: 444 // We are in the top Stack 445 updateStateForTopStackChild(algorithmState, 446 numberOfElementsCompletelyIn, i, childHeight, childViewState, scrollOffset); 447 clampPositionToTopStackEnd(childViewState, childHeight); 448 449 // check if we are overlapping with the bottom stack 450 if (childViewState.yTranslation + childHeight + mPaddingBetweenElements 451 >= bottomStackStart && !mIsExpansionChanging && i != 0 && mIsSmallScreen) { 452 // we just collapse this element slightly 453 int newSize = (int) Math.max(bottomStackStart - mPaddingBetweenElements - 454 childViewState.yTranslation, mCollapsedSize); 455 childViewState.height = newSize; 456 updateStateForChildTransitioningInBottom(algorithmState, bottomStackStart, 457 bottomPeekStart, childViewState.yTranslation, childViewState, 458 childHeight); 459 } 460 clampPositionToBottomStackStart(childViewState, childViewState.height, 461 ambientState); 462 } else if (nextYPosition >= bottomStackStart) { 463 // Case 2: 464 // We are in the bottom stack. 465 if (currentYPosition >= bottomStackStart) { 466 // According to the regular scroll view we are fully translated out of the 467 // bottom of the screen so we are fully in the bottom stack 468 updateStateForChildFullyInBottomStack(algorithmState, 469 bottomStackStart, childViewState, childHeight, ambientState); 470 } else { 471 // According to the regular scroll view we are currently translating out of / 472 // into the bottom of the screen 473 updateStateForChildTransitioningInBottom(algorithmState, 474 bottomStackStart, bottomPeekStart, currentYPosition, 475 childViewState, childHeight); 476 } 477 } else { 478 // Case 3: 479 // We are in the regular scroll area. 480 childViewState.location = StackViewState.LOCATION_MAIN_AREA; 481 clampYTranslation(childViewState, childHeight, ambientState); 482 } 483 484 // The first card is always rendered. 485 if (i == 0) { 486 childViewState.alpha = 1.0f; 487 childViewState.yTranslation = Math.max(mCollapsedSize - algorithmState.scrollY, 0); 488 if (childViewState.yTranslation + childViewState.height 489 > bottomPeekStart - mCollapseSecondCardPadding) { 490 childViewState.height = (int) Math.max( 491 bottomPeekStart - mCollapseSecondCardPadding 492 - childViewState.yTranslation, mCollapsedSize); 493 } 494 childViewState.location = StackViewState.LOCATION_FIRST_CARD; 495 } 496 if (childViewState.location == StackViewState.LOCATION_UNKNOWN) { 497 Log.wtf(LOG_TAG, "Failed to assign location for child " + i); 498 } 499 currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements; 500 yPositionInScrollView = yPositionInScrollViewAfterElement; 501 502 if (ambientState.isShadeExpanded() && topHeadsUpEntry != null 503 && child != topHeadsUpEntry) { 504 childViewState.yTranslation += topHeadsUpEntry.getHeadsUpHeight() - mCollapsedSize; 505 } 506 childViewState.yTranslation += ambientState.getTopPadding() 507 + ambientState.getStackTranslation(); 508 } 509 updateHeadsUpStates(resultState, algorithmState, ambientState); 510 } 511 updateHeadsUpStates(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)512 private void updateHeadsUpStates(StackScrollState resultState, 513 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 514 int childCount = algorithmState.visibleChildren.size(); 515 ExpandableNotificationRow topHeadsUpEntry = null; 516 for (int i = 0; i < childCount; i++) { 517 View child = algorithmState.visibleChildren.get(i); 518 if (!(child instanceof ExpandableNotificationRow)) { 519 break; 520 } 521 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 522 if (!row.isHeadsUp()) { 523 break; 524 } else if (topHeadsUpEntry == null) { 525 topHeadsUpEntry = row; 526 } 527 StackViewState childState = resultState.getViewStateForView(row); 528 boolean isTopEntry = topHeadsUpEntry == row; 529 if (mIsExpanded) { 530 if (isTopEntry) { 531 childState.height += row.getHeadsUpHeight() - mCollapsedSize; 532 } 533 childState.height = Math.max(childState.height, row.getHeadsUpHeight()); 534 // Ensure that the heads up is always visible even when scrolled off from the bottom 535 float bottomPosition = ambientState.getMaxHeadsUpTranslation() - childState.height; 536 childState.yTranslation = Math.min(childState.yTranslation, 537 bottomPosition); 538 } 539 if (row.isPinned()) { 540 childState.yTranslation = Math.max(childState.yTranslation, 541 mNotificationsTopPadding); 542 childState.height = row.getHeadsUpHeight(); 543 if (!isTopEntry) { 544 // Ensure that a headsUp doesn't vertically extend further than the heads-up at 545 // the top most z-position 546 StackViewState topState = resultState.getViewStateForView(topHeadsUpEntry); 547 childState.height = row.getHeadsUpHeight(); 548 childState.yTranslation = topState.yTranslation + topState.height 549 - childState.height; 550 } 551 } 552 } 553 } 554 555 /** 556 * Clamp the yTranslation both up and down to valid positions. 557 * 558 * @param childViewState the view state of the child 559 * @param childHeight the height of this child 560 */ clampYTranslation(StackViewState childViewState, int childHeight, AmbientState ambientState)561 private void clampYTranslation(StackViewState childViewState, int childHeight, 562 AmbientState ambientState) { 563 clampPositionToBottomStackStart(childViewState, childHeight, ambientState); 564 clampPositionToTopStackEnd(childViewState, childHeight); 565 } 566 567 /** 568 * Clamp the yTranslation of the child down such that its end is at most on the beginning of 569 * the bottom stack. 570 * 571 * @param childViewState the view state of the child 572 * @param childHeight the height of this child 573 */ clampPositionToBottomStackStart(StackViewState childViewState, int childHeight, AmbientState ambientState)574 private void clampPositionToBottomStackStart(StackViewState childViewState, 575 int childHeight, AmbientState ambientState) { 576 childViewState.yTranslation = Math.min(childViewState.yTranslation, 577 ambientState.getInnerHeight() - mBottomStackPeekSize - mCollapseSecondCardPadding 578 - childHeight); 579 } 580 581 /** 582 * Clamp the yTranslation of the child up such that its end is at lest on the end of the top 583 * stack. 584 * 585 * @param childViewState the view state of the child 586 * @param childHeight the height of this child 587 */ clampPositionToTopStackEnd(StackViewState childViewState, int childHeight)588 private void clampPositionToTopStackEnd(StackViewState childViewState, 589 int childHeight) { 590 childViewState.yTranslation = Math.max(childViewState.yTranslation, 591 mCollapsedSize - childHeight); 592 } 593 getMaxAllowedChildHeight(View child, AmbientState ambientState)594 private int getMaxAllowedChildHeight(View child, AmbientState ambientState) { 595 if (child instanceof ExpandableNotificationRow) { 596 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 597 if (ambientState == null && row.isHeadsUp() 598 || ambientState != null && ambientState.getTopHeadsUpEntry() == child) { 599 int extraSize = row.getIntrinsicHeight() - row.getHeadsUpHeight(); 600 return mCollapsedSize + extraSize; 601 } 602 return row.getIntrinsicHeight(); 603 } else if (child instanceof ExpandableView) { 604 ExpandableView expandableView = (ExpandableView) child; 605 return expandableView.getIntrinsicHeight(); 606 } 607 return child == null? mCollapsedSize : child.getHeight(); 608 } 609 updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, float transitioningPositionStart, float bottomPeakStart, float currentYPosition, StackViewState childViewState, int childHeight)610 private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, 611 float transitioningPositionStart, float bottomPeakStart, float currentYPosition, 612 StackViewState childViewState, int childHeight) { 613 614 // This is the transitioning element on top of bottom stack, calculate how far we are in. 615 algorithmState.partialInBottom = 1.0f - ( 616 (transitioningPositionStart - currentYPosition) / (childHeight + 617 mPaddingBetweenElements)); 618 619 // the offset starting at the transitionPosition of the bottom stack 620 float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom); 621 algorithmState.itemsInBottomStack += algorithmState.partialInBottom; 622 int newHeight = childHeight; 623 if (childHeight > mCollapsedSize && mIsSmallScreen) { 624 newHeight = (int) Math.max(Math.min(transitioningPositionStart + offset - 625 mPaddingBetweenElements - currentYPosition, childHeight), mCollapsedSize); 626 childViewState.height = newHeight; 627 } 628 childViewState.yTranslation = transitioningPositionStart + offset - newHeight 629 - mPaddingBetweenElements; 630 631 // We want at least to be at the end of the top stack when collapsing 632 clampPositionToTopStackEnd(childViewState, newHeight); 633 childViewState.location = StackViewState.LOCATION_MAIN_AREA; 634 } 635 updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, float transitioningPositionStart, StackViewState childViewState, int childHeight, AmbientState ambientState)636 private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, 637 float transitioningPositionStart, StackViewState childViewState, 638 int childHeight, AmbientState ambientState) { 639 float currentYPosition; 640 algorithmState.itemsInBottomStack += 1.0f; 641 if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) { 642 // We are visually entering the bottom stack 643 currentYPosition = transitioningPositionStart 644 + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack) 645 - mPaddingBetweenElements; 646 childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_PEEKING; 647 } else { 648 // we are fully inside the stack 649 if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) { 650 childViewState.alpha = 0.0f; 651 } else if (algorithmState.itemsInBottomStack 652 > MAX_ITEMS_IN_BOTTOM_STACK + 1) { 653 childViewState.alpha = 1.0f - algorithmState.partialInBottom; 654 } 655 childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_HIDDEN; 656 currentYPosition = ambientState.getInnerHeight(); 657 } 658 childViewState.yTranslation = currentYPosition - childHeight; 659 clampPositionToTopStackEnd(childViewState, childHeight); 660 } 661 updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, int numberOfElementsCompletelyIn, int i, int childHeight, StackViewState childViewState, float scrollOffset)662 private void updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, 663 int numberOfElementsCompletelyIn, int i, int childHeight, 664 StackViewState childViewState, float scrollOffset) { 665 666 667 // First we calculate the index relative to the current stack window of size at most 668 // {@link #MAX_ITEMS_IN_TOP_STACK} 669 int paddedIndex = i - 1 670 - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0); 671 if (paddedIndex >= 0) { 672 673 // We are currently visually entering the top stack 674 float distanceToStack = (childHeight + mPaddingBetweenElements) 675 - algorithmState.scrolledPixelsTop; 676 if (i == algorithmState.lastTopStackIndex 677 && distanceToStack > (mTopStackTotalSize + mPaddingBetweenElements)) { 678 679 // Child is currently translating into stack but not yet inside slow down zone. 680 // Handle it like the regular scrollview. 681 childViewState.yTranslation = scrollOffset; 682 } else { 683 // Apply stacking logic. 684 float numItemsBefore; 685 if (i == algorithmState.lastTopStackIndex) { 686 numItemsBefore = 1.0f 687 - (distanceToStack / (mTopStackTotalSize + mPaddingBetweenElements)); 688 } else { 689 numItemsBefore = algorithmState.itemsInTopStack - i; 690 } 691 // The end position of the current child 692 float currentChildEndY = mCollapsedSize + mTopStackTotalSize 693 - mTopStackIndentationFunctor.getValue(numItemsBefore); 694 childViewState.yTranslation = currentChildEndY - childHeight; 695 } 696 childViewState.location = StackViewState.LOCATION_TOP_STACK_PEEKING; 697 } else { 698 if (paddedIndex == -1) { 699 childViewState.alpha = 1.0f - algorithmState.partialInTop; 700 } else { 701 // We are hidden behind the top card and faded out, so we can hide ourselves. 702 childViewState.alpha = 0.0f; 703 } 704 childViewState.yTranslation = mCollapsedSize - childHeight; 705 childViewState.location = StackViewState.LOCATION_TOP_STACK_HIDDEN; 706 } 707 708 709 } 710 711 /** 712 * Find the number of items in the top stack and update the result state if needed. 713 * 714 * @param resultState The result state to update if a height change of an child occurs 715 * @param algorithmState The state in which the current pass of the algorithm is currently in 716 */ findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)717 private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState, 718 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 719 720 // The y Position if the element would be in a regular scrollView 721 float yPositionInScrollView = 0.0f; 722 int childCount = algorithmState.visibleChildren.size(); 723 724 // find the number of elements in the top stack. 725 for (int i = 0; i < childCount; i++) { 726 ExpandableView child = algorithmState.visibleChildren.get(i); 727 StackViewState childViewState = resultState.getViewStateForView(child); 728 int childHeight = getMaxAllowedChildHeight(child, ambientState); 729 float yPositionInScrollViewAfterElement = yPositionInScrollView 730 + childHeight 731 + mPaddingBetweenElements; 732 if (yPositionInScrollView < algorithmState.scrollY) { 733 if (i == 0 && algorithmState.scrollY <= mCollapsedSize) { 734 735 // The starting position of the bottom stack peek 736 int bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize - 737 mCollapseSecondCardPadding; 738 // Collapse and expand the first child while the shade is being expanded 739 float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding 740 ? mFirstChildMaxHeight 741 : childHeight; 742 childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight), 743 mCollapsedSize); 744 algorithmState.itemsInTopStack = 1.0f; 745 746 } else if (yPositionInScrollViewAfterElement < algorithmState.scrollY) { 747 // According to the regular scroll view we are fully off screen 748 algorithmState.itemsInTopStack += 1.0f; 749 if (i == 0) { 750 childViewState.height = mCollapsedSize; 751 } 752 } else { 753 // According to the regular scroll view we are partially off screen 754 755 // How much did we scroll into this child 756 algorithmState.scrolledPixelsTop = algorithmState.scrollY 757 - yPositionInScrollView; 758 algorithmState.partialInTop = (algorithmState.scrolledPixelsTop) / (childHeight 759 + mPaddingBetweenElements); 760 761 // Our element can be expanded, so this can get negative 762 algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop); 763 algorithmState.itemsInTopStack += algorithmState.partialInTop; 764 765 if (i == 0) { 766 // If it is expanded we have to collapse it to a new size 767 float newSize = yPositionInScrollViewAfterElement 768 - mPaddingBetweenElements 769 - algorithmState.scrollY + mCollapsedSize; 770 newSize = Math.max(mCollapsedSize, newSize); 771 algorithmState.itemsInTopStack = 1.0f; 772 childViewState.height = (int) newSize; 773 } 774 algorithmState.lastTopStackIndex = i; 775 break; 776 } 777 } else { 778 algorithmState.lastTopStackIndex = i - 1; 779 // We are already past the stack so we can end the loop 780 break; 781 } 782 yPositionInScrollView = yPositionInScrollViewAfterElement; 783 } 784 } 785 786 /** 787 * Calculate the Z positions for all children based on the number of items in both stacks and 788 * save it in the resultState 789 * 790 * @param resultState The result state to update the zTranslation values 791 * @param algorithmState The state in which the current pass of the algorithm is currently in 792 */ updateZValuesForState(StackScrollState resultState, StackScrollAlgorithmState algorithmState)793 private void updateZValuesForState(StackScrollState resultState, 794 StackScrollAlgorithmState algorithmState) { 795 int childCount = algorithmState.visibleChildren.size(); 796 for (int i = 0; i < childCount; i++) { 797 View child = algorithmState.visibleChildren.get(i); 798 StackViewState childViewState = resultState.getViewStateForView(child); 799 if (i < algorithmState.itemsInTopStack) { 800 float stackIndex = algorithmState.itemsInTopStack - i; 801 802 // Ensure that the topmost item is a little bit higher than the rest when fully 803 // scrolled, to avoid drawing errors when swiping it out 804 float max = MAX_ITEMS_IN_TOP_STACK + (i == 0 ? 2.5f : 2); 805 stackIndex = Math.min(stackIndex, max); 806 if (i == 0 && algorithmState.itemsInTopStack < 2.0f) { 807 808 // We only have the top item and an additional item in the top stack, 809 // Interpolate the index from 0 to 2 while the second item is 810 // translating in. 811 stackIndex -= 1.0f; 812 if (algorithmState.scrollY > mCollapsedSize) { 813 814 // Since there is a shadow treshhold, we cant just interpolate from 0 to 815 // 2 but we interpolate from 0.1f to 2.0f when scrolled in. The jump in 816 // height will not be noticable since we have padding in between. 817 stackIndex = 0.1f + stackIndex * 1.9f; 818 } 819 } 820 childViewState.zTranslation = mZBasicHeight 821 + stackIndex * mZDistanceBetweenElements; 822 } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) { 823 float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack); 824 float translationZ = mZBasicHeight 825 - numItemsAbove * mZDistanceBetweenElements; 826 childViewState.zTranslation = translationZ; 827 } else { 828 childViewState.zTranslation = mZBasicHeight; 829 } 830 } 831 } 832 833 /** 834 * Update whether the device is very small, i.e. Notifications can be in both the top and the 835 * bottom stack at the same time 836 * 837 * @param panelHeight The normal height of the panel when it's open 838 */ updateIsSmallScreen(int panelHeight)839 public void updateIsSmallScreen(int panelHeight) { 840 mIsSmallScreen = panelHeight < 841 mCollapsedSize /* top stack */ 842 + mBottomStackSlowDownLength + mBottomStackPeekSize /* bottom stack */ 843 + mMaxNotificationHeight; /* max notification height */ 844 } 845 846 public void onExpansionStarted(StackScrollState currentState) { 847 mIsExpansionChanging = true; 848 mExpandedOnStart = mIsExpanded; 849 ViewGroup hostView = currentState.getHostView(); 850 updateFirstChildHeightWhileExpanding(hostView); 851 } 852 853 private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) { 854 mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView); 855 if (mFirstChildWhileExpanding != null) { 856 if (mExpandedOnStart) { 857 858 // We are collapsing the shade, so the first child can get as most as high as the 859 // current height or the end value of the animation. 860 mFirstChildMaxHeight = StackStateAnimator.getFinalActualHeight( 861 mFirstChildWhileExpanding); 862 if (mFirstChildWhileExpanding instanceof ExpandableNotificationRow) { 863 ExpandableNotificationRow row = 864 (ExpandableNotificationRow) mFirstChildWhileExpanding; 865 if (row.isHeadsUp()) { 866 mFirstChildMaxHeight += mCollapsedSize - row.getHeadsUpHeight(); 867 } 868 } 869 } else { 870 updateFirstChildMaxSizeToMaxHeight(); 871 } 872 } else { 873 mFirstChildMaxHeight = 0; 874 } 875 } 876 877 private void updateFirstChildMaxSizeToMaxHeight() { 878 // We are expanding the shade, expand it to its full height. 879 if (!isMaxSizeInitialized(mFirstChildWhileExpanding)) { 880 881 // This child was not layouted yet, wait for a layout pass 882 mFirstChildWhileExpanding 883 .addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 884 @Override 885 public void onLayoutChange(View v, int left, int top, int right, 886 int bottom, int oldLeft, int oldTop, int oldRight, 887 int oldBottom) { 888 if (mFirstChildWhileExpanding != null) { 889 mFirstChildMaxHeight = getMaxAllowedChildHeight( 890 mFirstChildWhileExpanding, null); 891 } else { 892 mFirstChildMaxHeight = 0; 893 } 894 v.removeOnLayoutChangeListener(this); 895 } 896 }); 897 } else { 898 mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding, null); 899 } 900 } 901 902 private boolean isMaxSizeInitialized(ExpandableView child) { 903 if (child instanceof ExpandableNotificationRow) { 904 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 905 return row.isMaxExpandHeightInitialized(); 906 } 907 return child == null || child.getWidth() != 0; 908 } 909 910 private View findFirstVisibleChild(ViewGroup container) { 911 int childCount = container.getChildCount(); 912 for (int i = 0; i < childCount; i++) { 913 View child = container.getChildAt(i); 914 if (child.getVisibility() != View.GONE) { 915 return child; 916 } 917 } 918 return null; 919 } 920 921 public void onExpansionStopped() { 922 mIsExpansionChanging = false; 923 mFirstChildWhileExpanding = null; 924 } 925 926 public void setIsExpanded(boolean isExpanded) { 927 this.mIsExpanded = isExpanded; 928 } 929 930 public void notifyChildrenChanged(final ViewGroup hostView) { 931 if (mIsExpansionChanging) { 932 hostView.post(new Runnable() { 933 @Override 934 public void run() { 935 updateFirstChildHeightWhileExpanding(hostView); 936 } 937 }); 938 } 939 } 940 941 public void setDimmed(boolean dimmed) { 942 updatePadding(dimmed); 943 } 944 945 public void onReset(ExpandableView view) { 946 if (view.equals(mFirstChildWhileExpanding)) { 947 updateFirstChildMaxSizeToMaxHeight(); 948 } 949 } 950 951 public void setHeadsUpManager(HeadsUpManager headsUpManager) { 952 mHeadsUpManager = headsUpManager; 953 } 954 955 class StackScrollAlgorithmState { 956 957 /** 958 * The scroll position of the algorithm 959 */ 960 public int scrollY; 961 962 /** 963 * The quantity of items which are in the top stack. 964 */ 965 public float itemsInTopStack; 966 967 /** 968 * how far in is the element currently transitioning into the top stack 969 */ 970 public float partialInTop; 971 972 /** 973 * The number of pixels the last child in the top stack has scrolled in to the stack 974 */ 975 public float scrolledPixelsTop; 976 977 /** 978 * The last item index which is in the top stack. 979 */ 980 public int lastTopStackIndex; 981 982 /** 983 * The quantity of items which are in the bottom stack. 984 */ 985 public float itemsInBottomStack; 986 987 /** 988 * how far in is the element currently transitioning into the bottom stack 989 */ 990 public float partialInBottom; 991 992 /** 993 * The children from the host view which are not gone. 994 */ 995 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>(); 996 } 997 998 } 999