• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Log;
21 import android.view.View;
22 import android.view.ViewGroup;
23 
24 import com.android.systemui.R;
25 import com.android.systemui.statusbar.ExpandableNotificationRow;
26 import com.android.systemui.statusbar.ExpandableView;
27 import com.android.systemui.statusbar.notification.FakeShadowView;
28 import com.android.systemui.statusbar.notification.NotificationUtils;
29 
30 import java.util.ArrayList;
31 import java.util.HashMap;
32 import java.util.List;
33 
34 /**
35  * The Algorithm of the {@link com.android.systemui.statusbar.stack
36  * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
37  * .stack.StackScrollState}
38  */
39 public class StackScrollAlgorithm {
40 
41     private static final String LOG_TAG = "StackScrollAlgorithm";
42 
43     private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3;
44 
45     private int mPaddingBetweenElements;
46     private int mIncreasedPaddingBetweenElements;
47     private int mCollapsedSize;
48     private int mBottomStackPeekSize;
49     private int mZDistanceBetweenElements;
50     private int mZBasicHeight;
51 
52     private StackIndentationFunctor mBottomStackIndentationFunctor;
53 
54     private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
55     private boolean mIsExpanded;
56     private int mBottomStackSlowDownLength;
57 
StackScrollAlgorithm(Context context)58     public StackScrollAlgorithm(Context context) {
59         initView(context);
60     }
61 
initView(Context context)62     public void initView(Context context) {
63         initConstants(context);
64     }
65 
getBottomStackSlowDownLength()66     public int getBottomStackSlowDownLength() {
67         return mBottomStackSlowDownLength + mPaddingBetweenElements;
68     }
69 
initConstants(Context context)70     private void initConstants(Context context) {
71         mPaddingBetweenElements = Math.max(1, context.getResources()
72                 .getDimensionPixelSize(R.dimen.notification_divider_height));
73         mIncreasedPaddingBetweenElements = context.getResources()
74                 .getDimensionPixelSize(R.dimen.notification_divider_height_increased);
75         mCollapsedSize = context.getResources()
76                 .getDimensionPixelSize(R.dimen.notification_min_height);
77         mBottomStackPeekSize = context.getResources()
78                 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
79         mZDistanceBetweenElements = Math.max(1, context.getResources()
80                 .getDimensionPixelSize(R.dimen.z_distance_between_notifications));
81         mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements;
82         mBottomStackSlowDownLength = context.getResources()
83                 .getDimensionPixelSize(R.dimen.bottom_stack_slow_down_length);
84         mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
85                 MAX_ITEMS_IN_BOTTOM_STACK,
86                 mBottomStackPeekSize,
87                 getBottomStackSlowDownLength(),
88                 0.5f);
89     }
90 
getStackScrollState(AmbientState ambientState, StackScrollState resultState)91     public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) {
92         // The state of the local variables are saved in an algorithmState to easily subdivide it
93         // into multiple phases.
94         StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
95 
96         // First we reset the view states to their default values.
97         resultState.resetViewStates();
98 
99         initAlgorithmState(resultState, algorithmState, ambientState);
100 
101         updatePositionsForState(resultState, algorithmState, ambientState);
102 
103         updateZValuesForState(resultState, algorithmState, ambientState);
104 
105         updateHeadsUpStates(resultState, algorithmState, ambientState);
106 
107         handleDraggedViews(ambientState, resultState, algorithmState);
108         updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState);
109         updateClipping(resultState, algorithmState, ambientState);
110         updateSpeedBumpState(resultState, algorithmState, ambientState.getSpeedBumpIndex());
111         getNotificationChildrenStates(resultState, algorithmState);
112     }
113 
getNotificationChildrenStates(StackScrollState resultState, StackScrollAlgorithmState algorithmState)114     private void getNotificationChildrenStates(StackScrollState resultState,
115             StackScrollAlgorithmState algorithmState) {
116         int childCount = algorithmState.visibleChildren.size();
117         for (int i = 0; i < childCount; i++) {
118             ExpandableView v = algorithmState.visibleChildren.get(i);
119             if (v instanceof ExpandableNotificationRow) {
120                 ExpandableNotificationRow row = (ExpandableNotificationRow) v;
121                 row.getChildrenStates(resultState);
122             }
123         }
124     }
125 
updateSpeedBumpState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, int speedBumpIndex)126     private void updateSpeedBumpState(StackScrollState resultState,
127             StackScrollAlgorithmState algorithmState, int speedBumpIndex) {
128         int childCount = algorithmState.visibleChildren.size();
129         for (int i = 0; i < childCount; i++) {
130             View child = algorithmState.visibleChildren.get(i);
131             StackViewState childViewState = resultState.getViewStateForView(child);
132 
133             // The speed bump can also be gone, so equality needs to be taken when comparing
134             // indices.
135             childViewState.belowSpeedBump = speedBumpIndex != -1 && i >= speedBumpIndex;
136         }
137     }
138 
updateClipping(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)139     private void updateClipping(StackScrollState resultState,
140             StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
141         float drawStart = ambientState.getTopPadding() + ambientState.getStackTranslation();
142         float previousNotificationEnd = 0;
143         float previousNotificationStart = 0;
144         int childCount = algorithmState.visibleChildren.size();
145         for (int i = 0; i < childCount; i++) {
146             ExpandableView child = algorithmState.visibleChildren.get(i);
147             StackViewState state = resultState.getViewStateForView(child);
148             if (!child.mustStayOnScreen()) {
149                 previousNotificationEnd = Math.max(drawStart, previousNotificationEnd);
150                 previousNotificationStart = Math.max(drawStart, previousNotificationStart);
151             }
152             float newYTranslation = state.yTranslation;
153             float newHeight = state.height;
154             float newNotificationEnd = newYTranslation + newHeight;
155             boolean isHeadsUp = (child instanceof ExpandableNotificationRow)
156                     && ((ExpandableNotificationRow) child).isPinned();
157             if (newYTranslation < previousNotificationEnd
158                     && (!isHeadsUp || ambientState.isShadeExpanded())) {
159                 // The previous view is overlapping on top, clip!
160                 float overlapAmount = previousNotificationEnd - newYTranslation;
161                 state.clipTopAmount = (int) overlapAmount;
162             } else {
163                 state.clipTopAmount = 0;
164             }
165 
166             if (!child.isTransparent()) {
167                 // Only update the previous values if we are not transparent,
168                 // otherwise we would clip to a transparent view.
169                 previousNotificationEnd = newNotificationEnd;
170                 previousNotificationStart = newYTranslation;
171             }
172         }
173     }
174 
canChildBeDismissed(View v)175     public static boolean canChildBeDismissed(View v) {
176         if (!(v instanceof ExpandableNotificationRow)) {
177             return false;
178         }
179         ExpandableNotificationRow row = (ExpandableNotificationRow) v;
180         if (row.areGutsExposed()) {
181             return false;
182         }
183         return row.canViewBeDismissed();
184     }
185 
186     /**
187      * Updates the dimmed, activated and hiding sensitive states of the children.
188      */
updateDimmedActivatedHideSensitive(AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState)189     private void updateDimmedActivatedHideSensitive(AmbientState ambientState,
190             StackScrollState resultState, StackScrollAlgorithmState algorithmState) {
191         boolean dimmed = ambientState.isDimmed();
192         boolean dark = ambientState.isDark();
193         boolean hideSensitive = ambientState.isHideSensitive();
194         View activatedChild = ambientState.getActivatedChild();
195         int childCount = algorithmState.visibleChildren.size();
196         for (int i = 0; i < childCount; i++) {
197             View child = algorithmState.visibleChildren.get(i);
198             StackViewState childViewState = resultState.getViewStateForView(child);
199             childViewState.dimmed = dimmed;
200             childViewState.dark = dark;
201             childViewState.hideSensitive = hideSensitive;
202             boolean isActivatedChild = activatedChild == child;
203             if (dimmed && isActivatedChild) {
204                 childViewState.zTranslation += 2.0f * mZDistanceBetweenElements;
205             }
206         }
207     }
208 
209     /**
210      * Handle the special state when views are being dragged
211      */
handleDraggedViews(AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState)212     private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState,
213             StackScrollAlgorithmState algorithmState) {
214         ArrayList<View> draggedViews = ambientState.getDraggedViews();
215         for (View draggedView : draggedViews) {
216             int childIndex = algorithmState.visibleChildren.indexOf(draggedView);
217             if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) {
218                 View nextChild = algorithmState.visibleChildren.get(childIndex + 1);
219                 if (!draggedViews.contains(nextChild)) {
220                     // only if the view is not dragged itself we modify its state to be fully
221                     // visible
222                     StackViewState viewState = resultState.getViewStateForView(
223                             nextChild);
224                     // The child below the dragged one must be fully visible
225                     if (ambientState.isShadeExpanded()) {
226                         viewState.shadowAlpha = 1;
227                         viewState.hidden = false;
228                     }
229                 }
230 
231                 // Lets set the alpha to the one it currently has, as its currently being dragged
232                 StackViewState viewState = resultState.getViewStateForView(draggedView);
233                 // The dragged child should keep the set alpha
234                 viewState.alpha = draggedView.getAlpha();
235             }
236         }
237     }
238 
239     /**
240      * Initialize the algorithm state like updating the visible children.
241      */
initAlgorithmState(StackScrollState resultState, StackScrollAlgorithmState state, AmbientState ambientState)242     private void initAlgorithmState(StackScrollState resultState, StackScrollAlgorithmState state,
243             AmbientState ambientState) {
244         state.itemsInBottomStack = 0.0f;
245         state.partialInBottom = 0.0f;
246         float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */);
247 
248         int scrollY = ambientState.getScrollY();
249 
250         // Due to the overScroller, the stackscroller can have negative scroll state. This is
251         // already accounted for by the top padding and doesn't need an additional adaption
252         scrollY = Math.max(0, scrollY);
253         state.scrollY = (int) (scrollY + bottomOverScroll);
254 
255         //now init the visible children and update paddings
256         ViewGroup hostView = resultState.getHostView();
257         int childCount = hostView.getChildCount();
258         state.visibleChildren.clear();
259         state.visibleChildren.ensureCapacity(childCount);
260         state.increasedPaddingMap.clear();
261         int notGoneIndex = 0;
262         ExpandableView lastView = null;
263         for (int i = 0; i < childCount; i++) {
264             ExpandableView v = (ExpandableView) hostView.getChildAt(i);
265             if (v.getVisibility() != View.GONE) {
266                 notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
267                 float increasedPadding = v.getIncreasedPaddingAmount();
268                 if (increasedPadding != 0.0f) {
269                     state.increasedPaddingMap.put(v, increasedPadding);
270                     if (lastView != null) {
271                         Float prevValue = state.increasedPaddingMap.get(lastView);
272                         float newValue = prevValue != null
273                                 ? Math.max(prevValue, increasedPadding)
274                                 : increasedPadding;
275                         state.increasedPaddingMap.put(lastView, newValue);
276                     }
277                 }
278                 if (v instanceof ExpandableNotificationRow) {
279                     ExpandableNotificationRow row = (ExpandableNotificationRow) v;
280 
281                     // handle the notgoneIndex for the children as well
282                     List<ExpandableNotificationRow> children =
283                             row.getNotificationChildren();
284                     if (row.isSummaryWithChildren() && children != null) {
285                         for (ExpandableNotificationRow childRow : children) {
286                             if (childRow.getVisibility() != View.GONE) {
287                                 StackViewState childState
288                                         = resultState.getViewStateForView(childRow);
289                                 childState.notGoneIndex = notGoneIndex;
290                                 notGoneIndex++;
291                             }
292                         }
293                     }
294                 }
295                 lastView = v;
296             }
297         }
298     }
299 
updateNotGoneIndex(StackScrollState resultState, StackScrollAlgorithmState state, int notGoneIndex, ExpandableView v)300     private int updateNotGoneIndex(StackScrollState resultState,
301             StackScrollAlgorithmState state, int notGoneIndex,
302             ExpandableView v) {
303         StackViewState viewState = resultState.getViewStateForView(v);
304         viewState.notGoneIndex = notGoneIndex;
305         state.visibleChildren.add(v);
306         notGoneIndex++;
307         return notGoneIndex;
308     }
309 
310     /**
311      * Determine the positions for the views. This is the main part of the algorithm.
312      *
313      * @param resultState The result state to update if a change to the properties of a child occurs
314      * @param algorithmState The state in which the current pass of the algorithm is currently in
315      * @param ambientState The current ambient state
316      */
updatePositionsForState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)317     private void updatePositionsForState(StackScrollState resultState,
318             StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
319 
320         // The starting position of the bottom stack peek
321         float bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize;
322 
323         // The position where the bottom stack starts.
324         float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength;
325 
326         // The y coordinate of the current child.
327         float currentYPosition = -algorithmState.scrollY;
328 
329         int childCount = algorithmState.visibleChildren.size();
330         int paddingAfterChild;
331         for (int i = 0; i < childCount; i++) {
332             ExpandableView child = algorithmState.visibleChildren.get(i);
333             StackViewState childViewState = resultState.getViewStateForView(child);
334             childViewState.location = StackViewState.LOCATION_UNKNOWN;
335             paddingAfterChild = getPaddingAfterChild(algorithmState, child);
336             int childHeight = getMaxAllowedChildHeight(child);
337             int collapsedHeight = child.getCollapsedHeight();
338             childViewState.yTranslation = currentYPosition;
339             if (i == 0) {
340                 updateFirstChildHeight(child, childViewState, childHeight, ambientState);
341             }
342 
343             // The y position after this element
344             float nextYPosition = currentYPosition + childHeight +
345                     paddingAfterChild;
346             if (nextYPosition >= bottomStackStart) {
347                 // Case 1:
348                 // We are in the bottom stack.
349                 if (currentYPosition >= bottomStackStart) {
350                     // According to the regular scroll view we are fully translated out of the
351                     // bottom of the screen so we are fully in the bottom stack
352                     updateStateForChildFullyInBottomStack(algorithmState,
353                             bottomStackStart, childViewState, collapsedHeight, ambientState, child);
354                 } else {
355                     // According to the regular scroll view we are currently translating out of /
356                     // into the bottom of the screen
357                     updateStateForChildTransitioningInBottom(algorithmState,
358                             bottomStackStart, child, currentYPosition,
359                             childViewState, childHeight);
360                 }
361             } else {
362                 // Case 2:
363                 // We are in the regular scroll area.
364                 childViewState.location = StackViewState.LOCATION_MAIN_AREA;
365                 clampPositionToBottomStackStart(childViewState, childViewState.height, childHeight,
366                         ambientState);
367             }
368 
369             if (i == 0 && ambientState.getScrollY() <= 0) {
370                 // The first card can get into the bottom stack if it's the only one
371                 // on the lockscreen which pushes it up. Let's make sure that doesn't happen and
372                 // it stays at the top
373                 childViewState.yTranslation = Math.max(0, childViewState.yTranslation);
374             }
375             currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild;
376             if (currentYPosition <= 0) {
377                 childViewState.location = StackViewState.LOCATION_HIDDEN_TOP;
378             }
379             if (childViewState.location == StackViewState.LOCATION_UNKNOWN) {
380                 Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
381             }
382 
383             childViewState.yTranslation += ambientState.getTopPadding()
384                     + ambientState.getStackTranslation();
385         }
386     }
387 
getPaddingAfterChild(StackScrollAlgorithmState algorithmState, ExpandableView child)388     private int getPaddingAfterChild(StackScrollAlgorithmState algorithmState,
389             ExpandableView child) {
390         Float paddingValue = algorithmState.increasedPaddingMap.get(child);
391         return paddingValue == null
392                 ? mPaddingBetweenElements
393                 : (int) NotificationUtils.interpolate(mPaddingBetweenElements,
394                         mIncreasedPaddingBetweenElements,
395                         paddingValue);
396     }
397 
updateHeadsUpStates(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)398     private void updateHeadsUpStates(StackScrollState resultState,
399             StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
400         int childCount = algorithmState.visibleChildren.size();
401         ExpandableNotificationRow topHeadsUpEntry = null;
402         for (int i = 0; i < childCount; i++) {
403             View child = algorithmState.visibleChildren.get(i);
404             if (!(child instanceof ExpandableNotificationRow)) {
405                 break;
406             }
407             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
408             if (!row.isHeadsUp()) {
409                 break;
410             }
411             StackViewState childState = resultState.getViewStateForView(row);
412             if (topHeadsUpEntry == null) {
413                 topHeadsUpEntry = row;
414                 childState.location = StackViewState.LOCATION_FIRST_HUN;
415             }
416             boolean isTopEntry = topHeadsUpEntry == row;
417             float unmodifiedEndLocation = childState.yTranslation + childState.height;
418             if (mIsExpanded) {
419                 // Ensure that the heads up is always visible even when scrolled off
420                 clampHunToTop(ambientState, row, childState);
421                 clampHunToMaxTranslation(ambientState, row, childState);
422             }
423             if (row.isPinned()) {
424                 childState.yTranslation = Math.max(childState.yTranslation, 0);
425                 childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
426                 StackViewState topState = resultState.getViewStateForView(topHeadsUpEntry);
427                 if (!isTopEntry && (!mIsExpanded
428                         || unmodifiedEndLocation < topState.yTranslation + topState.height)) {
429                     // Ensure that a headsUp doesn't vertically extend further than the heads-up at
430                     // the top most z-position
431                     childState.height = row.getIntrinsicHeight();
432                     childState.yTranslation = topState.yTranslation + topState.height
433                             - childState.height;
434                 }
435             }
436         }
437     }
438 
clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row, StackViewState childState)439     private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row,
440             StackViewState childState) {
441         float newTranslation = Math.max(ambientState.getTopPadding()
442                 + ambientState.getStackTranslation(), childState.yTranslation);
443         childState.height = (int) Math.max(childState.height - (newTranslation
444                 - childState.yTranslation), row.getCollapsedHeight());
445         childState.yTranslation = newTranslation;
446     }
447 
clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, StackViewState childState)448     private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
449             StackViewState childState) {
450         float newTranslation;
451         float bottomPosition = ambientState.getMaxHeadsUpTranslation() - row.getCollapsedHeight();
452         newTranslation = Math.min(childState.yTranslation, bottomPosition);
453         childState.height = (int) Math.max(childState.height
454                 - (childState.yTranslation - newTranslation), row.getCollapsedHeight());
455         childState.yTranslation = newTranslation;
456     }
457 
458     /**
459      * Clamp the yTranslation of the child down such that its end is at most on the beginning of
460      * the bottom stack.
461      *
462      * @param childViewState the view state of the child
463      * @param childHeight the height of this child
464      * @param minHeight the minumum Height of the View
465      */
clampPositionToBottomStackStart(StackViewState childViewState, int childHeight, int minHeight, AmbientState ambientState)466     private void clampPositionToBottomStackStart(StackViewState childViewState,
467             int childHeight, int minHeight, AmbientState ambientState) {
468 
469         int bottomStackStart = ambientState.getInnerHeight()
470                 - mBottomStackPeekSize - mBottomStackSlowDownLength;
471         int childStart = bottomStackStart - childHeight;
472         if (childStart < childViewState.yTranslation) {
473             float newHeight = bottomStackStart - childViewState.yTranslation;
474             if (newHeight < minHeight) {
475                 newHeight = minHeight;
476                 childViewState.yTranslation = bottomStackStart - minHeight;
477             }
478             childViewState.height = (int) newHeight;
479         }
480     }
481 
getMaxAllowedChildHeight(View child)482     private int getMaxAllowedChildHeight(View child) {
483         if (child instanceof ExpandableView) {
484             ExpandableView expandableView = (ExpandableView) child;
485             return expandableView.getIntrinsicHeight();
486         }
487         return child == null? mCollapsedSize : child.getHeight();
488     }
489 
updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, float transitioningPositionStart, ExpandableView child, float currentYPosition, StackViewState childViewState, int childHeight)490     private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
491             float transitioningPositionStart, ExpandableView child, float currentYPosition,
492             StackViewState 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                         getPaddingAfterChild(algorithmState, child)));
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 > child.getCollapsedHeight()) {
504             newHeight = (int) Math.max(Math.min(transitioningPositionStart + offset -
505                     getPaddingAfterChild(algorithmState, child) - currentYPosition, childHeight),
506                     child.getCollapsedHeight());
507             childViewState.height = newHeight;
508         }
509         childViewState.yTranslation = transitioningPositionStart + offset - newHeight
510                 - getPaddingAfterChild(algorithmState, child);
511         childViewState.location = StackViewState.LOCATION_MAIN_AREA;
512     }
513 
updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, float transitioningPositionStart, StackViewState childViewState, int collapsedHeight, AmbientState ambientState, ExpandableView child)514     private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
515             float transitioningPositionStart, StackViewState childViewState,
516             int collapsedHeight, AmbientState ambientState, ExpandableView child) {
517         float currentYPosition;
518         algorithmState.itemsInBottomStack += 1.0f;
519         if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
520             // We are visually entering the bottom stack
521             currentYPosition = transitioningPositionStart
522                     + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack)
523                     - getPaddingAfterChild(algorithmState, child);
524             childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_PEEKING;
525         } else {
526             // we are fully inside the stack
527             if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) {
528                 childViewState.hidden = true;
529                 childViewState.shadowAlpha = 0.0f;
530             } else if (algorithmState.itemsInBottomStack
531                     > MAX_ITEMS_IN_BOTTOM_STACK + 1) {
532                 childViewState.shadowAlpha = 1.0f - algorithmState.partialInBottom;
533             }
534             childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_HIDDEN;
535             currentYPosition = ambientState.getInnerHeight();
536         }
537         childViewState.height = collapsedHeight;
538         childViewState.yTranslation = currentYPosition - collapsedHeight;
539     }
540 
541 
542     /**
543      * Update the height of the first child i.e clamp it to the bottom stack
544      *
545      * @param child the child to update
546      * @param childViewState the viewstate of the child
547      * @param childHeight the height of the child
548      * @param ambientState The ambient state of the algorithm
549      */
updateFirstChildHeight(ExpandableView child, StackViewState childViewState, int childHeight, AmbientState ambientState)550     private void updateFirstChildHeight(ExpandableView child, StackViewState childViewState,
551             int childHeight, AmbientState ambientState) {
552 
553             // The starting position of the bottom stack peek
554             int bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize -
555                     mBottomStackSlowDownLength + ambientState.getScrollY();
556             // Collapse and expand the first child while the shade is being expanded
557         childViewState.height = (int) Math.max(Math.min(bottomPeekStart, (float) childHeight),
558                     child.getCollapsedHeight());
559     }
560 
561     /**
562      * Calculate the Z positions for all children based on the number of items in both stacks and
563      * save it in the resultState
564      *  @param resultState The result state to update the zTranslation values
565      * @param algorithmState The state in which the current pass of the algorithm is currently in
566      * @param ambientState The ambient state of the algorithm
567      */
updateZValuesForState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)568     private void updateZValuesForState(StackScrollState resultState,
569             StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
570         int childCount = algorithmState.visibleChildren.size();
571         float childrenOnTop = 0.0f;
572         for (int i = childCount - 1; i >= 0; i--) {
573             ExpandableView child = algorithmState.visibleChildren.get(i);
574             StackViewState childViewState = resultState.getViewStateForView(child);
575             if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
576                 // We are in the bottom stack
577                 float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
578                 float zSubtraction;
579                 if (numItemsAbove <= 1.0f) {
580                     float factor = 0.2f;
581                     // Lets fade in slower to the threshold to make the shadow fade in look nicer
582                     if (numItemsAbove <= factor) {
583                         zSubtraction = FakeShadowView.SHADOW_SIBLING_TRESHOLD
584                                 * numItemsAbove * (1.0f / factor);
585                     } else {
586                         zSubtraction = FakeShadowView.SHADOW_SIBLING_TRESHOLD
587                                 + (numItemsAbove - factor) * (1.0f / (1.0f - factor))
588                                         * (mZDistanceBetweenElements
589                                                 - FakeShadowView.SHADOW_SIBLING_TRESHOLD);
590                     }
591                 } else {
592                     zSubtraction = numItemsAbove * mZDistanceBetweenElements;
593                 }
594                 childViewState.zTranslation = mZBasicHeight - zSubtraction;
595             } else if (child.mustStayOnScreen()
596                     && childViewState.yTranslation < ambientState.getTopPadding()
597                     + ambientState.getStackTranslation()) {
598                 if (childrenOnTop != 0.0f) {
599                     childrenOnTop++;
600                 } else {
601                     float overlap = ambientState.getTopPadding()
602                             + ambientState.getStackTranslation() - childViewState.yTranslation;
603                     childrenOnTop += Math.min(1.0f, overlap / childViewState.height);
604                 }
605                 childViewState.zTranslation = mZBasicHeight
606                         + childrenOnTop * mZDistanceBetweenElements;
607             } else {
608                 childViewState.zTranslation = mZBasicHeight;
609             }
610         }
611     }
612 
isMaxSizeInitialized(ExpandableView child)613     private boolean isMaxSizeInitialized(ExpandableView child) {
614         if (child instanceof ExpandableNotificationRow) {
615             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
616             return row.isMaxExpandHeightInitialized();
617         }
618         return child == null || child.getWidth() != 0;
619     }
620 
findFirstVisibleChild(ViewGroup container)621     private View findFirstVisibleChild(ViewGroup container) {
622         int childCount = container.getChildCount();
623         for (int i = 0; i < childCount; i++) {
624             View child = container.getChildAt(i);
625             if (child.getVisibility() != View.GONE) {
626                 return child;
627             }
628         }
629         return null;
630     }
631 
setIsExpanded(boolean isExpanded)632     public void setIsExpanded(boolean isExpanded) {
633         this.mIsExpanded = isExpanded;
634     }
635 
636     class StackScrollAlgorithmState {
637 
638         /**
639          * The scroll position of the algorithm
640          */
641         public int scrollY;
642 
643         /**
644          * The quantity of items which are in the bottom stack.
645          */
646         public float itemsInBottomStack;
647 
648         /**
649          * how far in is the element currently transitioning into the bottom stack
650          */
651         public float partialInBottom;
652 
653         /**
654          * The children from the host view which are not gone.
655          */
656         public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>();
657 
658         /**
659          * The children from the host that need an increased padding after them. A value of 0 means
660          * no increased padding, a value of 1 means full padding.
661          */
662         public final HashMap<ExpandableView, Float> increasedPaddingMap = new HashMap<>();
663     }
664 
665 }
666