• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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;
18 
19 import static com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress;
20 
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.content.res.Resources;
24 import android.graphics.Rect;
25 import android.util.AttributeSet;
26 import android.util.IndentingPrintWriter;
27 import android.util.MathUtils;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.view.ViewTreeObserver;
31 import android.view.accessibility.AccessibilityNodeInfo;
32 import android.view.animation.Interpolator;
33 import android.view.animation.PathInterpolator;
34 
35 import androidx.annotation.NonNull;
36 
37 import com.android.internal.annotations.VisibleForTesting;
38 import com.android.internal.policy.SystemBarUtils;
39 import com.android.systemui.R;
40 import com.android.systemui.animation.Interpolators;
41 import com.android.systemui.animation.ShadeInterpolation;
42 import com.android.systemui.flags.FeatureFlags;
43 import com.android.systemui.flags.Flags;
44 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
45 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
46 import com.android.systemui.statusbar.notification.LegacySourceType;
47 import com.android.systemui.statusbar.notification.NotificationUtils;
48 import com.android.systemui.statusbar.notification.SourceType;
49 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
50 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
51 import com.android.systemui.statusbar.notification.row.ExpandableView;
52 import com.android.systemui.statusbar.notification.stack.AmbientState;
53 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
54 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
55 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
56 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm;
57 import com.android.systemui.statusbar.notification.stack.ViewState;
58 import com.android.systemui.statusbar.phone.NotificationIconContainer;
59 import com.android.systemui.util.DumpUtilsKt;
60 
61 import java.io.PrintWriter;
62 
63 /**
64  * A notification shelf view that is placed inside the notification scroller. It manages the
65  * overflow icons that don't fit into the regular list anymore.
66  */
67 public class NotificationShelf extends ActivatableNotificationView implements
68         View.OnLayoutChangeListener, StateListener {
69 
70     private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag;
71     private static final String TAG = "NotificationShelf";
72 
73     // More extreme version of SLOW_OUT_LINEAR_IN which keeps the icon nearly invisible until after
74     // the next icon has translated out of the way, to avoid overlapping.
75     private static final Interpolator ICON_ALPHA_INTERPOLATOR =
76             new PathInterpolator(0.6f, 0f, 0.6f, 0f);
77     private static final SourceType BASE_VALUE = SourceType.from("BaseValue");
78     private static final SourceType SHELF_SCROLL = SourceType.from("ShelfScroll");
79 
80     private NotificationIconContainer mShelfIcons;
81     private int[] mTmp = new int[2];
82     private boolean mHideBackground;
83     private int mStatusBarHeight;
84     private boolean mEnableNotificationClipping;
85     private AmbientState mAmbientState;
86     private NotificationStackScrollLayoutController mHostLayoutController;
87     private int mPaddingBetweenElements;
88     private int mNotGoneIndex;
89     private boolean mHasItemsInStableShelf;
90     private NotificationIconContainer mCollapsedIcons;
91     private int mScrollFastThreshold;
92     private int mStatusBarState;
93     private boolean mInteractive;
94     private boolean mAnimationsEnabled = true;
95     private boolean mShowNotificationShelf;
96     private Rect mClipRect = new Rect();
97     private int mIndexOfFirstViewInShelf = -1;
98     private float mCornerAnimationDistance;
99     private NotificationShelfController mController;
100     private float mActualWidth = -1;
101 
NotificationShelf(Context context, AttributeSet attrs)102     public NotificationShelf(Context context, AttributeSet attrs) {
103         super(context, attrs);
104     }
105 
106     @VisibleForTesting
NotificationShelf(Context context, AttributeSet attrs, boolean showNotificationShelf)107     public NotificationShelf(Context context, AttributeSet attrs, boolean showNotificationShelf) {
108         super(context, attrs);
109         mShowNotificationShelf = showNotificationShelf;
110     }
111 
112     @Override
113     @VisibleForTesting
onFinishInflate()114     public void onFinishInflate() {
115         super.onFinishInflate();
116         mShelfIcons = findViewById(R.id.content);
117         mShelfIcons.setClipChildren(false);
118         mShelfIcons.setClipToPadding(false);
119 
120         setClipToActualHeight(false);
121         setClipChildren(false);
122         setClipToPadding(false);
123         mShelfIcons.setIsStaticLayout(false);
124         requestRoundness(/* top = */ 1f, /* bottom = */ 1f, BASE_VALUE, /* animate = */ false);
125 
126         if (!mUseRoundnessSourceTypes) {
127             // Setting this to first in section to get the clipping to the top roundness correct.
128             // This value determines the way we are clipping to the top roundness of the overall
129             // shade
130             setFirstInSection(true);
131         }
132         updateResources();
133     }
134 
bind(AmbientState ambientState, NotificationStackScrollLayoutController hostLayoutController)135     public void bind(AmbientState ambientState,
136                      NotificationStackScrollLayoutController hostLayoutController) {
137         mAmbientState = ambientState;
138         mHostLayoutController = hostLayoutController;
139         hostLayoutController.setOnNotificationRemovedListener((child, isTransferInProgress) -> {
140             child.requestRoundnessReset(SHELF_SCROLL);
141         });
142     }
143 
updateResources()144     private void updateResources() {
145         Resources res = getResources();
146         mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
147         mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
148 
149         ViewGroup.LayoutParams layoutParams = getLayoutParams();
150         final int newShelfHeight = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
151         if (newShelfHeight != layoutParams.height) {
152             layoutParams.height = newShelfHeight;
153             setLayoutParams(layoutParams);
154         }
155 
156         final int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding);
157         mShelfIcons.setPadding(padding, 0, padding, 0);
158         mScrollFastThreshold = res.getDimensionPixelOffset(R.dimen.scroll_fast_threshold);
159         mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf);
160         mCornerAnimationDistance = res.getDimensionPixelSize(
161                 R.dimen.notification_corner_animation_distance);
162         mEnableNotificationClipping = res.getBoolean(R.bool.notification_enable_clipping);
163 
164         mShelfIcons.setInNotificationIconShelf(true);
165         if (!mShowNotificationShelf) {
166             setVisibility(GONE);
167         }
168     }
169 
170     @Override
onConfigurationChanged(Configuration newConfig)171     protected void onConfigurationChanged(Configuration newConfig) {
172         super.onConfigurationChanged(newConfig);
173         updateResources();
174     }
175 
176     @Override
getContentView()177     protected View getContentView() {
178         return mShelfIcons;
179     }
180 
getShelfIcons()181     public NotificationIconContainer getShelfIcons() {
182         return mShelfIcons;
183     }
184 
185     @Override
186     @NonNull
createExpandableViewState()187     public ExpandableViewState createExpandableViewState() {
188         return new ShelfState();
189     }
190 
191     @Override
toString()192     public String toString() {
193         return "NotificationShelf("
194                 + "hideBackground=" + mHideBackground + " notGoneIndex=" + mNotGoneIndex
195                 + " hasItemsInStableShelf=" + mHasItemsInStableShelf
196                 + " statusBarState=" + mStatusBarState + " interactive=" + mInteractive
197                 + " animationsEnabled=" + mAnimationsEnabled
198                 + " showNotificationShelf=" + mShowNotificationShelf
199                 + " indexOfFirstViewInShelf=" + mIndexOfFirstViewInShelf + ')';
200     }
201 
202     /**
203      * Update the state of the shelf.
204      */
updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState, AmbientState ambientState)205     public void updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState,
206                             AmbientState ambientState) {
207         ExpandableView lastView = ambientState.getLastVisibleBackgroundChild();
208         ShelfState viewState = (ShelfState) getViewState();
209         if (mShowNotificationShelf && lastView != null) {
210             ExpandableViewState lastViewState = lastView.getViewState();
211             viewState.copyFrom(lastViewState);
212 
213             viewState.height = getIntrinsicHeight();
214             viewState.setZTranslation(ambientState.getBaseZHeight());
215             viewState.clipTopAmount = 0;
216 
217             if (ambientState.isExpansionChanging() && !ambientState.isOnKeyguard()) {
218                 float expansion = ambientState.getExpansionFraction();
219                 if (ambientState.isBouncerInTransit()) {
220                     viewState.setAlpha(aboutToShowBouncerProgress(expansion));
221                 } else {
222                     FeatureFlags flags = ambientState.getFeatureFlags();
223                     if (ambientState.isSmallScreen() || !flags.isEnabled(
224                             Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) {
225                         viewState.setAlpha(ShadeInterpolation.getContentAlpha(expansion));
226                     } else {
227                         LargeScreenShadeInterpolator interpolator =
228                                 ambientState.getLargeScreenShadeInterpolator();
229                         viewState.setAlpha(interpolator.getNotificationContentAlpha(expansion));
230                     }
231                 }
232             } else {
233                 viewState.setAlpha(1f - ambientState.getHideAmount());
234             }
235             viewState.belowSpeedBump = mHostLayoutController.getSpeedBumpIndex() == 0;
236             viewState.hideSensitive = false;
237             viewState.setXTranslation(getTranslationX());
238             viewState.hasItemsInStableShelf = lastViewState.inShelf;
239             viewState.firstViewInShelf = algorithmState.firstViewInShelf;
240             if (mNotGoneIndex != -1) {
241                 viewState.notGoneIndex = Math.min(viewState.notGoneIndex, mNotGoneIndex);
242             }
243 
244             viewState.hidden = !mAmbientState.isShadeExpanded()
245                     || algorithmState.firstViewInShelf == null;
246 
247             final int indexOfFirstViewInShelf = algorithmState.visibleChildren.indexOf(
248                     algorithmState.firstViewInShelf);
249 
250             if (mAmbientState.isExpansionChanging()
251                     && algorithmState.firstViewInShelf != null
252                     && indexOfFirstViewInShelf > 0) {
253 
254                 // Show shelf if section before it is showing.
255                 final ExpandableView viewBeforeShelf = algorithmState.visibleChildren.get(
256                         indexOfFirstViewInShelf - 1);
257                 if (viewBeforeShelf.getViewState().hidden) {
258                     viewState.hidden = true;
259                 }
260             }
261 
262             final float stackEnd = ambientState.getStackY() + ambientState.getStackHeight();
263             viewState.setYTranslation(stackEnd - viewState.height);
264         } else {
265             viewState.hidden = true;
266             viewState.location = ExpandableViewState.LOCATION_GONE;
267             viewState.hasItemsInStableShelf = false;
268         }
269     }
270 
271     /**
272      * @param fractionToShade Fraction of lockscreen to shade transition
273      * @param shortestWidth   Shortest width to use for lockscreen shelf
274      */
275     @VisibleForTesting
updateActualWidth(float fractionToShade, float shortestWidth)276     public void updateActualWidth(float fractionToShade, float shortestWidth) {
277         final float actualWidth = mAmbientState.isOnKeyguard()
278                 ? MathUtils.lerp(shortestWidth, getWidth(), fractionToShade)
279                 : getWidth();
280         setBackgroundWidth((int) actualWidth);
281         if (mShelfIcons != null) {
282             mShelfIcons.setActualLayoutWidth((int) actualWidth);
283         }
284         mActualWidth = actualWidth;
285     }
286 
287     @Override
getBoundsOnScreen(Rect outRect, boolean clipToParent)288     public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
289         super.getBoundsOnScreen(outRect, clipToParent);
290         final int actualWidth = getActualWidth();
291         if (isLayoutRtl()) {
292             outRect.left = outRect.right - actualWidth;
293         } else {
294             outRect.right = outRect.left + actualWidth;
295         }
296     }
297 
298     /**
299      * @return Actual width of shelf, accounting for possible ongoing width animation
300      */
getActualWidth()301     public int getActualWidth() {
302         return mActualWidth > -1 ? (int) mActualWidth : getWidth();
303     }
304 
305     /**
306      * @param localX Click x from left of screen
307      * @param slop   Margin of error within which we count x for valid click
308      * @param left   Left of shelf, from left of screen
309      * @param right  Right of shelf, from left of screen
310      * @return Whether click x was in view
311      */
312     @VisibleForTesting
isXInView(float localX, float slop, float left, float right)313     public boolean isXInView(float localX, float slop, float left, float right) {
314         return (left - slop) <= localX && localX < (right + slop);
315     }
316 
317     /**
318      * @param localY Click y from top of shelf
319      * @param slop   Margin of error within which we count y for valid click
320      * @param top    Top of shelf
321      * @param bottom Height of shelf
322      * @return Whether click y was in view
323      */
324     @VisibleForTesting
isYInView(float localY, float slop, float top, float bottom)325     public boolean isYInView(float localY, float slop, float top, float bottom) {
326         return (top - slop) <= localY && localY < (bottom + slop);
327     }
328 
329     /**
330      * @param localX Click x
331      * @param localY Click y
332      * @param slop   Margin of error for valid click
333      * @return Whether this click was on the visible (non-clipped) part of the shelf
334      */
335     @Override
pointInView(float localX, float localY, float slop)336     public boolean pointInView(float localX, float localY, float slop) {
337         final float containerWidth = getWidth();
338         final float shelfWidth = getActualWidth();
339 
340         final float left = isLayoutRtl() ? containerWidth - shelfWidth : 0;
341         final float right = isLayoutRtl() ? containerWidth : shelfWidth;
342 
343         final float top = mClipTopAmount;
344         final float bottom = getActualHeight();
345 
346         return isXInView(localX, slop, left, right)
347                 && isYInView(localY, slop, top, bottom);
348     }
349 
350     /**
351      * Update the shelf appearance based on the other notifications around it. This transforms
352      * the icons from the notification area into the shelf.
353      */
updateAppearance()354     public void updateAppearance() {
355         // If the shelf should not be shown, then there is no need to update anything.
356         if (!mShowNotificationShelf) {
357             return;
358         }
359         mShelfIcons.resetViewStates();
360         float shelfStart = getTranslationY();
361         float numViewsInShelf = 0.0f;
362         View lastChild = mAmbientState.getLastVisibleBackgroundChild();
363         mNotGoneIndex = -1;
364         //  find the first view that doesn't overlap with the shelf
365         int notGoneIndex = 0;
366         int colorOfViewBeforeLast = NO_COLOR;
367         boolean backgroundForceHidden = false;
368         if (mHideBackground && !((ShelfState) getViewState()).hasItemsInStableShelf) {
369             backgroundForceHidden = true;
370         }
371         int colorTwoBefore = NO_COLOR;
372         int previousColor = NO_COLOR;
373         float transitionAmount = 0.0f;
374         float currentScrollVelocity = mAmbientState.getCurrentScrollVelocity();
375         boolean scrollingFast = currentScrollVelocity > mScrollFastThreshold
376                 || (mAmbientState.isExpansionChanging()
377                 && Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold);
378         boolean expandingAnimated = mAmbientState.isExpansionChanging()
379                 && !mAmbientState.isPanelTracking();
380         int baseZHeight = mAmbientState.getBaseZHeight();
381         int clipTopAmount = 0;
382 
383         for (int i = 0; i < mHostLayoutController.getChildCount(); i++) {
384             ExpandableView child = mHostLayoutController.getChildAt(i);
385             if (!child.needsClippingToShelf() || child.getVisibility() == GONE) {
386                 continue;
387             }
388             float notificationClipEnd;
389             boolean aboveShelf = ViewState.getFinalTranslationZ(child) > baseZHeight
390                     || child.isPinned();
391             boolean isLastChild = child == lastChild;
392             final float viewStart = child.getTranslationY();
393             final float shelfClipStart = getTranslationY() - mPaddingBetweenElements;
394             final float inShelfAmount = getAmountInShelf(i, child, scrollingFast,
395                     expandingAnimated, isLastChild, shelfClipStart);
396 
397             // TODO(b/172289889) scale mPaddingBetweenElements with expansion amount
398             if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) {
399                 notificationClipEnd = shelfStart + getIntrinsicHeight();
400             } else {
401                 notificationClipEnd = shelfStart - mPaddingBetweenElements;
402             }
403             int clipTop = updateNotificationClipHeight(child, notificationClipEnd, notGoneIndex);
404             clipTopAmount = Math.max(clipTop, clipTopAmount);
405 
406             // If the current row is an ExpandableNotificationRow, update its color, roundedness,
407             // and icon state.
408             if (child instanceof ExpandableNotificationRow) {
409                 ExpandableNotificationRow expandableRow = (ExpandableNotificationRow) child;
410                 numViewsInShelf += inShelfAmount;
411                 int ownColorUntinted = expandableRow.getBackgroundColorWithoutTint();
412                 if (viewStart >= shelfStart && mNotGoneIndex == -1) {
413                     mNotGoneIndex = notGoneIndex;
414                     setTintColor(previousColor);
415                     setOverrideTintColor(colorTwoBefore, transitionAmount);
416 
417                 } else if (mNotGoneIndex == -1) {
418                     colorTwoBefore = previousColor;
419                     transitionAmount = inShelfAmount;
420                 }
421                 // We don't want to modify the color if the notification is hun'd
422                 if (isLastChild && mController.canModifyColorOfNotifications()) {
423                     if (colorOfViewBeforeLast == NO_COLOR) {
424                         colorOfViewBeforeLast = ownColorUntinted;
425                     }
426                     expandableRow.setOverrideTintColor(colorOfViewBeforeLast, inShelfAmount);
427                 } else {
428                     colorOfViewBeforeLast = ownColorUntinted;
429                     expandableRow.setOverrideTintColor(NO_COLOR, 0 /* overrideAmount */);
430                 }
431                 if (notGoneIndex != 0 || !aboveShelf) {
432                     expandableRow.setAboveShelf(false);
433                 }
434 
435                 previousColor = ownColorUntinted;
436                 notGoneIndex++;
437             }
438 
439             if (child instanceof ActivatableNotificationView) {
440                 ActivatableNotificationView anv =
441                         (ActivatableNotificationView) child;
442                 // Because we show whole notifications on the lockscreen, the bottom notification is
443                 // always "just about to enter the shelf" by normal scrolling rules.  This is fine
444                 // if the shelf is visible, but if the shelf is hidden, it causes incorrect curling.
445                 // notificationClipEnd handles the discrepancy between a visible and hidden shelf,
446                 // so we use that when on the keyguard (and while animating away) to reduce curling.
447                 final float keyguardSafeShelfStart =
448                         mAmbientState.isOnKeyguard() ? notificationClipEnd : shelfStart;
449                 updateCornerRoundnessOnScroll(anv, viewStart, keyguardSafeShelfStart);
450             }
451         }
452 
453         clipTransientViews();
454 
455         setClipTopAmount(clipTopAmount);
456 
457         boolean isHidden = getViewState().hidden
458                 || clipTopAmount >= getIntrinsicHeight()
459                 || !mShowNotificationShelf
460                 || numViewsInShelf < 1f;
461 
462         final float fractionToShade = Interpolators.STANDARD.getInterpolation(
463                 mAmbientState.getFractionToShade());
464         final float shortestWidth = mShelfIcons.calculateWidthFor(numViewsInShelf);
465         updateActualWidth(fractionToShade, shortestWidth);
466 
467         // TODO(b/172289889) transition last icon in shelf to notification icon and vice versa.
468         setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE);
469         mShelfIcons.setSpeedBumpIndex(mHostLayoutController.getSpeedBumpIndex());
470         mShelfIcons.calculateIconXTranslations();
471         mShelfIcons.applyIconStates();
472         for (int i = 0; i < mHostLayoutController.getChildCount(); i++) {
473             View child = mHostLayoutController.getChildAt(i);
474             if (!(child instanceof ExpandableNotificationRow)
475                     || child.getVisibility() == GONE) {
476                 continue;
477             }
478             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
479             updateContinuousClipping(row);
480         }
481         boolean hideBackground = isHidden;
482         setHideBackground(hideBackground);
483         if (mNotGoneIndex == -1) {
484             mNotGoneIndex = notGoneIndex;
485         }
486     }
487 
488     private void updateCornerRoundnessOnScroll(
489             ActivatableNotificationView anv,
490             float viewStart,
491             float shelfStart) {
492 
493         final boolean isUnlockedHeadsUp = !mAmbientState.isOnKeyguard()
494                 && !mAmbientState.isShadeExpanded()
495                 && anv instanceof ExpandableNotificationRow
496                 && anv.isHeadsUp();
497 
498         final boolean isHunGoingToShade = mAmbientState.isShadeExpanded()
499                 && anv == mAmbientState.getTrackedHeadsUpRow();
500 
501         final boolean shouldUpdateCornerRoundness = viewStart < shelfStart
502                 && !mHostLayoutController.isViewAffectedBySwipe(anv)
503                 && !isUnlockedHeadsUp
504                 && !isHunGoingToShade
505                 && !anv.isAboveShelf()
506                 && !mAmbientState.isPulsing()
507                 && !mAmbientState.isDozing();
508 
509         if (!shouldUpdateCornerRoundness) {
510             return;
511         }
512 
513         final float viewEnd = viewStart + anv.getActualHeight();
514         final float cornerAnimationDistance = mCornerAnimationDistance
515                 * mAmbientState.getExpansionFraction();
516         final float cornerAnimationTop = shelfStart - cornerAnimationDistance;
517 
518         final SourceType sourceType;
519         if (mUseRoundnessSourceTypes) {
520             sourceType = SHELF_SCROLL;
521         } else {
522             sourceType = LegacySourceType.OnScroll;
523         }
524 
525         final float topValue;
526         if (!mUseRoundnessSourceTypes && anv.isFirstInSection()) {
527             topValue = 1f;
528         } else if (viewStart >= cornerAnimationTop) {
529             // Round top corners within animation bounds
530             topValue = MathUtils.saturate(
531                     (viewStart - cornerAnimationTop) / cornerAnimationDistance);
532         } else {
533             // Fast scroll skips frames and leaves corners with unfinished rounding.
534             // Reset top and bottom corners outside of animation bounds.
535             topValue = 0f;
536         }
537         anv.requestTopRoundness(topValue, sourceType, /* animate = */ false);
538 
539         final float bottomValue;
540         if (!mUseRoundnessSourceTypes && anv.isLastInSection()) {
541             bottomValue = 1f;
542         } else if (viewEnd >= cornerAnimationTop) {
543             // Round bottom corners within animation bounds
544             bottomValue = MathUtils.saturate(
545                     (viewEnd - cornerAnimationTop) / cornerAnimationDistance);
546         } else {
547             // Fast scroll skips frames and leaves corners with unfinished rounding.
548             // Reset top and bottom corners outside of animation bounds.
549             bottomValue = 0f;
550         }
551         anv.requestBottomRoundness(bottomValue, sourceType, /* animate = */ false);
552     }
553 
554     /**
555      * Clips transient views to the top of the shelf - Transient views are only used for
556      * disappearing views/animations and need to be clipped correctly by the shelf to ensure they
557      * don't show underneath the notification stack when something is animating and the user
558      * swipes quickly.
559      */
560     private void clipTransientViews() {
561         for (int i = 0; i < mHostLayoutController.getTransientViewCount(); i++) {
562             View transientView = mHostLayoutController.getTransientView(i);
563             if (transientView instanceof ExpandableView) {
564                 ExpandableView transientExpandableView = (ExpandableView) transientView;
565                 updateNotificationClipHeight(transientExpandableView, getTranslationY(), -1);
566             }
567         }
568     }
569 
570     private void updateIconClipAmount(ExpandableNotificationRow row) {
571         float maxTop = row.getTranslationY();
572         if (getClipTopAmount() != 0) {
573             // if the shelf is clipped, lets make sure we also clip the icon
574             maxTop = Math.max(maxTop, getTranslationY() + getClipTopAmount());
575         }
576         StatusBarIconView icon = row.getEntry().getIcons().getShelfIcon();
577         float shelfIconPosition = getTranslationY() + icon.getTop() + icon.getTranslationY();
578         if (shelfIconPosition < maxTop && !mAmbientState.isFullyHidden()) {
579             int top = (int) (maxTop - shelfIconPosition);
580             Rect clipRect = new Rect(0, top, icon.getWidth(), Math.max(top, icon.getHeight()));
581             icon.setClipBounds(clipRect);
582         } else {
583             icon.setClipBounds(null);
584         }
585     }
586 
587     private void updateContinuousClipping(final ExpandableNotificationRow row) {
588         StatusBarIconView icon = row.getEntry().getIcons().getShelfIcon();
589         boolean needsContinuousClipping = ViewState.isAnimatingY(icon) && !mAmbientState.isDozing();
590         boolean isContinuousClipping = icon.getTag(TAG_CONTINUOUS_CLIPPING) != null;
591         if (needsContinuousClipping && !isContinuousClipping) {
592             final ViewTreeObserver observer = icon.getViewTreeObserver();
593             ViewTreeObserver.OnPreDrawListener predrawListener =
594                     new ViewTreeObserver.OnPreDrawListener() {
595                         @Override
596                         public boolean onPreDraw() {
597                             boolean animatingY = ViewState.isAnimatingY(icon);
598                             if (!animatingY) {
599                                 if (observer.isAlive()) {
600                                     observer.removeOnPreDrawListener(this);
601                                 }
602                                 icon.setTag(TAG_CONTINUOUS_CLIPPING, null);
603                                 return true;
604                             }
605                             updateIconClipAmount(row);
606                             return true;
607                         }
608                     };
609             observer.addOnPreDrawListener(predrawListener);
610             icon.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
611                 @Override
612                 public void onViewAttachedToWindow(View v) {
613                 }
614 
615                 @Override
616                 public void onViewDetachedFromWindow(View v) {
617                     if (v == icon) {
618                         if (observer.isAlive()) {
619                             observer.removeOnPreDrawListener(predrawListener);
620                         }
621                         icon.setTag(TAG_CONTINUOUS_CLIPPING, null);
622                     }
623                 }
624             });
625             icon.setTag(TAG_CONTINUOUS_CLIPPING, predrawListener);
626         }
627     }
628 
629     /**
630      * Update the clipping of this view.
631      *
632      * @return the amount that our own top should be clipped
633      */
634     private int updateNotificationClipHeight(ExpandableView view,
635                                              float notificationClipEnd, int childIndex) {
636         float viewEnd = view.getTranslationY() + view.getActualHeight();
637         boolean isPinned = (view.isPinned() || view.isHeadsUpAnimatingAway())
638                 && !mAmbientState.isDozingAndNotPulsing(view);
639         boolean shouldClipOwnTop;
640         if (mAmbientState.isPulseExpanding()) {
641             shouldClipOwnTop = childIndex == 0;
642         } else {
643             shouldClipOwnTop = view.showingPulsing();
644         }
645         if (!isPinned) {
646             if (viewEnd > notificationClipEnd && !shouldClipOwnTop) {
647                 int clipBottomAmount =
648                         mEnableNotificationClipping ? (int) (viewEnd - notificationClipEnd) : 0;
649                 view.setClipBottomAmount(clipBottomAmount);
650             } else {
651                 view.setClipBottomAmount(0);
652             }
653         }
654         if (shouldClipOwnTop) {
655             return (int) (viewEnd - getTranslationY());
656         } else {
657             return 0;
658         }
659     }
660 
661     @Override
662     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
663                                        int outlineTranslation) {
664         if (!mHasItemsInStableShelf) {
665             shadowIntensity = 0.0f;
666         }
667         super.setFakeShadowIntensity(shadowIntensity, outlineAlpha, shadowYEnd, outlineTranslation);
668     }
669 
670     /**
671      * @param i                 Index of the view in the host layout.
672      * @param view              The current ExpandableView.
673      * @param scrollingFast     Whether we are scrolling fast.
674      * @param expandingAnimated Whether we are expanding a notification.
675      * @param isLastChild       Whether this is the last view.
676      * @param shelfClipStart    The point at which notifications start getting clipped by the shelf.
677      * @return The amount how much this notification is in the shelf.
678      * 0f is not in shelf. 1f is completely in shelf.
679      */
680     @VisibleForTesting
681     public float getAmountInShelf(
682             int i,
683             ExpandableView view,
684             boolean scrollingFast,
685             boolean expandingAnimated,
686             boolean isLastChild,
687             float shelfClipStart
688     ) {
689 
690         // Let's calculate how much the view is in the shelf
691         float viewStart = view.getTranslationY();
692         int fullHeight = view.getActualHeight() + mPaddingBetweenElements;
693         float iconTransformStart = calculateIconTransformationStart(view);
694 
695         // Let's make sure the transform distance is
696         // at most to the icon (relevant for conversations)
697         float transformDistance = Math.min(
698                 viewStart + fullHeight - iconTransformStart,
699                 getIntrinsicHeight());
700 
701         if (isLastChild) {
702             fullHeight = Math.min(fullHeight, view.getMinHeight() - getIntrinsicHeight());
703             transformDistance = Math.min(
704                     transformDistance,
705                     view.getMinHeight() - getIntrinsicHeight());
706         }
707 
708         float viewEnd = viewStart + fullHeight;
709         float fullTransitionAmount = 0.0f;
710         float iconTransitionAmount = 0.0f;
711 
712         // Don't animate shelf icons during shade expansion.
713         if (mAmbientState.isExpansionChanging() && !mAmbientState.isOnKeyguard()) {
714             // TODO(b/172289889) handle icon placement for notification that is clipped by the shelf
715             if (mIndexOfFirstViewInShelf != -1 && i >= mIndexOfFirstViewInShelf) {
716                 fullTransitionAmount = 1f;
717                 iconTransitionAmount = 1f;
718             }
719 
720         } else if (viewEnd >= shelfClipStart
721                 && (!mAmbientState.isUnlockHintRunning() || view.isInShelf())
722                 && (mAmbientState.isShadeExpanded()
723                 || (!view.isPinned() && !view.isHeadsUpAnimatingAway()))) {
724 
725             if (viewStart < shelfClipStart && Math.abs(viewStart - shelfClipStart) > 0.001f) {
726                 // Partially clipped by shelf.
727                 float fullAmount = (shelfClipStart - viewStart) / fullHeight;
728                 fullAmount = Math.min(1.0f, fullAmount);
729                 fullTransitionAmount = 1.0f - fullAmount;
730                 if (isLastChild) {
731                     // Reduce icon transform distance to completely fade in shelf icon
732                     // by the time the notification icon fades out, and vice versa
733                     iconTransitionAmount = (shelfClipStart - viewStart)
734                             / (iconTransformStart - viewStart);
735                 } else {
736                     iconTransitionAmount = (shelfClipStart - iconTransformStart)
737                             / transformDistance;
738                 }
739                 iconTransitionAmount = MathUtils.constrain(iconTransitionAmount, 0.0f, 1.0f);
740                 iconTransitionAmount = 1.0f - iconTransitionAmount;
741             } else {
742                 // Fully in shelf.
743                 fullTransitionAmount = 1.0f;
744                 iconTransitionAmount = 1.0f;
745             }
746         }
747         updateIconPositioning(view, iconTransitionAmount,
748                 scrollingFast, expandingAnimated, isLastChild);
749         return fullTransitionAmount;
750     }
751 
752     /**
753      * @return the location where the transformation into the shelf should start.
754      */
calculateIconTransformationStart(ExpandableView view)755     private float calculateIconTransformationStart(ExpandableView view) {
756         View target = view.getShelfTransformationTarget();
757         if (target == null) {
758             return view.getTranslationY();
759         }
760         float start = view.getTranslationY() + view.getRelativeTopPadding(target);
761 
762         // Let's not start the transformation right at the icon but by the padding before it.
763         start -= view.getShelfIcon().getTop();
764         return start;
765     }
766 
updateIconPositioning( ExpandableView view, float iconTransitionAmount, boolean scrollingFast, boolean expandingAnimated, boolean isLastChild )767     private void updateIconPositioning(
768             ExpandableView view,
769             float iconTransitionAmount,
770             boolean scrollingFast,
771             boolean expandingAnimated,
772             boolean isLastChild
773     ) {
774         StatusBarIconView icon = view.getShelfIcon();
775         NotificationIconContainer.IconState iconState = getIconState(icon);
776         if (iconState == null) {
777             return;
778         }
779         boolean clampInShelf = iconTransitionAmount > 0.5f || isTargetClipped(view);
780         float clampedAmount = clampInShelf ? 1.0f : 0.0f;
781         if (iconTransitionAmount == clampedAmount) {
782             iconState.noAnimations = (scrollingFast || expandingAnimated) && !isLastChild;
783         }
784         if (!isLastChild
785                 && (scrollingFast || (expandingAnimated && !ViewState.isAnimatingY(icon)))) {
786             iconState.cancelAnimations(icon);
787             iconState.noAnimations = true;
788         }
789         float transitionAmount;
790         if (mAmbientState.isHiddenAtAll() && !view.isInShelf()) {
791             transitionAmount = mAmbientState.isFullyHidden() ? 1 : 0;
792         } else {
793             transitionAmount = iconTransitionAmount;
794             iconState.needsCannedAnimation = iconState.clampedAppearAmount != clampedAmount;
795         }
796         iconState.clampedAppearAmount = clampedAmount;
797         setIconTransformationAmount(view, transitionAmount);
798     }
799 
isTargetClipped(ExpandableView view)800     private boolean isTargetClipped(ExpandableView view) {
801         View target = view.getShelfTransformationTarget();
802         if (target == null) {
803             return false;
804         }
805         // We should never clip the target, let's instead put it into the shelf!
806         float endOfTarget = view.getTranslationY()
807                 + view.getContentTranslation()
808                 + view.getRelativeTopPadding(target)
809                 + target.getHeight();
810         return endOfTarget >= getTranslationY() - mPaddingBetweenElements;
811     }
812 
setIconTransformationAmount(ExpandableView view, float transitionAmount)813     private void setIconTransformationAmount(ExpandableView view, float transitionAmount) {
814         if (!(view instanceof ExpandableNotificationRow)) {
815             return;
816         }
817         ExpandableNotificationRow row = (ExpandableNotificationRow) view;
818         StatusBarIconView icon = row.getShelfIcon();
819         NotificationIconContainer.IconState iconState = getIconState(icon);
820         if (iconState == null) {
821             return;
822         }
823         iconState.setAlpha(ICON_ALPHA_INTERPOLATOR.getInterpolation(transitionAmount));
824         boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf();
825         iconState.hidden = isAppearing
826                 || (view instanceof ExpandableNotificationRow
827                 && ((ExpandableNotificationRow) view).isLowPriority()
828                 && mShelfIcons.areIconsOverflowing())
829                 || (transitionAmount == 0.0f && !iconState.isAnimating(icon))
830                 || row.isAboveShelf()
831                 || row.showingPulsing()
832                 || row.getTranslationZ() > mAmbientState.getBaseZHeight();
833 
834         iconState.iconAppearAmount = iconState.hidden ? 0f : transitionAmount;
835 
836         // Fade in icons at shelf start
837         // This is important for conversation icons, which are badged and need x reset
838         iconState.setXTranslation(mShelfIcons.getActualPaddingStart());
839 
840         final boolean stayingInShelf = row.isInShelf() && !row.isTransformingIntoShelf();
841         if (stayingInShelf) {
842             iconState.iconAppearAmount = 1.0f;
843             iconState.setAlpha(1.0f);
844             iconState.hidden = false;
845         }
846         int backgroundColor = getBackgroundColorWithoutTint();
847         int shelfColor = icon.getContrastedStaticDrawableColor(backgroundColor);
848         if (row.isShowingIcon() && shelfColor != StatusBarIconView.NO_COLOR) {
849             int iconColor = row.getOriginalIconColor();
850             shelfColor = NotificationUtils.interpolateColors(iconColor, shelfColor,
851                     iconState.iconAppearAmount);
852         }
853         iconState.iconColor = shelfColor;
854     }
855 
getIconState(StatusBarIconView icon)856     private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) {
857         if (mShelfIcons == null) {
858             return null;
859         }
860         return mShelfIcons.getIconState(icon);
861     }
862 
getFullyClosedTranslation()863     private float getFullyClosedTranslation() {
864         return -(getIntrinsicHeight() - mStatusBarHeight) / 2;
865     }
866 
867     @Override
hasNoContentHeight()868     public boolean hasNoContentHeight() {
869         return true;
870     }
871 
setHideBackground(boolean hideBackground)872     private void setHideBackground(boolean hideBackground) {
873         if (mHideBackground != hideBackground) {
874             mHideBackground = hideBackground;
875             updateOutline();
876         }
877     }
878 
879     @Override
needsOutline()880     protected boolean needsOutline() {
881         return !mHideBackground && super.needsOutline();
882     }
883 
884 
885     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)886     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
887         super.onLayout(changed, left, top, right, bottom);
888         updateRelativeOffset();
889 
890         // we always want to clip to our sides, such that nothing can draw outside of these bounds
891         int height = getResources().getDisplayMetrics().heightPixels;
892         mClipRect.set(0, -height, getWidth(), height);
893         if (mShelfIcons != null) {
894             mShelfIcons.setClipBounds(mClipRect);
895         }
896     }
897 
updateRelativeOffset()898     private void updateRelativeOffset() {
899         if (mCollapsedIcons != null) {
900             mCollapsedIcons.getLocationOnScreen(mTmp);
901         }
902         getLocationOnScreen(mTmp);
903     }
904 
905     /**
906      * @return the index of the notification at which the shelf visually resides
907      */
getNotGoneIndex()908     public int getNotGoneIndex() {
909         return mNotGoneIndex;
910     }
911 
setHasItemsInStableShelf(boolean hasItemsInStableShelf)912     private void setHasItemsInStableShelf(boolean hasItemsInStableShelf) {
913         if (mHasItemsInStableShelf != hasItemsInStableShelf) {
914             mHasItemsInStableShelf = hasItemsInStableShelf;
915             updateInteractiveness();
916         }
917     }
918 
919     /**
920      * @return whether the shelf has any icons in it when a potential animation has finished, i.e
921      * if the current state would be applied right now
922      */
hasItemsInStableShelf()923     public boolean hasItemsInStableShelf() {
924         return mHasItemsInStableShelf;
925     }
926 
setCollapsedIcons(NotificationIconContainer collapsedIcons)927     public void setCollapsedIcons(NotificationIconContainer collapsedIcons) {
928         mCollapsedIcons = collapsedIcons;
929         mCollapsedIcons.addOnLayoutChangeListener(this);
930     }
931 
932     @Override
onStateChanged(int newState)933     public void onStateChanged(int newState) {
934         mStatusBarState = newState;
935         updateInteractiveness();
936     }
937 
updateInteractiveness()938     private void updateInteractiveness() {
939         mInteractive = mStatusBarState == StatusBarState.KEYGUARD && mHasItemsInStableShelf;
940         setClickable(mInteractive);
941         setFocusable(mInteractive);
942         setImportantForAccessibility(mInteractive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
943                 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
944     }
945 
946     @Override
isInteractive()947     protected boolean isInteractive() {
948         return mInteractive;
949     }
950 
setAnimationsEnabled(boolean enabled)951     public void setAnimationsEnabled(boolean enabled) {
952         mAnimationsEnabled = enabled;
953         if (!enabled) {
954             // we need to wait with enabling the animations until the first frame has passed
955             mShelfIcons.setAnimationsEnabled(false);
956         }
957     }
958 
959     @Override
hasOverlappingRendering()960     public boolean hasOverlappingRendering() {
961         return false;  // Shelf only uses alpha for transitions where the difference can't be seen.
962     }
963 
964     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)965     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
966         super.onInitializeAccessibilityNodeInfo(info);
967         if (mInteractive) {
968             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
969             AccessibilityNodeInfo.AccessibilityAction unlock
970                     = new AccessibilityNodeInfo.AccessibilityAction(
971                     AccessibilityNodeInfo.ACTION_CLICK,
972                     getContext().getString(R.string.accessibility_overflow_action));
973             info.addAction(unlock);
974         }
975     }
976 
977     @Override
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)978     public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
979                                int oldTop, int oldRight, int oldBottom) {
980         updateRelativeOffset();
981     }
982 
983     @Override
needsClippingToShelf()984     public boolean needsClippingToShelf() {
985         return false;
986     }
987 
setController(NotificationShelfController notificationShelfController)988     public void setController(NotificationShelfController notificationShelfController) {
989         mController = notificationShelfController;
990     }
991 
setIndexOfFirstViewInShelf(ExpandableView firstViewInShelf)992     public void setIndexOfFirstViewInShelf(ExpandableView firstViewInShelf) {
993         mIndexOfFirstViewInShelf = mHostLayoutController.indexOfChild(firstViewInShelf);
994     }
995 
996     /**
997      * This method resets the OnScroll roundness of a view to 0f
998      * <p>
999      * Note: This should be the only class that handles roundness {@code SourceType.OnScroll}
1000      */
resetLegacyOnScrollRoundness(ExpandableView expandableView)1001     public static void resetLegacyOnScrollRoundness(ExpandableView expandableView) {
1002         expandableView.requestRoundnessReset(LegacySourceType.OnScroll);
1003     }
1004 
1005     @Override
dump(PrintWriter pwOriginal, String[] args)1006     public void dump(PrintWriter pwOriginal, String[] args) {
1007         IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
1008         super.dump(pw, args);
1009         if (DUMP_VERBOSE) {
1010             DumpUtilsKt.withIncreasedIndent(pw, () -> {
1011                 pw.println("mActualWidth: " + mActualWidth);
1012                 pw.println("mStatusBarHeight: " + mStatusBarHeight);
1013             });
1014         }
1015     }
1016 
1017     public class ShelfState extends ExpandableViewState {
1018         private boolean hasItemsInStableShelf;
1019         private ExpandableView firstViewInShelf;
1020 
1021         @Override
applyToView(View view)1022         public void applyToView(View view) {
1023             if (!mShowNotificationShelf) {
1024                 return;
1025             }
1026             super.applyToView(view);
1027             setIndexOfFirstViewInShelf(firstViewInShelf);
1028             updateAppearance();
1029             setHasItemsInStableShelf(hasItemsInStableShelf);
1030             mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
1031         }
1032 
1033         @Override
animateTo(View view, AnimationProperties properties)1034         public void animateTo(View view, AnimationProperties properties) {
1035             if (!mShowNotificationShelf) {
1036                 return;
1037             }
1038             super.animateTo(view, properties);
1039             setIndexOfFirstViewInShelf(firstViewInShelf);
1040             updateAppearance();
1041             setHasItemsInStableShelf(hasItemsInStableShelf);
1042             mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
1043         }
1044     }
1045 }
1046