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.systemui.Interpolators.FAST_OUT_SLOW_IN_REVERSE;
20 import static com.android.systemui.statusbar.phone.NotificationIconContainer.IconState.NO_VALUE;
21 import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
22 
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.content.res.Resources;
26 import android.graphics.Rect;
27 import android.os.SystemProperties;
28 import android.util.AttributeSet;
29 import android.util.Log;
30 import android.view.DisplayCutout;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.ViewTreeObserver;
34 import android.view.WindowInsets;
35 import android.view.accessibility.AccessibilityNodeInfo;
36 
37 import com.android.internal.annotations.VisibleForTesting;
38 import com.android.systemui.Dependency;
39 import com.android.systemui.Interpolators;
40 import com.android.systemui.R;
41 import com.android.systemui.plugins.statusbar.StatusBarStateController;
42 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
43 import com.android.systemui.statusbar.notification.NotificationUtils;
44 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
45 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
46 import com.android.systemui.statusbar.notification.row.ExpandableView;
47 import com.android.systemui.statusbar.notification.stack.AmbientState;
48 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
49 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
50 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
51 import com.android.systemui.statusbar.notification.stack.ViewState;
52 import com.android.systemui.statusbar.phone.KeyguardBypassController;
53 import com.android.systemui.statusbar.phone.NotificationIconContainer;
54 
55 import javax.inject.Inject;
56 import javax.inject.Named;
57 
58 /**
59  * A notification shelf view that is placed inside the notification scroller. It manages the
60  * overflow icons that don't fit into the regular list anymore.
61  */
62 public class NotificationShelf extends ActivatableNotificationView implements
63         View.OnLayoutChangeListener, StateListener {
64 
65     private static final boolean USE_ANIMATIONS_WHEN_OPENING =
66             SystemProperties.getBoolean("debug.icon_opening_animations", true);
67     private static final boolean ICON_ANMATIONS_WHILE_SCROLLING
68             = SystemProperties.getBoolean("debug.icon_scroll_animations", true);
69     private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag;
70     private static final String TAG = "NotificationShelf";
71     private final KeyguardBypassController mBypassController;
72 
73     private NotificationIconContainer mShelfIcons;
74     private int[] mTmp = new int[2];
75     private boolean mHideBackground;
76     private int mIconAppearTopPadding;
77     private float mHiddenShelfIconSize;
78     private int mStatusBarHeight;
79     private int mStatusBarPaddingStart;
80     private AmbientState mAmbientState;
81     private NotificationStackScrollLayout mHostLayout;
82     private int mMaxLayoutHeight;
83     private int mPaddingBetweenElements;
84     private int mNotGoneIndex;
85     private boolean mHasItemsInStableShelf;
86     private NotificationIconContainer mCollapsedIcons;
87     private int mScrollFastThreshold;
88     private int mIconSize;
89     private int mStatusBarState;
90     private float mMaxShelfEnd;
91     private int mRelativeOffset;
92     private boolean mInteractive;
93     private float mOpenedAmount;
94     private boolean mNoAnimationsInThisFrame;
95     private boolean mAnimationsEnabled = true;
96     private boolean mShowNotificationShelf;
97     private float mFirstElementRoundness;
98     private Rect mClipRect = new Rect();
99     private int mCutoutHeight;
100     private int mGapHeight;
101 
102     @Inject
NotificationShelf(@amedVIEW_CONTEXT) Context context, AttributeSet attrs, KeyguardBypassController keyguardBypassController)103     public NotificationShelf(@Named(VIEW_CONTEXT) Context context,
104             AttributeSet attrs,
105             KeyguardBypassController keyguardBypassController) {
106         super(context, attrs);
107         mBypassController = keyguardBypassController;
108     }
109 
110     @Override
111     @VisibleForTesting
onFinishInflate()112     public void onFinishInflate() {
113         super.onFinishInflate();
114         mShelfIcons = findViewById(R.id.content);
115         mShelfIcons.setClipChildren(false);
116         mShelfIcons.setClipToPadding(false);
117 
118         setClipToActualHeight(false);
119         setClipChildren(false);
120         setClipToPadding(false);
121         mShelfIcons.setIsStaticLayout(false);
122         setBottomRoundness(1.0f, false /* animate */);
123         initDimens();
124     }
125 
126     @Override
onAttachedToWindow()127     protected void onAttachedToWindow() {
128         super.onAttachedToWindow();
129         ((SysuiStatusBarStateController) Dependency.get(StatusBarStateController.class))
130                 .addCallback(this, SysuiStatusBarStateController.RANK_SHELF);
131     }
132 
133     @Override
onDetachedFromWindow()134     protected void onDetachedFromWindow() {
135         super.onDetachedFromWindow();
136         Dependency.get(StatusBarStateController.class).removeCallback(this);
137     }
138 
bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout)139     public void bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout) {
140         mAmbientState = ambientState;
141         mHostLayout = hostLayout;
142     }
143 
initDimens()144     private void initDimens() {
145         Resources res = getResources();
146         mIconAppearTopPadding = res.getDimensionPixelSize(R.dimen.notification_icon_appear_padding);
147         mStatusBarHeight = res.getDimensionPixelOffset(R.dimen.status_bar_height);
148         mStatusBarPaddingStart = res.getDimensionPixelOffset(R.dimen.status_bar_padding_start);
149         mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
150 
151         ViewGroup.LayoutParams layoutParams = getLayoutParams();
152         layoutParams.height = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
153         setLayoutParams(layoutParams);
154 
155         int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding);
156         mShelfIcons.setPadding(padding, 0, padding, 0);
157         mScrollFastThreshold = res.getDimensionPixelOffset(R.dimen.scroll_fast_threshold);
158         mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf);
159         mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size);
160         mHiddenShelfIconSize = res.getDimensionPixelOffset(R.dimen.hidden_shelf_icon_size);
161         mGapHeight = res.getDimensionPixelSize(R.dimen.qs_notification_padding);
162 
163         if (!mShowNotificationShelf) {
164             setVisibility(GONE);
165         }
166     }
167 
168     @Override
onConfigurationChanged(Configuration newConfig)169     protected void onConfigurationChanged(Configuration newConfig) {
170         super.onConfigurationChanged(newConfig);
171         initDimens();
172     }
173 
174     @Override
getContentView()175     protected View getContentView() {
176         return mShelfIcons;
177     }
178 
getShelfIcons()179     public NotificationIconContainer getShelfIcons() {
180         return mShelfIcons;
181     }
182 
183     @Override
createExpandableViewState()184     public ExpandableViewState createExpandableViewState() {
185         return new ShelfState();
186     }
187 
188     /** Update the state of the shelf. */
updateState(AmbientState ambientState)189     public void updateState(AmbientState ambientState) {
190         ExpandableView lastView = ambientState.getLastVisibleBackgroundChild();
191         ShelfState viewState = (ShelfState) getViewState();
192         if (mShowNotificationShelf && lastView != null) {
193             float maxShelfEnd = ambientState.getInnerHeight() + ambientState.getTopPadding()
194                     + ambientState.getStackTranslation();
195             ExpandableViewState lastViewState = lastView.getViewState();
196             float viewEnd = lastViewState.yTranslation + lastViewState.height;
197             viewState.copyFrom(lastViewState);
198             viewState.height = getIntrinsicHeight();
199 
200             viewState.yTranslation = Math.max(Math.min(viewEnd, maxShelfEnd) - viewState.height,
201                     getFullyClosedTranslation());
202             viewState.zTranslation = ambientState.getBaseZHeight();
203             // For the small display size, it's not enough to make the icon not covered by
204             // the top cutout so the denominator add the height of cutout.
205             // Totally, (getIntrinsicHeight() * 2 + mCutoutHeight) should be smaller then
206             // mAmbientState.getTopPadding().
207             float openedAmount = (viewState.yTranslation - getFullyClosedTranslation())
208                     / (getIntrinsicHeight() * 2 + mCutoutHeight);
209             openedAmount = Math.min(1.0f, openedAmount);
210             viewState.openedAmount = openedAmount;
211             viewState.clipTopAmount = 0;
212             viewState.alpha = 1;
213             viewState.belowSpeedBump = mAmbientState.getSpeedBumpIndex() == 0;
214             viewState.hideSensitive = false;
215             viewState.xTranslation = getTranslationX();
216             if (mNotGoneIndex != -1) {
217                 viewState.notGoneIndex = Math.min(viewState.notGoneIndex, mNotGoneIndex);
218             }
219             viewState.hasItemsInStableShelf = lastViewState.inShelf;
220             viewState.hidden = !mAmbientState.isShadeExpanded()
221                     || mAmbientState.isQsCustomizerShowing();
222             viewState.maxShelfEnd = maxShelfEnd;
223         } else {
224             viewState.hidden = true;
225             viewState.location = ExpandableViewState.LOCATION_GONE;
226             viewState.hasItemsInStableShelf = false;
227         }
228     }
229 
230     /**
231      * Update the shelf appearance based on the other notifications around it. This transforms
232      * the icons from the notification area into the shelf.
233      */
updateAppearance()234     public void updateAppearance() {
235         // If the shelf should not be shown, then there is no need to update anything.
236         if (!mShowNotificationShelf) {
237             return;
238         }
239 
240         mShelfIcons.resetViewStates();
241         float shelfStart = getTranslationY();
242         float numViewsInShelf = 0.0f;
243         View lastChild = mAmbientState.getLastVisibleBackgroundChild();
244         mNotGoneIndex = -1;
245         float interpolationStart = mMaxLayoutHeight - getIntrinsicHeight() * 2;
246         float expandAmount = 0.0f;
247         if (shelfStart >= interpolationStart) {
248             expandAmount = (shelfStart - interpolationStart) / getIntrinsicHeight();
249             expandAmount = Math.min(1.0f, expandAmount);
250         }
251         //  find the first view that doesn't overlap with the shelf
252         int notGoneIndex = 0;
253         int colorOfViewBeforeLast = NO_COLOR;
254         boolean backgroundForceHidden = false;
255         if (mHideBackground && !((ShelfState) getViewState()).hasItemsInStableShelf) {
256             backgroundForceHidden = true;
257         }
258         int colorTwoBefore = NO_COLOR;
259         int previousColor = NO_COLOR;
260         float transitionAmount = 0.0f;
261         float currentScrollVelocity = mAmbientState.getCurrentScrollVelocity();
262         boolean scrollingFast = currentScrollVelocity > mScrollFastThreshold
263                 || (mAmbientState.isExpansionChanging()
264                         && Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold);
265         boolean scrolling = currentScrollVelocity > 0;
266         boolean expandingAnimated = mAmbientState.isExpansionChanging()
267                 && !mAmbientState.isPanelTracking();
268         int baseZHeight = mAmbientState.getBaseZHeight();
269         int backgroundTop = 0;
270         int clipTopAmount = 0;
271         float firstElementRoundness = 0.0f;
272         ActivatableNotificationView previousRow = null;
273 
274         for (int i = 0; i < mHostLayout.getChildCount(); i++) {
275             ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
276 
277             if (!(child instanceof ActivatableNotificationView)
278                     || child.getVisibility() == GONE || child == this) {
279                 continue;
280             }
281 
282             ActivatableNotificationView row = (ActivatableNotificationView) child;
283             float notificationClipEnd;
284             boolean aboveShelf = ViewState.getFinalTranslationZ(row) > baseZHeight
285                     || row.isPinned();
286             boolean isLastChild = child == lastChild;
287             float rowTranslationY = row.getTranslationY();
288             if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) {
289                 notificationClipEnd = shelfStart + getIntrinsicHeight();
290             } else {
291                 notificationClipEnd = shelfStart - mPaddingBetweenElements;
292                 float height = notificationClipEnd - rowTranslationY;
293                 if (!row.isBelowSpeedBump() && height <= getNotificationMergeSize()) {
294                     // We want the gap to close when we reached the minimum size and only shrink
295                     // before
296                     notificationClipEnd = Math.min(shelfStart,
297                             rowTranslationY + getNotificationMergeSize());
298                 }
299             }
300             int clipTop = updateNotificationClipHeight(row, notificationClipEnd, notGoneIndex);
301             clipTopAmount = Math.max(clipTop, clipTopAmount);
302 
303             // If the current row is an ExpandableNotificationRow, update its color, roundedness,
304             // and icon state.
305             if (row instanceof ExpandableNotificationRow) {
306                 ExpandableNotificationRow expandableRow = (ExpandableNotificationRow) row;
307 
308                 float inShelfAmount = updateIconAppearance(expandableRow, expandAmount, scrolling,
309                         scrollingFast,
310                         expandingAnimated, isLastChild);
311                 numViewsInShelf += inShelfAmount;
312                 int ownColorUntinted = row.getBackgroundColorWithoutTint();
313                 if (rowTranslationY >= shelfStart && mNotGoneIndex == -1) {
314                     mNotGoneIndex = notGoneIndex;
315                     setTintColor(previousColor);
316                     setOverrideTintColor(colorTwoBefore, transitionAmount);
317 
318                 } else if (mNotGoneIndex == -1) {
319                     colorTwoBefore = previousColor;
320                     transitionAmount = inShelfAmount;
321                 }
322                 // We don't want to modify the color if the notification is hun'd
323                 boolean canModifyColor = mAmbientState.isShadeExpanded()
324                         && !(mAmbientState.isOnKeyguard() && mBypassController.getBypassEnabled());
325                 if (isLastChild && canModifyColor) {
326                     if (colorOfViewBeforeLast == NO_COLOR) {
327                         colorOfViewBeforeLast = ownColorUntinted;
328                     }
329                     row.setOverrideTintColor(colorOfViewBeforeLast, inShelfAmount);
330                 } else {
331                     colorOfViewBeforeLast = ownColorUntinted;
332                     row.setOverrideTintColor(NO_COLOR, 0 /* overrideAmount */);
333                 }
334                 if (notGoneIndex != 0 || !aboveShelf) {
335                     expandableRow.setAboveShelf(false);
336                 }
337                 if (notGoneIndex == 0) {
338                     StatusBarIconView icon = expandableRow.getEntry().expandedIcon;
339                     NotificationIconContainer.IconState iconState = getIconState(icon);
340                     // The icon state might be null in rare cases where the notification is actually
341                     // added to the layout, but not to the shelf. An example are replied messages,
342                     // since they don't show up on AOD
343                     if (iconState != null && iconState.clampedAppearAmount == 1.0f) {
344                         // only if the first icon is fully in the shelf we want to clip to it!
345                         backgroundTop = (int) (row.getTranslationY() - getTranslationY());
346                         firstElementRoundness = row.getCurrentTopRoundness();
347                     }
348                 }
349 
350                 previousColor = ownColorUntinted;
351                 notGoneIndex++;
352             }
353 
354             if (row.isFirstInSection() && previousRow != null && previousRow.isLastInSection()) {
355                 // If the top of the shelf is between the view before a gap and the view after a gap
356                 // then we need to adjust the shelf's top roundness.
357                 float distanceToGapBottom = row.getTranslationY() - getTranslationY();
358                 float distanceToGapTop = getTranslationY()
359                         - (previousRow.getTranslationY() + previousRow.getActualHeight());
360                 if (distanceToGapTop > 0) {
361                     // We interpolate our top roundness so that it's fully rounded if we're at the
362                     // bottom of the gap, and not rounded at all if we're at the top of the gap
363                     // (directly up against the bottom of previousRow)
364                     // Then we apply the same roundness to the bottom of previousRow so that the
365                     // corners join together as the shelf approaches previousRow.
366                     firstElementRoundness = (float) Math.min(1.0, distanceToGapTop / mGapHeight);
367                     previousRow.setBottomRoundness(firstElementRoundness,
368                             false /* don't animate */);
369                     backgroundTop = (int) distanceToGapBottom;
370                 }
371             }
372             previousRow = row;
373         }
374         clipTransientViews();
375 
376         setClipTopAmount(clipTopAmount);
377         boolean isHidden = getViewState().hidden || clipTopAmount >= getIntrinsicHeight();
378         if (mShowNotificationShelf) {
379             setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE);
380         }
381         setBackgroundTop(backgroundTop);
382         setFirstElementRoundness(firstElementRoundness);
383         mShelfIcons.setSpeedBumpIndex(mAmbientState.getSpeedBumpIndex());
384         mShelfIcons.calculateIconTranslations();
385         mShelfIcons.applyIconStates();
386         for (int i = 0; i < mHostLayout.getChildCount(); i++) {
387             View child = mHostLayout.getChildAt(i);
388             if (!(child instanceof ExpandableNotificationRow)
389                     || child.getVisibility() == GONE) {
390                 continue;
391             }
392             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
393             updateIconClipAmount(row);
394             updateContinuousClipping(row);
395         }
396         boolean hideBackground = numViewsInShelf < 1.0f;
397         setHideBackground(hideBackground || backgroundForceHidden);
398         if (mNotGoneIndex == -1) {
399             mNotGoneIndex = notGoneIndex;
400         }
401     }
402 
403     /**
404      * Clips transient views to the top of the shelf - Transient views are only used for
405      * disappearing views/animations and need to be clipped correctly by the shelf to ensure they
406      * don't show underneath the notification stack when something is animating and the user
407      * swipes quickly.
408      */
409     private void clipTransientViews() {
410         for (int i = 0; i < mHostLayout.getTransientViewCount(); i++) {
411             View transientView = mHostLayout.getTransientView(i);
412             if (transientView instanceof ExpandableNotificationRow) {
413                 ExpandableNotificationRow transientRow = (ExpandableNotificationRow) transientView;
414                 updateNotificationClipHeight(transientRow, getTranslationY(), -1);
415             } else {
416                 Log.e(TAG, "NotificationShelf.clipTransientViews(): "
417                         + "Trying to clip non-row transient view");
418             }
419         }
420     }
421 
422     private void setFirstElementRoundness(float firstElementRoundness) {
423         if (mFirstElementRoundness != firstElementRoundness) {
424             mFirstElementRoundness = firstElementRoundness;
425             setTopRoundness(firstElementRoundness, false /* animate */);
426         }
427     }
428 
429     private void updateIconClipAmount(ExpandableNotificationRow row) {
430         float maxTop = row.getTranslationY();
431         if (getClipTopAmount() != 0) {
432             // if the shelf is clipped, lets make sure we also clip the icon
433             maxTop = Math.max(maxTop, getTranslationY() + getClipTopAmount());
434         }
435         StatusBarIconView icon = row.getEntry().expandedIcon;
436         float shelfIconPosition = getTranslationY() + icon.getTop() + icon.getTranslationY();
437         if (shelfIconPosition < maxTop && !mAmbientState.isFullyHidden()) {
438             int top = (int) (maxTop - shelfIconPosition);
439             Rect clipRect = new Rect(0, top, icon.getWidth(), Math.max(top, icon.getHeight()));
440             icon.setClipBounds(clipRect);
441         } else {
442             icon.setClipBounds(null);
443         }
444     }
445 
446     private void updateContinuousClipping(final ExpandableNotificationRow row) {
447         StatusBarIconView icon = row.getEntry().expandedIcon;
448         boolean needsContinuousClipping = ViewState.isAnimatingY(icon) && !mAmbientState.isDozing();
449         boolean isContinuousClipping = icon.getTag(TAG_CONTINUOUS_CLIPPING) != null;
450         if (needsContinuousClipping && !isContinuousClipping) {
451             final ViewTreeObserver observer = icon.getViewTreeObserver();
452             ViewTreeObserver.OnPreDrawListener predrawListener =
453                     new ViewTreeObserver.OnPreDrawListener() {
454                         @Override
455                         public boolean onPreDraw() {
456                             boolean animatingY = ViewState.isAnimatingY(icon);
457                             if (!animatingY) {
458                                 if (observer.isAlive()) {
459                                     observer.removeOnPreDrawListener(this);
460                                 }
461                                 icon.setTag(TAG_CONTINUOUS_CLIPPING, null);
462                                 return true;
463                             }
464                             updateIconClipAmount(row);
465                             return true;
466                         }
467                     };
468             observer.addOnPreDrawListener(predrawListener);
469             icon.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
470                 @Override
471                 public void onViewAttachedToWindow(View v) {
472                 }
473 
474                 @Override
475                 public void onViewDetachedFromWindow(View v) {
476                     if (v == icon) {
477                         if (observer.isAlive()) {
478                             observer.removeOnPreDrawListener(predrawListener);
479                         }
480                         icon.setTag(TAG_CONTINUOUS_CLIPPING, null);
481                     }
482                 }
483             });
484             icon.setTag(TAG_CONTINUOUS_CLIPPING, predrawListener);
485         }
486     }
487 
488     /**
489      * Update the clipping of this view.
490      * @return the amount that our own top should be clipped
491      */
492     private int updateNotificationClipHeight(ActivatableNotificationView row,
493             float notificationClipEnd, int childIndex) {
494         float viewEnd = row.getTranslationY() + row.getActualHeight();
495         boolean isPinned = (row.isPinned() || row.isHeadsUpAnimatingAway())
496                 && !mAmbientState.isDozingAndNotPulsing(row);
497         boolean shouldClipOwnTop;
498         if (mAmbientState.isPulseExpanding()) {
499             shouldClipOwnTop = childIndex == 0;
500         } else {
501             shouldClipOwnTop = row.showingPulsing();
502         }
503         if (viewEnd > notificationClipEnd && !shouldClipOwnTop
504                 && (mAmbientState.isShadeExpanded() || !isPinned)) {
505             int clipBottomAmount = (int) (viewEnd - notificationClipEnd);
506             if (isPinned) {
507                 clipBottomAmount = Math.min(row.getIntrinsicHeight() - row.getCollapsedHeight(),
508                         clipBottomAmount);
509             }
510             row.setClipBottomAmount(clipBottomAmount);
511         } else {
512             row.setClipBottomAmount(0);
513         }
514         if (shouldClipOwnTop) {
515             return (int) (viewEnd - getTranslationY());
516         } else {
517             return 0;
518         }
519     }
520 
521     @Override
522     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
523             int outlineTranslation) {
524         if (!mHasItemsInStableShelf) {
525             shadowIntensity = 0.0f;
526         }
527         super.setFakeShadowIntensity(shadowIntensity, outlineAlpha, shadowYEnd, outlineTranslation);
528     }
529 
530     /**
531      * @return the icon amount how much this notification is in the shelf;
532      */
533     private float updateIconAppearance(ExpandableNotificationRow row, float expandAmount,
534             boolean scrolling, boolean scrollingFast, boolean expandingAnimated,
535             boolean isLastChild) {
536         StatusBarIconView icon = row.getEntry().expandedIcon;
537         NotificationIconContainer.IconState iconState = getIconState(icon);
538         if (iconState == null) {
539             return 0.0f;
540         }
541 
542         // Let calculate how much the view is in the shelf
543         float viewStart = row.getTranslationY();
544         int fullHeight = row.getActualHeight() + mPaddingBetweenElements;
545         float iconTransformDistance = getIntrinsicHeight() * 1.5f;
546         iconTransformDistance *= NotificationUtils.interpolate(1.f, 1.5f, expandAmount);
547         iconTransformDistance = Math.min(iconTransformDistance, fullHeight);
548         if (isLastChild) {
549             fullHeight = Math.min(fullHeight, row.getMinHeight() - getIntrinsicHeight());
550             iconTransformDistance = Math.min(iconTransformDistance, row.getMinHeight()
551                     - getIntrinsicHeight());
552         }
553         float viewEnd = viewStart + fullHeight;
554         // TODO: fix this check for anchor scrolling.
555         if (expandingAnimated && mAmbientState.getScrollY() == 0
556                 && !mAmbientState.isOnKeyguard() && !iconState.isLastExpandIcon) {
557             // We are expanding animated. Because we switch to a linear interpolation in this case,
558             // the last icon may be stuck in between the shelf position and the notification
559             // position, which looks pretty bad. We therefore optimize this case by applying a
560             // shorter transition such that the icon is either fully in the notification or we clamp
561             // it into the shelf if it's close enough.
562             // We need to persist this, since after the expansion, the behavior should still be the
563             // same.
564             float position = mAmbientState.getIntrinsicPadding()
565                     + mHostLayout.getPositionInLinearLayout(row);
566             int maxShelfStart = mMaxLayoutHeight - getIntrinsicHeight();
567             if (position < maxShelfStart && position + row.getIntrinsicHeight() >= maxShelfStart
568                     && row.getTranslationY() < position) {
569                 iconState.isLastExpandIcon = true;
570                 iconState.customTransformHeight = NO_VALUE;
571                 // Let's check if we're close enough to snap into the shelf
572                 boolean forceInShelf = mMaxLayoutHeight - getIntrinsicHeight() - position
573                         < getIntrinsicHeight();
574                 if (!forceInShelf) {
575                     // We are overlapping the shelf but not enough, so the icon needs to be
576                     // repositioned
577                     iconState.customTransformHeight = (int) (mMaxLayoutHeight
578                             - getIntrinsicHeight() - position);
579                 }
580             }
581         }
582         float fullTransitionAmount;
583         float iconTransitionAmount;
584         float shelfStart = getTranslationY();
585         if (iconState.hasCustomTransformHeight()) {
586             fullHeight = iconState.customTransformHeight;
587             iconTransformDistance = iconState.customTransformHeight;
588         }
589         boolean fullyInOrOut = true;
590         if (viewEnd >= shelfStart && (!mAmbientState.isUnlockHintRunning() || row.isInShelf())
591                 && (mAmbientState.isShadeExpanded()
592                         || (!row.isPinned() && !row.isHeadsUpAnimatingAway()))) {
593             if (viewStart < shelfStart) {
594                 float fullAmount = (shelfStart - viewStart) / fullHeight;
595                 fullAmount = Math.min(1.0f, fullAmount);
596                 float interpolatedAmount =  Interpolators.ACCELERATE_DECELERATE.getInterpolation(
597                         fullAmount);
598                 interpolatedAmount = NotificationUtils.interpolate(
599                         interpolatedAmount, fullAmount, expandAmount);
600                 fullTransitionAmount = 1.0f - interpolatedAmount;
601 
602                 iconTransitionAmount = (shelfStart - viewStart) / iconTransformDistance;
603                 iconTransitionAmount = Math.min(1.0f, iconTransitionAmount);
604                 iconTransitionAmount = 1.0f - iconTransitionAmount;
605                 fullyInOrOut = false;
606             } else {
607                 fullTransitionAmount = 1.0f;
608                 iconTransitionAmount = 1.0f;
609             }
610         } else {
611             fullTransitionAmount = 0.0f;
612             iconTransitionAmount = 0.0f;
613         }
614         if (fullyInOrOut && !expandingAnimated && iconState.isLastExpandIcon) {
615             iconState.isLastExpandIcon = false;
616             iconState.customTransformHeight = NO_VALUE;
617         }
618         updateIconPositioning(row, iconTransitionAmount, fullTransitionAmount,
619                 iconTransformDistance, scrolling, scrollingFast, expandingAnimated, isLastChild);
620         return fullTransitionAmount;
621     }
622 
623     private void updateIconPositioning(ExpandableNotificationRow row, float iconTransitionAmount,
624             float fullTransitionAmount, float iconTransformDistance, boolean scrolling,
625             boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) {
626         StatusBarIconView icon = row.getEntry().expandedIcon;
627         NotificationIconContainer.IconState iconState = getIconState(icon);
628         if (iconState == null) {
629             return;
630         }
631         boolean forceInShelf = iconState.isLastExpandIcon && !iconState.hasCustomTransformHeight();
632         float clampedAmount = iconTransitionAmount > 0.5f ? 1.0f : 0.0f;
633         if (clampedAmount == fullTransitionAmount) {
634             iconState.noAnimations = (scrollingFast || expandingAnimated) && !forceInShelf;
635             iconState.useFullTransitionAmount = iconState.noAnimations
636                 || (!ICON_ANMATIONS_WHILE_SCROLLING && fullTransitionAmount == 0.0f && scrolling);
637             iconState.useLinearTransitionAmount = !ICON_ANMATIONS_WHILE_SCROLLING
638                     && fullTransitionAmount == 0.0f && !mAmbientState.isExpansionChanging();
639             iconState.translateContent = mMaxLayoutHeight - getTranslationY()
640                     - getIntrinsicHeight() > 0;
641         }
642         if (!forceInShelf && (scrollingFast || (expandingAnimated
643                 && iconState.useFullTransitionAmount && !ViewState.isAnimatingY(icon)))) {
644             iconState.cancelAnimations(icon);
645             iconState.useFullTransitionAmount = true;
646             iconState.noAnimations = true;
647         }
648         if (iconState.hasCustomTransformHeight()) {
649             iconState.useFullTransitionAmount = true;
650         }
651         if (iconState.isLastExpandIcon) {
652             iconState.translateContent = false;
653         }
654         float transitionAmount;
655         if (mAmbientState.isHiddenAtAll() && !row.isInShelf()) {
656             transitionAmount = mAmbientState.isFullyHidden() ? 1 : 0;
657         } else if (isLastChild || !USE_ANIMATIONS_WHEN_OPENING || iconState.useFullTransitionAmount
658                 || iconState.useLinearTransitionAmount) {
659             transitionAmount = iconTransitionAmount;
660         } else {
661             // We take the clamped position instead
662             transitionAmount = clampedAmount;
663             iconState.needsCannedAnimation = iconState.clampedAppearAmount != clampedAmount
664                     && !mNoAnimationsInThisFrame;
665         }
666         iconState.iconAppearAmount = !USE_ANIMATIONS_WHEN_OPENING
667                     || iconState.useFullTransitionAmount
668                 ? fullTransitionAmount
669                 : transitionAmount;
670         iconState.clampedAppearAmount = clampedAmount;
671         float contentTransformationAmount = !row.isAboveShelf() && !row.showingPulsing()
672                     && (isLastChild || iconState.translateContent)
673                 ? iconTransitionAmount
674                 : 0.0f;
675         row.setContentTransformationAmount(contentTransformationAmount, isLastChild);
676         setIconTransformationAmount(row, transitionAmount, iconTransformDistance,
677                 clampedAmount != transitionAmount, isLastChild);
678     }
679 
680     private void setIconTransformationAmount(ExpandableNotificationRow row,
681             float transitionAmount, float iconTransformDistance, boolean usingLinearInterpolation,
682             boolean isLastChild) {
683         StatusBarIconView icon = row.getEntry().expandedIcon;
684         NotificationIconContainer.IconState iconState = getIconState(icon);
685 
686         View rowIcon = row.getNotificationIcon();
687         float notificationIconPosition = row.getTranslationY() + row.getContentTranslation();
688         boolean stayingInShelf = row.isInShelf() && !row.isTransformingIntoShelf();
689         if (usingLinearInterpolation && !stayingInShelf) {
690             // If we interpolate from the notification position, this might lead to a slightly
691             // odd interpolation, since the notification position changes as well. Let's interpolate
692             // from a fixed distance. We can only do this if we don't animate and the icon is
693             // always in the interpolated positon.
694             notificationIconPosition = getTranslationY() - iconTransformDistance;
695         }
696         float notificationIconSize = 0.0f;
697         int iconTopPadding;
698         if (rowIcon != null) {
699             iconTopPadding = row.getRelativeTopPadding(rowIcon);
700             notificationIconSize = rowIcon.getHeight();
701         } else {
702             iconTopPadding = mIconAppearTopPadding;
703         }
704         notificationIconPosition += iconTopPadding;
705         float shelfIconPosition = getTranslationY() + icon.getTop();
706         float iconSize = mAmbientState.isFullyHidden() ? mHiddenShelfIconSize : mIconSize;
707         shelfIconPosition += (icon.getHeight() - icon.getIconScale() * iconSize) / 2.0f;
708         float iconYTranslation = NotificationUtils.interpolate(
709                 notificationIconPosition - shelfIconPosition,
710                 0,
711                 transitionAmount);
712         float shelfIconSize = iconSize * icon.getIconScale();
713         float alpha = 1.0f;
714         boolean noIcon = !row.isShowingIcon();
715         if (noIcon) {
716             // The view currently doesn't have an icon, lets transform it in!
717             alpha = transitionAmount;
718             notificationIconSize = shelfIconSize / 2.0f;
719         }
720         // The notification size is different from the size in the shelf / statusbar
721         float newSize = NotificationUtils.interpolate(notificationIconSize, shelfIconSize,
722                 transitionAmount);
723         if (iconState != null) {
724             iconState.scaleX = newSize / shelfIconSize;
725             iconState.scaleY = iconState.scaleX;
726             iconState.hidden = transitionAmount == 0.0f && !iconState.isAnimating(icon);
727             boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf();
728             if (isAppearing) {
729                 iconState.hidden = true;
730                 iconState.iconAppearAmount = 0.0f;
731             }
732             iconState.alpha = alpha;
733             iconState.yTranslation = iconYTranslation;
734             if (stayingInShelf) {
735                 iconState.iconAppearAmount = 1.0f;
736                 iconState.alpha = 1.0f;
737                 iconState.scaleX = 1.0f;
738                 iconState.scaleY = 1.0f;
739                 iconState.hidden = false;
740             }
741             if (row.isAboveShelf()
742                     || row.showingPulsing()
743                     || (!row.isInShelf() && (isLastChild && row.areGutsExposed()
744                     || row.getTranslationZ() > mAmbientState.getBaseZHeight()))) {
745                 iconState.hidden = true;
746             }
747             int backgroundColor = getBackgroundColorWithoutTint();
748             int shelfColor = icon.getContrastedStaticDrawableColor(backgroundColor);
749             if (!noIcon && shelfColor != StatusBarIconView.NO_COLOR) {
750                 int iconColor = row.getVisibleNotificationHeader().getOriginalIconColor();
751                 shelfColor = NotificationUtils.interpolateColors(iconColor, shelfColor,
752                         iconState.iconAppearAmount);
753             }
754             iconState.iconColor = shelfColor;
755         }
756     }
757 
758     private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) {
759         return mShelfIcons.getIconState(icon);
760     }
761 
762     private float getFullyClosedTranslation() {
763         return - (getIntrinsicHeight() - mStatusBarHeight) / 2;
764     }
765 
766     public int getNotificationMergeSize() {
767         return getIntrinsicHeight();
768     }
769 
770     @Override
771     public boolean hasNoContentHeight() {
772         return true;
773     }
774 
775     private void setHideBackground(boolean hideBackground) {
776         if (mHideBackground != hideBackground) {
777             mHideBackground = hideBackground;
778             updateBackground();
779             updateOutline();
780         }
781     }
782 
783     @Override
784     protected boolean needsOutline() {
785         return !mHideBackground && super.needsOutline();
786     }
787 
788     @Override
789     protected boolean shouldHideBackground() {
790         return super.shouldHideBackground() || mHideBackground;
791     }
792 
793     @Override
794     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
795         super.onLayout(changed, left, top, right, bottom);
796         updateRelativeOffset();
797 
798         // we always want to clip to our sides, such that nothing can draw outside of these bounds
799         int height = getResources().getDisplayMetrics().heightPixels;
800         mClipRect.set(0, -height, getWidth(), height);
801         mShelfIcons.setClipBounds(mClipRect);
802     }
803 
804     private void updateRelativeOffset() {
805         mCollapsedIcons.getLocationOnScreen(mTmp);
806         mRelativeOffset = mTmp[0];
807         getLocationOnScreen(mTmp);
808         mRelativeOffset -= mTmp[0];
809     }
810 
811     @Override
812     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
813         WindowInsets ret = super.onApplyWindowInsets(insets);
814 
815         // NotificationShelf drag from the status bar and the status bar dock on the top
816         // of the display for current design so just focus on the top of ScreenDecorations.
817         // In landscape or multiple window split mode, the NotificationShelf still drag from
818         // the top and the physical notch/cutout goes to the right, left, or both side of the
819         // display so it doesn't matter for the NotificationSelf in landscape.
820         DisplayCutout displayCutout = insets.getDisplayCutout();
821         mCutoutHeight = displayCutout == null || displayCutout.getSafeInsetTop() < 0
822                 ? 0 : displayCutout.getSafeInsetTop();
823 
824         return ret;
825     }
826 
827     private void setOpenedAmount(float openedAmount) {
828         mNoAnimationsInThisFrame = openedAmount == 1.0f && mOpenedAmount == 0.0f;
829         mOpenedAmount = openedAmount;
830         if (!mAmbientState.isPanelFullWidth() || mAmbientState.isDozing()) {
831             // We don't do a transformation at all, lets just assume we are fully opened
832             openedAmount = 1.0f;
833         }
834         int start = mRelativeOffset;
835         if (isLayoutRtl()) {
836             start = getWidth() - start - mCollapsedIcons.getWidth();
837         }
838         int width = (int) NotificationUtils.interpolate(
839                 start + mCollapsedIcons.getFinalTranslationX(),
840                 mShelfIcons.getWidth(),
841                 FAST_OUT_SLOW_IN_REVERSE.getInterpolation(openedAmount));
842         mShelfIcons.setActualLayoutWidth(width);
843         boolean hasOverflow = mCollapsedIcons.hasOverflow();
844         int collapsedPadding = mCollapsedIcons.getPaddingEnd();
845         if (!hasOverflow) {
846             // we have to ensure that adding the low priority notification won't lead to an
847             // overflow
848             collapsedPadding -= mCollapsedIcons.getNoOverflowExtraPadding();
849         } else {
850             // Partial overflow padding will fill enough space to add extra dots
851             collapsedPadding -= mCollapsedIcons.getPartialOverflowExtraPadding();
852         }
853         float padding = NotificationUtils.interpolate(collapsedPadding,
854                 mShelfIcons.getPaddingEnd(),
855                 openedAmount);
856         mShelfIcons.setActualPaddingEnd(padding);
857         float paddingStart = NotificationUtils.interpolate(start,
858                 mShelfIcons.getPaddingStart(), openedAmount);
859         mShelfIcons.setActualPaddingStart(paddingStart);
860         mShelfIcons.setOpenedAmount(openedAmount);
861     }
862 
863     public void setMaxLayoutHeight(int maxLayoutHeight) {
864         mMaxLayoutHeight = maxLayoutHeight;
865     }
866 
867     /**
868      * @return the index of the notification at which the shelf visually resides
869      */
870     public int getNotGoneIndex() {
871         return mNotGoneIndex;
872     }
873 
874     private void setHasItemsInStableShelf(boolean hasItemsInStableShelf) {
875         if (mHasItemsInStableShelf != hasItemsInStableShelf) {
876             mHasItemsInStableShelf = hasItemsInStableShelf;
877             updateInteractiveness();
878         }
879     }
880 
881     /**
882      * @return whether the shelf has any icons in it when a potential animation has finished, i.e
883      *         if the current state would be applied right now
884      */
885     public boolean hasItemsInStableShelf() {
886         return mHasItemsInStableShelf;
887     }
888 
889     public void setCollapsedIcons(NotificationIconContainer collapsedIcons) {
890         mCollapsedIcons = collapsedIcons;
891         mCollapsedIcons.addOnLayoutChangeListener(this);
892     }
893 
894     @Override
895     public void onStateChanged(int newState) {
896         mStatusBarState = newState;
897         updateInteractiveness();
898     }
899 
900     private void updateInteractiveness() {
901         mInteractive = mStatusBarState == StatusBarState.KEYGUARD && mHasItemsInStableShelf;
902         setClickable(mInteractive);
903         setFocusable(mInteractive);
904         setImportantForAccessibility(mInteractive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
905                 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
906     }
907 
908     @Override
909     protected boolean isInteractive() {
910         return mInteractive;
911     }
912 
913     public void setMaxShelfEnd(float maxShelfEnd) {
914         mMaxShelfEnd = maxShelfEnd;
915     }
916 
917     public void setAnimationsEnabled(boolean enabled) {
918         mAnimationsEnabled = enabled;
919         if (!enabled) {
920             // we need to wait with enabling the animations until the first frame has passed
921             mShelfIcons.setAnimationsEnabled(false);
922         }
923     }
924 
925     @Override
926     public boolean hasOverlappingRendering() {
927         return false;  // Shelf only uses alpha for transitions where the difference can't be seen.
928     }
929 
930     @Override
931     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
932         super.onInitializeAccessibilityNodeInfo(info);
933         if (mInteractive) {
934             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
935             AccessibilityNodeInfo.AccessibilityAction unlock
936                     = new AccessibilityNodeInfo.AccessibilityAction(
937                     AccessibilityNodeInfo.ACTION_CLICK,
938                     getContext().getString(R.string.accessibility_overflow_action));
939             info.addAction(unlock);
940         }
941     }
942 
943     @Override
944     public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
945             int oldTop, int oldRight, int oldBottom) {
946         updateRelativeOffset();
947     }
948 
949     public void onUiModeChanged() {
950         updateBackgroundColors();
951     }
952 
953     private class ShelfState extends ExpandableViewState {
954         private float openedAmount;
955         private boolean hasItemsInStableShelf;
956         private float maxShelfEnd;
957 
958         @Override
959         public void applyToView(View view) {
960             if (!mShowNotificationShelf) {
961                 return;
962             }
963 
964             super.applyToView(view);
965             setMaxShelfEnd(maxShelfEnd);
966             setOpenedAmount(openedAmount);
967             updateAppearance();
968             setHasItemsInStableShelf(hasItemsInStableShelf);
969             mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
970         }
971 
972         @Override
973         public void animateTo(View child, AnimationProperties properties) {
974             if (!mShowNotificationShelf) {
975                 return;
976             }
977 
978             super.animateTo(child, properties);
979             setMaxShelfEnd(maxShelfEnd);
980             setOpenedAmount(openedAmount);
981             updateAppearance();
982             setHasItemsInStableShelf(hasItemsInStableShelf);
983             mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
984         }
985     }
986 }
987