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