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