blob: 6d2ba6a9d3bf5f136748ecf180fa2866ebdb592f [file] [log] [blame]
Selim Cinek67b22602014-03-10 15:40:16 +01001/*
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;
Christoph Studer6e3eceb2014-04-01 18:40:27 +020020import android.util.Log;
Selim Cinek67b22602014-03-10 15:40:16 +010021import 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
Christoph Studer6e3eceb2014-04-01 18:40:27 +020032 private static final String LOG_TAG = "StackScrollAlgorithm";
33
Selim Cinek67b22602014-03-10 15:40:16 +010034 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;
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200136 childViewState.location = StackScrollState.ViewState.LOCATION_UNKNOWN;
Selim Cinek67b22602014-03-10 15:40:16 +0100137 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);
Selim Cinek67b22602014-03-10 15:40:16 +0100150 } 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 = scrollOffset;
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200155 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING;
Selim Cinek67b22602014-03-10 15:40:16 +0100156 } 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);
Selim Cinek67b22602014-03-10 15:40:16 +0100163 } 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 }
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200172 } 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);
Selim Cinek67b22602014-03-10 15:40:16 +0100182 }
183 currentYPosition = nextYPosition;
184 yPositionInScrollView = yPositionInScrollViewAfterElement;
185 }
186 }
187
188 private float updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
189 float stackHeight, float transitioningPositionStart, float currentYPosition,
190 StackScrollState.ViewState childViewState, int childHeight, float nextYPosition) {
191 float newSize = transitioningPositionStart + mCollapsedSize - currentYPosition;
192 newSize = Math.min(childHeight, newSize);
193 // Transitioning element on top of bottom stack:
194 algorithmState.partialInBottom = 1.0f - (
195 (stackHeight - mBottomStackPeekSize - nextYPosition) / mCollapsedSize);
196 // Our element can be expanded, so we might even have to scroll further than
197 // mCollapsedSize
198 algorithmState.partialInBottom = Math.min(1.0f, algorithmState.partialInBottom);
199 float offset = mBottomStackIndentationFunctor.getValue(
200 algorithmState.partialInBottom);
201 nextYPosition = transitioningPositionStart + offset;
202 algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
203 // TODO: only temporarily collapse
204 if (childHeight != (int) newSize) {
205 childViewState.height = (int) newSize;
206 }
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200207 childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA;
208
Selim Cinek67b22602014-03-10 15:40:16 +0100209 return nextYPosition;
210 }
211
212 private float updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
213 float transitioningPositionStart, StackScrollState.ViewState childViewState,
214 int childHeight) {
215
216 float nextYPosition;
217 algorithmState.itemsInBottomStack += 1.0f;
218 if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
219 // We are visually entering the bottom stack
220 nextYPosition = transitioningPositionStart
221 + mBottomStackIndentationFunctor.getValue(
222 algorithmState.itemsInBottomStack);
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200223 childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_PEEKING;
Selim Cinek67b22602014-03-10 15:40:16 +0100224 } else {
225 // we are fully inside the stack
226 if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) {
227 childViewState.alpha = 0.0f;
228 } else if (algorithmState.itemsInBottomStack
229 > MAX_ITEMS_IN_BOTTOM_STACK + 1) {
230 childViewState.alpha = 1.0f - algorithmState.partialInBottom;
231 }
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200232 childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_HIDDEN;
Selim Cinek67b22602014-03-10 15:40:16 +0100233 nextYPosition = transitioningPositionStart + mBottomStackPeekSize;
234 }
235 // TODO: only temporarily collapse
236 if (childHeight != mCollapsedSize) {
237 childViewState.height = mCollapsedSize;
238 }
239 return nextYPosition;
240 }
241
242 private float updateStateForTopStackChild(StackScrollAlgorithmState algorithmState,
243 int numberOfElementsCompletelyIn, int i, StackScrollState.ViewState childViewState) {
244
245 float nextYPosition = 0;
246
247 // First we calculate the index relative to the current stack window of size at most
248 // {@link #MAX_ITEMS_IN_TOP_STACK}
249 int paddedIndex = i
250 - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0);
251 if (paddedIndex >= 0) {
252 // We are currently visually entering the top stack
253 nextYPosition = mCollapsedSize + mPaddingBetweenElements -
254 mTopStackIndentationFunctor.getValue(
255 algorithmState.itemsInTopStack - i - 1);
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200256 if (paddedIndex == 0) {
Selim Cinek67b22602014-03-10 15:40:16 +0100257 childViewState.alpha = 1.0f - algorithmState.partialInTop;
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200258 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN;
259 } else {
260 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING;
Selim Cinek67b22602014-03-10 15:40:16 +0100261 }
262 } else {
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200263 // We are hidden behind the top card and faded out, so we can hide ourselves.
264 childViewState.alpha = 0.0f;
265 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN;
Selim Cinek67b22602014-03-10 15:40:16 +0100266 }
267 return nextYPosition;
268 }
269
270 /**
271 * Find the number of items in the top stack and update the result state if needed.
272 *
273 * @param resultState The result state to update if a height change of an child occurs
274 * @param algorithmState The state in which the current pass of the algorithm is currently in
275 * and which will be updated
276 */
277 private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
278 StackScrollAlgorithmState algorithmState) {
279
280 // The y Position if the element would be in a regular scrollView
281 float yPositionInScrollView = 0.0f;
282 ViewGroup hostView = resultState.getHostView();
283 int childCount = hostView.getChildCount();
284
285 // find the number of elements in the top stack.
286 for (int i = 0; i < childCount; i++) {
287 View child = hostView.getChildAt(i);
288 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
289 int childHeight = child.getHeight();
290 float yPositionInScrollViewAfterElement = yPositionInScrollView
291 + childHeight
292 + mPaddingBetweenElements;
293 if (yPositionInScrollView < algorithmState.scrollY) {
294 if (yPositionInScrollViewAfterElement <= algorithmState.scrollY) {
295 // According to the regular scroll view we are fully off screen
296 algorithmState.itemsInTopStack += 1.0f;
297 if (childHeight != mCollapsedSize) {
298 childViewState.height = mCollapsedSize;
299 }
300 } else {
301 // According to the regular scroll view we are partially off screen
302 // If it is expanded we have to collapse it to a new size
303 float newSize = yPositionInScrollViewAfterElement
304 - mPaddingBetweenElements
305 - algorithmState.scrollY;
306
307 // How much did we scroll into this child
308 algorithmState.partialInTop = (mCollapsedSize - newSize) / (mCollapsedSize
309 + mPaddingBetweenElements);
310
311 // Our element can be expanded, so this can get negative
312 algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop);
313 algorithmState.itemsInTopStack += algorithmState.partialInTop;
314 // TODO: handle overlapping sizes with end stack
315 newSize = Math.max(mCollapsedSize, newSize);
316 // TODO: only temporarily collapse
317 if (newSize != childHeight) {
318 childViewState.height = (int) newSize;
319
320 // We decrease scrollY by the same amount we made this child smaller.
321 // The new scroll position is therefore the start of the element
322 algorithmState.scrollY = (int) yPositionInScrollView;
323 resultState.setScrollY(algorithmState.scrollY);
324 }
325 if (childHeight > mCollapsedSize) {
326 // If we are just resizing this child, this element is not treated to be
327 // transitioning into the stack and therefore it is the last element in
328 // the stack.
329 algorithmState.lastTopStackIndex = i;
330 break;
331 }
332 }
333 } else {
334 algorithmState.lastTopStackIndex = i;
335
336 // We are already past the stack so we can end the loop
337 break;
338 }
339 yPositionInScrollView = yPositionInScrollViewAfterElement;
340 }
341 }
342
343 /**
344 * Calculate the Z positions for all children based on the number of items in both stacks and
345 * save it in the resultState
346 *
347 * @param resultState The result state to update the zTranslation values
348 * @param algorithmState The state in which the current pass of the algorithm is currently in
349 */
350 private void updateZValuesForState(StackScrollState resultState,
351 StackScrollAlgorithmState algorithmState) {
352 ViewGroup hostView = resultState.getHostView();
353 int childCount = hostView.getChildCount();
354 for (int i = 0; i < childCount; i++) {
355 View child = hostView.getChildAt(i);
356 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
357 if (i < algorithmState.itemsInTopStack) {
358 float stackIndex = algorithmState.itemsInTopStack - i;
359 stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2);
360 childViewState.zTranslation = mZBasicHeight
361 + stackIndex * mZDistanceBetweenElements;
362 } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
363 float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
364 float translationZ = mZBasicHeight
365 - numItemsAbove * mZDistanceBetweenElements;
366 childViewState.zTranslation = translationZ;
367 } else {
368 childViewState.zTranslation = mZBasicHeight;
369 }
370 }
371 }
372
373 public float getLayoutHeight() {
374 return mLayoutHeight;
375 }
376
377 public void setLayoutHeight(float layoutHeight) {
378 this.mLayoutHeight = layoutHeight;
379 }
380
381 class StackScrollAlgorithmState {
382
383 /**
384 * The scroll position of the algorithm
385 */
386 public int scrollY;
387
388 /**
389 * The quantity of items which are in the top stack.
390 */
391 public float itemsInTopStack;
392
393 /**
394 * how far in is the element currently transitioning into the top stack
395 */
396 public float partialInTop;
397
398 /**
399 * The last item index which is in the top stack.
400 * NOTE: In the top stack the item after the transitioning element is also in the stack!
401 * This is needed to ensure a smooth transition between the y position in the regular
402 * scrollview and the one in the stack.
403 */
404 public int lastTopStackIndex;
405
406 /**
407 * The quantity of items which are in the bottom stack.
408 */
409 public float itemsInBottomStack;
410
411 /**
412 * how far in is the element currently transitioning into the bottom stack
413 */
414 public float partialInBottom;
415 }
416
417}