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