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