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.content.res.Resources;
21 import android.util.Log;
22 import android.view.View;
23 import android.view.ViewGroup;
24 import com.android.systemui.R;
25 import com.android.systemui.statusbar.EmptyShadeView;
26 import com.android.systemui.statusbar.ExpandableNotificationRow;
27 import com.android.systemui.statusbar.ExpandableView;
28 import com.android.systemui.statusbar.FooterView;
29 import com.android.systemui.statusbar.NotificationShelf;
30 import com.android.systemui.statusbar.notification.NotificationUtils;
31 
32 import java.util.ArrayList;
33 import java.util.HashMap;
34 import java.util.List;
35 
36 /**
37  * The Algorithm of the {@link com.android.systemui.statusbar.stack
38  * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
39  * .stack.StackScrollState}
40  */
41 public class StackScrollAlgorithm {
42 
43     private static final String LOG_TAG = "StackScrollAlgorithm";
44 
45     private int mPaddingBetweenElements;
46     private int mIncreasedPaddingBetweenElements;
47     private int mCollapsedSize;
48 
49     private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
50     private boolean mIsExpanded;
51     private boolean mClipNotificationScrollToTop;
52     private int mStatusBarHeight;
53     private float mHeadsUpInset;
54     private int mPinnedZTranslationExtra;
55 
StackScrollAlgorithm(Context context)56     public StackScrollAlgorithm(Context context) {
57         initView(context);
58     }
59 
initView(Context context)60     public void initView(Context context) {
61         initConstants(context);
62     }
63 
initConstants(Context context)64     private void initConstants(Context context) {
65         Resources res = context.getResources();
66         mPaddingBetweenElements = res.getDimensionPixelSize(
67                 R.dimen.notification_divider_height);
68         mIncreasedPaddingBetweenElements =
69                 res.getDimensionPixelSize(R.dimen.notification_divider_height_increased);
70         mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
71         mStatusBarHeight = res.getDimensionPixelSize(R.dimen.status_bar_height);
72         mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop);
73         mHeadsUpInset = mStatusBarHeight + res.getDimensionPixelSize(
74                 R.dimen.heads_up_status_bar_padding);
75         mPinnedZTranslationExtra = res.getDimensionPixelSize(
76                 R.dimen.heads_up_pinned_elevation);
77     }
78 
getStackScrollState(AmbientState ambientState, StackScrollState resultState)79     public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) {
80         // The state of the local variables are saved in an algorithmState to easily subdivide it
81         // into multiple phases.
82         StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
83 
84         // First we reset the view states to their default values.
85         resultState.resetViewStates();
86 
87         initAlgorithmState(resultState, algorithmState, ambientState);
88 
89         updatePositionsForState(resultState, algorithmState, ambientState);
90 
91         updateZValuesForState(resultState, algorithmState, ambientState);
92 
93         updateHeadsUpStates(resultState, algorithmState, ambientState);
94 
95         handleDraggedViews(ambientState, resultState, algorithmState);
96         updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState);
97         updateClipping(resultState, algorithmState, ambientState);
98         updateSpeedBumpState(resultState, algorithmState, ambientState);
99         updateShelfState(resultState, ambientState);
100         getNotificationChildrenStates(resultState, algorithmState, ambientState);
101     }
102 
getNotificationChildrenStates(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)103     private void getNotificationChildrenStates(StackScrollState resultState,
104             StackScrollAlgorithmState algorithmState,
105             AmbientState ambientState) {
106         int childCount = algorithmState.visibleChildren.size();
107         for (int i = 0; i < childCount; i++) {
108             ExpandableView v = algorithmState.visibleChildren.get(i);
109             if (v instanceof ExpandableNotificationRow) {
110                 ExpandableNotificationRow row = (ExpandableNotificationRow) v;
111                 row.getChildrenStates(resultState, ambientState);
112             }
113         }
114     }
115 
updateSpeedBumpState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)116     private void updateSpeedBumpState(StackScrollState resultState,
117             StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
118         int childCount = algorithmState.visibleChildren.size();
119         int belowSpeedBump = ambientState.getSpeedBumpIndex();
120         for (int i = 0; i < childCount; i++) {
121             View child = algorithmState.visibleChildren.get(i);
122             ExpandableViewState childViewState = resultState.getViewStateForView(child);
123 
124             // The speed bump can also be gone, so equality needs to be taken when comparing
125             // indices.
126             childViewState.belowSpeedBump = i >= belowSpeedBump;
127         }
128 
129     }
updateShelfState(StackScrollState resultState, AmbientState ambientState)130     private void updateShelfState(StackScrollState resultState, AmbientState ambientState) {
131         NotificationShelf shelf = ambientState.getShelf();
132         if (shelf != null) {
133             shelf.updateState(resultState, ambientState);
134         }
135     }
136 
updateClipping(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)137     private void updateClipping(StackScrollState resultState,
138             StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
139         float drawStart = !ambientState.isOnKeyguard() ? ambientState.getTopPadding()
140                 + ambientState.getStackTranslation() + ambientState.getExpandAnimationTopChange()
141                 : 0;
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             ExpandableViewState state = resultState.getViewStateForView(child);
148             if (!child.mustStayOnScreen() || state.headsUpIsVisible) {
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 (mClipNotificationScrollToTop
158                     && !state.inShelf && newYTranslation < previousNotificationEnd
159                     && (!isHeadsUp || ambientState.isShadeExpanded())) {
160                 // The previous view is overlapping on top, clip!
161                 float overlapAmount = previousNotificationEnd - newYTranslation;
162                 state.clipTopAmount = (int) overlapAmount;
163             } else {
164                 state.clipTopAmount = 0;
165             }
166 
167             if (!child.isTransparent()) {
168                 // Only update the previous values if we are not transparent,
169                 // otherwise we would clip to a transparent view.
170                 previousNotificationEnd = newNotificationEnd;
171                 previousNotificationStart = newYTranslation;
172             }
173         }
174     }
175 
canChildBeDismissed(View v)176     public static boolean canChildBeDismissed(View v) {
177         if (!(v instanceof ExpandableNotificationRow)) {
178             return false;
179         }
180         ExpandableNotificationRow row = (ExpandableNotificationRow) v;
181         if (row.areGutsExposed()) {
182             return false;
183         }
184         return row.canViewBeDismissed();
185     }
186 
187     /**
188      * Updates the dimmed, activated and hiding sensitive states of the children.
189      */
updateDimmedActivatedHideSensitive(AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState)190     private void updateDimmedActivatedHideSensitive(AmbientState ambientState,
191             StackScrollState resultState, StackScrollAlgorithmState algorithmState) {
192         boolean dimmed = ambientState.isDimmed();
193         boolean dark = ambientState.isFullyDark();
194         boolean hideSensitive = ambientState.isHideSensitive();
195         View activatedChild = ambientState.getActivatedChild();
196         int childCount = algorithmState.visibleChildren.size();
197         for (int i = 0; i < childCount; i++) {
198             View child = algorithmState.visibleChildren.get(i);
199             ExpandableViewState childViewState = resultState.getViewStateForView(child);
200             childViewState.dimmed = dimmed;
201             childViewState.dark = dark;
202             childViewState.hideSensitive = hideSensitive;
203             boolean isActivatedChild = activatedChild == child;
204             if (dimmed && isActivatedChild) {
205                 childViewState.zTranslation += 2.0f * ambientState.getZDistanceBetweenElements();
206             }
207         }
208     }
209 
210     /**
211      * Handle the special state when views are being dragged
212      */
handleDraggedViews(AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState)213     private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState,
214             StackScrollAlgorithmState algorithmState) {
215         ArrayList<View> draggedViews = ambientState.getDraggedViews();
216         for (View draggedView : draggedViews) {
217             int childIndex = algorithmState.visibleChildren.indexOf(draggedView);
218             if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) {
219                 View nextChild = algorithmState.visibleChildren.get(childIndex + 1);
220                 if (!draggedViews.contains(nextChild)) {
221                     // only if the view is not dragged itself we modify its state to be fully
222                     // visible
223                     ExpandableViewState viewState = resultState.getViewStateForView(
224                             nextChild);
225                     // The child below the dragged one must be fully visible
226                     if (ambientState.isShadeExpanded()) {
227                         viewState.shadowAlpha = 1;
228                         viewState.hidden = false;
229                     }
230                 }
231 
232                 // Lets set the alpha to the one it currently has, as its currently being dragged
233                 ExpandableViewState viewState = resultState.getViewStateForView(draggedView);
234                 // The dragged child should keep the set alpha
235                 viewState.alpha = draggedView.getAlpha();
236             }
237         }
238     }
239 
240     /**
241      * Initialize the algorithm state like updating the visible children.
242      */
initAlgorithmState(StackScrollState resultState, StackScrollAlgorithmState state, AmbientState ambientState)243     private void initAlgorithmState(StackScrollState resultState, StackScrollAlgorithmState state,
244             AmbientState ambientState) {
245         float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */);
246 
247         int scrollY = ambientState.getScrollY();
248 
249         // Due to the overScroller, the stackscroller can have negative scroll state. This is
250         // already accounted for by the top padding and doesn't need an additional adaption
251         scrollY = Math.max(0, scrollY);
252         state.scrollY = (int) (scrollY + bottomOverScroll);
253 
254         //now init the visible children and update paddings
255         ViewGroup hostView = resultState.getHostView();
256         int childCount = hostView.getChildCount();
257         state.visibleChildren.clear();
258         state.visibleChildren.ensureCapacity(childCount);
259         state.paddingMap.clear();
260         int notGoneIndex = 0;
261         ExpandableView lastView = null;
262         int firstHiddenIndex = ambientState.isDark()
263                 ? (ambientState.hasPulsingNotifications() ? 1 : 0)
264                 : childCount;
265 
266         // The goal here is to fill the padding map, by iterating over how much padding each child
267         // needs. The map is thereby reused, by first filling it with the padding amount and when
268         // iterating over it again, it's filled with the actual resolved value.
269 
270         for (int i = 0; i < childCount; i++) {
271             ExpandableView v = (ExpandableView) hostView.getChildAt(i);
272             if (v.getVisibility() != View.GONE) {
273                 if (v == ambientState.getShelf()) {
274                     continue;
275                 }
276                 if (i >= firstHiddenIndex) {
277                     // we need normal padding now, to be in sync with what the stack calculates
278                     lastView = null;
279                 }
280                 notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
281                 float increasedPadding = v.getIncreasedPaddingAmount();
282                 if (increasedPadding != 0.0f) {
283                     state.paddingMap.put(v, increasedPadding);
284                     if (lastView != null) {
285                         Float prevValue = state.paddingMap.get(lastView);
286                         float newValue = getPaddingForValue(increasedPadding);
287                         if (prevValue != null) {
288                             float prevPadding = getPaddingForValue(prevValue);
289                             if (increasedPadding > 0) {
290                                 newValue = NotificationUtils.interpolate(
291                                         prevPadding,
292                                         newValue,
293                                         increasedPadding);
294                             } else if (prevValue > 0) {
295                                 newValue = NotificationUtils.interpolate(
296                                         newValue,
297                                         prevPadding,
298                                         prevValue);
299                             }
300                         }
301                         state.paddingMap.put(lastView, newValue);
302                     }
303                 } else if (lastView != null) {
304 
305                     // Let's now resolve the value to an actual padding
306                     float newValue = getPaddingForValue(state.paddingMap.get(lastView));
307                     state.paddingMap.put(lastView, newValue);
308                 }
309                 if (v instanceof ExpandableNotificationRow) {
310                     ExpandableNotificationRow row = (ExpandableNotificationRow) v;
311 
312                     // handle the notgoneIndex for the children as well
313                     List<ExpandableNotificationRow> children =
314                             row.getNotificationChildren();
315                     if (row.isSummaryWithChildren() && children != null) {
316                         for (ExpandableNotificationRow childRow : children) {
317                             if (childRow.getVisibility() != View.GONE) {
318                                 ExpandableViewState childState
319                                         = resultState.getViewStateForView(childRow);
320                                 childState.notGoneIndex = notGoneIndex;
321                                 notGoneIndex++;
322                             }
323                         }
324                     }
325                 }
326                 lastView = v;
327             }
328         }
329         ExpandableNotificationRow expandingNotification = ambientState.getExpandingNotification();
330         state.indexOfExpandingNotification = expandingNotification != null
331                 ? expandingNotification.isChildInGroup()
332                     ? state.visibleChildren.indexOf(expandingNotification.getNotificationParent())
333                     : state.visibleChildren.indexOf(expandingNotification)
334                 : -1;
335     }
336 
getPaddingForValue(Float increasedPadding)337     private float getPaddingForValue(Float increasedPadding) {
338         if (increasedPadding == null) {
339             return mPaddingBetweenElements;
340         } else if (increasedPadding >= 0.0f) {
341             return NotificationUtils.interpolate(
342                     mPaddingBetweenElements,
343                     mIncreasedPaddingBetweenElements,
344                     increasedPadding);
345         } else {
346             return NotificationUtils.interpolate(
347                     0,
348                     mPaddingBetweenElements,
349                     1.0f + increasedPadding);
350         }
351     }
352 
updateNotGoneIndex(StackScrollState resultState, StackScrollAlgorithmState state, int notGoneIndex, ExpandableView v)353     private int updateNotGoneIndex(StackScrollState resultState,
354             StackScrollAlgorithmState state, int notGoneIndex,
355             ExpandableView v) {
356         ExpandableViewState viewState = resultState.getViewStateForView(v);
357         viewState.notGoneIndex = notGoneIndex;
358         state.visibleChildren.add(v);
359         notGoneIndex++;
360         return notGoneIndex;
361     }
362 
363     /**
364      * Determine the positions for the views. This is the main part of the algorithm.
365      *
366      * @param resultState The result state to update if a change to the properties of a child occurs
367      * @param algorithmState The state in which the current pass of the algorithm is currently in
368      * @param ambientState The current ambient state
369      */
updatePositionsForState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState)370     private void updatePositionsForState(StackScrollState resultState,
371             StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
372 
373         // The y coordinate of the current child.
374         float currentYPosition = -algorithmState.scrollY;
375         int childCount = algorithmState.visibleChildren.size();
376         for (int i = 0; i < childCount; i++) {
377             currentYPosition = updateChild(i, resultState, algorithmState, ambientState,
378                     currentYPosition);
379         }
380     }
381 
updateChild(int i, StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState, float currentYPosition)382     protected float updateChild(int i, StackScrollState resultState,
383             StackScrollAlgorithmState algorithmState, AmbientState ambientState,
384             float currentYPosition) {
385         ExpandableView child = algorithmState.visibleChildren.get(i);
386         ExpandableViewState childViewState = resultState.getViewStateForView(child);
387         childViewState.location = ExpandableViewState.LOCATION_UNKNOWN;
388         int paddingAfterChild = getPaddingAfterChild(algorithmState, child);
389         int childHeight = getMaxAllowedChildHeight(child);
390         childViewState.yTranslation = currentYPosition;
391         boolean isFooterView = child instanceof FooterView;
392         boolean isEmptyShadeView = child instanceof EmptyShadeView;
393 
394         childViewState.location = ExpandableViewState.LOCATION_MAIN_AREA;
395         float inset = ambientState.getTopPadding() + ambientState.getStackTranslation();
396         if (i <= algorithmState.getIndexOfExpandingNotification()) {
397             inset += ambientState.getExpandAnimationTopChange();
398         }
399         if (child.mustStayOnScreen() && childViewState.yTranslation >= 0) {
400             // Even if we're not scrolled away we're in view and we're also not in the
401             // shelf. We can relax the constraints and let us scroll off the top!
402             float end = childViewState.yTranslation + childViewState.height + inset;
403             childViewState.headsUpIsVisible = end < ambientState.getMaxHeadsUpTranslation();
404         }
405         if (isFooterView) {
406             childViewState.yTranslation = Math.min(childViewState.yTranslation,
407                     ambientState.getInnerHeight() - childHeight);
408         } else if (isEmptyShadeView) {
409             childViewState.yTranslation = ambientState.getInnerHeight() - childHeight
410                     + ambientState.getStackTranslation() * 0.25f;
411         } else {
412             clampPositionToShelf(child, childViewState, ambientState);
413         }
414 
415         currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild;
416         if (currentYPosition <= 0) {
417             childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
418         }
419         if (childViewState.location == ExpandableViewState.LOCATION_UNKNOWN) {
420             Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
421         }
422 
423         childViewState.yTranslation += inset;
424         return currentYPosition;
425     }
426 
427     protected int getPaddingAfterChild(StackScrollAlgorithmState algorithmState,
428             ExpandableView child) {
429         return algorithmState.getPaddingAfterChild(child);
430     }
431 
432     private void updateHeadsUpStates(StackScrollState resultState,
433             StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
434         int childCount = algorithmState.visibleChildren.size();
435         ExpandableNotificationRow topHeadsUpEntry = null;
436         for (int i = 0; i < childCount; i++) {
437             View child = algorithmState.visibleChildren.get(i);
438             if (!(child instanceof ExpandableNotificationRow)) {
439                 break;
440             }
441             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
442             if (!row.isHeadsUp()) {
443                 break;
444             }
445             ExpandableViewState childState = resultState.getViewStateForView(row);
446             if (topHeadsUpEntry == null && row.mustStayOnScreen() && !childState.headsUpIsVisible) {
447                 topHeadsUpEntry = row;
448                 childState.location = ExpandableViewState.LOCATION_FIRST_HUN;
449             }
450             boolean isTopEntry = topHeadsUpEntry == row;
451             float unmodifiedEndLocation = childState.yTranslation + childState.height;
452             if (mIsExpanded) {
453                 if (row.mustStayOnScreen() && !childState.headsUpIsVisible) {
454                     // Ensure that the heads up is always visible even when scrolled off
455                     clampHunToTop(ambientState, row, childState);
456                     if (i == 0 && ambientState.isAboveShelf(row)) {
457                         // the first hun can't get off screen.
458                         clampHunToMaxTranslation(ambientState, row, childState);
459                         childState.hidden = false;
460                     }
461                 }
462             }
463             if (row.isPinned()) {
464                 childState.yTranslation = Math.max(childState.yTranslation, mHeadsUpInset);
465                 childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
466                 childState.hidden = false;
467                 ExpandableViewState topState = resultState.getViewStateForView(topHeadsUpEntry);
468                 if (topState != null && !isTopEntry && (!mIsExpanded
469                         || unmodifiedEndLocation < topState.yTranslation + topState.height)) {
470                     // Ensure that a headsUp doesn't vertically extend further than the heads-up at
471                     // the top most z-position
472                     childState.height = row.getIntrinsicHeight();
473                     childState.yTranslation = topState.yTranslation + topState.height
474                             - childState.height;
475                 }
476             }
477             if (row.isHeadsUpAnimatingAway()) {
478                 childState.hidden = false;
479             }
480         }
481     }
482 
483     private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row,
484             ExpandableViewState childState) {
485         float newTranslation = Math.max(ambientState.getTopPadding()
486                 + ambientState.getStackTranslation(), childState.yTranslation);
487         childState.height = (int) Math.max(childState.height - (newTranslation
488                 - childState.yTranslation), row.getCollapsedHeight());
489         childState.yTranslation = newTranslation;
490     }
491 
492     private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
493             ExpandableViewState childState) {
494         float newTranslation;
495         float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
496         float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
497                 + ambientState.getStackTranslation();
498         maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition);
499         float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
500         newTranslation = Math.min(childState.yTranslation, bottomPosition);
501         childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation
502                 - newTranslation);
503         childState.yTranslation = newTranslation;
504     }
505 
506     /**
507      * Clamp the height of the child down such that its end is at most on the beginning of
508      * the shelf.
509      *
510      * @param child
511      * @param childViewState the view state of the child
512      * @param ambientState the ambient state
513      */
514     private void clampPositionToShelf(ExpandableView child,
515             ExpandableViewState childViewState,
516             AmbientState ambientState) {
517         if (ambientState.getShelf() == null) {
518             return;
519         }
520 
521         int shelfStart = ambientState.getInnerHeight()
522                 - ambientState.getShelf().getIntrinsicHeight();
523         if (ambientState.isAppearing() && !child.isAboveShelf()) {
524             // Don't show none heads-up notifications while in appearing phase.
525             childViewState.yTranslation = Math.max(childViewState.yTranslation, shelfStart);
526         }
527         childViewState.yTranslation = Math.min(childViewState.yTranslation, shelfStart);
528         if (childViewState.yTranslation >= shelfStart) {
529             childViewState.hidden = !child.isExpandAnimationRunning() && !child.hasExpandingChild();
530             childViewState.inShelf = true;
531             childViewState.headsUpIsVisible = false;
532         }
533     }
534 
535     protected int getMaxAllowedChildHeight(View child) {
536         if (child instanceof ExpandableView) {
537             ExpandableView expandableView = (ExpandableView) child;
538             return expandableView.getIntrinsicHeight();
539         }
540         return child == null? mCollapsedSize : child.getHeight();
541     }
542 
543     /**
544      * Calculate the Z positions for all children based on the number of items in both stacks and
545      * save it in the resultState
546      *  @param resultState The result state to update the zTranslation values
547      * @param algorithmState The state in which the current pass of the algorithm is currently in
548      * @param ambientState The ambient state of the algorithm
549      */
550     private void updateZValuesForState(StackScrollState resultState,
551             StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
552         int childCount = algorithmState.visibleChildren.size();
553         float childrenOnTop = 0.0f;
554         for (int i = childCount - 1; i >= 0; i--) {
555             childrenOnTop = updateChildZValue(i, childrenOnTop,
556                     resultState, algorithmState, ambientState);
557         }
558     }
559 
560     protected float updateChildZValue(int i, float childrenOnTop,
561             StackScrollState resultState, StackScrollAlgorithmState algorithmState,
562             AmbientState ambientState) {
563         ExpandableView child = algorithmState.visibleChildren.get(i);
564         ExpandableViewState childViewState = resultState.getViewStateForView(child);
565         int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements();
566         float baseZ = ambientState.getBaseZHeight();
567         if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible
568                 && !ambientState.isDozingAndNotPulsing(child)
569                 && childViewState.yTranslation < ambientState.getTopPadding()
570                 + ambientState.getStackTranslation()) {
571             if (childrenOnTop != 0.0f) {
572                 childrenOnTop++;
573             } else {
574                 float overlap = ambientState.getTopPadding()
575                         + ambientState.getStackTranslation() - childViewState.yTranslation;
576                 childrenOnTop += Math.min(1.0f, overlap / childViewState.height);
577             }
578             childViewState.zTranslation = baseZ
579                     + childrenOnTop * zDistanceBetweenElements;
580         } else if (i == 0 && ambientState.isAboveShelf(child)) {
581             // In case this is a new view that has never been measured before, we don't want to
582             // elevate if we are currently expanded more then the notification
583             int shelfHeight = ambientState.getShelf() == null ? 0 :
584                     ambientState.getShelf().getIntrinsicHeight();
585             float shelfStart = ambientState.getInnerHeight()
586                     - shelfHeight + ambientState.getTopPadding()
587                     + ambientState.getStackTranslation();
588             float notificationEnd = childViewState.yTranslation + child.getPinnedHeadsUpHeight()
589                     + mPaddingBetweenElements;
590             if (shelfStart > notificationEnd) {
591                 childViewState.zTranslation = baseZ;
592             } else {
593                 float factor = (notificationEnd - shelfStart) / shelfHeight;
594                 factor = Math.min(factor, 1.0f);
595                 childViewState.zTranslation = baseZ + factor * zDistanceBetweenElements;
596             }
597         } else {
598             childViewState.zTranslation = baseZ;
599         }
600 
601         // We need to scrim the notification more from its surrounding content when we are pinned,
602         // and we therefore elevate it higher.
603         // We can use the headerVisibleAmount for this, since the value nicely goes from 0 to 1 when
604         // expanding after which we have a normal elevation again.
605         childViewState.zTranslation += (1.0f - child.getHeaderVisibleAmount())
606                 * mPinnedZTranslationExtra;
607         return childrenOnTop;
608     }
609 
610     public void setIsExpanded(boolean isExpanded) {
611         this.mIsExpanded = isExpanded;
612     }
613 
614     public class StackScrollAlgorithmState {
615 
616         /**
617          * The scroll position of the algorithm
618          */
619         public int scrollY;
620 
621         /**
622          * The children from the host view which are not gone.
623          */
624         public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>();
625 
626         /**
627          * The padding after each child measured in pixels.
628          */
629         public final HashMap<ExpandableView, Float> paddingMap = new HashMap<>();
630         private int indexOfExpandingNotification;
631 
632         public int getPaddingAfterChild(ExpandableView child) {
633             Float padding = paddingMap.get(child);
634             if (padding == null) {
635                 // Should only happen for the last view
636                 return mPaddingBetweenElements;
637             }
638             return (int) padding.floatValue();
639         }
640 
641         public int getIndexOfExpandingNotification() {
642             return indexOfExpandingNotification;
643         }
644     }
645 
646 }
647