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