• 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.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.util.MathUtils;
24 import android.view.View;
25 import android.view.ViewGroup;
26 
27 import com.android.systemui.R;
28 import com.android.systemui.animation.Interpolators;
29 import com.android.systemui.statusbar.NotificationShelf;
30 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
31 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
32 import com.android.systemui.statusbar.notification.row.ExpandableView;
33 import com.android.systemui.statusbar.notification.row.FooterView;
34 
35 import java.util.ArrayList;
36 import java.util.List;
37 
38 /**
39  * The Algorithm of the
40  * {@link com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout} which can
41  * be queried for {@link StackScrollAlgorithmState}
42  */
43 public class StackScrollAlgorithm {
44 
45     public static final float START_FRACTION = 0.3f;
46 
47     private static final String LOG_TAG = "StackScrollAlgorithm";
48     private final ViewGroup mHostView;
49 
50     private int mPaddingBetweenElements;
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     private float mNotificationScrimPadding;
61 
StackScrollAlgorithm( Context context, ViewGroup hostView)62     public StackScrollAlgorithm(
63             Context context,
64             ViewGroup hostView) {
65         mHostView = hostView;
66         initView(context);
67     }
68 
initView(Context context)69     public void initView(Context context) {
70         initConstants(context);
71     }
72 
initConstants(Context context)73     private void initConstants(Context context) {
74         Resources res = context.getResources();
75         mPaddingBetweenElements = res.getDimensionPixelSize(
76                 R.dimen.notification_divider_height);
77         mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
78         mStatusBarHeight = res.getDimensionPixelSize(R.dimen.status_bar_height);
79         mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop);
80         mHeadsUpInset = mStatusBarHeight + res.getDimensionPixelSize(
81                 R.dimen.heads_up_status_bar_padding);
82         mPinnedZTranslationExtra = res.getDimensionPixelSize(
83                 R.dimen.heads_up_pinned_elevation);
84         mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height);
85         mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
86     }
87 
88     /**
89      * Updates the state of all children in the hostview based on this algorithm.
90      */
resetViewStates(AmbientState ambientState, int speedBumpIndex)91     public void resetViewStates(AmbientState ambientState, int speedBumpIndex) {
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         initAlgorithmState(algorithmState, ambientState);
99         updatePositionsForState(algorithmState, ambientState);
100         updateZValuesForState(algorithmState, ambientState);
101         updateHeadsUpStates(algorithmState, ambientState);
102         updatePulsingStates(algorithmState, ambientState);
103 
104         updateDimmedActivatedHideSensitive(ambientState, algorithmState);
105         updateClipping(algorithmState, ambientState);
106         updateSpeedBumpState(algorithmState, speedBumpIndex);
107         updateShelfState(algorithmState, ambientState);
108         getNotificationChildrenStates(algorithmState, ambientState);
109     }
110 
resetChildViewStates()111     private void resetChildViewStates() {
112         int numChildren = mHostView.getChildCount();
113         for (int i = 0; i < numChildren; i++) {
114             ExpandableView child = (ExpandableView) mHostView.getChildAt(i);
115             child.resetViewState();
116         }
117     }
118 
getNotificationChildrenStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)119     private void getNotificationChildrenStates(StackScrollAlgorithmState algorithmState,
120             AmbientState ambientState) {
121         int childCount = algorithmState.visibleChildren.size();
122         for (int i = 0; i < childCount; i++) {
123             ExpandableView v = algorithmState.visibleChildren.get(i);
124             if (v instanceof ExpandableNotificationRow) {
125                 ExpandableNotificationRow row = (ExpandableNotificationRow) v;
126                 row.updateChildrenStates(ambientState);
127             }
128         }
129     }
130 
updateSpeedBumpState(StackScrollAlgorithmState algorithmState, int speedBumpIndex)131     private void updateSpeedBumpState(StackScrollAlgorithmState algorithmState,
132             int speedBumpIndex) {
133         int childCount = algorithmState.visibleChildren.size();
134         int belowSpeedBump = speedBumpIndex;
135         for (int i = 0; i < childCount; i++) {
136             ExpandableView child = algorithmState.visibleChildren.get(i);
137             ExpandableViewState childViewState = child.getViewState();
138 
139             // The speed bump can also be gone, so equality needs to be taken when comparing
140             // indices.
141             childViewState.belowSpeedBump = i >= belowSpeedBump;
142         }
143 
144     }
145 
updateShelfState( StackScrollAlgorithmState algorithmState, AmbientState ambientState)146     private void updateShelfState(
147             StackScrollAlgorithmState algorithmState,
148             AmbientState ambientState) {
149 
150         NotificationShelf shelf = ambientState.getShelf();
151         if (shelf == null) {
152             return;
153         }
154 
155         shelf.updateState(algorithmState, ambientState);
156 
157         // After the shelf has updated its yTranslation, explicitly set alpha=0 for view below shelf
158         // to skip rendering them in the hardware layer. We do not set them invisible because that
159         // runs invalidate & onDraw when these views return onscreen, which is more expensive.
160         if (shelf.getViewState().hidden) {
161             // When the shelf is hidden, it won't clip views, so we don't hide rows
162             return;
163         }
164         final float shelfTop = shelf.getViewState().yTranslation;
165 
166         for (ExpandableView view : algorithmState.visibleChildren) {
167             final float viewTop = view.getViewState().yTranslation;
168             if (viewTop >= shelfTop) {
169                 view.getViewState().alpha = 0;
170             }
171         }
172     }
173 
updateClipping(StackScrollAlgorithmState algorithmState, AmbientState ambientState)174     private void updateClipping(StackScrollAlgorithmState algorithmState,
175             AmbientState ambientState) {
176         float drawStart = ambientState.isOnKeyguard() ? 0
177                 : ambientState.getStackY() - ambientState.getScrollY();
178         float clipStart = 0;
179         int childCount = algorithmState.visibleChildren.size();
180         boolean firstHeadsUp = true;
181         for (int i = 0; i < childCount; i++) {
182             ExpandableView child = algorithmState.visibleChildren.get(i);
183             ExpandableViewState state = child.getViewState();
184             if (!child.mustStayOnScreen() || state.headsUpIsVisible) {
185                 clipStart = Math.max(drawStart, clipStart);
186             }
187             float newYTranslation = state.yTranslation;
188             float newHeight = state.height;
189             float newNotificationEnd = newYTranslation + newHeight;
190             boolean isHeadsUp = (child instanceof ExpandableNotificationRow) && child.isPinned();
191             if (mClipNotificationScrollToTop
192                     && (!state.inShelf || (isHeadsUp && !firstHeadsUp))
193                     && newYTranslation < clipStart
194                     && !ambientState.isShadeOpening()) {
195                 // The previous view is overlapping on top, clip!
196                 float overlapAmount = clipStart - newYTranslation;
197                 state.clipTopAmount = (int) overlapAmount;
198             } else {
199                 state.clipTopAmount = 0;
200             }
201             if (isHeadsUp) {
202                 firstHeadsUp = false;
203             }
204             if (!child.isTransparent()) {
205                 // Only update the previous values if we are not transparent,
206                 // otherwise we would clip to a transparent view.
207                 clipStart = Math.max(clipStart, isHeadsUp ? newYTranslation : newNotificationEnd);
208             }
209         }
210     }
211 
212     /**
213      * Updates the dimmed, activated and hiding sensitive states of the children.
214      */
updateDimmedActivatedHideSensitive(AmbientState ambientState, StackScrollAlgorithmState algorithmState)215     private void updateDimmedActivatedHideSensitive(AmbientState ambientState,
216             StackScrollAlgorithmState algorithmState) {
217         boolean dimmed = ambientState.isDimmed();
218         boolean hideSensitive = ambientState.isHideSensitive();
219         View activatedChild = ambientState.getActivatedChild();
220         int childCount = algorithmState.visibleChildren.size();
221         for (int i = 0; i < childCount; i++) {
222             ExpandableView child = algorithmState.visibleChildren.get(i);
223             ExpandableViewState childViewState = child.getViewState();
224             childViewState.dimmed = dimmed;
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(StackScrollAlgorithmState state, AmbientState ambientState)236     private void initAlgorithmState(StackScrollAlgorithmState state, AmbientState ambientState) {
237         state.scrollY = ambientState.getScrollY();
238         state.mCurrentYPosition = -state.scrollY;
239         state.mCurrentExpandedYPosition = -state.scrollY;
240 
241         //now init the visible children and update paddings
242         int childCount = mHostView.getChildCount();
243         state.visibleChildren.clear();
244         state.visibleChildren.ensureCapacity(childCount);
245         int notGoneIndex = 0;
246         for (int i = 0; i < childCount; i++) {
247             ExpandableView v = (ExpandableView) mHostView.getChildAt(i);
248             if (v.getVisibility() != View.GONE) {
249                 if (v == ambientState.getShelf()) {
250                     continue;
251                 }
252                 notGoneIndex = updateNotGoneIndex(state, notGoneIndex, v);
253                 if (v instanceof ExpandableNotificationRow) {
254                     ExpandableNotificationRow row = (ExpandableNotificationRow) v;
255 
256                     // handle the notGoneIndex for the children as well
257                     List<ExpandableNotificationRow> children = row.getAttachedChildren();
258                     if (row.isSummaryWithChildren() && children != null) {
259                         for (ExpandableNotificationRow childRow : children) {
260                             if (childRow.getVisibility() != View.GONE) {
261                                 ExpandableViewState childState = childRow.getViewState();
262                                 childState.notGoneIndex = notGoneIndex;
263                                 notGoneIndex++;
264                             }
265                         }
266                     }
267                 }
268             }
269         }
270 
271         // Save the index of first view in shelf from when shade is fully
272         // expanded. Consider updating these states in updateContentView instead so that we don't
273         // have to recalculate in every frame.
274         float currentY = -ambientState.getScrollY();
275         if (!ambientState.isOnKeyguard()
276                 || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) {
277             // add top padding at the start as long as we're not on the lock screen
278             currentY += mNotificationScrimPadding;
279         }
280         state.firstViewInShelf = null;
281         for (int i = 0; i < state.visibleChildren.size(); i++) {
282             final ExpandableView view = state.visibleChildren.get(i);
283 
284             final boolean applyGapHeight = childNeedsGapHeight(
285                     ambientState.getSectionProvider(), i,
286                     view, getPreviousView(i, state));
287             if (applyGapHeight) {
288                 currentY += mGapHeight;
289             }
290 
291             if (ambientState.getShelf() != null) {
292                 final float shelfStart = ambientState.getStackEndHeight()
293                         - ambientState.getShelf().getIntrinsicHeight();
294                 if (currentY >= shelfStart
295                         && !(view instanceof FooterView)
296                         && state.firstViewInShelf == null) {
297                     state.firstViewInShelf = view;
298                 }
299             }
300             currentY = currentY
301                     + getMaxAllowedChildHeight(view)
302                     + mPaddingBetweenElements;
303         }
304     }
305 
updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, ExpandableView v)306     private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex,
307             ExpandableView v) {
308         ExpandableViewState viewState = v.getViewState();
309         viewState.notGoneIndex = notGoneIndex;
310         state.visibleChildren.add(v);
311         notGoneIndex++;
312         return notGoneIndex;
313     }
314 
getPreviousView(int i, StackScrollAlgorithmState algorithmState)315     private ExpandableView getPreviousView(int i, StackScrollAlgorithmState algorithmState) {
316         return i > 0 ? algorithmState.visibleChildren.get(i - 1) : null;
317     }
318 
319     /**
320      * Determine the positions for the views. This is the main part of the algorithm.
321      *
322      * @param algorithmState The state in which the current pass of the algorithm is currently in
323      * @param ambientState   The current ambient state
324      */
updatePositionsForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)325     private void updatePositionsForState(StackScrollAlgorithmState algorithmState,
326             AmbientState ambientState) {
327         if (!ambientState.isOnKeyguard()
328                 || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding())) {
329             algorithmState.mCurrentYPosition += mNotificationScrimPadding;
330             algorithmState.mCurrentExpandedYPosition += mNotificationScrimPadding;
331         }
332 
333         int childCount = algorithmState.visibleChildren.size();
334         for (int i = 0; i < childCount; i++) {
335             updateChild(i, algorithmState, ambientState);
336         }
337     }
338 
setLocation(ExpandableViewState expandableViewState, float currentYPosition, int i)339     private void setLocation(ExpandableViewState expandableViewState, float currentYPosition,
340             int i) {
341         expandableViewState.location = ExpandableViewState.LOCATION_MAIN_AREA;
342         if (currentYPosition <= 0) {
343             expandableViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
344         }
345     }
346 
347     /**
348      * @return Fraction to apply to view height and gap between views.
349      *         Does not include shelf height even if shelf is showing.
350      */
getExpansionFractionWithoutShelf( StackScrollAlgorithmState algorithmState, AmbientState ambientState)351     private float getExpansionFractionWithoutShelf(
352             StackScrollAlgorithmState algorithmState,
353             AmbientState ambientState) {
354 
355         final boolean showingShelf = ambientState.getShelf() != null
356                 && algorithmState.firstViewInShelf != null;
357 
358         final float shelfHeight = showingShelf ? ambientState.getShelf().getIntrinsicHeight() : 0f;
359         final float scrimPadding = ambientState.isOnKeyguard()
360                 && (!ambientState.isBypassEnabled() || !ambientState.isPulseExpanding())
361                 ? 0 : mNotificationScrimPadding;
362 
363         final float stackHeight = ambientState.getStackHeight()  - shelfHeight - scrimPadding;
364         final float stackEndHeight = ambientState.getStackEndHeight() - shelfHeight - scrimPadding;
365 
366         return stackHeight / stackEndHeight;
367     }
368 
369     // TODO(b/172289889) polish shade open from HUN
370     /**
371      * Populates the {@link ExpandableViewState} for a single child.
372      *
373      * @param i                The index of the child in
374      * {@link StackScrollAlgorithmState#visibleChildren}.
375      * @param algorithmState   The overall output state of the algorithm.
376      * @param ambientState     The input state provided to the algorithm.
377      */
updateChild( int i, StackScrollAlgorithmState algorithmState, AmbientState ambientState)378     protected void updateChild(
379             int i,
380             StackScrollAlgorithmState algorithmState,
381             AmbientState ambientState) {
382 
383         ExpandableView view = algorithmState.visibleChildren.get(i);
384         ExpandableViewState viewState = view.getViewState();
385         viewState.location = ExpandableViewState.LOCATION_UNKNOWN;
386 
387         final boolean isHunGoingToShade = ambientState.isShadeExpanded()
388                 && view == ambientState.getTrackedHeadsUpRow();
389         if (isHunGoingToShade) {
390             // Keep 100% opacity for heads up notification going to shade.
391         } else if (ambientState.isOnKeyguard()) {
392             // Adjust alpha for wakeup to lockscreen.
393             viewState.alpha = 1f - ambientState.getHideAmount();
394         } else if (ambientState.isExpansionChanging()) {
395             // Adjust alpha for shade open & close.
396             viewState.alpha = Interpolators.getNotificationScrimAlpha(
397                     ambientState.getExpansionFraction(), true /* notification */);
398         }
399 
400         if (ambientState.isShadeExpanded() && view.mustStayOnScreen()
401                 && viewState.yTranslation >= 0) {
402             // Even if we're not scrolled away we're in view and we're also not in the
403             // shelf. We can relax the constraints and let us scroll off the top!
404             float end = viewState.yTranslation + viewState.height + ambientState.getStackY();
405             viewState.headsUpIsVisible = end < ambientState.getMaxHeadsUpTranslation();
406         }
407 
408         final float expansionFraction = getExpansionFractionWithoutShelf(
409                 algorithmState, ambientState);
410 
411         // Add gap between sections.
412         final boolean applyGapHeight =
413                 childNeedsGapHeight(
414                         ambientState.getSectionProvider(), i,
415                         view, getPreviousView(i, algorithmState));
416         if (applyGapHeight) {
417             algorithmState.mCurrentYPosition += expansionFraction * mGapHeight;
418             algorithmState.mCurrentExpandedYPosition += mGapHeight;
419         }
420 
421         viewState.yTranslation = algorithmState.mCurrentYPosition;
422 
423         if (view instanceof FooterView) {
424             final boolean shadeClosed = !ambientState.isShadeExpanded();
425             final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
426             if (shadeClosed) {
427                 viewState.hidden = true;
428             } else {
429                 final float footerEnd = algorithmState.mCurrentExpandedYPosition
430                         + view.getIntrinsicHeight();
431                 final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight();
432                 ((FooterView.FooterViewState) viewState).hideContent =
433                         isShelfShowing || noSpaceForFooter;
434             }
435         } else {
436             if (view != ambientState.getTrackedHeadsUpRow()) {
437                 if (ambientState.isExpansionChanging()) {
438                     // We later update shelf state, then hide views below the shelf.
439                     viewState.hidden = false;
440                     viewState.inShelf = algorithmState.firstViewInShelf != null
441                             && i >= algorithmState.visibleChildren.indexOf(
442                                     algorithmState.firstViewInShelf);
443                 } else if (ambientState.getShelf() != null) {
444                     // When pulsing (incoming notification on AOD), innerHeight is 0; clamp all
445                     // to shelf start, thereby hiding all notifications (except the first one, which
446                     // we later unhide in updatePulsingState)
447                     // TODO(b/192348384): merge InnerHeight with StackHeight
448                     // Note: Bypass pulse looks different, but when it is not expanding, we need
449                     //  to use the innerHeight which doesn't update continuously, otherwise we show
450                     //  more notifications than we should during this special transitional states.
451                     boolean bypassPulseNotExpanding = ambientState.isBypassEnabled()
452                             && ambientState.isOnKeyguard() && !ambientState.isPulseExpanding();
453                     final int stackBottom =
454                             !ambientState.isShadeExpanded() || ambientState.isDozing()
455                                     || bypassPulseNotExpanding
456                                     ? ambientState.getInnerHeight()
457                                     : (int) ambientState.getStackHeight();
458                     final int shelfStart =
459                             stackBottom - ambientState.getShelf().getIntrinsicHeight();
460                     viewState.yTranslation = Math.min(viewState.yTranslation, shelfStart);
461                     if (viewState.yTranslation >= shelfStart) {
462                         viewState.hidden = !view.isExpandAnimationRunning()
463                                 && !view.hasExpandingChild();
464                         viewState.inShelf = true;
465                         // Notifications in the shelf cannot be visible HUNs.
466                         viewState.headsUpIsVisible = false;
467                     }
468                 }
469             }
470 
471             // Clip height of view right before shelf.
472             viewState.height = (int) (getMaxAllowedChildHeight(view) * expansionFraction);
473         }
474 
475         algorithmState.mCurrentYPosition += viewState.height
476                 + expansionFraction * mPaddingBetweenElements;
477         algorithmState.mCurrentExpandedYPosition += view.getIntrinsicHeight()
478                 + mPaddingBetweenElements;
479 
480         setLocation(view.getViewState(), algorithmState.mCurrentYPosition, i);
481         viewState.yTranslation += ambientState.getStackY();
482     }
483 
484     /**
485      * Get the gap height needed for before a view
486      *
487      * @param sectionProvider the sectionProvider used to understand the sections
488      * @param visibleIndex the visible index of this view in the list
489      * @param child the child asked about
490      * @param previousChild the child right before it or null if none
491      * @return the size of the gap needed or 0 if none is needed
492      */
getGapHeightForChild( SectionProvider sectionProvider, int visibleIndex, View child, View previousChild)493     public float getGapHeightForChild(
494             SectionProvider sectionProvider,
495             int visibleIndex,
496             View child,
497             View previousChild) {
498 
499         if (childNeedsGapHeight(sectionProvider, visibleIndex, child,
500                 previousChild)) {
501             return mGapHeight;
502         } else {
503             return 0;
504         }
505     }
506 
507     /**
508      * Does a given child need a gap, i.e spacing before a view?
509      *
510      * @param sectionProvider the sectionProvider used to understand the sections
511      * @param visibleIndex the visible index of this view in the list
512      * @param child the child asked about
513      * @param previousChild the child right before it or null if none
514      * @return if the child needs a gap height
515      */
childNeedsGapHeight( SectionProvider sectionProvider, int visibleIndex, View child, View previousChild)516     private boolean childNeedsGapHeight(
517             SectionProvider sectionProvider,
518             int visibleIndex,
519             View child,
520             View previousChild) {
521         return sectionProvider.beginsSection(child, previousChild)
522                 && visibleIndex > 0
523                 && !(previousChild instanceof SectionHeaderView)
524                 && !(child instanceof FooterView);
525     }
526 
updatePulsingStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)527     private void updatePulsingStates(StackScrollAlgorithmState algorithmState,
528             AmbientState ambientState) {
529         int childCount = algorithmState.visibleChildren.size();
530         for (int i = 0; i < childCount; i++) {
531             View child = algorithmState.visibleChildren.get(i);
532             if (!(child instanceof ExpandableNotificationRow)) {
533                 continue;
534             }
535             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
536             if (!row.showingPulsing() || (i == 0 && ambientState.isPulseExpanding())) {
537                 continue;
538             }
539             ExpandableViewState viewState = row.getViewState();
540             viewState.hidden = false;
541         }
542     }
543 
updateHeadsUpStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)544     private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState,
545             AmbientState ambientState) {
546         int childCount = algorithmState.visibleChildren.size();
547 
548         // Move the tracked heads up into position during the appear animation, by interpolating
549         // between the HUN inset (where it will appear as a HUN) and the end position in the shade
550         ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow();
551         if (trackedHeadsUpRow != null) {
552             ExpandableViewState childState = trackedHeadsUpRow.getViewState();
553             if (childState != null) {
554                 float endPosition = childState.yTranslation - ambientState.getStackTranslation();
555                 childState.yTranslation = MathUtils.lerp(
556                         mHeadsUpInset, endPosition, ambientState.getAppearFraction());
557             }
558         }
559 
560         ExpandableNotificationRow topHeadsUpEntry = null;
561         for (int i = 0; i < childCount; i++) {
562             View child = algorithmState.visibleChildren.get(i);
563             if (!(child instanceof ExpandableNotificationRow)) {
564                 continue;
565             }
566             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
567             if (!(row.isHeadsUp() || row.isHeadsUpAnimatingAway())) {
568                 continue;
569             }
570             ExpandableViewState childState = row.getViewState();
571             if (topHeadsUpEntry == null && row.mustStayOnScreen() && !childState.headsUpIsVisible) {
572                 topHeadsUpEntry = row;
573                 childState.location = ExpandableViewState.LOCATION_FIRST_HUN;
574             }
575             boolean isTopEntry = topHeadsUpEntry == row;
576             float unmodifiedEndLocation = childState.yTranslation + childState.height;
577             if (mIsExpanded) {
578                 if (row.mustStayOnScreen() && !childState.headsUpIsVisible
579                         && !row.showingPulsing()) {
580                     // Ensure that the heads up is always visible even when scrolled off
581                     clampHunToTop(ambientState, row, childState);
582                     if (isTopEntry && row.isAboveShelf()) {
583                         // the first hun can't get off screen.
584                         clampHunToMaxTranslation(ambientState, row, childState);
585                         childState.hidden = false;
586                     }
587                 }
588             }
589             if (row.isPinned()) {
590                 childState.yTranslation = Math.max(childState.yTranslation, mHeadsUpInset);
591                 childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
592                 childState.hidden = false;
593                 ExpandableViewState topState =
594                         topHeadsUpEntry == null ? null : topHeadsUpEntry.getViewState();
595                 if (topState != null && !isTopEntry && (!mIsExpanded
596                         || unmodifiedEndLocation > topState.yTranslation + topState.height)) {
597                     // Ensure that a headsUp doesn't vertically extend further than the heads-up at
598                     // the top most z-position
599                     childState.height = row.getIntrinsicHeight();
600                     childState.yTranslation = Math.min(topState.yTranslation + topState.height
601                             - childState.height, childState.yTranslation);
602                 }
603 
604                 // heads up notification show and this row is the top entry of heads up
605                 // notifications. i.e. this row should be the only one row that has input field
606                 // To check if the row need to do translation according to scroll Y
607                 // heads up show full of row's content and any scroll y indicate that the
608                 // translationY need to move up the HUN.
609                 if (!mIsExpanded && isTopEntry && ambientState.getScrollY() > 0) {
610                     childState.yTranslation -= ambientState.getScrollY();
611                 }
612             }
613             if (row.isHeadsUpAnimatingAway()) {
614                 childState.yTranslation = Math.max(childState.yTranslation, mHeadsUpInset);
615                 childState.hidden = false;
616             }
617         }
618     }
619 
clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row, ExpandableViewState childState)620     private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row,
621             ExpandableViewState childState) {
622         float newTranslation = Math.max(ambientState.getTopPadding()
623                 + ambientState.getStackTranslation(), childState.yTranslation);
624         childState.height = (int) Math.max(childState.height - (newTranslation
625                 - childState.yTranslation), row.getCollapsedHeight());
626         childState.yTranslation = newTranslation;
627     }
628 
clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, ExpandableViewState childState)629     private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
630             ExpandableViewState childState) {
631         float newTranslation;
632         float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
633         float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
634                 + ambientState.getStackTranslation();
635         maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition);
636         float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
637         newTranslation = Math.min(childState.yTranslation, bottomPosition);
638         childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation
639                 - newTranslation);
640         childState.yTranslation = newTranslation;
641     }
642 
getMaxAllowedChildHeight(View child)643     protected int getMaxAllowedChildHeight(View child) {
644         if (child instanceof ExpandableView) {
645             ExpandableView expandableView = (ExpandableView) child;
646             return expandableView.getIntrinsicHeight();
647         }
648         return child == null ? mCollapsedSize : child.getHeight();
649     }
650 
651     /**
652      * Calculate the Z positions for all children based on the number of items in both stacks and
653      * save it in the resultState
654      *
655      * @param algorithmState The state in which the current pass of the algorithm is currently in
656      * @param ambientState   The ambient state of the algorithm
657      */
updateZValuesForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)658     private void updateZValuesForState(StackScrollAlgorithmState algorithmState,
659             AmbientState ambientState) {
660         int childCount = algorithmState.visibleChildren.size();
661         float childrenOnTop = 0.0f;
662 
663         int topHunIndex = -1;
664         for (int i = 0; i < childCount; i++) {
665             ExpandableView child = algorithmState.visibleChildren.get(i);
666             if (child instanceof ActivatableNotificationView
667                     && (child.isAboveShelf() || child.showingPulsing())) {
668                 topHunIndex = i;
669                 break;
670             }
671         }
672 
673         for (int i = childCount - 1; i >= 0; i--) {
674             childrenOnTop = updateChildZValue(i, childrenOnTop,
675                     algorithmState, ambientState, i == topHunIndex);
676         }
677     }
678 
updateChildZValue(int i, float childrenOnTop, StackScrollAlgorithmState algorithmState, AmbientState ambientState, boolean shouldElevateHun)679     protected float updateChildZValue(int i, float childrenOnTop,
680             StackScrollAlgorithmState algorithmState,
681             AmbientState ambientState,
682             boolean shouldElevateHun) {
683         ExpandableView child = algorithmState.visibleChildren.get(i);
684         ExpandableViewState childViewState = child.getViewState();
685         int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements();
686         float baseZ = ambientState.getBaseZHeight();
687         if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible
688                 && !ambientState.isDozingAndNotPulsing(child)
689                 && childViewState.yTranslation < ambientState.getTopPadding()
690                 + ambientState.getStackTranslation()) {
691             if (childrenOnTop != 0.0f) {
692                 childrenOnTop++;
693             } else {
694                 float overlap = ambientState.getTopPadding()
695                         + ambientState.getStackTranslation() - childViewState.yTranslation;
696                 childrenOnTop += Math.min(1.0f, overlap / childViewState.height);
697             }
698             childViewState.zTranslation = baseZ
699                     + childrenOnTop * zDistanceBetweenElements;
700         } else if (shouldElevateHun) {
701             // In case this is a new view that has never been measured before, we don't want to
702             // elevate if we are currently expanded more then the notification
703             int shelfHeight = ambientState.getShelf() == null ? 0 :
704                     ambientState.getShelf().getIntrinsicHeight();
705             float shelfStart = ambientState.getInnerHeight()
706                     - shelfHeight + ambientState.getTopPadding()
707                     + ambientState.getStackTranslation();
708             float notificationEnd = childViewState.yTranslation + child.getIntrinsicHeight()
709                     + mPaddingBetweenElements;
710             if (shelfStart > notificationEnd) {
711                 childViewState.zTranslation = baseZ;
712             } else {
713                 float factor = (notificationEnd - shelfStart) / shelfHeight;
714                 factor = Math.min(factor, 1.0f);
715                 childViewState.zTranslation = baseZ + factor * zDistanceBetweenElements;
716             }
717         } else {
718             childViewState.zTranslation = baseZ;
719         }
720 
721         // We need to scrim the notification more from its surrounding content when we are pinned,
722         // and we therefore elevate it higher.
723         // We can use the headerVisibleAmount for this, since the value nicely goes from 0 to 1 when
724         // expanding after which we have a normal elevation again.
725         childViewState.zTranslation += (1.0f - child.getHeaderVisibleAmount())
726                 * mPinnedZTranslationExtra;
727         return childrenOnTop;
728     }
729 
setIsExpanded(boolean isExpanded)730     public void setIsExpanded(boolean isExpanded) {
731         this.mIsExpanded = isExpanded;
732     }
733 
734     public static class StackScrollAlgorithmState {
735 
736         /**
737          * The scroll position of the algorithm (absolute scrolling).
738          */
739         public int scrollY;
740 
741         /**
742          * First view in shelf.
743          */
744         public ExpandableView firstViewInShelf;
745 
746         /**
747          * The children from the host view which are not gone.
748          */
749         public final ArrayList<ExpandableView> visibleChildren = new ArrayList<>();
750 
751         /**
752          * Y position of the current view during updating children
753          * with expansion factor applied.
754          */
755         private int mCurrentYPosition;
756 
757         /**
758          * Y position of the current view during updating children
759          * without applying the expansion factor.
760          */
761         private int mCurrentExpandedYPosition;
762     }
763 
764     /**
765      * Interface for telling the SSA when a new notification section begins (so it can add in
766      * appropriate margins).
767      */
768     public interface SectionProvider {
769         /**
770          * True if this view starts a new "section" of notifications, such as the gentle
771          * notifications section. False if sections are not enabled.
772          */
beginsSection(@onNull View view, @Nullable View previous)773         boolean beginsSection(@NonNull View view, @Nullable View previous);
774     }
775 
776     /**
777      * Interface for telling the StackScrollAlgorithm information about the bypass state
778      */
779     public interface BypassController {
780         /**
781          * True if bypass is enabled.  Note that this is always false if face auth is not enabled.
782          */
isBypassEnabled()783         boolean isBypassEnabled();
784     }
785 }
786