StackScrollAlgorithm.java revision 9cbadd3c08a7d7dd3412743dd04aecb16c5a1595
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 private int mPaddingBetweenElements; 43 private int mCollapsedSize; 44 private int mTopStackPeekSize; 45 private int mBottomStackPeekSize; 46 private int mZDistanceBetweenElements; 47 private int mZBasicHeight; 48 49 private StackIndentationFunctor mTopStackIndentationFunctor; 50 private StackIndentationFunctor mBottomStackIndentationFunctor; 51 52 private int mLayoutHeight; 53 54 /** mLayoutHeight - mTopPadding */ 55 private int mInnerHeight; 56 private int mTopPadding; 57 private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); 58 private boolean mIsExpansionChanging; 59 private int mFirstChildMaxHeight; 60 private boolean mIsExpanded; 61 private ExpandableView mFirstChildWhileExpanding; 62 private boolean mExpandedOnStart; 63 private int mTopStackTotalSize; 64 65 public StackScrollAlgorithm(Context context) { 66 initConstants(context); 67 } 68 69 private void initConstants(Context context) { 70 mPaddingBetweenElements = context.getResources() 71 .getDimensionPixelSize(R.dimen.notification_padding); 72 mCollapsedSize = context.getResources() 73 .getDimensionPixelSize(R.dimen.notification_min_height); 74 mTopStackPeekSize = context.getResources() 75 .getDimensionPixelSize(R.dimen.top_stack_peek_amount); 76 mBottomStackPeekSize = context.getResources() 77 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); 78 mZDistanceBetweenElements = context.getResources() 79 .getDimensionPixelSize(R.dimen.z_distance_between_notifications); 80 mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements; 81 mTopStackTotalSize = mCollapsedSize + mPaddingBetweenElements; 82 mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( 83 MAX_ITEMS_IN_TOP_STACK, 84 mTopStackPeekSize, 85 mTopStackTotalSize, 86 0.5f); 87 mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( 88 MAX_ITEMS_IN_BOTTOM_STACK, 89 mBottomStackPeekSize, 90 mCollapsedSize + mBottomStackPeekSize + mPaddingBetweenElements, 91 0.5f); 92 } 93 94 95 public void getStackScrollState(StackScrollState resultState) { 96 // The state of the local variables are saved in an algorithmState to easily subdivide it 97 // into multiple phases. 98 StackScrollAlgorithmState algorithmState = mTempAlgorithmState; 99 100 // First we reset the view states to their default values. 101 resultState.resetViewStates(); 102 103 algorithmState.itemsInTopStack = 0.0f; 104 algorithmState.partialInTop = 0.0f; 105 algorithmState.lastTopStackIndex = 0; 106 algorithmState.scrolledPixelsTop = 0; 107 algorithmState.itemsInBottomStack = 0.0f; 108 algorithmState.partialInBottom = 0.0f; 109 algorithmState.scrollY = resultState.getScrollY() + mCollapsedSize; 110 111 updateVisibleChildren(resultState, algorithmState); 112 113 // Phase 1: 114 findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState); 115 116 // Phase 2: 117 updatePositionsForState(resultState, algorithmState); 118 119 // Phase 3: 120 updateZValuesForState(resultState, algorithmState); 121 } 122 123 /** 124 * Update the visible children on the state. 125 */ 126 private void updateVisibleChildren(StackScrollState resultState, 127 StackScrollAlgorithmState state) { 128 ViewGroup hostView = resultState.getHostView(); 129 int childCount = hostView.getChildCount(); 130 state.visibleChildren.clear(); 131 state.visibleChildren.ensureCapacity(childCount); 132 for (int i = 0; i < childCount; i++) { 133 ExpandableView v = (ExpandableView) hostView.getChildAt(i); 134 if (v.getVisibility() != View.GONE) { 135 state.visibleChildren.add(v); 136 } 137 } 138 } 139 140 /** 141 * Determine the positions for the views. This is the main part of the algorithm. 142 * 143 * @param resultState The result state to update if a change to the properties of a child occurs 144 * @param algorithmState The state in which the current pass of the algorithm is currently in 145 * and which will be updated 146 */ 147 private void updatePositionsForState(StackScrollState resultState, 148 StackScrollAlgorithmState algorithmState) { 149 150 // The starting position of the bottom stack peek 151 float bottomPeekStart = mInnerHeight - mBottomStackPeekSize; 152 153 // The position where the bottom stack starts. 154 float bottomStackStart = bottomPeekStart - mCollapsedSize; 155 156 // The y coordinate of the current child. 157 float currentYPosition = 0.0f; 158 159 // How far in is the element currently transitioning into the bottom stack. 160 float yPositionInScrollView = 0.0f; 161 162 int childCount = algorithmState.visibleChildren.size(); 163 int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack; 164 for (int i = 0; i < childCount; i++) { 165 ExpandableView child = algorithmState.visibleChildren.get(i); 166 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); 167 childViewState.location = StackScrollState.ViewState.LOCATION_UNKNOWN; 168 int childHeight = getMaxAllowedChildHeight(child); 169 float yPositionInScrollViewAfterElement = yPositionInScrollView 170 + childHeight 171 + mPaddingBetweenElements; 172 float scrollOffset = yPositionInScrollView - algorithmState.scrollY + mCollapsedSize; 173 174 if (i == algorithmState.lastTopStackIndex + 1) { 175 // Normally the position of this child is the position in the regular scrollview, 176 // but if the two stacks are very close to each other, 177 // then have have to push it even more upwards to the position of the bottom 178 // stack start. 179 currentYPosition = Math.min(scrollOffset, bottomStackStart); 180 } 181 childViewState.yTranslation = currentYPosition; 182 183 // The y position after this element 184 float nextYPosition = currentYPosition + childHeight + 185 mPaddingBetweenElements; 186 187 if (i <= algorithmState.lastTopStackIndex) { 188 // Case 1: 189 // We are in the top Stack 190 updateStateForTopStackChild(algorithmState, 191 numberOfElementsCompletelyIn, i, childHeight, childViewState, scrollOffset); 192 clampYTranslation(childViewState, childHeight); 193 // check if we are overlapping with the bottom stack 194 if (childViewState.yTranslation + childHeight + mPaddingBetweenElements 195 >= bottomStackStart && !mIsExpansionChanging && i != 0) { 196 // TODO: handle overlapping sizes with end stack better 197 // we just collapse this element 198 childViewState.height = mCollapsedSize; 199 } 200 } else if (nextYPosition >= bottomStackStart) { 201 // Case 2: 202 // We are in the bottom stack. 203 if (currentYPosition >= bottomStackStart) { 204 // According to the regular scroll view we are fully translated out of the 205 // bottom of the screen so we are fully in the bottom stack 206 updateStateForChildFullyInBottomStack(algorithmState, 207 bottomStackStart, childViewState, childHeight); 208 } else { 209 // According to the regular scroll view we are currently translating out of / 210 // into the bottom of the screen 211 updateStateForChildTransitioningInBottom(algorithmState, 212 bottomStackStart, bottomPeekStart, currentYPosition, 213 childViewState, childHeight); 214 } 215 } else { 216 // Case 3: 217 // We are in the regular scroll area. 218 childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA; 219 clampYTranslation(childViewState, childHeight); 220 } 221 222 // The first card is always rendered. 223 if (i == 0) { 224 childViewState.alpha = 1.0f; 225 childViewState.yTranslation = 0; 226 childViewState.location = StackScrollState.ViewState.LOCATION_FIRST_CARD; 227 } 228 if (childViewState.location == StackScrollState.ViewState.LOCATION_UNKNOWN) { 229 Log.wtf(LOG_TAG, "Failed to assign location for child " + i); 230 } 231 currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements; 232 yPositionInScrollView = yPositionInScrollViewAfterElement; 233 234 childViewState.yTranslation += mTopPadding; 235 } 236 } 237 238 /** 239 * Clamp the yTranslation both up and down to valid positions. 240 * 241 * @param childViewState the view state of the child 242 * @param childHeight the height of this child 243 */ 244 private void clampYTranslation(StackScrollState.ViewState childViewState, int childHeight) { 245 clampPositionToBottomStackStart(childViewState, childHeight); 246 clampPositionToTopStackEnd(childViewState, childHeight); 247 } 248 249 /** 250 * Clamp the yTranslation of the child down such that its end is at most on the beginning of 251 * the bottom stack. 252 * 253 * @param childViewState the view state of the child 254 * @param childHeight the height of this child 255 */ 256 private void clampPositionToBottomStackStart(StackScrollState.ViewState childViewState, 257 int childHeight) { 258 childViewState.yTranslation = Math.min(childViewState.yTranslation, 259 mInnerHeight - mBottomStackPeekSize - childHeight); 260 } 261 262 /** 263 * Clamp the yTranslation of the child up such that its end is at lest on the end of the top 264 * stack.get 265 * 266 * @param childViewState the view state of the child 267 * @param childHeight the height of this child 268 */ 269 private void clampPositionToTopStackEnd(StackScrollState.ViewState childViewState, 270 int childHeight) { 271 childViewState.yTranslation = Math.max(childViewState.yTranslation, 272 mCollapsedSize - childHeight); 273 } 274 275 private int getMaxAllowedChildHeight(View child) { 276 if (child instanceof ExpandableNotificationRow) { 277 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 278 return row.getIntrinsicHeight(); 279 } else if (child instanceof ExpandableView) { 280 ExpandableView expandableView = (ExpandableView) child; 281 return expandableView.getActualHeight(); 282 } 283 return child == null? mCollapsedSize : child.getHeight(); 284 } 285 286 private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, 287 float transitioningPositionStart, float bottomPeakStart, float currentYPosition, 288 StackScrollState.ViewState childViewState, int childHeight) { 289 290 // This is the transitioning element on top of bottom stack, calculate how far we are in. 291 algorithmState.partialInBottom = 1.0f - ( 292 (transitioningPositionStart - currentYPosition) / (childHeight + 293 mPaddingBetweenElements)); 294 295 // the offset starting at the transitionPosition of the bottom stack 296 float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom); 297 algorithmState.itemsInBottomStack += algorithmState.partialInBottom; 298 childViewState.yTranslation = transitioningPositionStart + offset - childHeight 299 - mPaddingBetweenElements; 300 301 // We want at least to be at the end of the top stack when collapsing 302 clampPositionToTopStackEnd(childViewState, childHeight); 303 childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA; 304 } 305 306 private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, 307 float transitioningPositionStart, StackScrollState.ViewState childViewState, 308 int childHeight) { 309 310 float currentYPosition; 311 algorithmState.itemsInBottomStack += 1.0f; 312 if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) { 313 // We are visually entering the bottom stack 314 currentYPosition = transitioningPositionStart 315 + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack) 316 - mPaddingBetweenElements; 317 childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_PEEKING; 318 } else { 319 // we are fully inside the stack 320 if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) { 321 childViewState.alpha = 0.0f; 322 } else if (algorithmState.itemsInBottomStack 323 > MAX_ITEMS_IN_BOTTOM_STACK + 1) { 324 childViewState.alpha = 1.0f - algorithmState.partialInBottom; 325 } 326 childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_HIDDEN; 327 currentYPosition = mInnerHeight; 328 } 329 childViewState.yTranslation = currentYPosition - childHeight; 330 clampPositionToTopStackEnd(childViewState, childHeight); 331 } 332 333 private void updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, 334 int numberOfElementsCompletelyIn, int i, int childHeight, 335 StackScrollState.ViewState childViewState, float scrollOffset) { 336 337 338 // First we calculate the index relative to the current stack window of size at most 339 // {@link #MAX_ITEMS_IN_TOP_STACK} 340 int paddedIndex = i - 1 341 - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0); 342 if (paddedIndex >= 0) { 343 344 // We are currently visually entering the top stack 345 float distanceToStack = childHeight - algorithmState.scrolledPixelsTop; 346 if (i == algorithmState.lastTopStackIndex && distanceToStack > mTopStackTotalSize) { 347 348 // Child is currently translating into stack but not yet inside slow down zone. 349 // Handle it like the regular scrollview. 350 childViewState.yTranslation = scrollOffset; 351 } else { 352 // Apply stacking logic. 353 float numItemsBefore; 354 if (i == algorithmState.lastTopStackIndex) { 355 numItemsBefore = 1.0f - (distanceToStack / mTopStackTotalSize); 356 } else { 357 numItemsBefore = algorithmState.itemsInTopStack - i; 358 } 359 // The end position of the current child 360 float currentChildEndY = mCollapsedSize + mTopStackTotalSize - 361 mTopStackIndentationFunctor.getValue(numItemsBefore); 362 childViewState.yTranslation = currentChildEndY - childHeight; 363 } 364 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING; 365 } else { 366 if (paddedIndex == -1) { 367 childViewState.alpha = 1.0f - algorithmState.partialInTop; 368 } else { 369 // We are hidden behind the top card and faded out, so we can hide ourselves. 370 childViewState.alpha = 0.0f; 371 } 372 childViewState.yTranslation = mCollapsedSize - childHeight; 373 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN; 374 } 375 376 377 } 378 379 /** 380 * Find the number of items in the top stack and update the result state if needed. 381 * 382 * @param resultState The result state to update if a height change of an child occurs 383 * @param algorithmState The state in which the current pass of the algorithm is currently in 384 * and which will be updated 385 */ 386 private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState, 387 StackScrollAlgorithmState algorithmState) { 388 389 // The y Position if the element would be in a regular scrollView 390 float yPositionInScrollView = 0.0f; 391 int childCount = algorithmState.visibleChildren.size(); 392 393 // find the number of elements in the top stack. 394 for (int i = 0; i < childCount; i++) { 395 ExpandableView child = algorithmState.visibleChildren.get(i); 396 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); 397 int childHeight = getMaxAllowedChildHeight(child); 398 float yPositionInScrollViewAfterElement = yPositionInScrollView 399 + childHeight 400 + mPaddingBetweenElements; 401 if (yPositionInScrollView < algorithmState.scrollY) { 402 if (i == 0 && algorithmState.scrollY == mCollapsedSize) { 403 404 // The starting position of the bottom stack peek 405 int bottomPeekStart = mInnerHeight - mBottomStackPeekSize; 406 // Collapse and expand the first child while the shade is being expanded 407 float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding 408 ? mFirstChildMaxHeight 409 : childHeight; 410 childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight), 411 mCollapsedSize); 412 algorithmState.itemsInTopStack = 1.0f; 413 414 } else if (yPositionInScrollViewAfterElement < algorithmState.scrollY) { 415 // According to the regular scroll view we are fully off screen 416 algorithmState.itemsInTopStack += 1.0f; 417 if (i == 0) { 418 childViewState.height = mCollapsedSize; 419 } 420 } else { 421 // According to the regular scroll view we are partially off screen 422 // If it is expanded we have to collapse it to a new size 423 float newSize = yPositionInScrollViewAfterElement 424 - mPaddingBetweenElements 425 - algorithmState.scrollY; 426 427 if (i == 0) { 428 newSize += mCollapsedSize; 429 } 430 431 // How much did we scroll into this child 432 algorithmState.scrolledPixelsTop = childHeight - newSize; 433 algorithmState.partialInTop = (algorithmState.scrolledPixelsTop) / (childHeight 434 + mPaddingBetweenElements); 435 436 // Our element can be expanded, so this can get negative 437 algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop); 438 algorithmState.itemsInTopStack += algorithmState.partialInTop; 439 newSize = Math.max(mCollapsedSize, newSize); 440 if (i == 0) { 441 childViewState.height = (int) newSize; 442 } 443 algorithmState.lastTopStackIndex = i; 444 break; 445 } 446 } else { 447 algorithmState.lastTopStackIndex = i - 1; 448 // We are already past the stack so we can end the loop 449 break; 450 } 451 yPositionInScrollView = yPositionInScrollViewAfterElement; 452 } 453 } 454 455 /** 456 * Calculate the Z positions for all children based on the number of items in both stacks and 457 * save it in the resultState 458 * 459 * @param resultState The result state to update the zTranslation values 460 * @param algorithmState The state in which the current pass of the algorithm is currently in 461 */ 462 private void updateZValuesForState(StackScrollState resultState, 463 StackScrollAlgorithmState algorithmState) { 464 int childCount = algorithmState.visibleChildren.size(); 465 for (int i = 0; i < childCount; i++) { 466 View child = algorithmState.visibleChildren.get(i); 467 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); 468 if (i < algorithmState.itemsInTopStack) { 469 float stackIndex = algorithmState.itemsInTopStack - i; 470 stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2); 471 childViewState.zTranslation = mZBasicHeight 472 + stackIndex * mZDistanceBetweenElements; 473 } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) { 474 float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack); 475 float translationZ = mZBasicHeight 476 - numItemsAbove * mZDistanceBetweenElements; 477 childViewState.zTranslation = translationZ; 478 } else { 479 childViewState.zTranslation = mZBasicHeight; 480 } 481 } 482 } 483 484 public void setLayoutHeight(int layoutHeight) { 485 this.mLayoutHeight = layoutHeight; 486 updateInnerHeight(); 487 } 488 489 public void setTopPadding(int topPadding) { 490 mTopPadding = topPadding; 491 updateInnerHeight(); 492 } 493 494 private void updateInnerHeight() { 495 mInnerHeight = mLayoutHeight - mTopPadding; 496 } 497 498 public void onExpansionStarted(StackScrollState currentState) { 499 mIsExpansionChanging = true; 500 mExpandedOnStart = mIsExpanded; 501 ViewGroup hostView = currentState.getHostView(); 502 updateFirstChildHeightWhileExpanding(hostView); 503 } 504 505 private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) { 506 mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView); 507 if (mFirstChildWhileExpanding != null) { 508 if (mExpandedOnStart) { 509 510 // We are collapsing the shade, so the first child can get as most as high as the 511 // current height. 512 mFirstChildMaxHeight = mFirstChildWhileExpanding.getActualHeight(); 513 } else { 514 515 // We are expanding the shade, expand it to its full height. 516 if (mFirstChildWhileExpanding.getWidth() == 0) { 517 518 // This child was not layouted yet, wait for a layout pass 519 mFirstChildWhileExpanding 520 .addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 521 @Override 522 public void onLayoutChange(View v, int left, int top, int right, 523 int bottom, int oldLeft, int oldTop, int oldRight, 524 int oldBottom) { 525 if (mFirstChildWhileExpanding != null) { 526 mFirstChildMaxHeight = getMaxAllowedChildHeight( 527 mFirstChildWhileExpanding); 528 } else { 529 mFirstChildMaxHeight = 0; 530 } 531 v.removeOnLayoutChangeListener(this); 532 } 533 }); 534 } else { 535 mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding); 536 } 537 } 538 } else { 539 mFirstChildMaxHeight = 0; 540 } 541 } 542 543 private View findFirstVisibleChild(ViewGroup container) { 544 int childCount = container.getChildCount(); 545 for (int i = 0; i < childCount; i++) { 546 View child = container.getChildAt(i); 547 if (child.getVisibility() != View.GONE) { 548 return child; 549 } 550 } 551 return null; 552 } 553 554 public void onExpansionStopped() { 555 mIsExpansionChanging = false; 556 mFirstChildWhileExpanding = null; 557 } 558 559 public void setIsExpanded(boolean isExpanded) { 560 this.mIsExpanded = isExpanded; 561 } 562 563 public void notifyChildrenChanged(ViewGroup hostView) { 564 if (mIsExpansionChanging) { 565 updateFirstChildHeightWhileExpanding(hostView); 566 } 567 } 568 569 class StackScrollAlgorithmState { 570 571 /** 572 * The scroll position of the algorithm 573 */ 574 public int scrollY; 575 576 /** 577 * The quantity of items which are in the top stack. 578 */ 579 public float itemsInTopStack; 580 581 /** 582 * how far in is the element currently transitioning into the top stack 583 */ 584 public float partialInTop; 585 586 /** 587 * The number of pixels the last child in the top stack has scrolled in to the stack 588 */ 589 public float scrolledPixelsTop; 590 591 /** 592 * The last item index which is in the top stack. 593 */ 594 public int lastTopStackIndex; 595 596 /** 597 * The quantity of items which are in the bottom stack. 598 */ 599 public float itemsInBottomStack; 600 601 /** 602 * how far in is the element currently transitioning into the bottom stack 603 */ 604 public float partialInBottom; 605 606 /** 607 * The children from the host view which are not gone. 608 */ 609 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>(); 610 } 611 612} 613