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