• 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.internal.annotations.VisibleForTesting;
28 import com.android.internal.policy.SystemBarUtils;
29 import com.android.keyguard.BouncerPanelExpansionCalculator;
30 import com.android.systemui.Flags;
31 import com.android.systemui.animation.ShadeInterpolation;
32 import com.android.systemui.res.R;
33 import com.android.systemui.scene.shared.flag.SceneContainerFlag;
34 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
35 import com.android.systemui.statusbar.NotificationShelf;
36 import com.android.systemui.statusbar.notification.SourceType;
37 import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView;
38 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
39 import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimator;
40 import com.android.systemui.statusbar.notification.headsup.NotificationsHunSharedAnimationValues;
41 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
42 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
43 import com.android.systemui.statusbar.notification.row.ExpandableView;
44 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
45 import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling;
46 
47 import java.util.ArrayList;
48 import java.util.List;
49 
50 /**
51  * The Algorithm of the
52  * {@link com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout} which can
53  * be queried for {@link StackScrollAlgorithmState}
54  */
55 public class StackScrollAlgorithm {
56 
57     public static final float START_FRACTION = 0.5f;
58 
59     private static final String TAG = "StackScrollAlgorithm";
60     private static final SourceType STACK_SCROLL_ALGO = SourceType.from("StackScrollAlgorithm");
61     private final ViewGroup mHostView;
62     @Nullable
63     private final HeadsUpAnimator mHeadsUpAnimator;
64 
65     private float mPaddingBetweenElements;
66     private float mGapHeight;
67     private float mGapHeightOnLockscreen;
68     private int mCollapsedSize;
69     private boolean mEnableNotificationClipping;
70 
71     private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
72     private boolean mIsExpanded;
73     private boolean mClipNotificationScrollToTop;
74     @VisibleForTesting
75     float mHeadsUpInset;
76     @VisibleForTesting
77     float mHeadsUpAppearStartAboveScreen;
78     private int mPinnedZTranslationExtra;
79     private float mNotificationScrimPadding;
80     private int mMarginBottom;
81     private float mQuickQsOffsetHeight;
82     private float mSmallCornerRadius;
83     private float mLargeCornerRadius;
84     private int mHeadsUpAppearHeightBottom;
85     private int mHeadsUpCyclingPadding;
86 
StackScrollAlgorithm( Context context, ViewGroup hostView, @Nullable HeadsUpAnimator headsUpAnimator)87     public StackScrollAlgorithm(
88             Context context,
89             ViewGroup hostView,
90             @Nullable HeadsUpAnimator headsUpAnimator) {
91         mHostView = hostView;
92         mHeadsUpAnimator = headsUpAnimator;
93         initView(context);
94     }
95 
initView(Context context)96     public void initView(Context context) {
97         updateResources(context);
98     }
99 
updateResources(Context context)100     private void updateResources(Context context) {
101         Resources res = context.getResources();
102         mPaddingBetweenElements = res.getDimensionPixelSize(
103                 R.dimen.notification_divider_height);
104         mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
105         mEnableNotificationClipping = res.getBoolean(R.bool.notification_enable_clipping);
106         mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop);
107         int statusBarHeight = SystemBarUtils.getStatusBarHeight(context);
108         mHeadsUpInset = statusBarHeight + res.getDimensionPixelSize(
109                 R.dimen.heads_up_status_bar_padding);
110         mHeadsUpAppearStartAboveScreen = res.getDimensionPixelSize(
111                 R.dimen.heads_up_appear_y_above_screen);
112         mHeadsUpCyclingPadding = context.getResources()
113                 .getDimensionPixelSize(R.dimen.heads_up_cycling_padding);
114         mPinnedZTranslationExtra = res.getDimensionPixelSize(
115                 R.dimen.heads_up_pinned_elevation);
116         mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height);
117         mGapHeightOnLockscreen = res.getDimensionPixelSize(
118                 R.dimen.notification_section_divider_height_lockscreen);
119         mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
120         mMarginBottom = res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom);
121         mQuickQsOffsetHeight = SystemBarUtils.getQuickQsOffsetHeight(context);
122         mSmallCornerRadius = res.getDimension(R.dimen.notification_corner_radius_small);
123         mLargeCornerRadius = res.getDimension(R.dimen.notification_corner_radius);
124         if (NotificationsHunSharedAnimationValues.isEnabled()) {
125             mHeadsUpAnimator.updateResources(context);
126         }
127     }
128 
129     /**
130      * Updates the state of all children in the hostview based on this algorithm.
131      */
resetViewStates(AmbientState ambientState, int speedBumpIndex)132     public void resetViewStates(AmbientState ambientState, int speedBumpIndex) {
133         // The state of the local variables are saved in an algorithmState to easily subdivide it
134         // into multiple phases.
135         StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
136 
137         // First we reset the view states to their default values.
138         resetChildViewStates();
139         initAlgorithmState(algorithmState, ambientState);
140         updatePositionsForState(algorithmState, ambientState);
141         updateZValuesForState(algorithmState, ambientState);
142         updateHeadsUpStates(algorithmState, ambientState);
143         updatePulsingStates(algorithmState, ambientState);
144 
145         updateDimmedAndHideSensitive(ambientState, algorithmState);
146         updateClipping(algorithmState, ambientState);
147         updateSpeedBumpState(algorithmState, speedBumpIndex);
148         updateShelfState(algorithmState, ambientState);
149         updateAlphaState(algorithmState, ambientState);
150         getNotificationChildrenStates(algorithmState);
151     }
152 
updateAlphaState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)153     private void updateAlphaState(StackScrollAlgorithmState algorithmState,
154             AmbientState ambientState) {
155         for (ExpandableView view : algorithmState.visibleChildren) {
156             final ViewState viewState = view.getViewState();
157             final boolean isHunGoingToShade = ambientState.isShadeExpanded()
158                     && view == ambientState.getTrackedHeadsUpRow();
159 
160             if (isHunGoingToShade) {
161                 // Keep 100% opacity for heads up notification going to shade.
162                 viewState.setAlpha(1f);
163             } else if (!SceneContainerFlag.isEnabled() && ambientState.isOnKeyguard()) {
164                 // Adjust alpha for wakeup to lockscreen.
165                 if (view.isHeadsUpState()) {
166                     // Pulsing HUN should be visible on AOD and stay visible during
167                     // AOD=>lockscreen transition
168                     viewState.setAlpha(1f - ambientState.getHideAmount());
169                 } else {
170                     // Normal notifications are hidden on AOD and should fade in during
171                     // AOD=>lockscreen transition
172                     viewState.setAlpha(1f - ambientState.getDozeAmount());
173                 }
174             } else if (SceneContainerFlag.isEnabled()
175                     && ambientState.isShowingStackOnLockscreen()) {
176                     // Adjust alpha for wakeup to lockscreen.
177                 if (view.isHeadsUpState()) {
178                     // Pulsing HUN should be visible on AOD and stay visible during
179                     // AOD=>lockscreen transition
180                     viewState.setAlpha(1f - ambientState.getHideAmount());
181                 } else {
182                     // Take into account scene container-specific Lockscreen fade-in progress
183                     float fadeAlpha = ambientState.getLockscreenStackFadeInProgress();
184                     float dozeAlpha = 1f - ambientState.getDozeAmount();
185                     viewState.setAlpha(Math.min(dozeAlpha, fadeAlpha));
186                 }
187             } else if (ambientState.isExpansionChanging()) {
188                 // Adjust alpha for shade open & close.
189                 float expansion = ambientState.getExpansionFraction();
190                 if (ambientState.isBouncerInTransit()) {
191                     viewState.setAlpha(
192                             BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(expansion));
193                 } else if (view instanceof FooterView) {
194                     viewState.setAlpha(interpolateFooterAlpha(ambientState));
195                 } else {
196                     viewState.setAlpha(interpolateNotificationContentAlpha(ambientState));
197                 }
198             }
199 
200             // On the final call to {@link #resetViewState}, the alpha is set back to 1f but
201             // ambientState.isExpansionChanging() is now false. This causes a flicker on the
202             // EmptyShadeView after the shade is collapsed. Make sure the empty shade view
203             // isn't visible unless the shade is expanded.
204             if (view instanceof EmptyShadeView && ambientState.getExpansionFraction() == 0f) {
205                 viewState.setAlpha(0f);
206             }
207 
208             // For EmptyShadeView if on keyguard, we need to control the alpha to create
209             // a nice transition when the user is dragging down the notification panel.
210             if (view instanceof EmptyShadeView && ambientState.isOnKeyguard()) {
211                 final float fractionToShade = ambientState.getFractionToShade();
212                 viewState.setAlpha(ShadeInterpolation.getContentAlpha(fractionToShade));
213             }
214 
215             NotificationShelf shelf = ambientState.getShelf();
216             if (shelf != null) {
217                 final ViewState shelfState = shelf.getViewState();
218 
219                 // After the shelf has updated its yTranslation, explicitly set alpha=0 for view
220                 // below shelf to skip rendering them in the hardware layer. We do not set them
221                 // invisible because that runs invalidate & onDraw when these views return onscreen,
222                 // which is more expensive.
223                 if (shelfState.hidden) {
224                     // When the shelf is hidden, it won't clip views, so we don't hide rows
225                     continue;
226                 }
227 
228                 final float shelfTop = shelfState.getYTranslation();
229                 final float viewTop = viewState.getYTranslation();
230                 if (viewTop >= shelfTop) {
231                     viewState.setAlpha(0);
232                 }
233             }
234         }
235     }
236 
interpolateFooterAlpha(AmbientState ambientState)237     private float interpolateFooterAlpha(AmbientState ambientState) {
238         float expansion = ambientState.getExpansionFraction();
239         if (ambientState.isSmallScreen()) {
240             return ShadeInterpolation.getContentAlpha(expansion);
241         }
242         LargeScreenShadeInterpolator interpolator = ambientState.getLargeScreenShadeInterpolator();
243         return interpolator.getNotificationFooterAlpha(expansion);
244     }
245 
interpolateNotificationContentAlpha(AmbientState ambientState)246     private float interpolateNotificationContentAlpha(AmbientState ambientState) {
247         float expansion = ambientState.getExpansionFraction();
248         if (ambientState.isSmallScreen()) {
249             return ShadeInterpolation.getContentAlpha(expansion);
250         }
251         LargeScreenShadeInterpolator interpolator = ambientState.getLargeScreenShadeInterpolator();
252         return interpolator.getNotificationContentAlpha(expansion);
253     }
254 
255     /**
256      * How expanded or collapsed notifications are when pulling down the shade.
257      *
258      * @param ambientState Current ambient state.
259      * @return 0 when fully collapsed, 1 when expanded.
260      */
getNotificationSquishinessFraction(AmbientState ambientState)261     public float getNotificationSquishinessFraction(AmbientState ambientState) {
262         return getExpansionFractionWithoutShelf(mTempAlgorithmState, ambientState);
263     }
264 
setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom)265     public void setHeadsUpAppearHeightBottom(int headsUpAppearHeightBottom) {
266         NotificationsHunSharedAnimationValues.assertInLegacyMode();
267         mHeadsUpAppearHeightBottom = headsUpAppearHeightBottom;
268     }
269 
270     /**
271      * If the QuickSettings is showing full screen, we want to animate the HeadsUp Notifications
272      * from the bottom of the screen.
273      *
274      * @param ambientState Current ambient state.
275      * @param viewState The state of the HUN that is being queried to appear from the bottom.
276      *
277      * @return true if the HeadsUp Notifications should appear from the bottom
278      */
shouldHunAppearFromBottom(AmbientState ambientState, ExpandableViewState viewState)279     public boolean shouldHunAppearFromBottom(AmbientState ambientState,
280             ExpandableViewState viewState) {
281         return viewState.getYTranslation() + viewState.height
282                 >= ambientState.getMaxHeadsUpTranslation();
283     }
284 
debugLog(String s)285     public static void debugLog(String s) {
286         android.util.Log.i(TAG, s);
287     }
288 
debugLogView(View view, String s)289     public static void debugLogView(View view, String s) {
290         String viewString = "";
291         if (view instanceof ExpandableNotificationRow row) {
292             viewString = row.getKey();
293         } else if (view == null) {
294             viewString = "View is null";
295         } else if (view instanceof SectionHeaderView) {
296             viewString = "SectionHeaderView";
297         } else if (view instanceof FooterView) {
298             viewString = "FooterView";
299         } else if (view instanceof MediaContainerView) {
300             viewString = "MediaContainerView";
301         } else if (view instanceof EmptyShadeView) {
302             viewString = "EmptyShadeView";
303         } else {
304             viewString = view.toString();
305         }
306         debugLog(viewString + " " + s);
307     }
308 
resetChildViewStates()309     private void resetChildViewStates() {
310         int numChildren = mHostView.getChildCount();
311         for (int i = 0; i < numChildren; i++) {
312             ExpandableView child = (ExpandableView) mHostView.getChildAt(i);
313             child.resetViewState();
314         }
315     }
316 
getNotificationChildrenStates(StackScrollAlgorithmState algorithmState)317     private void getNotificationChildrenStates(StackScrollAlgorithmState algorithmState) {
318         int childCount = algorithmState.visibleChildren.size();
319         for (int i = 0; i < childCount; i++) {
320             ExpandableView v = algorithmState.visibleChildren.get(i);
321             if (v instanceof ExpandableNotificationRow row) {
322                 row.updateChildrenStates();
323             }
324         }
325     }
326 
updateSpeedBumpState(StackScrollAlgorithmState algorithmState, int speedBumpIndex)327     private void updateSpeedBumpState(StackScrollAlgorithmState algorithmState,
328             int speedBumpIndex) {
329         int childCount = algorithmState.visibleChildren.size();
330         int belowSpeedBump = speedBumpIndex;
331         for (int i = 0; i < childCount; i++) {
332             ExpandableView child = algorithmState.visibleChildren.get(i);
333             ExpandableViewState childViewState = child.getViewState();
334 
335             // The speed bump can also be gone, so equality needs to be taken when comparing
336             // indices.
337             childViewState.belowSpeedBump = i >= belowSpeedBump;
338         }
339 
340     }
341 
updateShelfState( StackScrollAlgorithmState algorithmState, AmbientState ambientState)342     private void updateShelfState(
343             StackScrollAlgorithmState algorithmState,
344             AmbientState ambientState) {
345 
346         NotificationShelf shelf = ambientState.getShelf();
347         if (shelf == null) {
348             return;
349         }
350 
351         shelf.updateState(algorithmState, ambientState);
352     }
353 
updateClipping(StackScrollAlgorithmState algorithmState, AmbientState ambientState)354     private void updateClipping(StackScrollAlgorithmState algorithmState,
355             AmbientState ambientState) {
356         float stackTop = SceneContainerFlag.isEnabled() ? ambientState.getStackTop()
357                 : ambientState.getStackY() - ambientState.getScrollY();
358         float drawStart = ambientState.isOnKeyguard() ? 0
359                 : stackTop;
360         float clipStart = 0;
361         int childCount = algorithmState.visibleChildren.size();
362         boolean firstHeadsUp = true;
363         float firstHeadsUpEnd = 0;
364         for (int i = 0; i < childCount; i++) {
365             ExpandableView child = algorithmState.visibleChildren.get(i);
366             ExpandableViewState state = child.getViewState();
367             if (!child.mustStayOnScreen() || state.headsUpIsVisible) {
368                 clipStart = Math.max(drawStart, clipStart);
369             }
370             float newYTranslation = state.getYTranslation();
371             float newHeight = state.height;
372             float newNotificationEnd = newYTranslation + newHeight;
373             boolean isHeadsUp = (child instanceof ExpandableNotificationRow) && child.isPinned();
374             if (mClipNotificationScrollToTop
375                     && !firstHeadsUp
376                     && (isHeadsUp || child.isHeadsUpAnimatingAway())
377                     && newNotificationEnd > firstHeadsUpEnd
378                     && !ambientState.isShadeExpanded()
379                     && !skipClipBottomForCycling(child, ambientState)) {
380                 // The bottom of this view is peeking out from under the previous view.
381                 // Clip the part that is peeking out.
382                 float overlapAmount = newNotificationEnd - firstHeadsUpEnd;
383                 state.clipBottomAmount = mEnableNotificationClipping ? (int) overlapAmount : 0;
384             } else {
385                 state.clipBottomAmount = 0;
386             }
387             if (firstHeadsUp) {
388                 firstHeadsUpEnd = newNotificationEnd;
389             }
390             if (isHeadsUp) {
391                 firstHeadsUp = false;
392             }
393             if (!child.isTransparent()) {
394                 // Only update the previous values if we are not transparent,
395                 // otherwise we would clip to a transparent view.
396                 clipStart = Math.max(clipStart, isHeadsUp ? newYTranslation : newNotificationEnd);
397             }
398         }
399     }
400 
401     /**
402      * @return Should we skip clipping the bottom clipping when new hun has lower bottom line for
403      *         the hun cycling animation.
404      */
skipClipBottomForCycling(ExpandableView view, AmbientState ambientState)405     private boolean skipClipBottomForCycling(ExpandableView view, AmbientState ambientState) {
406         if (!NotificationHeadsUpCycling.isEnabled()) return false;
407         if (!isCyclingOut(view, ambientState)) return false;
408         // skip bottom clipping if we animate the bottom line
409         return NotificationHeadsUpCycling.getAnimateTallToShort();
410     }
411 
412     /**
413      * Whether the view is the hun that is cycling out by the notification avalanche.
414      */
isCyclingOut(ExpandableView view, AmbientState ambientState)415     public boolean isCyclingOut(ExpandableView view, AmbientState ambientState) {
416         if (!NotificationHeadsUpCycling.isEnabled()) return false;
417         if (!(view instanceof ExpandableNotificationRow)) return false;
418         return isCyclingOut((ExpandableNotificationRow) view, ambientState);
419     }
420 
421     /**
422      * Whether the row is the hun that is cycling out by the notification avalanche.
423      */
isCyclingOut(ExpandableNotificationRow row, AmbientState ambientState)424     public boolean isCyclingOut(ExpandableNotificationRow row, AmbientState ambientState) {
425         if (!NotificationHeadsUpCycling.isEnabled()) return false;
426         String cyclingOutKey = ambientState.getAvalanchePreviousHunKey();
427         return row.getKey().equals(cyclingOutKey);
428     }
429 
430     /**
431      * Whether the row is the hun that is cycling in by the notification avalanche.
432      */
isCyclingIn(ExpandableNotificationRow row, AmbientState ambientState)433     public boolean isCyclingIn(ExpandableNotificationRow row, AmbientState ambientState) {
434         if (!NotificationHeadsUpCycling.isEnabled()) return false;
435         String cyclingInKey = ambientState.getAvalancheShowingHunKey();
436         return row.getKey().equals(cyclingInKey);
437     }
438 
439     /** Updates the dimmed and hiding sensitive states of the children. */
updateDimmedAndHideSensitive(AmbientState ambientState, StackScrollAlgorithmState algorithmState)440     private void updateDimmedAndHideSensitive(AmbientState ambientState,
441             StackScrollAlgorithmState algorithmState) {
442         boolean hideSensitive = ambientState.isHideSensitive();
443         int childCount = algorithmState.visibleChildren.size();
444         for (int i = 0; i < childCount; i++) {
445             ExpandableView child = algorithmState.visibleChildren.get(i);
446             ExpandableViewState childViewState = child.getViewState();
447             childViewState.hideSensitive = hideSensitive;
448         }
449     }
450 
451     /**
452      * Initialize the algorithm state like updating the visible children.
453      */
initAlgorithmState(StackScrollAlgorithmState state, AmbientState ambientState)454     private void initAlgorithmState(StackScrollAlgorithmState state, AmbientState ambientState) {
455         state.scrollY = ambientState.getScrollY();
456         state.mCurrentYPosition = -state.scrollY;
457         state.mCurrentExpandedYPosition = -state.scrollY;
458 
459         //now init the visible children and update paddings
460         int childCount = mHostView.getChildCount();
461         state.visibleChildren.clear();
462         state.visibleChildren.ensureCapacity(childCount);
463         int notGoneIndex = 0;
464         boolean emptyShadeVisible = false;
465         for (int i = 0; i < childCount; i++) {
466             ExpandableView v = (ExpandableView) mHostView.getChildAt(i);
467             if (v.getVisibility() != View.GONE) {
468                 if (v == ambientState.getShelf()) {
469                     continue;
470                 }
471                 if (v instanceof EmptyShadeView) {
472                     emptyShadeVisible = true;
473                 }
474                 if (v instanceof FooterView footerView) {
475                     if (emptyShadeVisible || notGoneIndex == 0) {
476                         // if the empty shade is visible or the footer is the first visible
477                         // view, we're in a transitory state so let's leave the footer alone.
478                         if (Flags.notificationsFooterVisibilityFix()
479                                 && !SceneContainerFlag.isEnabled()) {
480                             // ...except for the hidden state, to prevent it from flashing on
481                             // the screen (this piece is copied from updateChild, and is not
482                             // necessary in flexiglass).
483                             if (footerView.shouldBeHidden() || !ambientState.isShadeExpanded()) {
484                                 footerView.getViewState().hidden = true;
485                             }
486                         }
487                         continue;
488                     }
489                 }
490 
491                 notGoneIndex = updateNotGoneIndex(state, notGoneIndex, v);
492                 if (v instanceof ExpandableNotificationRow row) {
493 
494                     // handle the notGoneIndex for the children as well
495                     List<ExpandableNotificationRow> children = row.getAttachedChildren();
496                     if (row.isSummaryWithChildren() && children != null) {
497                         for (ExpandableNotificationRow childRow : children) {
498                             if (childRow.getVisibility() != View.GONE) {
499                                 ExpandableViewState childState = childRow.getViewState();
500                                 childState.notGoneIndex = notGoneIndex;
501                                 notGoneIndex++;
502                             }
503                         }
504                     }
505                 }
506             }
507         }
508 
509         // Save the index of first view in shelf from when shade is fully
510         // expanded. Consider updating these states in updateContentView instead so that we don't
511         // have to recalculate in every frame.
512         float currentY = -ambientState.getScrollY();
513         // add top padding at the start as long as we're not on the lock screen
514         currentY += getScrimTopPaddingOrZero(ambientState);
515         state.firstViewInShelf = null;
516         for (int i = 0; i < state.visibleChildren.size(); i++) {
517             final ExpandableView view = state.visibleChildren.get(i);
518 
519             final boolean applyGapHeight = childNeedsGapHeight(
520                     ambientState.getSectionProvider(), i,
521                     view, getPreviousView(i, state));
522             if (applyGapHeight) {
523                 currentY += getGapForLocation(
524                         ambientState.getFractionToShade(), ambientState.isOnKeyguard());
525             }
526 
527             if (ambientState.getShelf() != null) {
528                 final float shelfStart = ambientState.getStackEndHeight()
529                         - ambientState.getShelf().getIntrinsicHeight()
530                         - mPaddingBetweenElements;
531                 if (currentY >= shelfStart
532                         && !(view instanceof FooterView)
533                         && state.firstViewInShelf == null) {
534                     state.firstViewInShelf = view;
535                 }
536             }
537             currentY = currentY
538                     + getMaxAllowedChildHeight(view)
539                     + mPaddingBetweenElements;
540         }
541     }
542 
updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, ExpandableView v)543     private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex,
544             ExpandableView v) {
545         ExpandableViewState viewState = v.getViewState();
546         viewState.notGoneIndex = notGoneIndex;
547         state.visibleChildren.add(v);
548         notGoneIndex++;
549         return notGoneIndex;
550     }
551 
getPreviousView(int i, StackScrollAlgorithmState algorithmState)552     private ExpandableView getPreviousView(int i, StackScrollAlgorithmState algorithmState) {
553         return i > 0 ? algorithmState.visibleChildren.get(i - 1) : null;
554     }
555 
556     /**
557      * Update the position of QS Frame.
558      */
updateQSFrameTop(int qsHeight)559     public void updateQSFrameTop(int qsHeight) {
560         // Intentionally empty for sub-classes in other device form factors to override
561     }
562 
563     /**
564      * Determine the positions for the views. This is the main part of the algorithm.
565      *
566      * @param algorithmState The state in which the current pass of the algorithm is currently in
567      * @param ambientState   The current ambient state
568      */
updatePositionsForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)569     protected void updatePositionsForState(StackScrollAlgorithmState algorithmState,
570             AmbientState ambientState) {
571         float scrimTopPadding = getScrimTopPaddingOrZero(ambientState);
572         algorithmState.mCurrentYPosition += scrimTopPadding;
573         algorithmState.mCurrentExpandedYPosition += scrimTopPadding;
574 
575         int childCount = algorithmState.visibleChildren.size();
576         for (int i = 0; i < childCount; i++) {
577             updateChild(i, algorithmState, ambientState);
578         }
579     }
580 
setLocation(ExpandableViewState expandableViewState, float currentYPosition, int i)581     private void setLocation(ExpandableViewState expandableViewState, float currentYPosition,
582             int i) {
583         expandableViewState.location = ExpandableViewState.LOCATION_MAIN_AREA;
584         if (currentYPosition <= 0) {
585             expandableViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
586         }
587     }
588 
589     /**
590      * @return Fraction to apply to view height and gap between views.
591      * Does not include shelf height even if shelf is showing.
592      */
getExpansionFractionWithoutShelf( StackScrollAlgorithmState algorithmState, AmbientState ambientState)593     protected float getExpansionFractionWithoutShelf(
594             StackScrollAlgorithmState algorithmState,
595             AmbientState ambientState) {
596 
597         final boolean showingShelf = ambientState.getShelf() != null
598                 && algorithmState.firstViewInShelf != null;
599 
600         final float shelfHeight = showingShelf ? ambientState.getShelf().getIntrinsicHeight() : 0f;
601         final float scrimPadding = getScrimTopPaddingOrZero(ambientState);
602 
603         final float stackHeight =
604                 ambientState.getInterpolatedStackHeight() - shelfHeight - scrimPadding;
605         final float stackEndHeight = ambientState.getStackEndHeight() - shelfHeight - scrimPadding;
606         if (stackEndHeight == 0f) {
607             // This should not happen, since even when the shade is empty we show EmptyShadeView
608             // but check just in case, so we don't return infinity or NaN.
609             return 0f;
610         }
611         return stackHeight / stackEndHeight;
612     }
613 
614     /**
615      * Returns the top scrim padding, or zero if the SceneContainer flag is enabled.
616      */
getScrimTopPaddingOrZero(AmbientState ambientState)617     private float getScrimTopPaddingOrZero(AmbientState ambientState) {
618         if (SceneContainerFlag.isEnabled()) {
619             // the scrim padding is set on the notification placeholder
620             return 0f;
621         }
622 
623         boolean shouldUsePadding =
624                 !ambientState.isOnKeyguard()
625                         || (ambientState.isBypassEnabled() && ambientState.isPulseExpanding());
626         return shouldUsePadding ? mNotificationScrimPadding : 0f;
627     }
628 
hasNonClearableNotifs(StackScrollAlgorithmState algorithmState)629     private boolean hasNonClearableNotifs(StackScrollAlgorithmState algorithmState) {
630         for (int i = 0; i < algorithmState.visibleChildren.size(); i++) {
631             View child = algorithmState.visibleChildren.get(i);
632             if (!(child instanceof ExpandableNotificationRow row)) {
633                 continue;
634             }
635             if (!row.canViewBeCleared()) {
636                 return true;
637             }
638         }
639         return false;
640     }
641 
642     @VisibleForTesting
maybeUpdateHeadsUpIsVisible( ExpandableViewState viewState, boolean isShadeExpanded, boolean mustStayOnScreen, boolean topVisible, float viewEnd, float hunMax)643     void maybeUpdateHeadsUpIsVisible(
644             ExpandableViewState viewState,
645             boolean isShadeExpanded,
646             boolean mustStayOnScreen,
647             boolean topVisible,
648             float viewEnd,
649             float hunMax) {
650         if (isShadeExpanded && mustStayOnScreen && topVisible) {
651             viewState.headsUpIsVisible = viewEnd < hunMax;
652         }
653     }
654 
655     // TODO(b/172289889) polish shade open from HUN
656 
657     /**
658      * Populates the {@link ExpandableViewState} for a single child.
659      *
660      * @param i              The index of the child in
661      *                       {@link StackScrollAlgorithmState#visibleChildren}.
662      * @param algorithmState The overall output state of the algorithm.
663      * @param ambientState   The input state provided to the algorithm.
664      */
665     protected void updateChild(
666             int i,
667             StackScrollAlgorithmState algorithmState,
668             AmbientState ambientState) {
669 
670         ExpandableView view = algorithmState.visibleChildren.get(i);
671         ExpandableViewState viewState = view.getViewState();
672         viewState.location = ExpandableViewState.LOCATION_UNKNOWN;
673 
674         float expansionFraction = getExpansionFractionWithoutShelf(
675                 algorithmState, ambientState);
676 
677         // Add gap between sections.
678         final boolean applyGapHeight =
679                 childNeedsGapHeight(
680                         ambientState.getSectionProvider(), i,
681                         view, getPreviousView(i, algorithmState));
682         if (applyGapHeight) {
683             final float gap = getGapForLocation(
684                     ambientState.getFractionToShade(), ambientState.isOnKeyguard());
685             algorithmState.mCurrentYPosition += expansionFraction * gap;
686             algorithmState.mCurrentExpandedYPosition += gap;
687         }
688 
689         // Must set viewState.yTranslation _before_ use.
690         // Incoming views have yTranslation=0 by default.
691         viewState.setYTranslation(algorithmState.mCurrentYPosition);
692 
693         float stackTop = SceneContainerFlag.isEnabled()
694                 ? ambientState.getStackTop()
695                 : ambientState.getStackY();
696         float viewEnd = stackTop + viewState.getYTranslation() + viewState.height;
697         maybeUpdateHeadsUpIsVisible(viewState, ambientState.isShadeExpanded(),
698                 view.mustStayOnScreen(),
699                 // TODO(b/332574413) use the position from the HeadsUpNotificationPlaceholder
700                 /* topVisible= */ viewState.getYTranslation() >= mNotificationScrimPadding,
701                 viewEnd, /* hunMax */ ambientState.getMaxHeadsUpTranslation()
702         );
703         if (view instanceof FooterView) {
704             if (SceneContainerFlag.isEnabled()) {
705                 final float footerEnd =
706                         stackTop + viewState.getYTranslation() + view.getIntrinsicHeight();
707                 final boolean noSpaceForFooter = footerEnd > ambientState.getStackCutoff();
708                 ((FooterView.FooterViewState) viewState).hideContent =
709                         noSpaceForFooter || (ambientState.isClearAllInProgress()
710                                 && !hasNonClearableNotifs(algorithmState));
711             } else {
712                 // TODO(b/333445519): shouldBeHidden should reflect whether the shade is closed
713                 //  already, so we shouldn't need to use ambientState here. However,
714                 //  currently it doesn't get updated quickly enough and can cause the footer to
715                 //  flash when closing the shade. As such, we temporarily also check the
716                 //  ambientState directly.
717                 if (((FooterView) view).shouldBeHidden() || !ambientState.isShadeExpanded()) {
718                     viewState.hidden = true;
719                 } else {
720                     final float footerEnd = algorithmState.mCurrentExpandedYPosition
721                             + view.getIntrinsicHeight();
722                     final boolean noSpaceForFooter =
723                             footerEnd > ambientState.getStackEndHeight();
724                     ((FooterView.FooterViewState) viewState).hideContent =
725                             noSpaceForFooter || (ambientState.isClearAllInProgress()
726                                     && !hasNonClearableNotifs(algorithmState));
727                 }
728             }
729         } else {
730             if (view instanceof EmptyShadeView) {
731                 float fullHeight = SceneContainerFlag.isEnabled()
732                         ? ambientState.getStackCutoff() - ambientState.getStackTop()
733                         : ambientState.getLayoutMaxHeight() + mMarginBottom
734                         - ambientState.getStackY();
735                 viewState.setYTranslation((fullHeight - getMaxAllowedChildHeight(view)) / 2f);
736             } else if (view != ambientState.getTrackedHeadsUpRow()) {
737                 if (ambientState.isExpansionChanging()) {
738                     // We later update shelf state, then hide views below the shelf.
739                     viewState.hidden = false;
740                     viewState.inShelf = algorithmState.firstViewInShelf != null
741                             && i >= algorithmState.visibleChildren.indexOf(
742                             algorithmState.firstViewInShelf);
743                 } else if (ambientState.getShelf() != null) {
744                     // When pulsing (incoming notification on AOD), innerHeight is 0; clamp all
745                     // to shelf start, thereby hiding all notifications (except the first one, which
746                     // we later unhide in updatePulsingState)
747                     // TODO(b/192348384): merge InnerHeight with StackHeight
748                     // Note: Bypass pulse looks different, but when it is not expanding, we need
749                     //  to use the innerHeight which doesn't update continuously, otherwise we show
750                     //  more notifications than we should during this special transitional states.
751                     boolean bypassPulseNotExpanding = ambientState.isBypassEnabled()
752                             && ambientState.isOnKeyguard() && !ambientState.isPulseExpanding();
753                     final float stackBottom = !ambientState.isShadeExpanded()
754                             || ambientState.getDozeAmount() == 1f
755                             || bypassPulseNotExpanding
756                             ? ambientState.getInnerHeight()
757                             : ambientState.getInterpolatedStackHeight();
758                     final float shelfStart = stackBottom
759                             - ambientState.getShelf().getIntrinsicHeight()
760                             - mPaddingBetweenElements;
761                     updateViewWithShelf(view, viewState, shelfStart);
762                 }
763             }
764             viewState.height = getMaxAllowedChildHeight(view);
765             if (!view.isPinned() && !view.isHeadsUpAnimatingAway()
766                     && !ambientState.isPulsingRow(view)) {
767                 // The expansion fraction should not affect HUNs or pulsing notifications.
768                 viewState.height *= expansionFraction;
769             }
770         }
771 
772         algorithmState.mCurrentYPosition +=
773                 expansionFraction * (getMaxAllowedChildHeight(view) + mPaddingBetweenElements);
774         algorithmState.mCurrentExpandedYPosition += view.getIntrinsicHeight()
775                 + mPaddingBetweenElements;
776 
777         setLocation(view.getViewState(), algorithmState.mCurrentYPosition, i);
778         viewState.setYTranslation(viewState.getYTranslation() + stackTop);
779     }
780 
781     @VisibleForTesting
updateViewWithShelf(ExpandableView view, ExpandableViewState viewState, float shelfStart)782     void updateViewWithShelf(ExpandableView view, ExpandableViewState viewState, float shelfStart) {
783         viewState.setYTranslation(Math.min(viewState.getYTranslation(), shelfStart));
784         if (viewState.getYTranslation() >= shelfStart) {
785             viewState.hidden = !view.isExpandAnimationRunning()
786                     && !view.hasExpandingChild();
787             viewState.inShelf = true;
788             // Notifications in the shelf cannot be visible HUNs.
789             viewState.headsUpIsVisible = false;
790         }
791     }
792 
793     /**
794      * Get the gap height needed for before a view
795      *
796      * @param sectionProvider the sectionProvider used to understand the sections
797      * @param visibleIndex    the visible index of this view in the list
798      * @param child           the child asked about
799      * @param previousChild   the child right before it or null if none
800      * @return the size of the gap needed or 0 if none is needed
801      */
getGapHeightForChild( SectionProvider sectionProvider, int visibleIndex, View child, View previousChild, float fractionToShade, boolean onKeyguard)802     public float getGapHeightForChild(
803             SectionProvider sectionProvider,
804             int visibleIndex,
805             View child,
806             View previousChild,
807             float fractionToShade,
808             boolean onKeyguard) {
809 
810         if (childNeedsGapHeight(sectionProvider, visibleIndex, child,
811                 previousChild)) {
812             return getGapForLocation(fractionToShade, onKeyguard);
813         } else {
814             return 0;
815         }
816     }
817 
818     @VisibleForTesting
getGapForLocation(float fractionToShade, boolean onKeyguard)819     float getGapForLocation(float fractionToShade, boolean onKeyguard) {
820         if (fractionToShade > 0f) {
821             return MathUtils.lerp(mGapHeightOnLockscreen, mGapHeight, fractionToShade);
822         }
823         if (onKeyguard) {
824             return mGapHeightOnLockscreen;
825         }
826         return mGapHeight;
827     }
828 
829     /**
830      * Does a given child need a gap, i.e spacing before a view?
831      *
832      * @param sectionProvider the sectionProvider used to understand the sections
833      * @param visibleIndex    the visible index of this view in the list
834      * @param child           the child asked about
835      * @param previousChild   the child right before it or null if none
836      * @return if the child needs a gap height
837      */
childNeedsGapHeight( SectionProvider sectionProvider, int visibleIndex, View child, View previousChild)838     private boolean childNeedsGapHeight(
839             SectionProvider sectionProvider,
840             int visibleIndex,
841             View child,
842             View previousChild) {
843         return sectionProvider.beginsSection(child, previousChild)
844                 && visibleIndex > 0
845                 && !(previousChild instanceof SectionHeaderView)
846                 && !(child instanceof FooterView);
847     }
848 
849     @VisibleForTesting
updatePulsingStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)850     void updatePulsingStates(StackScrollAlgorithmState algorithmState,
851             AmbientState ambientState) {
852         int childCount = algorithmState.visibleChildren.size();
853         ExpandableNotificationRow pulsingRow = null;
854         for (int i = 0; i < childCount; i++) {
855             View child = algorithmState.visibleChildren.get(i);
856             if (!(child instanceof ExpandableNotificationRow row)) {
857                 continue;
858             }
859             if (!row.showingPulsing() || (i == 0 && ambientState.isPulseExpanding())) {
860                 continue;
861             }
862             ExpandableViewState viewState = row.getViewState();
863             viewState.hidden = false;
864             pulsingRow = row;
865         }
866 
867         // Set AmbientState#pulsingRow to the current pulsing row when on AOD.
868         // Set AmbientState#pulsingRow=null when on lockscreen, since AmbientState#pulsingRow
869         // is only used for skipping the unfurl animation for (the notification that was already
870         // showing at full height on AOD) during the AOD=>lockscreen transition, where
871         // dozeAmount=[1f, 0f). We also need to reset the pulsingRow once it is no longer used
872         // because it will interfere with future unfurling animations - for example, during the
873         // LS=>AOD animation, the pulsingRow may stay at full height when it should squish with the
874         // rest of the stack.
875         if (ambientState.getDozeAmount() == 0.0f || ambientState.getDozeAmount() == 1.0f) {
876             ambientState.setPulsingRow(pulsingRow);
877         }
878     }
879 
updateHeadsUpStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)880     private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState,
881             AmbientState ambientState) {
882         int childCount = algorithmState.visibleChildren.size();
883 
884         // Move the tracked heads up into position during the appear animation, by interpolating
885         // between the HUN inset (where it will appear as a HUN) and the end position in the shade
886         float headsUpTranslation =
887                 SceneContainerFlag.isEnabled()
888                         ? ambientState.getHeadsUpTop()
889                         : mHeadsUpInset - ambientState.getStackTopMargin();
890         ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow();
891         if (trackedHeadsUpRow != null) {
892             ExpandableViewState childState = trackedHeadsUpRow.getViewState();
893             if (childState != null) {
894                 float endPos = childState.getYTranslation() - ambientState.getStackTranslation();
895                 childState.setYTranslation(MathUtils.lerp(
896                         headsUpTranslation, endPos, ambientState.getAppearFraction()));
897             }
898         }
899 
900         ExpandableNotificationRow topHeadsUpEntry = null;
901         int cyclingInHunHeight = -1;
902         for (int i = 0; i < childCount; i++) {
903             View child = algorithmState.visibleChildren.get(i);
904             if (!(child instanceof ExpandableNotificationRow row)) {
905                 continue;
906             }
907             if (!(row.isHeadsUp() || row.isHeadsUpAnimatingAway())) {
908                 continue;
909             }
910             ExpandableViewState childState = row.getViewState();
911             boolean shouldSetTopHeadsUpEntry;
912             if (SceneContainerFlag.isEnabled()) {
913                 shouldSetTopHeadsUpEntry = row.isHeadsUp();
914             } else {
915                 shouldSetTopHeadsUpEntry = row.mustStayOnScreen();
916             }
917             if (topHeadsUpEntry == null && shouldSetTopHeadsUpEntry
918                     && !childState.headsUpIsVisible) {
919                 topHeadsUpEntry = row;
920                 childState.location = ExpandableViewState.LOCATION_FIRST_HUN;
921             }
922             boolean isTopEntry = topHeadsUpEntry == row;
923             float unmodifiedEndLocation = childState.getYTranslation() + childState.height;
924             if (mIsExpanded) {
925                 if (SceneContainerFlag.isEnabled()) {
926                     if (shouldHunBeVisibleWhenScrolled(row.isHeadsUp(),
927                             childState.headsUpIsVisible, row.showingPulsing(),
928                             ambientState.isOnKeyguard(), NotificationBundleUi.isEnabled()
929                                     ? row.getEntryAdapter().canPeek()
930                                     : row.getEntryLegacy().isStickyAndNotDemoted())) {
931                         // the height of this child before clamping it to the top
932                         float unmodifiedChildHeight = childState.height;
933                         clampHunToTop(
934                                 /* headsUpTop = */ headsUpTranslation,
935                                 /* collapsedHeight = */ row.getCollapsedHeight(),
936                                 /* viewState = */ childState
937                         );
938                         float baseZ = ambientState.getBaseZHeight();
939                         if (headsUpTranslation > ambientState.getStackTop()
940                                 && row.isAboveShelf()) {
941                             // HUN displayed outside of the stack during transition from Gone/LS;
942                             // add a shadow that corresponds to the transition progress.
943                             float fraction = 1 - ambientState.getExpansionFraction();
944                             childState.setZTranslation(baseZ + fraction * mPinnedZTranslationExtra);
945                         } else if (headsUpTranslation < ambientState.getStackTop()
946                                 && row.isAboveShelf()) {
947                             // HUN displayed outside of the stack during transition from QS;
948                             // add a shadow that corresponds to the transition progress.
949                             float fraction = ambientState.getQsExpansionFraction();
950                             childState.setZTranslation(baseZ + fraction * mPinnedZTranslationExtra);
951                         } else if (headsUpTranslation > ambientState.getStackTop()) {
952                             // HUN displayed within the stack, add a shadow if it overlaps with
953                             // other elements.
954                             //
955                             // Views stack vertically from the top. Add the HUN's original height
956                             // (before clamping) to the stack top, to determine the starting
957                             // point for the remaining content.
958                             float scrollingContentTop =
959                                     ambientState.getStackTop() + unmodifiedChildHeight;
960                             updateZTranslationForHunInStack(
961                                     /* scrollingContentTop = */ scrollingContentTop,
962                                     /* scrollingContentTopPadding = */ mGapHeight,
963                                     /* baseZ = */ baseZ,
964                                     /* viewState = */ childState
965                             );
966                         } else {
967                             childState.setZTranslation(baseZ);
968                         }
969                         if (isTopEntry && row.isAboveShelf()) {
970                             float headsUpBottom = NotificationsHunSharedAnimationValues.isEnabled()
971                                     ? mHeadsUpAnimator.getHeadsUpAppearHeightBottom()
972                                     : ambientState.getHeadsUpBottom();
973                             clampHunToMaxTranslation(
974                                     /* headsUpTop =  */ headsUpTranslation,
975                                     /* headsUpBottom =  */ headsUpBottom,
976                                     /* viewState = */ childState
977                             );
978                             updateCornerRoundnessForPinnedHun(row, ambientState.getStackTop());
979                             childState.hidden = false;
980                         }
981                     }
982                 } else {
983                     if (shouldHunBeVisibleWhenScrolled(row.mustStayOnScreen(),
984                             childState.headsUpIsVisible, row.showingPulsing(),
985                             ambientState.isOnKeyguard(), NotificationBundleUi.isEnabled()
986                                     ? row.getEntryAdapter().canPeek()
987                                     : row.getEntryLegacy().isStickyAndNotDemoted())) {
988                         // Ensure that the heads up is always visible even when scrolled off.
989                         // NSSL y starts at top of screen in non-split-shade, but below the qs
990                         // offset
991                         // in split shade, so we only need to inset by the scrim padding in split
992                         // shade.
993                         final float clampInset = ambientState.getUseSplitShade()
994                                 ? mNotificationScrimPadding : mQuickQsOffsetHeight;
995                         clampHunToTop(clampInset, ambientState.getStackTranslation(),
996                                 row.getCollapsedHeight(), childState);
997                         if (isTopEntry && row.isAboveShelf()) {
998                             // the first hun can't get off screen.
999                             clampHunToMaxTranslation(ambientState, row, childState);
1000                             updateCornerRoundnessForPinnedHun(row, ambientState.getStackY());
1001                             childState.hidden = false;
1002                         }
1003                     }
1004                 }
1005             }
1006             if (row.isPinned()) {
1007                 // Make sure row yTranslation is at at least the HUN yTranslation,
1008                 // which accounts for AmbientState.stackTopMargin in split-shade.
1009                 // Once we start opening the shade, we keep the previously calculated translation.
1010                 childState.setYTranslation(
1011                         Math.max(childState.getYTranslation(), headsUpTranslation));
1012                 childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
1013                 if (NotificationHeadsUpCycling.isEnabled()) {
1014                     if (isCyclingIn(row, ambientState)) {
1015                         if (cyclingInHunHeight == -1) {
1016                             cyclingInHunHeight = childState.height;
1017                         }
1018                     }
1019                 }
1020                 childState.hidden = false;
1021                 ExpandableViewState topState =
1022                         topHeadsUpEntry == null ? null : topHeadsUpEntry.getViewState();
1023                 if (topState != null && !isTopEntry && (!mIsExpanded
1024                         || unmodifiedEndLocation > topState.getYTranslation() + topState.height)) {
1025                     // Ensure that a headsUp doesn't vertically extend further than the heads-up at
1026                     // the top most z-position
1027                     childState.height = row.getIntrinsicHeight();
1028                 }
1029 
1030                 // heads up notification show and this row is the top entry of heads up
1031                 // notifications. i.e. this row should be the only one row that has input field
1032                 // To check if the row need to do translation according to scroll Y
1033                 // heads up show full of row's content and any scroll y indicate that the
1034                 // translationY need to move up the HUN.
1035                 if (!mIsExpanded && isTopEntry && ambientState.getScrollY() > 0) {
1036                     childState.setYTranslation(
1037                             childState.getYTranslation() - ambientState.getScrollY());
1038                 }
1039             }
1040             if (row.isHeadsUpAnimatingAway()) {
1041                 if (NotificationHeadsUpCycling.isEnabled() && isCyclingOut(row, ambientState)) {
1042                     // If the two HUNs in the cycling animation have different heights, we need
1043                     // an extra y translation to align the animation.
1044                     int extraTranslation;
1045                     if (NotificationHeadsUpCycling.getAnimateTallToShort()) {
1046                         if (cyclingInHunHeight > 0) {
1047                             extraTranslation = cyclingInHunHeight - childState.height;
1048                         } else {
1049                             extraTranslation = 0;
1050                         }
1051                     } else {
1052                         extraTranslation = cyclingInHunHeight >= childState.height
1053                                 ? cyclingInHunHeight - childState.height : 0;
1054                     }
1055                     extraTranslation += mHeadsUpCyclingPadding;
1056                     float inSpaceTranslation = Math.max(childState.getYTranslation(),
1057                             headsUpTranslation);
1058                     childState.setYTranslation(inSpaceTranslation + extraTranslation);
1059                     cyclingInHunHeight = -1;
1060                 } else if (!ambientState.isDozing()) {
1061                     boolean shouldHunAppearFromBottom =
1062                             shouldHunAppearFromBottom(ambientState, childState);
1063                     if (NotificationsHunSharedAnimationValues.isEnabled()) {
1064                         int yTranslation =
1065                                 mHeadsUpAnimator.getHeadsUpYTranslation(
1066                                         shouldHunAppearFromBottom,
1067                                         row.hasStatusBarChipDuringHeadsUpAnimation());
1068                         childState.setYTranslation(yTranslation);
1069                     } else {
1070                         if (shouldHunAppearFromBottom) {
1071                             // move to the bottom of the screen
1072                             childState.setYTranslation(
1073                                     mHeadsUpAppearHeightBottom + mHeadsUpAppearStartAboveScreen);
1074                         } else {
1075                             // move to the top of the screen
1076                             childState.setYTranslation(-ambientState.getStackTopMargin()
1077                                     - mHeadsUpAppearStartAboveScreen);
1078                         }
1079                     }
1080                 } else {
1081                     // Make sure row yTranslation is at maximum the HUN yTranslation,
1082                     // which accounts for AmbientState.stackTopMargin in split-shade.
1083                     childState.setYTranslation(
1084                             Math.max(childState.getYTranslation(), headsUpTranslation));
1085                 }
1086                 // keep it visible for the animation
1087                 childState.hidden = false;
1088             }
1089         }
1090     }
1091 
1092     @VisibleForTesting
shouldHunBeVisibleWhenScrolled(boolean mustStayOnScreen, boolean headsUpIsVisible, boolean showingPulsing, boolean isOnKeyguard, boolean headsUpOnKeyguard)1093     boolean shouldHunBeVisibleWhenScrolled(boolean mustStayOnScreen, boolean headsUpIsVisible,
1094             boolean showingPulsing, boolean isOnKeyguard, boolean headsUpOnKeyguard) {
1095         return mustStayOnScreen && !headsUpIsVisible
1096                 && !showingPulsing
1097                 && (!isOnKeyguard || headsUpOnKeyguard);
1098     }
1099 
1100     /**
1101      * When shade is open and we are scrolled to the bottom of notifications,
1102      * clamp incoming HUN in its collapsed form, right below qs offset.
1103      * Transition pinned collapsed HUN to full height when scrolling back up.
1104      */
1105     @VisibleForTesting
clampHunToTop(float clampInset, float stackTranslation, float collapsedHeight, ExpandableViewState viewState)1106     void clampHunToTop(float clampInset, float stackTranslation, float collapsedHeight,
1107             ExpandableViewState viewState) {
1108         SceneContainerFlag.assertInLegacyMode();
1109         clampHunToTop(clampInset + stackTranslation, collapsedHeight, viewState);
1110     }
1111 
1112     @VisibleForTesting
clampHunToTop(float headsUpTop, float collapsedHeight, ExpandableViewState viewState)1113     void clampHunToTop(float headsUpTop, float collapsedHeight, ExpandableViewState viewState) {
1114         final float newTranslation = Math.max(headsUpTop, viewState.getYTranslation());
1115 
1116         // Transition from collapsed pinned state to fully expanded state
1117         // when the pinned HUN approaches its actual location (when scrolling back to top).
1118         final float distToRealY = newTranslation - viewState.getYTranslation();
1119         final float availableHeight = viewState.height - distToRealY;
1120 
1121         viewState.setYTranslation(newTranslation);
1122         viewState.height = (int) Math.max(availableHeight, collapsedHeight);
1123     }
1124 
1125     @VisibleForTesting
updateZTranslationForHunInStack(float scrollingContentTop, float scrollingContentTopPadding, float baseZ, ExpandableViewState viewState)1126     void updateZTranslationForHunInStack(float scrollingContentTop,
1127             float scrollingContentTopPadding, float baseZ, ExpandableViewState viewState) {
1128         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1129         float hunBottom = viewState.getYTranslation() + viewState.height;
1130         float overlap = Math.max(0f, hunBottom - scrollingContentTop);
1131 
1132         float shadowFraction = 1f;
1133         if (scrollingContentTopPadding > 0f) {
1134             // scrollingContentTopPadding makes a gap between the bottom of the HUN and the top
1135             // of the scrolling content. Use this to animate to the full shadow.
1136             shadowFraction = Math.clamp(overlap / scrollingContentTopPadding, 0f, 1f);
1137         }
1138 
1139         if (overlap > 0.0f) {
1140             // add a shadow to this HUN, because it overlaps with the scrolling stack
1141             viewState.setZTranslation(baseZ + shadowFraction * mPinnedZTranslationExtra);
1142         }
1143     }
1144 
1145     // Pin HUN to bottom of expanded QS
1146     // while the rest of notifications are scrolled offscreen.
clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, ExpandableViewState childState)1147     private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
1148             ExpandableViewState childState) {
1149         SceneContainerFlag.assertInLegacyMode();
1150         float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
1151         final float maxShelfPosition =
1152                 ambientState.getInnerHeight()
1153                         + ambientState.getTopPadding()
1154                         + ambientState.getStackTranslation();
1155         maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition);
1156 
1157         final float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
1158         final float newTranslation = Math.min(childState.getYTranslation(), bottomPosition);
1159         childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation
1160                 - newTranslation);
1161         childState.setYTranslation(newTranslation);
1162     }
1163 
clampHunToMaxTranslation(float headsUpTop, float headsUpBottom, ExpandableViewState viewState)1164     private void clampHunToMaxTranslation(float headsUpTop, float headsUpBottom,
1165             ExpandableViewState viewState) {
1166         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
1167         final float maxHeight = Math.max(0f, headsUpBottom - headsUpTop);
1168         viewState.setYTranslation(Math.min(headsUpTop, viewState.getYTranslation()));
1169         viewState.height = (int) Math.min(maxHeight, viewState.height);
1170     }
1171 
updateCornerRoundnessForPinnedHun(ExpandableNotificationRow row, float stackTop)1172     private void updateCornerRoundnessForPinnedHun(ExpandableNotificationRow row, float stackTop) {
1173         // Animate pinned HUN bottom corners to and from original roundness.
1174         final float originalCornerRadius =
1175                 row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
1176         final float bottomValue = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
1177                 stackTop, getMaxAllowedChildHeight(row), originalCornerRadius);
1178         row.requestBottomRoundness(bottomValue, STACK_SCROLL_ALGO);
1179         row.addOnDetachResetRoundness(STACK_SCROLL_ALGO);
1180     }
1181 
1182     @VisibleForTesting
computeCornerRoundnessForPinnedHun(float hostViewHeight, float stackY, float viewMaxHeight, float originalCornerRadius)1183     float computeCornerRoundnessForPinnedHun(float hostViewHeight, float stackY,
1184             float viewMaxHeight, float originalCornerRadius) {
1185 
1186         // Compute y where corner roundness should be in its original unpinned state.
1187         // We use view max height because the pinned collapsed HUN expands to max height
1188         // when it becomes unpinned.
1189         final float originalRoundnessY = hostViewHeight - viewMaxHeight;
1190 
1191         final float distToOriginalRoundness = Math.max(0f, stackY - originalRoundnessY);
1192         final float progressToPinnedRoundness = Math.min(1f,
1193                 distToOriginalRoundness / viewMaxHeight);
1194 
1195         return MathUtils.lerp(originalCornerRadius, 1f, progressToPinnedRoundness);
1196     }
1197 
getMaxAllowedChildHeight(View child)1198     protected int getMaxAllowedChildHeight(View child) {
1199         if (child instanceof ExpandableView expandableView) {
1200             return expandableView.getIntrinsicHeight();
1201         }
1202         return child == null ? mCollapsedSize : child.getHeight();
1203     }
1204 
1205     /**
1206      * Calculate the Z positions for all children based on the number of items in both stacks and
1207      * save it in the resultState
1208      *
1209      * @param algorithmState The state in which the current pass of the algorithm is currently in
1210      * @param ambientState   The ambient state of the algorithm
1211      */
updateZValuesForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)1212     private void updateZValuesForState(StackScrollAlgorithmState algorithmState,
1213             AmbientState ambientState) {
1214         int childCount = algorithmState.visibleChildren.size();
1215         float childrenOnTop = 0.0f;
1216 
1217         int topHunIndex = -1;
1218         for (int i = 0; i < childCount; i++) {
1219             ExpandableView child = algorithmState.visibleChildren.get(i);
1220             if (child instanceof ActivatableNotificationView
1221                     && (child.isAboveShelf() || child.showingPulsing())) {
1222                 topHunIndex = i;
1223                 break;
1224             }
1225         }
1226 
1227         for (int i = childCount - 1; i >= 0; i--) {
1228             childrenOnTop = updateChildZValue(i, childrenOnTop,
1229                     algorithmState, ambientState, i == topHunIndex);
1230         }
1231     }
1232 
1233     /**
1234      * Calculate and update the Z positions for a given child. We currently only give shadows to
1235      * HUNs to distinguish a HUN from its surroundings.
1236      *
1237      * @param isTopHun      Whether the child is a top HUN. A top HUN means a HUN that shows on the
1238      *                      vertically top of screen. Top HUNs should have drop shadows
1239      * @param childrenOnTop It is greater than 0 when there's an existing HUN that is elevated
1240      * @return childrenOnTop The decimal part represents the fraction of the elevated HUN's height
1241      * that overlaps with QQS Panel. The integer part represents the count of
1242      * previous HUNs whose Z positions are greater than 0.
1243      */
updateChildZValue(int i, float childrenOnTop, StackScrollAlgorithmState algorithmState, AmbientState ambientState, boolean isTopHun)1244     protected float updateChildZValue(int i, float childrenOnTop,
1245             StackScrollAlgorithmState algorithmState,
1246             AmbientState ambientState,
1247             boolean isTopHun) {
1248         ExpandableView child = algorithmState.visibleChildren.get(i);
1249         ExpandableViewState childViewState = child.getViewState();
1250         float baseZ = ambientState.getBaseZHeight();
1251 
1252         if (SceneContainerFlag.isEnabled()) {
1253             // SceneContainer simplifies this logic, because:
1254             // - there are no overlapping HUNs anymore, no need for multiplying their shadows
1255             // - shadows for HUNs overlapping with the stack are now set from updateHeadsUpStates
1256             if (child.isPinned() || ambientState.getTrackedHeadsUpRow() == child) {
1257                 // set a default elevation on the HUN, which would be overridden
1258                 // from updateHeadsUpStates if it is displayed in the shade
1259                 childViewState.setZTranslation(baseZ + mPinnedZTranslationExtra);
1260             } else {
1261                 // set baseZ for every notification
1262                 childViewState.setZTranslation(baseZ);
1263             }
1264         } else {
1265             if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible
1266                     && !ambientState.isDozingAndNotPulsing(child)
1267                     && childViewState.getYTranslation() < ambientState.getTopPadding()
1268                     + ambientState.getStackTranslation()) {
1269 
1270                 if (childrenOnTop != 0.0f) {
1271                     // To elevate the later HUN over previous HUN when multiple HUNs exist
1272                     childrenOnTop++;
1273                 } else {
1274                     // Handles HUN shadow when Shade is opened, and AmbientState.mScrollY > 0
1275                     // Calculate the HUN's z-value based on its overlapping fraction with QQS Panel.
1276                     // When scrolling down shade to make HUN back to in-position in Notif Panel,
1277                     // The overlapping fraction goes to 0, and shadows hides gradually.
1278                     float overlap = ambientState.getTopPadding()
1279                             + ambientState.getStackTranslation() - childViewState.getYTranslation();
1280                     // To prevent over-shadow during HUN entry
1281                     childrenOnTop += Math.min(
1282                             1.0f,
1283                             overlap / childViewState.height
1284                     );
1285                 }
1286                 childViewState.setZTranslation(baseZ
1287                         + childrenOnTop * mPinnedZTranslationExtra);
1288             } else if (isTopHun) {
1289                 // In case this is a new view that has never been measured before, we don't want to
1290                 // elevate if we are currently expanded more than the notification
1291                 int shelfHeight = ambientState.getShelf() == null ? 0 :
1292                         ambientState.getShelf().getIntrinsicHeight();
1293                 float shelfStart = ambientState.getInnerHeight()
1294                         - shelfHeight + ambientState.getTopPadding()
1295                         + ambientState.getStackTranslation();
1296                 float notificationEnd =
1297                         childViewState.getYTranslation() + child.getIntrinsicHeight()
1298                                 + mPaddingBetweenElements;
1299                 if (shelfStart > notificationEnd) {
1300                     // When the notification doesn't overlap with Notification Shelf,
1301                     // there's no shadow
1302                     childViewState.setZTranslation(baseZ);
1303                 } else {
1304                     // Give shadow to the notification if it overlaps with Notification Shelf
1305                     float factor = (notificationEnd - shelfStart) / shelfHeight;
1306                     if (Float.isNaN(factor)) { // Avoid problems when the above is 0/0.
1307                         factor = 1.0f;
1308                     }
1309                     factor = Math.min(factor, 1.0f);
1310                     childViewState.setZTranslation(baseZ + factor * mPinnedZTranslationExtra);
1311                 }
1312             } else {
1313                 childViewState.setZTranslation(baseZ);
1314             }
1315         }
1316 
1317         // While HUN is showing and Shade is closed: headerVisibleAmount stays 0, shadow stays.
1318         // During HUN-to-Shade (eg. dragging down HUN to open Shade): headerVisibleAmount goes
1319         // gradually from 0 to 1, shadow hides gradually.
1320         // Header visibility is a deprecated concept, we are using headerVisibleAmount only because
1321         // this value nicely goes from 0 to 1 during the HUN-to-Shade process.
1322 
1323         childViewState.setZTranslation(childViewState.getZTranslation()
1324                 + (1.0f - child.getHeaderVisibleAmount()) * mPinnedZTranslationExtra);
1325         return childrenOnTop;
1326     }
1327 
setIsExpanded(boolean isExpanded)1328     public void setIsExpanded(boolean isExpanded) {
1329         this.mIsExpanded = isExpanded;
1330     }
1331 
1332     public static class StackScrollAlgorithmState {
1333 
1334         /**
1335          * The scroll position of the algorithm (absolute scrolling).
1336          */
1337         public int scrollY;
1338 
1339         /**
1340          * First view in shelf.
1341          */
1342         public ExpandableView firstViewInShelf;
1343 
1344         /**
1345          * The children from the host view which are not gone.
1346          */
1347         public final ArrayList<ExpandableView> visibleChildren = new ArrayList<>();
1348 
1349         /**
1350          * Y position of the current view during updating children
1351          * with expansion factor applied.
1352          */
1353         private float mCurrentYPosition;
1354 
1355         /**
1356          * Y position of the current view during updating children
1357          * without applying the expansion factor.
1358          */
1359         private float mCurrentExpandedYPosition;
1360     }
1361 
1362     /**
1363      * Interface for telling the SSA when a new notification section begins (so it can add in
1364      * appropriate margins).
1365      */
1366     public interface SectionProvider {
1367         /**
1368          * True if this view starts a new "section" of notifications, such as the gentle
1369          * notifications section. False if sections are not enabled.
1370          */
1371         boolean beginsSection(@NonNull View view, @Nullable View previous);
1372     }
1373 
1374     /**
1375      * Interface for telling the StackScrollAlgorithm information about the bypass state
1376      */
1377     public interface BypassController {
1378         /**
1379          * True if bypass is enabled.  Note that this is always false if face auth is not enabled.
1380          */
1381         boolean isBypassEnabled();
1382     }
1383 }
1384