StackScrollAlgorithm.java revision b6d85ebfe4f9f5d3b7d7ab7b6123af02a0deb516
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; 24 25/** 26 * The Algorithm of the {@link com.android.systemui.statusbar.stack 27 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar 28 * .stack.StackScrollState} 29 */ 30public class StackScrollAlgorithm { 31 32 private static final String LOG_TAG = "StackScrollAlgorithm"; 33 34 private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3; 35 private static final int MAX_ITEMS_IN_TOP_STACK = 3; 36 37 private int mPaddingBetweenElements; 38 private int mCollapsedSize; 39 private int mTopStackPeekSize; 40 private int mBottomStackPeekSize; 41 private int mZDistanceBetweenElements; 42 private int mZBasicHeight; 43 44 private StackIndentationFunctor mTopStackIndentationFunctor; 45 private StackIndentationFunctor mBottomStackIndentationFunctor; 46 47 private float mLayoutHeight; 48 private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); 49 50 public StackScrollAlgorithm(Context context) { 51 initConstants(context); 52 } 53 54 private void initConstants(Context context) { 55 56 // currently the padding is in the elements themself 57 mPaddingBetweenElements = 0; 58 mCollapsedSize = context.getResources() 59 .getDimensionPixelSize(R.dimen.notification_row_min_height); 60 mTopStackPeekSize = context.getResources() 61 .getDimensionPixelSize(R.dimen.top_stack_peek_amount); 62 mBottomStackPeekSize = context.getResources() 63 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); 64 mZDistanceBetweenElements = context.getResources() 65 .getDimensionPixelSize(R.dimen.z_distance_between_notifications); 66 mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements; 67 68 mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( 69 MAX_ITEMS_IN_TOP_STACK, 70 mTopStackPeekSize, 71 mCollapsedSize + mPaddingBetweenElements, 72 0.5f); 73 mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( 74 MAX_ITEMS_IN_BOTTOM_STACK, 75 mBottomStackPeekSize, 76 mBottomStackPeekSize, 77 0.5f); 78 } 79 80 81 public void getStackScrollState(StackScrollState resultState) { 82 // The state of the local variables are saved in an algorithmState to easily subdivide it 83 // into multiple phases. 84 StackScrollAlgorithmState algorithmState = mTempAlgorithmState; 85 86 // First we reset the view states to their default values. 87 resultState.resetViewStates(); 88 89 // The first element is always in there so it's initialized with 1.0f; 90 algorithmState.itemsInTopStack = 1.0f; 91 algorithmState.partialInTop = 0.0f; 92 algorithmState.lastTopStackIndex = 0; 93 algorithmState.scrollY = resultState.getScrollY(); 94 algorithmState.itemsInBottomStack = 0.0f; 95 96 // Phase 1: 97 findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState); 98 99 // Phase 2: 100 updatePositionsForState(resultState, algorithmState); 101 102 // Phase 3: 103 updateZValuesForState(resultState, algorithmState); 104 105 // write the algorithm state to the result 106 resultState.setScrollY(algorithmState.scrollY); 107 } 108 109 /** 110 * Determine the positions for the views. This is the main part of the algorithm. 111 * 112 * @param resultState The result state to update if a change to the properties of a child occurs 113 * @param algorithmState The state in which the current pass of the algorithm is currently in 114 * and which will be updated 115 */ 116 private void updatePositionsForState(StackScrollState resultState, 117 StackScrollAlgorithmState algorithmState) { 118 float stackHeight = getLayoutHeight(); 119 120 // The position where the bottom stack starts. 121 float transitioningPositionStart = stackHeight - mCollapsedSize - mBottomStackPeekSize; 122 123 // The y coordinate of the current child. 124 float currentYPosition = 0.0f; 125 126 // How far in is the element currently transitioning into the bottom stack. 127 float yPositionInScrollView = 0.0f; 128 129 ViewGroup hostView = resultState.getHostView(); 130 int childCount = hostView.getChildCount(); 131 int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack; 132 for (int i = 0; i < childCount; i++) { 133 View child = hostView.getChildAt(i); 134 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); 135 childViewState.yTranslation = currentYPosition; 136 childViewState.location = StackScrollState.ViewState.LOCATION_UNKNOWN; 137 int childHeight = child.getHeight(); 138 // The y position after this element 139 float nextYPosition = currentYPosition + childHeight + mPaddingBetweenElements; 140 float yPositionInScrollViewAfterElement = yPositionInScrollView 141 + childHeight 142 + mPaddingBetweenElements; 143 float scrollOffset = yPositionInScrollViewAfterElement - algorithmState.scrollY; 144 if (i < algorithmState.lastTopStackIndex) { 145 // Case 1: 146 // We are in the top Stack 147 nextYPosition = updateStateForTopStackChild(algorithmState, 148 numberOfElementsCompletelyIn, 149 i, childViewState); 150 } else if (i == algorithmState.lastTopStackIndex) { 151 // Case 2: 152 // First element of regular scrollview comes next, so the position is just the 153 // scrolling position 154 nextYPosition = Math.min(scrollOffset, transitioningPositionStart); 155 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING; 156 } else if (nextYPosition >= transitioningPositionStart) { 157 if (currentYPosition >= transitioningPositionStart) { 158 // Case 3: 159 // According to the regular scroll view we are fully translated out of the 160 // bottom of the screen so we are fully in the bottom stack 161 nextYPosition = updateStateForChildFullyInBottomStack(algorithmState, 162 transitioningPositionStart, childViewState, childHeight); 163 } else { 164 // Case 4: 165 // According to the regular scroll view we are currently translating out of / 166 // into the bottom of the screen 167 nextYPosition = updateStateForChildTransitioningInBottom( 168 algorithmState, stackHeight, transitioningPositionStart, 169 currentYPosition, childViewState, 170 childHeight, nextYPosition); 171 } 172 } else { 173 childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA; 174 } 175 // The first card is always rendered. 176 if (i == 0) { 177 childViewState.alpha = 1.0f; 178 childViewState.location = StackScrollState.ViewState.LOCATION_FIRST_CARD; 179 } 180 if (childViewState.location == StackScrollState.ViewState.LOCATION_UNKNOWN) { 181 Log.wtf(LOG_TAG, "Failed to assign location for child " + i); 182 } 183 nextYPosition = Math.max(0, nextYPosition); 184 currentYPosition = nextYPosition; 185 yPositionInScrollView = yPositionInScrollViewAfterElement; 186 } 187 } 188 189 private float updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, 190 float stackHeight, float transitioningPositionStart, float currentYPosition, 191 StackScrollState.ViewState childViewState, int childHeight, float nextYPosition) { 192 float newSize = transitioningPositionStart + mCollapsedSize - currentYPosition; 193 newSize = Math.min(childHeight, newSize); 194 // Transitioning element on top of bottom stack: 195 algorithmState.partialInBottom = 1.0f - ( 196 (stackHeight - mBottomStackPeekSize - nextYPosition) / mCollapsedSize); 197 // Our element can be expanded, so we might even have to scroll further than 198 // mCollapsedSize 199 algorithmState.partialInBottom = Math.min(1.0f, algorithmState.partialInBottom); 200 float offset = mBottomStackIndentationFunctor.getValue( 201 algorithmState.partialInBottom); 202 nextYPosition = transitioningPositionStart + offset; 203 algorithmState.itemsInBottomStack += algorithmState.partialInBottom; 204 // TODO: only temporarily collapse 205 if (childHeight != (int) newSize) { 206 childViewState.height = (int) newSize; 207 } 208 childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA; 209 210 return nextYPosition; 211 } 212 213 private float updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, 214 float transitioningPositionStart, StackScrollState.ViewState childViewState, 215 int childHeight) { 216 217 float nextYPosition; 218 algorithmState.itemsInBottomStack += 1.0f; 219 if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) { 220 // We are visually entering the bottom stack 221 nextYPosition = transitioningPositionStart 222 + mBottomStackIndentationFunctor.getValue( 223 algorithmState.itemsInBottomStack); 224 childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_PEEKING; 225 } else { 226 // we are fully inside the stack 227 if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) { 228 childViewState.alpha = 0.0f; 229 } else if (algorithmState.itemsInBottomStack 230 > MAX_ITEMS_IN_BOTTOM_STACK + 1) { 231 childViewState.alpha = 1.0f - algorithmState.partialInBottom; 232 } 233 childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_HIDDEN; 234 nextYPosition = transitioningPositionStart + mBottomStackPeekSize; 235 } 236 // TODO: only temporarily collapse 237 if (childHeight != mCollapsedSize) { 238 childViewState.height = mCollapsedSize; 239 } 240 return nextYPosition; 241 } 242 243 private float updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, 244 int numberOfElementsCompletelyIn, int i, StackScrollState.ViewState childViewState) { 245 246 float nextYPosition = 0; 247 248 // First we calculate the index relative to the current stack window of size at most 249 // {@link #MAX_ITEMS_IN_TOP_STACK} 250 int paddedIndex = i 251 - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0); 252 if (paddedIndex >= 0) { 253 // We are currently visually entering the top stack 254 nextYPosition = mCollapsedSize + mPaddingBetweenElements - 255 mTopStackIndentationFunctor.getValue( 256 algorithmState.itemsInTopStack - i - 1); 257 nextYPosition = Math.min(nextYPosition, mLayoutHeight - mCollapsedSize 258 - mBottomStackPeekSize); 259 if (paddedIndex == 0) { 260 childViewState.alpha = 1.0f - algorithmState.partialInTop; 261 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN; 262 } else { 263 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING; 264 } 265 } else { 266 // We are hidden behind the top card and faded out, so we can hide ourselves. 267 childViewState.alpha = 0.0f; 268 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN; 269 } 270 return nextYPosition; 271 } 272 273 /** 274 * Find the number of items in the top stack and update the result state if needed. 275 * 276 * @param resultState The result state to update if a height change of an child occurs 277 * @param algorithmState The state in which the current pass of the algorithm is currently in 278 * and which will be updated 279 */ 280 private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState, 281 StackScrollAlgorithmState algorithmState) { 282 283 // The y Position if the element would be in a regular scrollView 284 float yPositionInScrollView = 0.0f; 285 ViewGroup hostView = resultState.getHostView(); 286 int childCount = hostView.getChildCount(); 287 288 // find the number of elements in the top stack. 289 for (int i = 0; i < childCount; i++) { 290 View child = hostView.getChildAt(i); 291 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); 292 int childHeight = child.getHeight(); 293 float yPositionInScrollViewAfterElement = yPositionInScrollView 294 + childHeight 295 + mPaddingBetweenElements; 296 if (yPositionInScrollView < algorithmState.scrollY) { 297 if (yPositionInScrollViewAfterElement <= algorithmState.scrollY) { 298 // According to the regular scroll view we are fully off screen 299 algorithmState.itemsInTopStack += 1.0f; 300 if (childHeight != mCollapsedSize) { 301 childViewState.height = mCollapsedSize; 302 } 303 } else { 304 // According to the regular scroll view we are partially off screen 305 // If it is expanded we have to collapse it to a new size 306 float newSize = yPositionInScrollViewAfterElement 307 - mPaddingBetweenElements 308 - algorithmState.scrollY; 309 310 // How much did we scroll into this child 311 algorithmState.partialInTop = (mCollapsedSize - newSize) / (mCollapsedSize 312 + mPaddingBetweenElements); 313 314 // Our element can be expanded, so this can get negative 315 algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop); 316 algorithmState.itemsInTopStack += algorithmState.partialInTop; 317 // TODO: handle overlapping sizes with end stack 318 newSize = Math.max(mCollapsedSize, newSize); 319 // TODO: only temporarily collapse 320 if (newSize != childHeight) { 321 childViewState.height = (int) newSize; 322 323 // We decrease scrollY by the same amount we made this child smaller. 324 // The new scroll position is therefore the start of the element 325 algorithmState.scrollY = (int) yPositionInScrollView; 326 resultState.setScrollY(algorithmState.scrollY); 327 } 328 if (childHeight > mCollapsedSize) { 329 // If we are just resizing this child, this element is not treated to be 330 // transitioning into the stack and therefore it is the last element in 331 // the stack. 332 algorithmState.lastTopStackIndex = i; 333 break; 334 } 335 } 336 } else { 337 algorithmState.lastTopStackIndex = i; 338 339 // We are already past the stack so we can end the loop 340 break; 341 } 342 yPositionInScrollView = yPositionInScrollViewAfterElement; 343 } 344 } 345 346 /** 347 * Calculate the Z positions for all children based on the number of items in both stacks and 348 * save it in the resultState 349 * 350 * @param resultState The result state to update the zTranslation values 351 * @param algorithmState The state in which the current pass of the algorithm is currently in 352 */ 353 private void updateZValuesForState(StackScrollState resultState, 354 StackScrollAlgorithmState algorithmState) { 355 ViewGroup hostView = resultState.getHostView(); 356 int childCount = hostView.getChildCount(); 357 for (int i = 0; i < childCount; i++) { 358 View child = hostView.getChildAt(i); 359 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); 360 if (i < algorithmState.itemsInTopStack) { 361 float stackIndex = algorithmState.itemsInTopStack - i; 362 stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2); 363 childViewState.zTranslation = mZBasicHeight 364 + stackIndex * mZDistanceBetweenElements; 365 } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) { 366 float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack); 367 float translationZ = mZBasicHeight 368 - numItemsAbove * mZDistanceBetweenElements; 369 childViewState.zTranslation = translationZ; 370 } else { 371 childViewState.zTranslation = mZBasicHeight; 372 } 373 } 374 } 375 376 public float getLayoutHeight() { 377 return mLayoutHeight; 378 } 379 380 public void setLayoutHeight(float layoutHeight) { 381 this.mLayoutHeight = layoutHeight; 382 } 383 384 class StackScrollAlgorithmState { 385 386 /** 387 * The scroll position of the algorithm 388 */ 389 public int scrollY; 390 391 /** 392 * The quantity of items which are in the top stack. 393 */ 394 public float itemsInTopStack; 395 396 /** 397 * how far in is the element currently transitioning into the top stack 398 */ 399 public float partialInTop; 400 401 /** 402 * The last item index which is in the top stack. 403 * NOTE: In the top stack the item after the transitioning element is also in the stack! 404 * This is needed to ensure a smooth transition between the y position in the regular 405 * scrollview and the one in the stack. 406 */ 407 public int lastTopStackIndex; 408 409 /** 410 * The quantity of items which are in the bottom stack. 411 */ 412 public float itemsInBottomStack; 413 414 /** 415 * how far in is the element currently transitioning into the bottom stack 416 */ 417 public float partialInBottom; 418 } 419 420} 421