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