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