StackScrollAlgorithm.java revision 9f347ae27c9c9051f5130ac27fffb0e4fbef01a3
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; 23import com.android.systemui.R; 24import com.android.systemui.statusbar.ExpandableNotificationRow; 25 26import java.util.ArrayList; 27 28/** 29 * The Algorithm of the {@link com.android.systemui.statusbar.stack 30 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar 31 * .stack.StackScrollState} 32 */ 33public class StackScrollAlgorithm { 34 35 private static final String LOG_TAG = "StackScrollAlgorithm"; 36 37 private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3; 38 private static final int MAX_ITEMS_IN_TOP_STACK = 3; 39 40 private int mPaddingBetweenElements; 41 private int mCollapsedSize; 42 private int mTopStackPeekSize; 43 private int mBottomStackPeekSize; 44 private int mZDistanceBetweenElements; 45 private int mZBasicHeight; 46 47 private StackIndentationFunctor mTopStackIndentationFunctor; 48 private StackIndentationFunctor mBottomStackIndentationFunctor; 49 50 private float mLayoutHeight; 51 private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); 52 private boolean mIsExpansionChanging; 53 private int mFirstChildMaxHeight; 54 private boolean mIsExpanded; 55 private View mFirstChildWhileExpanding; 56 private boolean mExpandedOnStart; 57 58 public StackScrollAlgorithm(Context context) { 59 initConstants(context); 60 } 61 62 private void initConstants(Context context) { 63 64 // currently the padding is in the elements themself 65 mPaddingBetweenElements = 0; 66 mCollapsedSize = context.getResources() 67 .getDimensionPixelSize(R.dimen.notification_row_min_height); 68 mTopStackPeekSize = context.getResources() 69 .getDimensionPixelSize(R.dimen.top_stack_peek_amount); 70 mBottomStackPeekSize = context.getResources() 71 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); 72 mZDistanceBetweenElements = context.getResources() 73 .getDimensionPixelSize(R.dimen.z_distance_between_notifications); 74 mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements; 75 76 mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( 77 MAX_ITEMS_IN_TOP_STACK, 78 mTopStackPeekSize, 79 mCollapsedSize + mPaddingBetweenElements, 80 0.5f); 81 mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( 82 MAX_ITEMS_IN_BOTTOM_STACK, 83 mBottomStackPeekSize, 84 mBottomStackPeekSize, 85 0.5f); 86 } 87 88 89 public void getStackScrollState(StackScrollState resultState) { 90 // The state of the local variables are saved in an algorithmState to easily subdivide it 91 // into multiple phases. 92 StackScrollAlgorithmState algorithmState = mTempAlgorithmState; 93 94 // First we reset the view states to their default values. 95 resultState.resetViewStates(); 96 97 // The first element is always in there so it's initialized with 1.0f; 98 algorithmState.itemsInTopStack = 1.0f; 99 algorithmState.partialInTop = 0.0f; 100 algorithmState.lastTopStackIndex = 0; 101 algorithmState.scrollY = resultState.getScrollY(); 102 algorithmState.itemsInBottomStack = 0.0f; 103 updateVisibleChildren(resultState, algorithmState); 104 105 // Phase 1: 106 findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState); 107 108 // Phase 2: 109 updatePositionsForState(resultState, algorithmState); 110 111 // Phase 3: 112 updateZValuesForState(resultState, algorithmState); 113 114 // write the algorithm state to the result 115 resultState.setScrollY(algorithmState.scrollY); 116 } 117 118 /** 119 * Update the visible children on the state. 120 */ 121 private void updateVisibleChildren(StackScrollState resultState, 122 StackScrollAlgorithmState state) { 123 ViewGroup hostView = resultState.getHostView(); 124 int childCount = hostView.getChildCount(); 125 state.visibleChildren.clear(); 126 state.visibleChildren.ensureCapacity(childCount); 127 for (int i = 0; i < childCount; i++) { 128 View v = hostView.getChildAt(i); 129 if (v.getVisibility() != View.GONE) { 130 state.visibleChildren.add(v); 131 } 132 } 133 } 134 135 /** 136 * Determine the positions for the views. This is the main part of the algorithm. 137 * 138 * @param resultState The result state to update if a change to the properties of a child occurs 139 * @param algorithmState The state in which the current pass of the algorithm is currently in 140 * and which will be updated 141 */ 142 private void updatePositionsForState(StackScrollState resultState, 143 StackScrollAlgorithmState algorithmState) { 144 float stackHeight = getLayoutHeight(); 145 146 // The starting position of the bottom stack peek 147 float bottomPeekStart = stackHeight - mBottomStackPeekSize; 148 149 // The position where the bottom stack starts. 150 float transitioningPositionStart = 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 View child = algorithmState.visibleChildren.get(i); 162 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); 163 childViewState.yTranslation = currentYPosition; 164 childViewState.location = StackScrollState.ViewState.LOCATION_UNKNOWN; 165 int childHeight = child.getHeight(); 166 // The y position after this element 167 float nextYPosition = currentYPosition + childHeight + mPaddingBetweenElements; 168 float yPositionInScrollViewAfterElement = yPositionInScrollView 169 + childHeight 170 + mPaddingBetweenElements; 171 float scrollOffset = yPositionInScrollViewAfterElement - algorithmState.scrollY; 172 if (i < algorithmState.lastTopStackIndex) { 173 // Case 1: 174 // We are in the top Stack 175 nextYPosition = updateStateForTopStackChild(algorithmState, 176 numberOfElementsCompletelyIn, 177 i, childViewState); 178 } else if (i == algorithmState.lastTopStackIndex) { 179 // Case 2: 180 // First element of regular scrollview comes next, so the position is just the 181 // scrolling position 182 nextYPosition = updateStateForFirstScrollingChild(transitioningPositionStart, 183 childViewState, scrollOffset); 184 } else if (nextYPosition >= transitioningPositionStart) { 185 if (currentYPosition >= transitioningPositionStart) { 186 // Case 3: 187 // According to the regular scroll view we are fully translated out of the 188 // bottom of the screen so we are fully in the bottom stack 189 nextYPosition = updateStateForChildFullyInBottomStack(algorithmState, 190 transitioningPositionStart, childViewState, childHeight); 191 } else { 192 // Case 4: 193 // According to the regular scroll view we are currently translating out of / 194 // into the bottom of the screen 195 nextYPosition = updateStateForChildTransitioningInBottom( 196 algorithmState, stackHeight, transitioningPositionStart, 197 currentYPosition, childViewState, 198 childHeight, nextYPosition); 199 } 200 } else { 201 childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA; 202 } 203 // The first card is always rendered. 204 if (i == 0) { 205 childViewState.alpha = 1.0f; 206 childViewState.location = StackScrollState.ViewState.LOCATION_FIRST_CARD; 207 } 208 if (childViewState.location == StackScrollState.ViewState.LOCATION_UNKNOWN) { 209 Log.wtf(LOG_TAG, "Failed to assign location for child " + i); 210 } 211 nextYPosition = Math.max(0, nextYPosition); 212 currentYPosition = nextYPosition; 213 yPositionInScrollView = yPositionInScrollViewAfterElement; 214 } 215 } 216 217 /** 218 * Update the state for the first child which is in the regular scrolling area. 219 * 220 * @param transitioningPositionStart the transition starting position of the bottom stack 221 * @param childViewState the view state of the child 222 * @param scrollOffset the position in the regular scroll view after this child 223 * @return the next child position 224 */ 225 private float updateStateForFirstScrollingChild(float transitioningPositionStart, 226 StackScrollState.ViewState childViewState, float scrollOffset) { 227 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING; 228 if (scrollOffset < transitioningPositionStart) { 229 return scrollOffset; 230 } else { 231 return transitioningPositionStart; 232 } 233 } 234 235 private int getMaxAllowedChildHeight(View child) { 236 if (child instanceof ExpandableNotificationRow) { 237 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 238 return row.getMaximumAllowedExpandHeight(); 239 } 240 return child.getHeight(); 241 } 242 243 private float updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, 244 float stackHeight, float transitioningPositionStart, float currentYPosition, 245 StackScrollState.ViewState childViewState, int childHeight, float nextYPosition) { 246 float newSize = transitioningPositionStart + mCollapsedSize - currentYPosition; 247 newSize = Math.min(childHeight, newSize); 248 // Transitioning element on top of bottom stack: 249 algorithmState.partialInBottom = 1.0f - ( 250 (stackHeight - mBottomStackPeekSize - nextYPosition) / mCollapsedSize); 251 // Our element can be expanded, so we might even have to scroll further than 252 // mCollapsedSize 253 algorithmState.partialInBottom = Math.min(1.0f, algorithmState.partialInBottom); 254 float offset = mBottomStackIndentationFunctor.getValue( 255 algorithmState.partialInBottom); 256 nextYPosition = transitioningPositionStart + offset; 257 algorithmState.itemsInBottomStack += algorithmState.partialInBottom; 258 // TODO: only temporarily collapse 259 if (childHeight != (int) newSize) { 260 childViewState.height = (int) newSize; 261 } 262 childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA; 263 264 return nextYPosition; 265 } 266 267 private float updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, 268 float transitioningPositionStart, StackScrollState.ViewState childViewState, 269 int childHeight) { 270 271 float nextYPosition; 272 algorithmState.itemsInBottomStack += 1.0f; 273 if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) { 274 // We are visually entering the bottom stack 275 nextYPosition = transitioningPositionStart 276 + mBottomStackIndentationFunctor.getValue( 277 algorithmState.itemsInBottomStack); 278 childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_PEEKING; 279 } else { 280 // we are fully inside the stack 281 if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) { 282 childViewState.alpha = 0.0f; 283 } else if (algorithmState.itemsInBottomStack 284 > MAX_ITEMS_IN_BOTTOM_STACK + 1) { 285 childViewState.alpha = 1.0f - algorithmState.partialInBottom; 286 } 287 childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_HIDDEN; 288 nextYPosition = transitioningPositionStart + mBottomStackPeekSize; 289 } 290 // TODO: only temporarily collapse 291 if (childHeight != mCollapsedSize) { 292 childViewState.height = mCollapsedSize; 293 } 294 return nextYPosition; 295 } 296 297 private float updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, 298 int numberOfElementsCompletelyIn, int i, StackScrollState.ViewState childViewState) { 299 300 float nextYPosition = 0; 301 302 // First we calculate the index relative to the current stack window of size at most 303 // {@link #MAX_ITEMS_IN_TOP_STACK} 304 int paddedIndex = i 305 - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0); 306 if (paddedIndex >= 0) { 307 // We are currently visually entering the top stack 308 nextYPosition = mCollapsedSize + mPaddingBetweenElements - 309 mTopStackIndentationFunctor.getValue( 310 algorithmState.itemsInTopStack - i - 1); 311 nextYPosition = Math.min(nextYPosition, mLayoutHeight - mCollapsedSize 312 - mBottomStackPeekSize); 313 if (paddedIndex == 0) { 314 childViewState.alpha = 1.0f - algorithmState.partialInTop; 315 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN; 316 } else { 317 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING; 318 } 319 } else { 320 // We are hidden behind the top card and faded out, so we can hide ourselves. 321 childViewState.alpha = 0.0f; 322 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN; 323 } 324 return nextYPosition; 325 } 326 327 /** 328 * Find the number of items in the top stack and update the result state if needed. 329 * 330 * @param resultState The result state to update if a height change of an child occurs 331 * @param algorithmState The state in which the current pass of the algorithm is currently in 332 * and which will be updated 333 */ 334 private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState, 335 StackScrollAlgorithmState algorithmState) { 336 337 // The y Position if the element would be in a regular scrollView 338 float yPositionInScrollView = 0.0f; 339 int childCount = algorithmState.visibleChildren.size(); 340 341 // find the number of elements in the top stack. 342 for (int i = 0; i < childCount; i++) { 343 View child = algorithmState.visibleChildren.get(i); 344 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); 345 int childHeight = child.getHeight(); 346 float yPositionInScrollViewAfterElement = yPositionInScrollView 347 + childHeight 348 + mPaddingBetweenElements; 349 if (yPositionInScrollView < algorithmState.scrollY) { 350 if (yPositionInScrollViewAfterElement <= algorithmState.scrollY) { 351 // According to the regular scroll view we are fully off screen 352 algorithmState.itemsInTopStack += 1.0f; 353 if (childHeight != mCollapsedSize) { 354 childViewState.height = mCollapsedSize; 355 } 356 } else { 357 // According to the regular scroll view we are partially off screen 358 // If it is expanded we have to collapse it to a new size 359 float newSize = yPositionInScrollViewAfterElement 360 - mPaddingBetweenElements 361 - algorithmState.scrollY; 362 363 // How much did we scroll into this child 364 algorithmState.partialInTop = (mCollapsedSize - newSize) / (mCollapsedSize 365 + mPaddingBetweenElements); 366 367 // Our element can be expanded, so this can get negative 368 algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop); 369 algorithmState.itemsInTopStack += algorithmState.partialInTop; 370 // TODO: handle overlapping sizes with end stack 371 newSize = Math.max(mCollapsedSize, newSize); 372 // TODO: only temporarily collapse 373 if (newSize != childHeight) { 374 childViewState.height = (int) newSize; 375 376 // We decrease scrollY by the same amount we made this child smaller. 377 // The new scroll position is therefore the start of the element 378 algorithmState.scrollY = (int) yPositionInScrollView; 379 resultState.setScrollY(algorithmState.scrollY); 380 } 381 if (childHeight > mCollapsedSize) { 382 // If we are just resizing this child, this element is not treated to be 383 // transitioning into the stack and therefore it is the last element in 384 // the stack. 385 algorithmState.lastTopStackIndex = i; 386 break; 387 } 388 } 389 } else { 390 algorithmState.lastTopStackIndex = i; 391 if (i == 0) { 392 393 // The starting position of the bottom stack peek 394 float bottomPeekStart = getLayoutHeight() - mBottomStackPeekSize; 395 // Collapse and expand the first child while the shade is being expanded 396 float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding 397 ? mFirstChildMaxHeight 398 : childHeight; 399 childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight), 400 mCollapsedSize); 401 } 402 // We are already past the stack so we can end the loop 403 break; 404 } 405 yPositionInScrollView = yPositionInScrollViewAfterElement; 406 } 407 } 408 409 /** 410 * Calculate the Z positions for all children based on the number of items in both stacks and 411 * save it in the resultState 412 * 413 * @param resultState The result state to update the zTranslation values 414 * @param algorithmState The state in which the current pass of the algorithm is currently in 415 */ 416 private void updateZValuesForState(StackScrollState resultState, 417 StackScrollAlgorithmState algorithmState) { 418 int childCount = algorithmState.visibleChildren.size(); 419 for (int i = 0; i < childCount; i++) { 420 View child = algorithmState.visibleChildren.get(i); 421 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); 422 if (i < algorithmState.itemsInTopStack) { 423 float stackIndex = algorithmState.itemsInTopStack - i; 424 stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2); 425 childViewState.zTranslation = mZBasicHeight 426 + stackIndex * mZDistanceBetweenElements; 427 } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) { 428 float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack); 429 float translationZ = mZBasicHeight 430 - numItemsAbove * mZDistanceBetweenElements; 431 childViewState.zTranslation = translationZ; 432 } else { 433 childViewState.zTranslation = mZBasicHeight; 434 } 435 } 436 } 437 438 public float getLayoutHeight() { 439 return mLayoutHeight; 440 } 441 442 public void setLayoutHeight(float layoutHeight) { 443 this.mLayoutHeight = layoutHeight; 444 } 445 446 public void onExpansionStarted(StackScrollState currentState) { 447 mIsExpansionChanging = true; 448 mExpandedOnStart = mIsExpanded; 449 ViewGroup hostView = currentState.getHostView(); 450 updateFirstChildHeightWhileExpanding(hostView); 451 } 452 453 private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) { 454 mFirstChildWhileExpanding = findFirstVisibleChild(hostView); 455 if (mFirstChildWhileExpanding != null) { 456 if (mExpandedOnStart) { 457 458 // We are collapsing the shade, so the first child can get as most as high as the 459 // current height. 460 mFirstChildMaxHeight = mFirstChildWhileExpanding.getHeight(); 461 } else { 462 463 // We are expanding the shade, expand it to its full height. 464 mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding); 465 } 466 } else { 467 mFirstChildMaxHeight = 0; 468 } 469 } 470 471 private View findFirstVisibleChild(ViewGroup container) { 472 int childCount = container.getChildCount(); 473 for (int i = 0; i < childCount; i++) { 474 View child = container.getChildAt(i); 475 if (child.getVisibility() != View.GONE) { 476 return child; 477 } 478 } 479 return null; 480 } 481 482 public void onExpansionStopped() { 483 mIsExpansionChanging = false; 484 mFirstChildWhileExpanding = null; 485 } 486 487 public void setIsExpanded(boolean isExpanded) { 488 this.mIsExpanded = isExpanded; 489 } 490 491 public void notifyChildrenChanged(ViewGroup hostView) { 492 if (mIsExpansionChanging) { 493 updateFirstChildHeightWhileExpanding(hostView); 494 } 495 } 496 497 class StackScrollAlgorithmState { 498 499 /** 500 * The scroll position of the algorithm 501 */ 502 public int scrollY; 503 504 /** 505 * The quantity of items which are in the top stack. 506 */ 507 public float itemsInTopStack; 508 509 /** 510 * how far in is the element currently transitioning into the top stack 511 */ 512 public float partialInTop; 513 514 /** 515 * The last item index which is in the top stack. 516 * NOTE: In the top stack the item after the transitioning element is also in the stack! 517 * This is needed to ensure a smooth transition between the y position in the regular 518 * scrollview and the one in the stack. 519 */ 520 public int lastTopStackIndex; 521 522 /** 523 * The quantity of items which are in the bottom stack. 524 */ 525 public float itemsInBottomStack; 526 527 /** 528 * how far in is the element currently transitioning into the bottom stack 529 */ 530 public float partialInBottom; 531 532 /** 533 * The children from the host view which are not gone. 534 */ 535 public final ArrayList<View> visibleChildren = new ArrayList<View>(); 536 } 537 538} 539