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