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 android.content.Context;
20 import android.content.res.Configuration;
21 import android.content.res.Resources;
22 import android.graphics.Rect;
23 import android.util.AttributeSet;
24 import android.util.MathUtils;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import android.view.ViewTreeObserver;
28 import android.view.accessibility.AccessibilityNodeInfo;
29 import android.view.animation.Interpolator;
30 import android.view.animation.PathInterpolator;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.internal.policy.SystemBarUtils;
34 import com.android.systemui.R;
35 import com.android.systemui.animation.ShadeInterpolation;
36 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
37 import com.android.systemui.statusbar.notification.NotificationUtils;
38 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
39 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
40 import com.android.systemui.statusbar.notification.row.ExpandableView;
41 import com.android.systemui.statusbar.notification.stack.AmbientState;
42 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
43 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
44 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
45 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm;
46 import com.android.systemui.statusbar.notification.stack.ViewState;
47 import com.android.systemui.statusbar.phone.NotificationIconContainer;
48 
49 /**
50  * A notification shelf view that is placed inside the notification scroller. It manages the
51  * overflow icons that don't fit into the regular list anymore.
52  */
53 public class NotificationShelf extends ActivatableNotificationView implements
54         View.OnLayoutChangeListener, StateListener {
55 
56     private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag;
57     private static final String TAG = "NotificationShelf";
58 
59     // More extreme version of SLOW_OUT_LINEAR_IN which keeps the icon nearly invisible until after
60     // the next icon has translated out of the way, to avoid overlapping.
61     private static final Interpolator ICON_ALPHA_INTERPOLATOR =
62             new PathInterpolator(0.6f, 0f, 0.6f, 0f);
63 
64     private NotificationIconContainer mShelfIcons;
65     private int[] mTmp = new int[2];
66     private boolean mHideBackground;
67     private int mStatusBarHeight;
68     private AmbientState mAmbientState;
69     private NotificationStackScrollLayoutController mHostLayoutController;
70     private int mPaddingBetweenElements;
71     private int mNotGoneIndex;
72     private boolean mHasItemsInStableShelf;
73     private NotificationIconContainer mCollapsedIcons;
74     private int mScrollFastThreshold;
75     private int mStatusBarState;
76     private boolean mInteractive;
77     private boolean mAnimationsEnabled = true;
78     private boolean mShowNotificationShelf;
79     private float mFirstElementRoundness;
80     private Rect mClipRect = new Rect();
81     private int mIndexOfFirstViewInShelf = -1;
82     private float mCornerAnimationDistance;
83     private NotificationShelfController mController;
84 
NotificationShelf(Context context, AttributeSet attrs)85     public NotificationShelf(Context context, AttributeSet attrs) {
86         super(context, attrs);
87     }
88 
89     @Override
90     @VisibleForTesting
onFinishInflate()91     public void onFinishInflate() {
92         super.onFinishInflate();
93         mShelfIcons = findViewById(R.id.content);
94         mShelfIcons.setClipChildren(false);
95         mShelfIcons.setClipToPadding(false);
96 
97         setClipToActualHeight(false);
98         setClipChildren(false);
99         setClipToPadding(false);
100         mShelfIcons.setIsStaticLayout(false);
101         setBottomRoundness(1.0f, false /* animate */);
102         setTopRoundness(1f, false /* animate */);
103 
104         // Setting this to first in section to get the clipping to the top roundness correct. This
105         // value determines the way we are clipping to the top roundness of the overall shade
106         setFirstInSection(true);
107         initDimens();
108     }
109 
bind(AmbientState ambientState, NotificationStackScrollLayoutController hostLayoutController)110     public void bind(AmbientState ambientState,
111             NotificationStackScrollLayoutController hostLayoutController) {
112         mAmbientState = ambientState;
113         mHostLayoutController = hostLayoutController;
114     }
115 
initDimens()116     private void initDimens() {
117         Resources res = getResources();
118         mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
119         mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
120 
121         ViewGroup.LayoutParams layoutParams = getLayoutParams();
122         layoutParams.height = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
123         setLayoutParams(layoutParams);
124 
125         int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding);
126         mShelfIcons.setPadding(padding, 0, padding, 0);
127         mScrollFastThreshold = res.getDimensionPixelOffset(R.dimen.scroll_fast_threshold);
128         mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf);
129         mCornerAnimationDistance = res.getDimensionPixelSize(
130                 R.dimen.notification_corner_animation_distance);
131 
132         mShelfIcons.setInNotificationIconShelf(true);
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
getContentView()145     protected View getContentView() {
146         return mShelfIcons;
147     }
148 
getShelfIcons()149     public NotificationIconContainer getShelfIcons() {
150         return mShelfIcons;
151     }
152 
153     @Override
createExpandableViewState()154     public ExpandableViewState createExpandableViewState() {
155         return new ShelfState();
156     }
157 
158     /** Update the state of the shelf. */
updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState, AmbientState ambientState)159     public void updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState,
160             AmbientState ambientState) {
161         ExpandableView lastView = ambientState.getLastVisibleBackgroundChild();
162         ShelfState viewState = (ShelfState) getViewState();
163         if (mShowNotificationShelf && lastView != null) {
164             ExpandableViewState lastViewState = lastView.getViewState();
165             viewState.copyFrom(lastViewState);
166 
167             viewState.height = getIntrinsicHeight();
168             viewState.zTranslation = ambientState.getBaseZHeight();
169             viewState.clipTopAmount = 0;
170 
171             if (ambientState.isExpansionChanging() && !ambientState.isOnKeyguard()) {
172                 float expansion = ambientState.getExpansionFraction();
173                 viewState.alpha = ShadeInterpolation.getContentAlpha(expansion);
174             } else {
175                 viewState.alpha = 1f - ambientState.getHideAmount();
176             }
177             viewState.belowSpeedBump = mHostLayoutController.getSpeedBumpIndex() == 0;
178             viewState.hideSensitive = false;
179             viewState.xTranslation = getTranslationX();
180             viewState.hasItemsInStableShelf = lastViewState.inShelf;
181             viewState.firstViewInShelf = algorithmState.firstViewInShelf;
182             if (mNotGoneIndex != -1) {
183                 viewState.notGoneIndex = Math.min(viewState.notGoneIndex, mNotGoneIndex);
184             }
185 
186             viewState.hidden = !mAmbientState.isShadeExpanded()
187                     || algorithmState.firstViewInShelf == null;
188 
189             final int indexOfFirstViewInShelf = algorithmState.visibleChildren.indexOf(
190                     algorithmState.firstViewInShelf);
191 
192             if (mAmbientState.isExpansionChanging()
193                     && algorithmState.firstViewInShelf != null
194                     && indexOfFirstViewInShelf > 0) {
195 
196                 // Show shelf if section before it is showing.
197                 final ExpandableView viewBeforeShelf = algorithmState.visibleChildren.get(
198                         indexOfFirstViewInShelf - 1);
199                 if (viewBeforeShelf.getViewState().hidden) {
200                     viewState.hidden = true;
201                 }
202             }
203 
204             final float stackEnd = ambientState.getStackY() + ambientState.getStackHeight();
205             viewState.yTranslation = stackEnd - viewState.height;
206         } else {
207             viewState.hidden = true;
208             viewState.location = ExpandableViewState.LOCATION_GONE;
209             viewState.hasItemsInStableShelf = false;
210         }
211     }
212 
213     /**
214      * Update the shelf appearance based on the other notifications around it. This transforms
215      * the icons from the notification area into the shelf.
216      */
updateAppearance()217     public void updateAppearance() {
218         // If the shelf should not be shown, then there is no need to update anything.
219         if (!mShowNotificationShelf) {
220             return;
221         }
222         mShelfIcons.resetViewStates();
223         float shelfStart = getTranslationY();
224         float numViewsInShelf = 0.0f;
225         View lastChild = mAmbientState.getLastVisibleBackgroundChild();
226         mNotGoneIndex = -1;
227         //  find the first view that doesn't overlap with the shelf
228         int notGoneIndex = 0;
229         int colorOfViewBeforeLast = NO_COLOR;
230         boolean backgroundForceHidden = false;
231         if (mHideBackground && !((ShelfState) getViewState()).hasItemsInStableShelf) {
232             backgroundForceHidden = true;
233         }
234         int colorTwoBefore = NO_COLOR;
235         int previousColor = NO_COLOR;
236         float transitionAmount = 0.0f;
237         float currentScrollVelocity = mAmbientState.getCurrentScrollVelocity();
238         boolean scrollingFast = currentScrollVelocity > mScrollFastThreshold
239                 || (mAmbientState.isExpansionChanging()
240                 && Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold);
241         boolean expandingAnimated = mAmbientState.isExpansionChanging()
242                 && !mAmbientState.isPanelTracking();
243         int baseZHeight = mAmbientState.getBaseZHeight();
244         int backgroundTop = 0;
245         int clipTopAmount = 0;
246         float firstElementRoundness = 0.0f;
247 
248         for (int i = 0; i < mHostLayoutController.getChildCount(); i++) {
249             ExpandableView child = mHostLayoutController.getChildAt(i);
250             if (!child.needsClippingToShelf() || child.getVisibility() == GONE) {
251                 continue;
252             }
253             float notificationClipEnd;
254             boolean aboveShelf = ViewState.getFinalTranslationZ(child) > baseZHeight
255                     || child.isPinned();
256             boolean isLastChild = child == lastChild;
257             final float viewStart = child.getTranslationY();
258 
259             final float inShelfAmount = updateShelfTransformation(i, child, scrollingFast,
260                     expandingAnimated, isLastChild);
261 
262             // TODO(b/172289889) scale mPaddingBetweenElements with expansion amount
263             if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) {
264                 notificationClipEnd = shelfStart + getIntrinsicHeight();
265             } else {
266                 notificationClipEnd = shelfStart - mPaddingBetweenElements;
267             }
268             int clipTop = updateNotificationClipHeight(child, notificationClipEnd, notGoneIndex);
269             clipTopAmount = Math.max(clipTop, clipTopAmount);
270 
271             // If the current row is an ExpandableNotificationRow, update its color, roundedness,
272             // and icon state.
273             if (child instanceof ExpandableNotificationRow) {
274                 ExpandableNotificationRow expandableRow = (ExpandableNotificationRow) child;
275                 numViewsInShelf += inShelfAmount;
276                 int ownColorUntinted = expandableRow.getBackgroundColorWithoutTint();
277                 if (viewStart >= shelfStart && mNotGoneIndex == -1) {
278                     mNotGoneIndex = notGoneIndex;
279                     setTintColor(previousColor);
280                     setOverrideTintColor(colorTwoBefore, transitionAmount);
281 
282                 } else if (mNotGoneIndex == -1) {
283                     colorTwoBefore = previousColor;
284                     transitionAmount = inShelfAmount;
285                 }
286                 // We don't want to modify the color if the notification is hun'd
287                 if (isLastChild && mController.canModifyColorOfNotifications()) {
288                     if (colorOfViewBeforeLast == NO_COLOR) {
289                         colorOfViewBeforeLast = ownColorUntinted;
290                     }
291                     expandableRow.setOverrideTintColor(colorOfViewBeforeLast, inShelfAmount);
292                 } else {
293                     colorOfViewBeforeLast = ownColorUntinted;
294                     expandableRow.setOverrideTintColor(NO_COLOR, 0 /* overrideAmount */);
295                 }
296                 if (notGoneIndex != 0 || !aboveShelf) {
297                     expandableRow.setAboveShelf(false);
298                 }
299                 if (notGoneIndex == 0) {
300                     StatusBarIconView icon = expandableRow.getEntry().getIcons().getShelfIcon();
301                     NotificationIconContainer.IconState iconState = getIconState(icon);
302                     // The icon state might be null in rare cases where the notification is actually
303                     // added to the layout, but not to the shelf. An example are replied messages,
304                     // since they don't show up on AOD
305                     if (iconState != null && iconState.clampedAppearAmount == 1.0f) {
306                         // only if the first icon is fully in the shelf we want to clip to it!
307                         backgroundTop = (int) (child.getTranslationY() - getTranslationY());
308                         firstElementRoundness = expandableRow.getCurrentTopRoundness();
309                     }
310                 }
311 
312                 previousColor = ownColorUntinted;
313                 notGoneIndex++;
314             }
315 
316             if (child instanceof ActivatableNotificationView) {
317                 ActivatableNotificationView anv =
318                         (ActivatableNotificationView) child;
319                 updateCornerRoundnessOnScroll(anv, viewStart, shelfStart);
320             }
321         }
322 
323         clipTransientViews();
324 
325         setClipTopAmount(clipTopAmount);
326 
327         boolean isHidden = getViewState().hidden
328                 || clipTopAmount >= getIntrinsicHeight()
329                 || !mShowNotificationShelf
330                 || numViewsInShelf < 1f;
331 
332         // TODO(b/172289889) transition last icon in shelf to notification icon and vice versa.
333         setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE);
334         setBackgroundTop(backgroundTop);
335         setFirstElementRoundness(firstElementRoundness);
336         mShelfIcons.setSpeedBumpIndex(mHostLayoutController.getSpeedBumpIndex());
337         mShelfIcons.calculateIconTranslations();
338         mShelfIcons.applyIconStates();
339         for (int i = 0; i < mHostLayoutController.getChildCount(); i++) {
340             View child = mHostLayoutController.getChildAt(i);
341             if (!(child instanceof ExpandableNotificationRow)
342                     || child.getVisibility() == GONE) {
343                 continue;
344             }
345             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
346             updateContinuousClipping(row);
347         }
348         boolean hideBackground = isHidden;
349         setHideBackground(hideBackground);
350         if (mNotGoneIndex == -1) {
351             mNotGoneIndex = notGoneIndex;
352         }
353     }
354 
355     private void updateCornerRoundnessOnScroll(ActivatableNotificationView anv, float viewStart,
356             float shelfStart) {
357 
358         final boolean isUnlockedHeadsUp = !mAmbientState.isOnKeyguard()
359                 && !mAmbientState.isShadeExpanded()
360                 && anv instanceof ExpandableNotificationRow
361                 && ((ExpandableNotificationRow) anv).isHeadsUp();
362 
363         final boolean isHunGoingToShade = mAmbientState.isShadeExpanded()
364                 && anv == mAmbientState.getTrackedHeadsUpRow();
365 
366         final boolean shouldUpdateCornerRoundness = viewStart < shelfStart
367                 && !mHostLayoutController.isViewAffectedBySwipe(anv)
368                 && !isUnlockedHeadsUp
369                 && !isHunGoingToShade
370                 && !anv.isAboveShelf()
371                 && !mAmbientState.isPulsing()
372                 && !mAmbientState.isDozing();
373 
374         if (!shouldUpdateCornerRoundness) {
375             return;
376         }
377 
378         final float smallCornerRadius =
379                 getResources().getDimension(R.dimen.notification_corner_radius_small)
380                 /  getResources().getDimension(R.dimen.notification_corner_radius);
381         final float viewEnd = viewStart + anv.getActualHeight();
382         final float cornerAnimationDistance = mCornerAnimationDistance
383                 * mAmbientState.getExpansionFraction();
384         final float cornerAnimationTop = shelfStart - cornerAnimationDistance;
385 
386         if (viewEnd >= cornerAnimationTop) {
387             // Round bottom corners within animation bounds
388             final float changeFraction = MathUtils.saturate(
389                     (viewEnd - cornerAnimationTop) / cornerAnimationDistance);
390             anv.setBottomRoundness(anv.isLastInSection() ? 1f : changeFraction,
391                     false /* animate */);
392 
393         } else if (viewEnd < cornerAnimationTop) {
394             // Fast scroll skips frames and leaves corners with unfinished rounding.
395             // Reset top and bottom corners outside of animation bounds.
396             anv.setBottomRoundness(anv.isLastInSection() ? 1f : smallCornerRadius,
397                     false /* animate */);
398         }
399 
400         if (viewStart >= cornerAnimationTop) {
401             // Round top corners within animation bounds
402             final float changeFraction = MathUtils.saturate(
403                     (viewStart - cornerAnimationTop) / cornerAnimationDistance);
404             anv.setTopRoundness(anv.isFirstInSection() ? 1f : changeFraction,
405                     false /* animate */);
406 
407         } else if (viewStart < cornerAnimationTop) {
408             // Fast scroll skips frames and leaves corners with unfinished rounding.
409             // Reset top and bottom corners outside of animation bounds.
410             anv.setTopRoundness(anv.isFirstInSection() ? 1f : smallCornerRadius,
411                     false /* animate */);
412         }
413     }
414 
415     /**
416      * Clips transient views to the top of the shelf - Transient views are only used for
417      * disappearing views/animations and need to be clipped correctly by the shelf to ensure they
418      * don't show underneath the notification stack when something is animating and the user
419      * swipes quickly.
420      */
421     private void clipTransientViews() {
422         for (int i = 0; i < mHostLayoutController.getTransientViewCount(); i++) {
423             View transientView = mHostLayoutController.getTransientView(i);
424             if (transientView instanceof ExpandableView) {
425                 ExpandableView transientExpandableView = (ExpandableView) transientView;
426                 updateNotificationClipHeight(transientExpandableView, getTranslationY(), -1);
427             }
428         }
429     }
430 
431     private void setFirstElementRoundness(float firstElementRoundness) {
432         if (mFirstElementRoundness != firstElementRoundness) {
433             mFirstElementRoundness = firstElementRoundness;
434         }
435     }
436 
437     private void updateIconClipAmount(ExpandableNotificationRow row) {
438         float maxTop = row.getTranslationY();
439         if (getClipTopAmount() != 0) {
440             // if the shelf is clipped, lets make sure we also clip the icon
441             maxTop = Math.max(maxTop, getTranslationY() + getClipTopAmount());
442         }
443         StatusBarIconView icon = row.getEntry().getIcons().getShelfIcon();
444         float shelfIconPosition = getTranslationY() + icon.getTop() + icon.getTranslationY();
445         if (shelfIconPosition < maxTop && !mAmbientState.isFullyHidden()) {
446             int top = (int) (maxTop - shelfIconPosition);
447             Rect clipRect = new Rect(0, top, icon.getWidth(), Math.max(top, icon.getHeight()));
448             icon.setClipBounds(clipRect);
449         } else {
450             icon.setClipBounds(null);
451         }
452     }
453 
454     private void updateContinuousClipping(final ExpandableNotificationRow row) {
455         StatusBarIconView icon = row.getEntry().getIcons().getShelfIcon();
456         boolean needsContinuousClipping = ViewState.isAnimatingY(icon) && !mAmbientState.isDozing();
457         boolean isContinuousClipping = icon.getTag(TAG_CONTINUOUS_CLIPPING) != null;
458         if (needsContinuousClipping && !isContinuousClipping) {
459             final ViewTreeObserver observer = icon.getViewTreeObserver();
460             ViewTreeObserver.OnPreDrawListener predrawListener =
461                     new ViewTreeObserver.OnPreDrawListener() {
462                         @Override
463                         public boolean onPreDraw() {
464                             boolean animatingY = ViewState.isAnimatingY(icon);
465                             if (!animatingY) {
466                                 if (observer.isAlive()) {
467                                     observer.removeOnPreDrawListener(this);
468                                 }
469                                 icon.setTag(TAG_CONTINUOUS_CLIPPING, null);
470                                 return true;
471                             }
472                             updateIconClipAmount(row);
473                             return true;
474                         }
475                     };
476             observer.addOnPreDrawListener(predrawListener);
477             icon.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
478                 @Override
479                 public void onViewAttachedToWindow(View v) {
480                 }
481 
482                 @Override
483                 public void onViewDetachedFromWindow(View v) {
484                     if (v == icon) {
485                         if (observer.isAlive()) {
486                             observer.removeOnPreDrawListener(predrawListener);
487                         }
488                         icon.setTag(TAG_CONTINUOUS_CLIPPING, null);
489                     }
490                 }
491             });
492             icon.setTag(TAG_CONTINUOUS_CLIPPING, predrawListener);
493         }
494     }
495 
496     /**
497      * Update the clipping of this view.
498      * @return the amount that our own top should be clipped
499      */
500     private int updateNotificationClipHeight(ExpandableView view,
501             float notificationClipEnd, int childIndex) {
502         float viewEnd = view.getTranslationY() + view.getActualHeight();
503         boolean isPinned = (view.isPinned() || view.isHeadsUpAnimatingAway())
504                 && !mAmbientState.isDozingAndNotPulsing(view);
505         boolean shouldClipOwnTop;
506         if (mAmbientState.isPulseExpanding()) {
507             shouldClipOwnTop = childIndex == 0;
508         } else {
509             shouldClipOwnTop = view.showingPulsing();
510         }
511         if (viewEnd > notificationClipEnd && !shouldClipOwnTop
512                 && (mAmbientState.isShadeExpanded() || !isPinned)) {
513             int clipBottomAmount = (int) (viewEnd - notificationClipEnd);
514             if (isPinned) {
515                 clipBottomAmount = Math.min(view.getIntrinsicHeight() - view.getCollapsedHeight(),
516                         clipBottomAmount);
517             }
518             view.setClipBottomAmount(clipBottomAmount);
519         } else {
520             view.setClipBottomAmount(0);
521         }
522         if (shouldClipOwnTop) {
523             return (int) (viewEnd - getTranslationY());
524         } else {
525             return 0;
526         }
527     }
528 
529     @Override
530     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
531             int outlineTranslation) {
532         if (!mHasItemsInStableShelf) {
533             shadowIntensity = 0.0f;
534         }
535         super.setFakeShadowIntensity(shadowIntensity, outlineAlpha, shadowYEnd, outlineTranslation);
536     }
537 
538     /**
539      * @return the amount how much this notification is in the shelf
540      */
541     private float updateShelfTransformation(int i, ExpandableView view, boolean scrollingFast,
542             boolean expandingAnimated, boolean isLastChild) {
543 
544         // Let's calculate how much the view is in the shelf
545         float viewStart = view.getTranslationY();
546         int fullHeight = view.getActualHeight() + mPaddingBetweenElements;
547         float iconTransformStart = calculateIconTransformationStart(view);
548 
549         // Let's make sure the transform distance is
550         // at most to the icon (relevant for conversations)
551         float transformDistance = Math.min(
552                 viewStart + fullHeight - iconTransformStart,
553                 getIntrinsicHeight());
554 
555         if (isLastChild) {
556             fullHeight = Math.min(fullHeight, view.getMinHeight() - getIntrinsicHeight());
557             transformDistance = Math.min(
558                     transformDistance,
559                     view.getMinHeight() - getIntrinsicHeight());
560         }
561 
562         float viewEnd = viewStart + fullHeight;
563         float fullTransitionAmount = 0.0f;
564         float iconTransitionAmount = 0.0f;
565         float shelfStart = getTranslationY();
566         if (mAmbientState.isExpansionChanging() && !mAmbientState.isOnKeyguard()) {
567             // TODO(b/172289889) handle icon placement for notification that is clipped by the shelf
568             if (mIndexOfFirstViewInShelf != -1 && i >= mIndexOfFirstViewInShelf) {
569                 fullTransitionAmount = 1f;
570                 iconTransitionAmount = 1f;
571             }
572         } else if (viewEnd >= shelfStart
573                 && (!mAmbientState.isUnlockHintRunning() || view.isInShelf())
574                 && (mAmbientState.isShadeExpanded()
575                 || (!view.isPinned() && !view.isHeadsUpAnimatingAway()))) {
576 
577             if (viewStart < shelfStart) {
578                 float fullAmount = (shelfStart - viewStart) / fullHeight;
579                 fullAmount = Math.min(1.0f, fullAmount);
580                 fullTransitionAmount = 1.0f - fullAmount;
581                 if (isLastChild) {
582                     // Reduce icon transform distance to completely fade in shelf icon
583                     // by the time the notification icon fades out, and vice versa
584                     iconTransitionAmount = (shelfStart - viewStart)
585                             / (iconTransformStart - viewStart);
586                 } else {
587                     iconTransitionAmount = (shelfStart - iconTransformStart) / transformDistance;
588                 }
589                 iconTransitionAmount = MathUtils.constrain(iconTransitionAmount, 0.0f, 1.0f);
590                 iconTransitionAmount = 1.0f - iconTransitionAmount;
591             } else {
592                 // Fully in shelf.
593                 fullTransitionAmount = 1.0f;
594                 iconTransitionAmount = 1.0f;
595             }
596         }
597         updateIconPositioning(view, iconTransitionAmount,
598                 scrollingFast, expandingAnimated, isLastChild);
599         return fullTransitionAmount;
600     }
601 
602     /**
603      * @return the location where the transformation into the shelf should start.
604      */
605     private float calculateIconTransformationStart(ExpandableView view) {
606         View target = view.getShelfTransformationTarget();
607         if (target == null) {
608             return view.getTranslationY();
609         }
610         float start = view.getTranslationY() + view.getRelativeTopPadding(target);
611 
612         // Let's not start the transformation right at the icon but by the padding before it.
613         start -= view.getShelfIcon().getTop();
614         return start;
615     }
616 
617     private void updateIconPositioning(ExpandableView view, float iconTransitionAmount,
618             boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) {
619         StatusBarIconView icon = view.getShelfIcon();
620         NotificationIconContainer.IconState iconState = getIconState(icon);
621         if (iconState == null) {
622             return;
623         }
624         boolean clampInShelf = iconTransitionAmount > 0.5f || isTargetClipped(view);
625         float clampedAmount = clampInShelf ? 1.0f : 0.0f;
626         if (iconTransitionAmount == clampedAmount) {
627             iconState.noAnimations = (scrollingFast || expandingAnimated) && !isLastChild;
628         }
629         if (!isLastChild
630                 && (scrollingFast || (expandingAnimated && !ViewState.isAnimatingY(icon)))) {
631             iconState.cancelAnimations(icon);
632             iconState.noAnimations = true;
633         }
634         float transitionAmount;
635         if (mAmbientState.isHiddenAtAll() && !view.isInShelf()) {
636             transitionAmount = mAmbientState.isFullyHidden() ? 1 : 0;
637         } else {
638             transitionAmount = iconTransitionAmount;
639             iconState.needsCannedAnimation = iconState.clampedAppearAmount != clampedAmount;
640         }
641         iconState.clampedAppearAmount = clampedAmount;
642         setIconTransformationAmount(view, transitionAmount);
643     }
644 
645     private boolean isTargetClipped(ExpandableView view) {
646         View target = view.getShelfTransformationTarget();
647         if (target == null) {
648             return false;
649         }
650         // We should never clip the target, let's instead put it into the shelf!
651         float endOfTarget = view.getTranslationY()
652                 + view.getContentTranslation()
653                 + view.getRelativeTopPadding(target)
654                 + target.getHeight();
655         return endOfTarget >= getTranslationY() - mPaddingBetweenElements;
656     }
657 
658     private void setIconTransformationAmount(ExpandableView view, float transitionAmount) {
659         if (!(view instanceof ExpandableNotificationRow)) {
660             return;
661         }
662         ExpandableNotificationRow row = (ExpandableNotificationRow) view;
663         StatusBarIconView icon = row.getShelfIcon();
664         NotificationIconContainer.IconState iconState = getIconState(icon);
665         if (iconState == null) {
666             return;
667         }
668         iconState.alpha = ICON_ALPHA_INTERPOLATOR.getInterpolation(transitionAmount);
669         boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf();
670         iconState.hidden = isAppearing
671                 || (view instanceof ExpandableNotificationRow
672                 && ((ExpandableNotificationRow) view).isLowPriority()
673                 && mShelfIcons.hasMaxNumDot())
674                 || (transitionAmount == 0.0f && !iconState.isAnimating(icon))
675                 || row.isAboveShelf()
676                 || row.showingPulsing()
677                 || row.getTranslationZ() > mAmbientState.getBaseZHeight();
678 
679         iconState.iconAppearAmount = iconState.hidden? 0f : transitionAmount;
680 
681         // Fade in icons at shelf start
682         // This is important for conversation icons, which are badged and need x reset
683         iconState.xTranslation = mShelfIcons.getActualPaddingStart();
684 
685         final boolean stayingInShelf = row.isInShelf() && !row.isTransformingIntoShelf();
686         if (stayingInShelf) {
687             iconState.iconAppearAmount = 1.0f;
688             iconState.alpha = 1.0f;
689             iconState.hidden = false;
690         }
691         int backgroundColor = getBackgroundColorWithoutTint();
692         int shelfColor = icon.getContrastedStaticDrawableColor(backgroundColor);
693         if (row.isShowingIcon() && shelfColor != StatusBarIconView.NO_COLOR) {
694             int iconColor = row.getOriginalIconColor();
695             shelfColor = NotificationUtils.interpolateColors(iconColor, shelfColor,
696                     iconState.iconAppearAmount);
697         }
698         iconState.iconColor = shelfColor;
699     }
700 
getIconState(StatusBarIconView icon)701     private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) {
702         return mShelfIcons.getIconState(icon);
703     }
704 
getFullyClosedTranslation()705     private float getFullyClosedTranslation() {
706         return - (getIntrinsicHeight() - mStatusBarHeight) / 2;
707     }
708 
709     @Override
hasNoContentHeight()710     public boolean hasNoContentHeight() {
711         return true;
712     }
713 
setHideBackground(boolean hideBackground)714     private void setHideBackground(boolean hideBackground) {
715         if (mHideBackground != hideBackground) {
716             mHideBackground = hideBackground;
717             updateOutline();
718         }
719     }
720 
721     @Override
needsOutline()722     protected boolean needsOutline() {
723         return !mHideBackground && super.needsOutline();
724     }
725 
726 
727     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)728     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
729         super.onLayout(changed, left, top, right, bottom);
730         updateRelativeOffset();
731 
732         // we always want to clip to our sides, such that nothing can draw outside of these bounds
733         int height = getResources().getDisplayMetrics().heightPixels;
734         mClipRect.set(0, -height, getWidth(), height);
735         mShelfIcons.setClipBounds(mClipRect);
736     }
737 
updateRelativeOffset()738     private void updateRelativeOffset() {
739         mCollapsedIcons.getLocationOnScreen(mTmp);
740         getLocationOnScreen(mTmp);
741     }
742 
743     /**
744      * @return the index of the notification at which the shelf visually resides
745      */
getNotGoneIndex()746     public int getNotGoneIndex() {
747         return mNotGoneIndex;
748     }
749 
setHasItemsInStableShelf(boolean hasItemsInStableShelf)750     private void setHasItemsInStableShelf(boolean hasItemsInStableShelf) {
751         if (mHasItemsInStableShelf != hasItemsInStableShelf) {
752             mHasItemsInStableShelf = hasItemsInStableShelf;
753             updateInteractiveness();
754         }
755     }
756 
757     /**
758      * @return whether the shelf has any icons in it when a potential animation has finished, i.e
759      *         if the current state would be applied right now
760      */
hasItemsInStableShelf()761     public boolean hasItemsInStableShelf() {
762         return mHasItemsInStableShelf;
763     }
764 
setCollapsedIcons(NotificationIconContainer collapsedIcons)765     public void setCollapsedIcons(NotificationIconContainer collapsedIcons) {
766         mCollapsedIcons = collapsedIcons;
767         mCollapsedIcons.addOnLayoutChangeListener(this);
768     }
769 
770     @Override
onStateChanged(int newState)771     public void onStateChanged(int newState) {
772         mStatusBarState = newState;
773         updateInteractiveness();
774     }
775 
updateInteractiveness()776     private void updateInteractiveness() {
777         mInteractive = mStatusBarState == StatusBarState.KEYGUARD && mHasItemsInStableShelf;
778         setClickable(mInteractive);
779         setFocusable(mInteractive);
780         setImportantForAccessibility(mInteractive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
781                 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
782     }
783 
784     @Override
isInteractive()785     protected boolean isInteractive() {
786         return mInteractive;
787     }
788 
setAnimationsEnabled(boolean enabled)789     public void setAnimationsEnabled(boolean enabled) {
790         mAnimationsEnabled = enabled;
791         if (!enabled) {
792             // we need to wait with enabling the animations until the first frame has passed
793             mShelfIcons.setAnimationsEnabled(false);
794         }
795     }
796 
797     @Override
hasOverlappingRendering()798     public boolean hasOverlappingRendering() {
799         return false;  // Shelf only uses alpha for transitions where the difference can't be seen.
800     }
801 
802     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)803     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
804         super.onInitializeAccessibilityNodeInfo(info);
805         if (mInteractive) {
806             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
807             AccessibilityNodeInfo.AccessibilityAction unlock
808                     = new AccessibilityNodeInfo.AccessibilityAction(
809                     AccessibilityNodeInfo.ACTION_CLICK,
810                     getContext().getString(R.string.accessibility_overflow_action));
811             info.addAction(unlock);
812         }
813     }
814 
815     @Override
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)816     public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
817             int oldTop, int oldRight, int oldBottom) {
818         updateRelativeOffset();
819     }
820 
821     @Override
needsClippingToShelf()822     public boolean needsClippingToShelf() {
823         return false;
824     }
825 
setController(NotificationShelfController notificationShelfController)826     public void setController(NotificationShelfController notificationShelfController) {
827         mController = notificationShelfController;
828     }
829 
setIndexOfFirstViewInShelf(ExpandableView firstViewInShelf)830     public void setIndexOfFirstViewInShelf(ExpandableView firstViewInShelf) {
831         mIndexOfFirstViewInShelf = mHostLayoutController.indexOfChild(firstViewInShelf);
832     }
833 
834     private class ShelfState extends ExpandableViewState {
835         private boolean hasItemsInStableShelf;
836         private ExpandableView firstViewInShelf;
837 
838         @Override
applyToView(View view)839         public void applyToView(View view) {
840             if (!mShowNotificationShelf) {
841                 return;
842             }
843 
844             super.applyToView(view);
845             setIndexOfFirstViewInShelf(firstViewInShelf);
846             updateAppearance();
847             setHasItemsInStableShelf(hasItemsInStableShelf);
848             mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
849         }
850 
851         @Override
animateTo(View child, AnimationProperties properties)852         public void animateTo(View child, AnimationProperties properties) {
853             if (!mShowNotificationShelf) {
854                 return;
855             }
856 
857             super.animateTo(child, properties);
858             setIndexOfFirstViewInShelf(firstViewInShelf);
859             updateAppearance();
860             setHasItemsInStableShelf(hasItemsInStableShelf);
861             mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
862         }
863     }
864 }
865