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 
17 package com.android.systemui.statusbar.stack;
18 
19 import android.content.Context;
20 import android.util.DisplayMetrics;
21 import android.util.Log;
22 import android.view.View;
23 import android.view.ViewGroup;
24 
25 import com.android.systemui.R;
26 import com.android.systemui.statusbar.ExpandableNotificationRow;
27 import com.android.systemui.statusbar.ExpandableView;
28 
29 import java.util.ArrayList;
30 
31 /**
32  * The Algorithm of the {@link com.android.systemui.statusbar.stack
33  * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
34  * .stack.StackScrollState}
35  */
36 public class StackScrollAlgorithm {
37 
38     private static final String LOG_TAG = "StackScrollAlgorithm";
39 
40     private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3;
41     private static final int MAX_ITEMS_IN_TOP_STACK = 3;
42 
43     public static final float DIMMED_SCALE = 0.95f;
44 
45     private int mPaddingBetweenElements;
46     private int mCollapsedSize;
47     private int mTopStackPeekSize;
48     private int mBottomStackPeekSize;
49     private int mZDistanceBetweenElements;
50     private int mZBasicHeight;
51     private int mRoundedRectCornerRadius;
52 
53     private StackIndentationFunctor mTopStackIndentationFunctor;
54     private StackIndentationFunctor mBottomStackIndentationFunctor;
55 
56     private int mLayoutHeight;
57 
58     /** mLayoutHeight - mTopPadding */
59     private int mInnerHeight;
60     private int mTopPadding;
61     private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
62     private boolean mIsExpansionChanging;
63     private int mFirstChildMaxHeight;
64     private boolean mIsExpanded;
65     private ExpandableView mFirstChildWhileExpanding;
66     private boolean mExpandedOnStart;
67     private int mTopStackTotalSize;
68     private int mPaddingBetweenElementsDimmed;
69     private int mPaddingBetweenElementsNormal;
70     private int mBottomStackSlowDownLength;
71     private int mTopStackSlowDownLength;
72     private int mCollapseSecondCardPadding;
73     private boolean mIsSmallScreen;
74     private int mMaxNotificationHeight;
75     private boolean mScaleDimmed;
76 
StackScrollAlgorithm(Context context)77     public StackScrollAlgorithm(Context context) {
78         initConstants(context);
79         updatePadding(false);
80     }
81 
updatePadding(boolean dimmed)82     private void updatePadding(boolean dimmed) {
83         mPaddingBetweenElements = dimmed && mScaleDimmed
84                 ? mPaddingBetweenElementsDimmed
85                 : mPaddingBetweenElementsNormal;
86         mTopStackTotalSize = mTopStackSlowDownLength + mPaddingBetweenElements
87                 + mTopStackPeekSize;
88         mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
89                 MAX_ITEMS_IN_TOP_STACK,
90                 mTopStackPeekSize,
91                 mTopStackTotalSize - mTopStackPeekSize,
92                 0.5f);
93         mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
94                 MAX_ITEMS_IN_BOTTOM_STACK,
95                 mBottomStackPeekSize,
96                 getBottomStackSlowDownLength(),
97                 0.5f);
98     }
99 
getBottomStackSlowDownLength()100     public int getBottomStackSlowDownLength() {
101         return mBottomStackSlowDownLength + mPaddingBetweenElements;
102     }
103 
initConstants(Context context)104     private void initConstants(Context context) {
105         mPaddingBetweenElementsDimmed = context.getResources()
106                 .getDimensionPixelSize(R.dimen.notification_padding_dimmed);
107         mPaddingBetweenElementsNormal = context.getResources()
108                 .getDimensionPixelSize(R.dimen.notification_padding);
109         mCollapsedSize = context.getResources()
110                 .getDimensionPixelSize(R.dimen.notification_min_height);
111         mMaxNotificationHeight = context.getResources()
112                 .getDimensionPixelSize(R.dimen.notification_max_height);
113         mTopStackPeekSize = context.getResources()
114                 .getDimensionPixelSize(R.dimen.top_stack_peek_amount);
115         mBottomStackPeekSize = context.getResources()
116                 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
117         mZDistanceBetweenElements = context.getResources()
118                 .getDimensionPixelSize(R.dimen.z_distance_between_notifications);
119         mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements;
120         mBottomStackSlowDownLength = context.getResources()
121                 .getDimensionPixelSize(R.dimen.bottom_stack_slow_down_length);
122         mTopStackSlowDownLength = context.getResources()
123                 .getDimensionPixelSize(R.dimen.top_stack_slow_down_length);
124         mRoundedRectCornerRadius = context.getResources().getDimensionPixelSize(
125                 R.dimen.notification_material_rounded_rect_radius);
126         mCollapseSecondCardPadding = context.getResources().getDimensionPixelSize(
127                 R.dimen.notification_collapse_second_card_padding);
128         mScaleDimmed = context.getResources().getDisplayMetrics().densityDpi
129                 >= DisplayMetrics.DENSITY_XXHIGH;
130     }
131 
shouldScaleDimmed()132     public boolean shouldScaleDimmed() {
133         return mScaleDimmed;
134     }
135 
getStackScrollState(AmbientState ambientState, StackScrollState resultState)136     public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) {
137         // The state of the local variables are saved in an algorithmState to easily subdivide it
138         // into multiple phases.
139         StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
140 
141         // First we reset the view states to their default values.
142         resultState.resetViewStates();
143 
144         algorithmState.itemsInTopStack = 0.0f;
145         algorithmState.partialInTop = 0.0f;
146         algorithmState.lastTopStackIndex = 0;
147         algorithmState.scrolledPixelsTop = 0;
148         algorithmState.itemsInBottomStack = 0.0f;
149         algorithmState.partialInBottom = 0.0f;
150         float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */);
151 
152         int scrollY = ambientState.getScrollY();
153 
154         // Due to the overScroller, the stackscroller can have negative scroll state. This is
155         // already accounted for by the top padding and doesn't need an additional adaption
156         scrollY = Math.max(0, scrollY);
157         algorithmState.scrollY = (int) (scrollY + mCollapsedSize + bottomOverScroll);
158 
159         updateVisibleChildren(resultState, algorithmState);
160 
161         // Phase 1:
162         findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState);
163 
164         // Phase 2:
165         updatePositionsForState(resultState, algorithmState);
166 
167         // Phase 3:
168         updateZValuesForState(resultState, algorithmState);
169 
170         handleDraggedViews(ambientState, resultState, algorithmState);
171         updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState);
172         updateClipping(resultState, algorithmState);
173         updateSpeedBumpState(resultState, algorithmState, ambientState.getSpeedBumpIndex());
174     }
175 
updateSpeedBumpState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, int speedBumpIndex)176     private void updateSpeedBumpState(StackScrollState resultState,
177             StackScrollAlgorithmState algorithmState, int speedBumpIndex) {
178         int childCount = algorithmState.visibleChildren.size();
179         for (int i = 0; i < childCount; i++) {
180             View child = algorithmState.visibleChildren.get(i);
181             StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
182 
183             // The speed bump can also be gone, so equality needs to be taken when comparing
184             // indices.
185             childViewState.belowSpeedBump = speedBumpIndex != -1 && i >= speedBumpIndex;
186         }
187     }
188 
updateClipping(StackScrollState resultState, StackScrollAlgorithmState algorithmState)189     private void updateClipping(StackScrollState resultState,
190             StackScrollAlgorithmState algorithmState) {
191         float previousNotificationEnd = 0;
192         float previousNotificationStart = 0;
193         boolean previousNotificationIsSwiped = false;
194         int childCount = algorithmState.visibleChildren.size();
195         for (int i = 0; i < childCount; i++) {
196             ExpandableView child = algorithmState.visibleChildren.get(i);
197             StackScrollState.ViewState state = resultState.getViewStateForView(child);
198             float newYTranslation = state.yTranslation + state.height * (1f - state.scale) / 2f;
199             float newHeight = state.height * state.scale;
200             // apply clipping and shadow
201             float newNotificationEnd = newYTranslation + newHeight;
202 
203             float clipHeight;
204             if (previousNotificationIsSwiped) {
205                 // When the previous notification is swiped, we don't clip the content to the
206                 // bottom of it.
207                 clipHeight = newHeight;
208             } else {
209                 clipHeight = newNotificationEnd - previousNotificationEnd;
210                 clipHeight = Math.max(0.0f, clipHeight);
211                 if (clipHeight != 0.0f) {
212 
213                     // In the unlocked shade we have to clip a little bit higher because of the rounded
214                     // corners of the notifications, but only if we are not fully overlapped by
215                     // the top card.
216                     float clippingCorrection = state.dimmed
217                             ? 0
218                             : mRoundedRectCornerRadius * state.scale;
219                     clipHeight += clippingCorrection;
220                 }
221             }
222 
223             updateChildClippingAndBackground(state, newHeight, clipHeight,
224                     newHeight - (previousNotificationStart - newYTranslation));
225 
226             if (!child.isTransparent()) {
227                 // Only update the previous values if we are not transparent,
228                 // otherwise we would clip to a transparent view.
229                 previousNotificationStart = newYTranslation + state.clipTopAmount * state.scale;
230                 previousNotificationEnd = newNotificationEnd;
231                 previousNotificationIsSwiped = child.getTranslationX() != 0;
232             }
233         }
234     }
235 
236     /**
237      * Updates the shadow outline and the clipping for a view.
238      *
239      * @param state the viewState to update
240      * @param realHeight the currently applied height of the view
241      * @param clipHeight the desired clip height, the rest of the view will be clipped from the top
242      * @param backgroundHeight the desired background height. The shadows of the view will be
243      *                         based on this height and the content will be clipped from the top
244      */
updateChildClippingAndBackground(StackScrollState.ViewState state, float realHeight, float clipHeight, float backgroundHeight)245     private void updateChildClippingAndBackground(StackScrollState.ViewState state,
246             float realHeight, float clipHeight, float backgroundHeight) {
247         if (realHeight > clipHeight) {
248             // Rather overlap than create a hole.
249             state.topOverLap = (int) Math.floor((realHeight - clipHeight) / state.scale);
250         } else {
251             state.topOverLap = 0;
252         }
253         if (realHeight > backgroundHeight) {
254             // Rather overlap than create a hole.
255             state.clipTopAmount = (int) Math.floor((realHeight - backgroundHeight) / state.scale);
256         } else {
257             state.clipTopAmount = 0;
258         }
259     }
260 
261     /**
262      * Updates the dimmed, activated and hiding sensitive states of the children.
263      */
updateDimmedActivatedHideSensitive(AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState)264     private void updateDimmedActivatedHideSensitive(AmbientState ambientState,
265             StackScrollState resultState, StackScrollAlgorithmState algorithmState) {
266         boolean dimmed = ambientState.isDimmed();
267         boolean dark = ambientState.isDark();
268         boolean hideSensitive = ambientState.isHideSensitive();
269         View activatedChild = ambientState.getActivatedChild();
270         int childCount = algorithmState.visibleChildren.size();
271         for (int i = 0; i < childCount; i++) {
272             View child = algorithmState.visibleChildren.get(i);
273             StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
274             childViewState.dimmed = dimmed;
275             childViewState.dark = dark;
276             childViewState.hideSensitive = hideSensitive;
277             boolean isActivatedChild = activatedChild == child;
278             childViewState.scale = !mScaleDimmed || !dimmed || isActivatedChild
279                     ? 1.0f
280                     : DIMMED_SCALE;
281             if (dimmed && isActivatedChild) {
282                 childViewState.zTranslation += 2.0f * mZDistanceBetweenElements;
283             }
284         }
285     }
286 
287     /**
288      * Handle the special state when views are being dragged
289      */
handleDraggedViews(AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState)290     private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState,
291             StackScrollAlgorithmState algorithmState) {
292         ArrayList<View> draggedViews = ambientState.getDraggedViews();
293         for (View draggedView : draggedViews) {
294             int childIndex = algorithmState.visibleChildren.indexOf(draggedView);
295             if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) {
296                 View nextChild = algorithmState.visibleChildren.get(childIndex + 1);
297                 if (!draggedViews.contains(nextChild)) {
298                     // only if the view is not dragged itself we modify its state to be fully
299                     // visible
300                     StackScrollState.ViewState viewState = resultState.getViewStateForView(
301                             nextChild);
302                     // The child below the dragged one must be fully visible
303                     viewState.alpha = 1;
304                 }
305 
306                 // Lets set the alpha to the one it currently has, as its currently being dragged
307                 StackScrollState.ViewState viewState = resultState.getViewStateForView(draggedView);
308                 // The dragged child should keep the set alpha
309                 viewState.alpha = draggedView.getAlpha();
310             }
311         }
312     }
313 
314     /**
315      * Update the visible children on the state.
316      */
updateVisibleChildren(StackScrollState resultState, StackScrollAlgorithmState state)317     private void updateVisibleChildren(StackScrollState resultState,
318             StackScrollAlgorithmState state) {
319         ViewGroup hostView = resultState.getHostView();
320         int childCount = hostView.getChildCount();
321         state.visibleChildren.clear();
322         state.visibleChildren.ensureCapacity(childCount);
323         for (int i = 0; i < childCount; i++) {
324             ExpandableView v = (ExpandableView) hostView.getChildAt(i);
325             if (v.getVisibility() != View.GONE) {
326                 StackScrollState.ViewState viewState = resultState.getViewStateForView(v);
327                 viewState.notGoneIndex = state.visibleChildren.size();
328                 state.visibleChildren.add(v);
329             }
330         }
331     }
332 
333     /**
334      * Determine the positions for the views. This is the main part of the algorithm.
335      *
336      * @param resultState The result state to update if a change to the properties of a child occurs
337      * @param algorithmState The state in which the current pass of the algorithm is currently in
338      */
updatePositionsForState(StackScrollState resultState, StackScrollAlgorithmState algorithmState)339     private void updatePositionsForState(StackScrollState resultState,
340             StackScrollAlgorithmState algorithmState) {
341 
342         // The starting position of the bottom stack peek
343         float bottomPeekStart = mInnerHeight - mBottomStackPeekSize;
344 
345         // The position where the bottom stack starts.
346         float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength;
347 
348         // The y coordinate of the current child.
349         float currentYPosition = 0.0f;
350 
351         // How far in is the element currently transitioning into the bottom stack.
352         float yPositionInScrollView = 0.0f;
353 
354         int childCount = algorithmState.visibleChildren.size();
355         int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack;
356         for (int i = 0; i < childCount; i++) {
357             ExpandableView child = algorithmState.visibleChildren.get(i);
358             StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
359             childViewState.location = StackScrollState.ViewState.LOCATION_UNKNOWN;
360             int childHeight = getMaxAllowedChildHeight(child);
361             float yPositionInScrollViewAfterElement = yPositionInScrollView
362                     + childHeight
363                     + mPaddingBetweenElements;
364             float scrollOffset = yPositionInScrollView - algorithmState.scrollY + mCollapsedSize;
365 
366             if (i == algorithmState.lastTopStackIndex + 1) {
367                 // Normally the position of this child is the position in the regular scrollview,
368                 // but if the two stacks are very close to each other,
369                 // then have have to push it even more upwards to the position of the bottom
370                 // stack start.
371                 currentYPosition = Math.min(scrollOffset, bottomStackStart);
372             }
373             childViewState.yTranslation = currentYPosition;
374 
375             // The y position after this element
376             float nextYPosition = currentYPosition + childHeight +
377                     mPaddingBetweenElements;
378 
379             if (i <= algorithmState.lastTopStackIndex) {
380                 // Case 1:
381                 // We are in the top Stack
382                 updateStateForTopStackChild(algorithmState,
383                         numberOfElementsCompletelyIn, i, childHeight, childViewState, scrollOffset);
384                 clampPositionToTopStackEnd(childViewState, childHeight);
385 
386                 // check if we are overlapping with the bottom stack
387                 if (childViewState.yTranslation + childHeight + mPaddingBetweenElements
388                         >= bottomStackStart && !mIsExpansionChanging && i != 0 && mIsSmallScreen) {
389                     // we just collapse this element slightly
390                     int newSize = (int) Math.max(bottomStackStart - mPaddingBetweenElements -
391                             childViewState.yTranslation, mCollapsedSize);
392                     childViewState.height = newSize;
393                     updateStateForChildTransitioningInBottom(algorithmState, bottomStackStart,
394                             bottomPeekStart, childViewState.yTranslation, childViewState,
395                             childHeight);
396                 }
397                 clampPositionToBottomStackStart(childViewState, childViewState.height);
398             } else if (nextYPosition >= bottomStackStart) {
399                 // Case 2:
400                 // We are in the bottom stack.
401                 if (currentYPosition >= bottomStackStart) {
402                     // According to the regular scroll view we are fully translated out of the
403                     // bottom of the screen so we are fully in the bottom stack
404                     updateStateForChildFullyInBottomStack(algorithmState,
405                             bottomStackStart, childViewState, childHeight);
406                 } else {
407                     // According to the regular scroll view we are currently translating out of /
408                     // into the bottom of the screen
409                     updateStateForChildTransitioningInBottom(algorithmState,
410                             bottomStackStart, bottomPeekStart, currentYPosition,
411                             childViewState, childHeight);
412                 }
413             } else {
414                 // Case 3:
415                 // We are in the regular scroll area.
416                 childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA;
417                 clampYTranslation(childViewState, childHeight);
418             }
419 
420             // The first card is always rendered.
421             if (i == 0) {
422                 childViewState.alpha = 1.0f;
423                 childViewState.yTranslation = Math.max(mCollapsedSize - algorithmState.scrollY, 0);
424                 if (childViewState.yTranslation + childViewState.height
425                         > bottomPeekStart - mCollapseSecondCardPadding) {
426                     childViewState.height = (int) Math.max(
427                             bottomPeekStart - mCollapseSecondCardPadding
428                                     - childViewState.yTranslation, mCollapsedSize);
429                 }
430                 childViewState.location = StackScrollState.ViewState.LOCATION_FIRST_CARD;
431             }
432             if (childViewState.location == StackScrollState.ViewState.LOCATION_UNKNOWN) {
433                 Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
434             }
435             currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements;
436             yPositionInScrollView = yPositionInScrollViewAfterElement;
437 
438             childViewState.yTranslation += mTopPadding;
439         }
440     }
441 
442     /**
443      * Clamp the yTranslation both up and down to valid positions.
444      *
445      * @param childViewState the view state of the child
446      * @param childHeight the height of this child
447      */
clampYTranslation(StackScrollState.ViewState childViewState, int childHeight)448     private void clampYTranslation(StackScrollState.ViewState childViewState, int childHeight) {
449         clampPositionToBottomStackStart(childViewState, childHeight);
450         clampPositionToTopStackEnd(childViewState, childHeight);
451     }
452 
453     /**
454      * Clamp the yTranslation of the child down such that its end is at most on the beginning of
455      * the bottom stack.
456      *
457      * @param childViewState the view state of the child
458      * @param childHeight the height of this child
459      */
clampPositionToBottomStackStart(StackScrollState.ViewState childViewState, int childHeight)460     private void clampPositionToBottomStackStart(StackScrollState.ViewState childViewState,
461             int childHeight) {
462         childViewState.yTranslation = Math.min(childViewState.yTranslation,
463                 mInnerHeight - mBottomStackPeekSize - mCollapseSecondCardPadding - childHeight);
464     }
465 
466     /**
467      * Clamp the yTranslation of the child up such that its end is at lest on the end of the top
468      * stack.get
469      *
470      * @param childViewState the view state of the child
471      * @param childHeight the height of this child
472      */
clampPositionToTopStackEnd(StackScrollState.ViewState childViewState, int childHeight)473     private void clampPositionToTopStackEnd(StackScrollState.ViewState childViewState,
474             int childHeight) {
475         childViewState.yTranslation = Math.max(childViewState.yTranslation,
476                 mCollapsedSize - childHeight);
477     }
478 
getMaxAllowedChildHeight(View child)479     private int getMaxAllowedChildHeight(View child) {
480         if (child instanceof ExpandableNotificationRow) {
481             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
482             return row.getIntrinsicHeight();
483         } else if (child instanceof ExpandableView) {
484             ExpandableView expandableView = (ExpandableView) child;
485             return expandableView.getActualHeight();
486         }
487         return child == null? mCollapsedSize : child.getHeight();
488     }
489 
updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, float transitioningPositionStart, float bottomPeakStart, float currentYPosition, StackScrollState.ViewState childViewState, int childHeight)490     private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
491             float transitioningPositionStart, float bottomPeakStart, float currentYPosition,
492             StackScrollState.ViewState childViewState, int childHeight) {
493 
494         // This is the transitioning element on top of bottom stack, calculate how far we are in.
495         algorithmState.partialInBottom = 1.0f - (
496                 (transitioningPositionStart - currentYPosition) / (childHeight +
497                         mPaddingBetweenElements));
498 
499         // the offset starting at the transitionPosition of the bottom stack
500         float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom);
501         algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
502         int newHeight = childHeight;
503         if (childHeight > mCollapsedSize && mIsSmallScreen) {
504             newHeight = (int) Math.max(Math.min(transitioningPositionStart + offset -
505                     mPaddingBetweenElements - currentYPosition, childHeight), mCollapsedSize);
506             childViewState.height = newHeight;
507         }
508         childViewState.yTranslation = transitioningPositionStart + offset - newHeight
509                 - mPaddingBetweenElements;
510 
511         // We want at least to be at the end of the top stack when collapsing
512         clampPositionToTopStackEnd(childViewState, newHeight);
513         childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA;
514     }
515 
updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, float transitioningPositionStart, StackScrollState.ViewState childViewState, int childHeight)516     private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
517             float transitioningPositionStart, StackScrollState.ViewState childViewState,
518             int childHeight) {
519 
520         float currentYPosition;
521         algorithmState.itemsInBottomStack += 1.0f;
522         if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
523             // We are visually entering the bottom stack
524             currentYPosition = transitioningPositionStart
525                     + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack)
526                     - mPaddingBetweenElements;
527             childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_PEEKING;
528         } else {
529             // we are fully inside the stack
530             if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) {
531                 childViewState.alpha = 0.0f;
532             } else if (algorithmState.itemsInBottomStack
533                     > MAX_ITEMS_IN_BOTTOM_STACK + 1) {
534                 childViewState.alpha = 1.0f - algorithmState.partialInBottom;
535             }
536             childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_HIDDEN;
537             currentYPosition = mInnerHeight;
538         }
539         childViewState.yTranslation = currentYPosition - childHeight;
540         clampPositionToTopStackEnd(childViewState, childHeight);
541     }
542 
updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, int numberOfElementsCompletelyIn, int i, int childHeight, StackScrollState.ViewState childViewState, float scrollOffset)543     private void updateStateForTopStackChild(StackScrollAlgorithmState algorithmState,
544             int numberOfElementsCompletelyIn, int i, int childHeight,
545             StackScrollState.ViewState childViewState, float scrollOffset) {
546 
547 
548         // First we calculate the index relative to the current stack window of size at most
549         // {@link #MAX_ITEMS_IN_TOP_STACK}
550         int paddedIndex = i - 1
551                 - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0);
552         if (paddedIndex >= 0) {
553 
554             // We are currently visually entering the top stack
555             float distanceToStack = (childHeight + mPaddingBetweenElements)
556                     - algorithmState.scrolledPixelsTop;
557             if (i == algorithmState.lastTopStackIndex
558                     && distanceToStack > (mTopStackTotalSize + mPaddingBetweenElements)) {
559 
560                 // Child is currently translating into stack but not yet inside slow down zone.
561                 // Handle it like the regular scrollview.
562                 childViewState.yTranslation = scrollOffset;
563             } else {
564                 // Apply stacking logic.
565                 float numItemsBefore;
566                 if (i == algorithmState.lastTopStackIndex) {
567                     numItemsBefore = 1.0f
568                             - (distanceToStack / (mTopStackTotalSize + mPaddingBetweenElements));
569                 } else {
570                     numItemsBefore = algorithmState.itemsInTopStack - i;
571                 }
572                 // The end position of the current child
573                 float currentChildEndY = mCollapsedSize + mTopStackTotalSize
574                         - mTopStackIndentationFunctor.getValue(numItemsBefore);
575                 childViewState.yTranslation = currentChildEndY - childHeight;
576             }
577             childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING;
578         } else {
579             if (paddedIndex == -1) {
580                 childViewState.alpha = 1.0f - algorithmState.partialInTop;
581             } else {
582                 // We are hidden behind the top card and faded out, so we can hide ourselves.
583                 childViewState.alpha = 0.0f;
584             }
585             childViewState.yTranslation = mCollapsedSize - childHeight;
586             childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN;
587         }
588 
589 
590     }
591 
592     /**
593      * Find the number of items in the top stack and update the result state if needed.
594      *
595      * @param resultState The result state to update if a height change of an child occurs
596      * @param algorithmState The state in which the current pass of the algorithm is currently in
597      */
findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState, StackScrollAlgorithmState algorithmState)598     private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
599             StackScrollAlgorithmState algorithmState) {
600 
601         // The y Position if the element would be in a regular scrollView
602         float yPositionInScrollView = 0.0f;
603         int childCount = algorithmState.visibleChildren.size();
604 
605         // find the number of elements in the top stack.
606         for (int i = 0; i < childCount; i++) {
607             ExpandableView child = algorithmState.visibleChildren.get(i);
608             StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
609             int childHeight = getMaxAllowedChildHeight(child);
610             float yPositionInScrollViewAfterElement = yPositionInScrollView
611                     + childHeight
612                     + mPaddingBetweenElements;
613             if (yPositionInScrollView < algorithmState.scrollY) {
614                 if (i == 0 && algorithmState.scrollY <= mCollapsedSize) {
615 
616                     // The starting position of the bottom stack peek
617                     int bottomPeekStart = mInnerHeight - mBottomStackPeekSize -
618                             mCollapseSecondCardPadding;
619                     // Collapse and expand the first child while the shade is being expanded
620                     float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding
621                             ? mFirstChildMaxHeight
622                             : childHeight;
623                     childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight),
624                             mCollapsedSize);
625                     algorithmState.itemsInTopStack = 1.0f;
626 
627                 } else if (yPositionInScrollViewAfterElement < algorithmState.scrollY) {
628                     // According to the regular scroll view we are fully off screen
629                     algorithmState.itemsInTopStack += 1.0f;
630                     if (i == 0) {
631                         childViewState.height = mCollapsedSize;
632                     }
633                 } else {
634                     // According to the regular scroll view we are partially off screen
635 
636                     // How much did we scroll into this child
637                     algorithmState.scrolledPixelsTop = algorithmState.scrollY
638                             - yPositionInScrollView;
639                     algorithmState.partialInTop = (algorithmState.scrolledPixelsTop) / (childHeight
640                             + mPaddingBetweenElements);
641 
642                     // Our element can be expanded, so this can get negative
643                     algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop);
644                     algorithmState.itemsInTopStack += algorithmState.partialInTop;
645 
646                     if (i == 0) {
647                         // If it is expanded we have to collapse it to a new size
648                         float newSize = yPositionInScrollViewAfterElement
649                                 - mPaddingBetweenElements
650                                 - algorithmState.scrollY + mCollapsedSize;
651                         newSize = Math.max(mCollapsedSize, newSize);
652                         algorithmState.itemsInTopStack = 1.0f;
653                         childViewState.height = (int) newSize;
654                     }
655                     algorithmState.lastTopStackIndex = i;
656                     break;
657                 }
658             } else {
659                 algorithmState.lastTopStackIndex = i - 1;
660                 // We are already past the stack so we can end the loop
661                 break;
662             }
663             yPositionInScrollView = yPositionInScrollViewAfterElement;
664         }
665     }
666 
667     /**
668      * Calculate the Z positions for all children based on the number of items in both stacks and
669      * save it in the resultState
670      *
671      * @param resultState The result state to update the zTranslation values
672      * @param algorithmState The state in which the current pass of the algorithm is currently in
673      */
updateZValuesForState(StackScrollState resultState, StackScrollAlgorithmState algorithmState)674     private void updateZValuesForState(StackScrollState resultState,
675             StackScrollAlgorithmState algorithmState) {
676         int childCount = algorithmState.visibleChildren.size();
677         for (int i = 0; i < childCount; i++) {
678             View child = algorithmState.visibleChildren.get(i);
679             StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
680             if (i < algorithmState.itemsInTopStack) {
681                 float stackIndex = algorithmState.itemsInTopStack - i;
682 
683                 // Ensure that the topmost item is a little bit higher than the rest when fully
684                 // scrolled, to avoid drawing errors when swiping it out
685                 float max = MAX_ITEMS_IN_TOP_STACK + (i == 0 ? 2.5f : 2);
686                 stackIndex = Math.min(stackIndex, max);
687                 if (i == 0 && algorithmState.itemsInTopStack < 2.0f) {
688 
689                     // We only have the top item and an additional item in the top stack,
690                     // Interpolate the index from 0 to 2 while the second item is
691                     // translating in.
692                     stackIndex -= 1.0f;
693                     if (algorithmState.scrollY > mCollapsedSize) {
694 
695                         // Since there is a shadow treshhold, we cant just interpolate from 0 to
696                         // 2 but we interpolate from 0.1f to 2.0f when scrolled in. The jump in
697                         // height will not be noticable since we have padding in between.
698                         stackIndex = 0.1f + stackIndex * 1.9f;
699                     }
700                 }
701                 childViewState.zTranslation = mZBasicHeight
702                         + stackIndex * mZDistanceBetweenElements;
703             } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
704                 float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
705                 float translationZ = mZBasicHeight
706                         - numItemsAbove * mZDistanceBetweenElements;
707                 childViewState.zTranslation = translationZ;
708             } else {
709                 childViewState.zTranslation = mZBasicHeight;
710             }
711         }
712     }
713 
setLayoutHeight(int layoutHeight)714     public void setLayoutHeight(int layoutHeight) {
715         this.mLayoutHeight = layoutHeight;
716         updateInnerHeight();
717     }
718 
setTopPadding(int topPadding)719     public void setTopPadding(int topPadding) {
720         mTopPadding = topPadding;
721         updateInnerHeight();
722     }
723 
updateInnerHeight()724     private void updateInnerHeight() {
725         mInnerHeight = mLayoutHeight - mTopPadding;
726     }
727 
728 
729     /**
730      * Update whether the device is very small, i.e. Notifications can be in both the top and the
731      * bottom stack at the same time
732      *
733      * @param panelHeight The normal height of the panel when it's open
734      */
updateIsSmallScreen(int panelHeight)735     public void updateIsSmallScreen(int panelHeight) {
736         mIsSmallScreen = panelHeight <
737                 mCollapsedSize  /* top stack */
738                 + mBottomStackSlowDownLength + mBottomStackPeekSize /* bottom stack */
739                 + mMaxNotificationHeight; /* max notification height */
740     }
741 
742     public void onExpansionStarted(StackScrollState currentState) {
743         mIsExpansionChanging = true;
744         mExpandedOnStart = mIsExpanded;
745         ViewGroup hostView = currentState.getHostView();
746         updateFirstChildHeightWhileExpanding(hostView);
747     }
748 
749     private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) {
750         mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView);
751         if (mFirstChildWhileExpanding != null) {
752             if (mExpandedOnStart) {
753 
754                 // We are collapsing the shade, so the first child can get as most as high as the
755                 // current height or the end value of the animation.
756                 mFirstChildMaxHeight = StackStateAnimator.getFinalActualHeight(
757                         mFirstChildWhileExpanding);
758             } else {
759                 updateFirstChildMaxSizeToMaxHeight();
760             }
761         } else {
762             mFirstChildMaxHeight = 0;
763         }
764     }
765 
766     private void updateFirstChildMaxSizeToMaxHeight() {
767         // We are expanding the shade, expand it to its full height.
768         if (!isMaxSizeInitialized(mFirstChildWhileExpanding)) {
769 
770             // This child was not layouted yet, wait for a layout pass
771             mFirstChildWhileExpanding
772                     .addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
773                         @Override
774                         public void onLayoutChange(View v, int left, int top, int right,
775                                 int bottom, int oldLeft, int oldTop, int oldRight,
776                                 int oldBottom) {
777                             if (mFirstChildWhileExpanding != null) {
778                                 mFirstChildMaxHeight = getMaxAllowedChildHeight(
779                                         mFirstChildWhileExpanding);
780                             } else {
781                                 mFirstChildMaxHeight = 0;
782                             }
783                             v.removeOnLayoutChangeListener(this);
784                         }
785                     });
786         } else {
787             mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding);
788         }
789     }
790 
791     private boolean isMaxSizeInitialized(ExpandableView child) {
792         if (child instanceof ExpandableNotificationRow) {
793             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
794             return row.isMaxExpandHeightInitialized();
795         }
796         return child == null || child.getWidth() != 0;
797     }
798 
799     private View findFirstVisibleChild(ViewGroup container) {
800         int childCount = container.getChildCount();
801         for (int i = 0; i < childCount; i++) {
802             View child = container.getChildAt(i);
803             if (child.getVisibility() != View.GONE) {
804                 return child;
805             }
806         }
807         return null;
808     }
809 
810     public void onExpansionStopped() {
811         mIsExpansionChanging = false;
812         mFirstChildWhileExpanding = null;
813     }
814 
815     public void setIsExpanded(boolean isExpanded) {
816         this.mIsExpanded = isExpanded;
817     }
818 
819     public void notifyChildrenChanged(final ViewGroup hostView) {
820         if (mIsExpansionChanging) {
821             hostView.post(new Runnable() {
822                 @Override
823                 public void run() {
824                     updateFirstChildHeightWhileExpanding(hostView);
825                 }
826             });
827         }
828     }
829 
830     public void setDimmed(boolean dimmed) {
831         updatePadding(dimmed);
832     }
833 
834     public void onReset(ExpandableView view) {
835         if (view.equals(mFirstChildWhileExpanding)) {
836             updateFirstChildMaxSizeToMaxHeight();
837         }
838     }
839 
840     class StackScrollAlgorithmState {
841 
842         /**
843          * The scroll position of the algorithm
844          */
845         public int scrollY;
846 
847         /**
848          *  The quantity of items which are in the top stack.
849          */
850         public float itemsInTopStack;
851 
852         /**
853          * how far in is the element currently transitioning into the top stack
854          */
855         public float partialInTop;
856 
857         /**
858          * The number of pixels the last child in the top stack has scrolled in to the stack
859          */
860         public float scrolledPixelsTop;
861 
862         /**
863          * The last item index which is in the top stack.
864          */
865         public int lastTopStackIndex;
866 
867         /**
868          * The quantity of items which are in the bottom stack.
869          */
870         public float itemsInBottomStack;
871 
872         /**
873          * how far in is the element currently transitioning into the bottom stack
874          */
875         public float partialInBottom;
876 
877         /**
878          * The children from the host view which are not gone.
879          */
880         public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>();
881     }
882 
883 }
884