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