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.phone;
18 
19 import static com.android.systemui.statusbar.phone.HeadsUpAppearanceController.CONTENT_FADE_DELAY;
20 import static com.android.systemui.statusbar.phone.HeadsUpAppearanceController.CONTENT_FADE_DURATION;
21 
22 import android.content.Context;
23 import android.content.res.Configuration;
24 import android.graphics.Canvas;
25 import android.graphics.Color;
26 import android.graphics.Paint;
27 import android.graphics.Rect;
28 import android.graphics.drawable.Icon;
29 import android.util.AttributeSet;
30 import android.util.Property;
31 import android.view.ContextThemeWrapper;
32 import android.view.View;
33 import android.view.animation.Interpolator;
34 
35 import androidx.collection.ArrayMap;
36 
37 import com.android.internal.statusbar.StatusBarIcon;
38 import com.android.settingslib.Utils;
39 import com.android.systemui.R;
40 import com.android.systemui.animation.Interpolators;
41 import com.android.systemui.statusbar.AlphaOptimizedFrameLayout;
42 import com.android.systemui.statusbar.StatusBarIconView;
43 import com.android.systemui.statusbar.notification.stack.AnimationFilter;
44 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
45 import com.android.systemui.statusbar.notification.stack.ViewState;
46 
47 import java.util.ArrayList;
48 import java.util.HashMap;
49 import java.util.function.Consumer;
50 
51 /**
52  * A container for notification icons. It handles overflowing icons properly and positions them
53  * correctly on the screen.
54  */
55 public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
56     /**
57      * A float value indicating how much before the overflow start the icons should transform into
58      * a dot. A value of 0 means that they are exactly at the end and a value of 1 means it starts
59      * 1 icon width early.
60      */
61     public static final float OVERFLOW_EARLY_AMOUNT = 0.2f;
62     private static final int NO_VALUE = Integer.MIN_VALUE;
63     private static final String TAG = "NotificationIconContainer";
64     private static final boolean DEBUG = false;
65     private static final boolean DEBUG_OVERFLOW = false;
66     private static final int CANNED_ANIMATION_DURATION = 100;
67     private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() {
68         private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
69 
70         @Override
71         public AnimationFilter getAnimationFilter() {
72             return mAnimationFilter;
73         }
74     }.setDuration(200);
75 
76     private static final AnimationProperties ICON_ANIMATION_PROPERTIES = new AnimationProperties() {
77         private AnimationFilter mAnimationFilter = new AnimationFilter()
78                 .animateX()
79                 .animateY()
80                 .animateAlpha()
81                 .animateScale();
82 
83         @Override
84         public AnimationFilter getAnimationFilter() {
85             return mAnimationFilter;
86         }
87 
88     }.setDuration(CANNED_ANIMATION_DURATION);
89 
90     /**
91      * Temporary AnimationProperties to avoid unnecessary allocations.
92      */
93     private static final AnimationProperties sTempProperties = new AnimationProperties() {
94         private AnimationFilter mAnimationFilter = new AnimationFilter();
95 
96         @Override
97         public AnimationFilter getAnimationFilter() {
98             return mAnimationFilter;
99         }
100     };
101 
102     private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
103         private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
104 
105         @Override
106         public AnimationFilter getAnimationFilter() {
107             return mAnimationFilter;
108         }
109     }.setDuration(200).setDelay(50);
110 
111     /**
112      * The animation property used for all icons that were not isolated, when the isolation ends.
113      * This just fades the alpha and doesn't affect the movement and has a delay.
114      */
115     private static final AnimationProperties UNISOLATION_PROPERTY_OTHERS
116             = new AnimationProperties() {
117         private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
118 
119         @Override
120         public AnimationFilter getAnimationFilter() {
121             return mAnimationFilter;
122         }
123     }.setDuration(CONTENT_FADE_DURATION);
124 
125     /**
126      * The animation property used for the icon when its isolation ends.
127      * This animates the translation back to the right position.
128      */
129     private static final AnimationProperties UNISOLATION_PROPERTY = new AnimationProperties() {
130         private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
131 
132         @Override
133         public AnimationFilter getAnimationFilter() {
134             return mAnimationFilter;
135         }
136     }.setDuration(CONTENT_FADE_DURATION);
137 
138     private static final int MAX_VISIBLE_ICONS_ON_LOCK = 5;
139     public static final int MAX_STATIC_ICONS = 4;
140     private static final int MAX_DOTS = 1;
141 
142     private boolean mIsStaticLayout = true;
143     private final HashMap<View, IconState> mIconStates = new HashMap<>();
144     private int mDotPadding;
145     private int mStaticDotRadius;
146     private int mStaticDotDiameter;
147     private int mOverflowWidth;
148     private int mActualLayoutWidth = NO_VALUE;
149     private float mActualPaddingEnd = NO_VALUE;
150     private float mActualPaddingStart = NO_VALUE;
151     private boolean mDozing;
152     private boolean mOnLockScreen;
153     private boolean mInNotificationIconShelf;
154     private boolean mChangingViewPositions;
155     private int mAddAnimationStartIndex = -1;
156     private int mCannedAnimationStartIndex = -1;
157     private int mSpeedBumpIndex = -1;
158     private int mIconSize;
159     private boolean mDisallowNextAnimation;
160     private boolean mAnimationsEnabled = true;
161     private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIcons;
162     // Keep track of the last visible icon so collapsed container can report on its location
163     private IconState mLastVisibleIconState;
164     private IconState mFirstVisibleIconState;
165     private float mVisualOverflowStart;
166     // Keep track of overflow in range [0, 3]
167     private int mNumDots;
168     private StatusBarIconView mIsolatedIcon;
169     private Rect mIsolatedIconLocation;
170     private int[] mAbsolutePosition = new int[2];
171     private View mIsolatedIconForAnimation;
172     private int mThemedTextColorPrimary;
173 
NotificationIconContainer(Context context, AttributeSet attrs)174     public NotificationIconContainer(Context context, AttributeSet attrs) {
175         super(context, attrs);
176         initDimens();
177         setWillNotDraw(!(DEBUG || DEBUG_OVERFLOW));
178     }
179 
initDimens()180     private void initDimens() {
181         mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
182         mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
183         mStaticDotDiameter = 2 * mStaticDotRadius;
184         final Context themedContext = new ContextThemeWrapper(getContext(),
185                 com.android.internal.R.style.Theme_DeviceDefault_DayNight);
186         mThemedTextColorPrimary = Utils.getColorAttr(themedContext,
187                 com.android.internal.R.attr.textColorPrimary).getDefaultColor();
188     }
189 
190     @Override
onDraw(Canvas canvas)191     protected void onDraw(Canvas canvas) {
192         super.onDraw(canvas);
193         Paint paint = new Paint();
194         paint.setColor(Color.RED);
195         paint.setStyle(Paint.Style.STROKE);
196         canvas.drawRect(getActualPaddingStart(), 0, getLayoutEnd(), getHeight(), paint);
197 
198         if (DEBUG_OVERFLOW) {
199             if (mLastVisibleIconState == null) {
200                 return;
201             }
202 
203             int height = getHeight();
204             int end = getFinalTranslationX();
205 
206             // Visualize the "end" of the layout
207             paint.setColor(Color.BLUE);
208             canvas.drawLine(end, 0, end, height, paint);
209 
210             paint.setColor(Color.GREEN);
211             int lastIcon = (int) mLastVisibleIconState.xTranslation;
212             canvas.drawLine(lastIcon, 0, lastIcon, height, paint);
213 
214             if (mFirstVisibleIconState != null) {
215                 int firstIcon = (int) mFirstVisibleIconState.xTranslation;
216                 canvas.drawLine(firstIcon, 0, firstIcon, height, paint);
217             }
218 
219             paint.setColor(Color.RED);
220             canvas.drawLine(mVisualOverflowStart, 0, mVisualOverflowStart, height, paint);
221 
222             paint.setColor(Color.YELLOW);
223             float overflow = getMaxOverflowStart();
224             canvas.drawLine(overflow, 0, overflow, height, paint);
225         }
226     }
227 
228     @Override
onConfigurationChanged(Configuration newConfig)229     protected void onConfigurationChanged(Configuration newConfig) {
230         super.onConfigurationChanged(newConfig);
231         initDimens();
232     }
233 
234     @Override
onLayout(boolean changed, int l, int t, int r, int b)235     protected void onLayout(boolean changed, int l, int t, int r, int b) {
236         float centerY = getHeight() / 2.0f;
237         // we layout all our children on the left at the top
238         mIconSize = 0;
239         for (int i = 0; i < getChildCount(); i++) {
240             View child = getChildAt(i);
241             // We need to layout all children even the GONE ones, such that the heights are
242             // calculated correctly as they are used to calculate how many we can fit on the screen
243             int width = child.getMeasuredWidth();
244             int height = child.getMeasuredHeight();
245             int top = (int) (centerY - height / 2.0f);
246             child.layout(0, top, width, top + height);
247             if (i == 0) {
248                 setIconSize(child.getWidth());
249             }
250         }
251         getLocationOnScreen(mAbsolutePosition);
252         if (mIsStaticLayout) {
253             updateState();
254         }
255     }
256 
setIconSize(int size)257     private void setIconSize(int size) {
258         mIconSize = size;
259         mOverflowWidth = mIconSize + (MAX_DOTS - 1) * (mStaticDotDiameter + mDotPadding);
260     }
261 
updateState()262     private void updateState() {
263         resetViewStates();
264         calculateIconTranslations();
265         applyIconStates();
266     }
267 
applyIconStates()268     public void applyIconStates() {
269         for (int i = 0; i < getChildCount(); i++) {
270             View child = getChildAt(i);
271             ViewState childState = mIconStates.get(child);
272             if (childState != null) {
273                 childState.applyToView(child);
274             }
275         }
276         mAddAnimationStartIndex = -1;
277         mCannedAnimationStartIndex = -1;
278         mDisallowNextAnimation = false;
279         mIsolatedIconForAnimation = null;
280     }
281 
282     @Override
onViewAdded(View child)283     public void onViewAdded(View child) {
284         super.onViewAdded(child);
285         boolean isReplacingIcon = isReplacingIcon(child);
286         if (!mChangingViewPositions) {
287             IconState v = new IconState(child);
288             if (isReplacingIcon) {
289                 v.justAdded = false;
290                 v.justReplaced = true;
291             }
292             mIconStates.put(child, v);
293         }
294         int childIndex = indexOfChild(child);
295         if (childIndex < getChildCount() - 1 && !isReplacingIcon
296             && mIconStates.get(getChildAt(childIndex + 1)).iconAppearAmount > 0.0f) {
297             if (mAddAnimationStartIndex < 0) {
298                 mAddAnimationStartIndex = childIndex;
299             } else {
300                 mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, childIndex);
301             }
302         }
303         if (child instanceof StatusBarIconView) {
304             ((StatusBarIconView) child).setDozing(mDozing, false, 0);
305         }
306     }
307 
isReplacingIcon(View child)308     private boolean isReplacingIcon(View child) {
309         if (mReplacingIcons == null) {
310             return false;
311         }
312         if (!(child instanceof StatusBarIconView)) {
313             return false;
314         }
315         StatusBarIconView iconView = (StatusBarIconView) child;
316         Icon sourceIcon = iconView.getSourceIcon();
317         String groupKey = iconView.getNotification().getGroupKey();
318         ArrayList<StatusBarIcon> statusBarIcons = mReplacingIcons.get(groupKey);
319         if (statusBarIcons != null) {
320             StatusBarIcon replacedIcon = statusBarIcons.get(0);
321             if (sourceIcon.sameAs(replacedIcon.icon)) {
322                 return true;
323             }
324         }
325         return false;
326     }
327 
328     @Override
onViewRemoved(View child)329     public void onViewRemoved(View child) {
330         super.onViewRemoved(child);
331 
332         if (child instanceof StatusBarIconView) {
333             boolean isReplacingIcon = isReplacingIcon(child);
334             final StatusBarIconView icon = (StatusBarIconView) child;
335             if (areAnimationsEnabled(icon) && icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
336                     && child.getVisibility() == VISIBLE && isReplacingIcon) {
337                 int animationStartIndex = findFirstViewIndexAfter(icon.getTranslationX());
338                 if (mAddAnimationStartIndex < 0) {
339                     mAddAnimationStartIndex = animationStartIndex;
340                 } else {
341                     mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, animationStartIndex);
342                 }
343             }
344             if (!mChangingViewPositions) {
345                 mIconStates.remove(child);
346                 if (areAnimationsEnabled(icon) && !isReplacingIcon) {
347                     addTransientView(icon, 0);
348                     boolean isIsolatedIcon = child == mIsolatedIcon;
349                     icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, true /* animate */,
350                             () -> removeTransientView(icon),
351                             isIsolatedIcon ? CONTENT_FADE_DURATION : 0);
352                 }
353             }
354         }
355     }
356 
hasMaxNumDot()357     public boolean hasMaxNumDot() {
358         return mNumDots >= MAX_DOTS;
359     }
360 
areAnimationsEnabled(StatusBarIconView icon)361     private boolean areAnimationsEnabled(StatusBarIconView icon) {
362         return mAnimationsEnabled || icon == mIsolatedIcon;
363     }
364 
365     /**
366      * Finds the first view with a translation bigger then a given value
367      */
findFirstViewIndexAfter(float translationX)368     private int findFirstViewIndexAfter(float translationX) {
369         for (int i = 0; i < getChildCount(); i++) {
370             View view = getChildAt(i);
371             if (view.getTranslationX() > translationX) {
372                 return i;
373             }
374         }
375         return getChildCount();
376     }
377 
resetViewStates()378     public void resetViewStates() {
379         for (int i = 0; i < getChildCount(); i++) {
380             View view = getChildAt(i);
381             ViewState iconState = mIconStates.get(view);
382             iconState.initFrom(view);
383             iconState.alpha = mIsolatedIcon == null || view == mIsolatedIcon ? 1.0f : 0.0f;
384             iconState.hidden = false;
385         }
386     }
387 
388     /**
389      * Calculate the horizontal translations for each notification based on how much the icons
390      * are inserted into the notification container.
391      * If this is not a whole number, the fraction means by how much the icon is appearing.
392      */
calculateIconTranslations()393     public void calculateIconTranslations() {
394         float translationX = getActualPaddingStart();
395         int firstOverflowIndex = -1;
396         int childCount = getChildCount();
397         int maxVisibleIcons = mOnLockScreen ? MAX_VISIBLE_ICONS_ON_LOCK :
398                 mIsStaticLayout ? MAX_STATIC_ICONS : childCount;
399         float layoutEnd = getLayoutEnd();
400         float overflowStart = getMaxOverflowStart();
401         mVisualOverflowStart = 0;
402         mFirstVisibleIconState = null;
403         for (int i = 0; i < childCount; i++) {
404             View view = getChildAt(i);
405             IconState iconState = mIconStates.get(view);
406             if (iconState.iconAppearAmount == 1.0f) {
407                 // We only modify the xTranslation if it's fully inside of the container
408                 // since during the transition to the shelf, the translations are controlled
409                 // from the outside
410                 iconState.xTranslation = translationX;
411             }
412             if (mFirstVisibleIconState == null) {
413                 mFirstVisibleIconState = iconState;
414             }
415             boolean forceOverflow = mSpeedBumpIndex != -1 && i >= mSpeedBumpIndex
416                     && iconState.iconAppearAmount > 0.0f || i >= maxVisibleIcons;
417             boolean noOverflowAfter = i == childCount - 1;
418             float drawingScale = mOnLockScreen && view instanceof StatusBarIconView
419                     ? ((StatusBarIconView) view).getIconScaleIncreased()
420                     : 1f;
421             iconState.visibleState = iconState.hidden
422                     ? StatusBarIconView.STATE_HIDDEN
423                     : StatusBarIconView.STATE_ICON;
424 
425             boolean isOverflowing =
426                     (translationX > (noOverflowAfter ? layoutEnd - mIconSize
427                             : overflowStart - mIconSize));
428             if (firstOverflowIndex == -1 && (forceOverflow || isOverflowing)) {
429                 firstOverflowIndex = noOverflowAfter && !forceOverflow ? i - 1 : i;
430                 mVisualOverflowStart = layoutEnd - mOverflowWidth;
431                 if (forceOverflow || mIsStaticLayout) {
432                     mVisualOverflowStart = Math.min(translationX, mVisualOverflowStart);
433                 }
434             }
435             translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
436         }
437         mNumDots = 0;
438         if (firstOverflowIndex != -1) {
439             translationX = mVisualOverflowStart;
440             for (int i = firstOverflowIndex; i < childCount; i++) {
441                 View view = getChildAt(i);
442                 IconState iconState = mIconStates.get(view);
443                 int dotWidth = mStaticDotDiameter + mDotPadding;
444                 iconState.xTranslation = translationX;
445                 if (mNumDots < MAX_DOTS) {
446                     if (mNumDots == 0 && iconState.iconAppearAmount < 0.8f) {
447                         iconState.visibleState = StatusBarIconView.STATE_ICON;
448                     } else {
449                         iconState.visibleState = StatusBarIconView.STATE_DOT;
450                         mNumDots++;
451                     }
452                     translationX += (mNumDots == MAX_DOTS ? MAX_DOTS * dotWidth : dotWidth)
453                             * iconState.iconAppearAmount;
454                     mLastVisibleIconState = iconState;
455                 } else {
456                     iconState.visibleState = StatusBarIconView.STATE_HIDDEN;
457                 }
458             }
459         } else if (childCount > 0) {
460             View lastChild = getChildAt(childCount - 1);
461             mLastVisibleIconState = mIconStates.get(lastChild);
462             mFirstVisibleIconState = mIconStates.get(getChildAt(0));
463         }
464 
465         if (isLayoutRtl()) {
466             for (int i = 0; i < childCount; i++) {
467                 View view = getChildAt(i);
468                 IconState iconState = mIconStates.get(view);
469                 iconState.xTranslation = getWidth() - iconState.xTranslation - view.getWidth();
470             }
471         }
472         if (mIsolatedIcon != null) {
473             IconState iconState = mIconStates.get(mIsolatedIcon);
474             if (iconState != null) {
475                 // Most of the time the icon isn't yet added when this is called but only happening
476                 // later
477                 iconState.xTranslation = mIsolatedIconLocation.left - mAbsolutePosition[0]
478                         - (1 - mIsolatedIcon.getIconScale()) * mIsolatedIcon.getWidth() / 2.0f;
479                 iconState.visibleState = StatusBarIconView.STATE_ICON;
480             }
481         }
482     }
483 
getLayoutEnd()484     private float getLayoutEnd() {
485         return getActualWidth() - getActualPaddingEnd();
486     }
487 
getActualPaddingEnd()488     private float getActualPaddingEnd() {
489         if (mActualPaddingEnd == NO_VALUE) {
490             return getPaddingEnd();
491         }
492         return mActualPaddingEnd;
493     }
494 
495     /**
496      * @return the actual startPadding of this view
497      */
getActualPaddingStart()498     public float getActualPaddingStart() {
499         if (mActualPaddingStart == NO_VALUE) {
500             return getPaddingStart();
501         }
502         return mActualPaddingStart;
503     }
504 
505     /**
506      * Sets whether the layout should always show the same number of icons.
507      * If this is true, the icon positions will be updated on layout.
508      * If this if false, the layout is managed from the outside and layouting won't trigger a
509      * repositioning of the icons.
510      */
setIsStaticLayout(boolean isStaticLayout)511     public void setIsStaticLayout(boolean isStaticLayout) {
512         mIsStaticLayout = isStaticLayout;
513     }
514 
setActualLayoutWidth(int actualLayoutWidth)515     public void setActualLayoutWidth(int actualLayoutWidth) {
516         mActualLayoutWidth = actualLayoutWidth;
517         if (DEBUG) {
518             invalidate();
519         }
520     }
521 
setActualPaddingEnd(float paddingEnd)522     public void setActualPaddingEnd(float paddingEnd) {
523         mActualPaddingEnd = paddingEnd;
524         if (DEBUG) {
525             invalidate();
526         }
527     }
528 
setActualPaddingStart(float paddingStart)529     public void setActualPaddingStart(float paddingStart) {
530         mActualPaddingStart = paddingStart;
531         if (DEBUG) {
532             invalidate();
533         }
534     }
535 
getActualWidth()536     public int getActualWidth() {
537         if (mActualLayoutWidth == NO_VALUE) {
538             return getWidth();
539         }
540         return mActualLayoutWidth;
541     }
542 
getFinalTranslationX()543     public int getFinalTranslationX() {
544         if (mLastVisibleIconState == null) {
545             return 0;
546         }
547 
548         int translation = (int) (isLayoutRtl() ? getWidth() - mLastVisibleIconState.xTranslation
549                 : mLastVisibleIconState.xTranslation + mIconSize);
550         // There's a chance that last translation goes beyond the edge maybe
551         return Math.min(getWidth(), translation);
552     }
553 
getMaxOverflowStart()554     private float getMaxOverflowStart() {
555         return getLayoutEnd() - mOverflowWidth;
556     }
557 
setChangingViewPositions(boolean changingViewPositions)558     public void setChangingViewPositions(boolean changingViewPositions) {
559         mChangingViewPositions = changingViewPositions;
560     }
561 
setDozing(boolean dozing, boolean fade, long delay)562     public void setDozing(boolean dozing, boolean fade, long delay) {
563         mDozing = dozing;
564         mDisallowNextAnimation |= !fade;
565         for (int i = 0; i < getChildCount(); i++) {
566             View view = getChildAt(i);
567             if (view instanceof StatusBarIconView) {
568                 ((StatusBarIconView) view).setDozing(dozing, fade, delay);
569             }
570         }
571     }
572 
getIconState(StatusBarIconView icon)573     public IconState getIconState(StatusBarIconView icon) {
574         return mIconStates.get(icon);
575     }
576 
setSpeedBumpIndex(int speedBumpIndex)577     public void setSpeedBumpIndex(int speedBumpIndex) {
578         mSpeedBumpIndex = speedBumpIndex;
579     }
580 
hasOverflow()581     public boolean hasOverflow() {
582         return mNumDots > 0;
583     }
584 
585     /**
586      * If the overflow is in the range [1, max_dots - 1) (basically 1 or 2 dots), then
587      * extra padding will have to be accounted for
588      *
589      * This method has no meaning for non-static containers
590      */
hasPartialOverflow()591     public boolean hasPartialOverflow() {
592         return mNumDots > 0 && mNumDots < MAX_DOTS;
593     }
594 
595     /**
596      * Get padding that can account for extra dots up to the max. The only valid values for
597      * this method are for 1 or 2 dots.
598      * @return only extraDotPadding or extraDotPadding * 2
599      */
getPartialOverflowExtraPadding()600     public int getPartialOverflowExtraPadding() {
601         if (!hasPartialOverflow()) {
602             return 0;
603         }
604 
605         int partialOverflowAmount = (MAX_DOTS - mNumDots) * (mStaticDotDiameter + mDotPadding);
606 
607         int adjustedWidth = getFinalTranslationX() + partialOverflowAmount;
608         // In case we actually give too much padding...
609         if (adjustedWidth > getWidth()) {
610             partialOverflowAmount = getWidth() - getFinalTranslationX();
611         }
612 
613         return partialOverflowAmount;
614     }
615 
616     // Give some extra room for btw notifications if we can
getNoOverflowExtraPadding()617     public int getNoOverflowExtraPadding() {
618         if (mNumDots != 0) {
619             return 0;
620         }
621 
622         int collapsedPadding = mOverflowWidth;
623 
624         if (collapsedPadding + getFinalTranslationX() > getWidth()) {
625             collapsedPadding = getWidth() - getFinalTranslationX();
626         }
627 
628         return collapsedPadding;
629     }
630 
getIconSize()631     public int getIconSize() {
632         return mIconSize;
633     }
634 
setAnimationsEnabled(boolean enabled)635     public void setAnimationsEnabled(boolean enabled) {
636         if (!enabled && mAnimationsEnabled) {
637             for (int i = 0; i < getChildCount(); i++) {
638                 View child = getChildAt(i);
639                 ViewState childState = mIconStates.get(child);
640                 if (childState != null) {
641                     childState.cancelAnimations(child);
642                     childState.applyToView(child);
643                 }
644             }
645         }
646         mAnimationsEnabled = enabled;
647     }
648 
setReplacingIcons(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons)649     public void setReplacingIcons(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) {
650         mReplacingIcons = replacingIcons;
651     }
652 
showIconIsolated(StatusBarIconView icon, boolean animated)653     public void showIconIsolated(StatusBarIconView icon, boolean animated) {
654         if (animated) {
655             mIsolatedIconForAnimation = icon != null ? icon : mIsolatedIcon;
656         }
657         mIsolatedIcon = icon;
658         updateState();
659     }
660 
setIsolatedIconLocation(Rect isolatedIconLocation, boolean requireUpdate)661     public void setIsolatedIconLocation(Rect isolatedIconLocation, boolean requireUpdate) {
662         mIsolatedIconLocation = isolatedIconLocation;
663         if (requireUpdate) {
664             updateState();
665         }
666     }
667 
668     /**
669      * Set whether the device is on the lockscreen and which lockscreen mode the device is
670      * configured to. Depending on these values, the layout of the AOD icons change.
671      */
setOnLockScreen(boolean onLockScreen)672     public void setOnLockScreen(boolean onLockScreen) {
673         mOnLockScreen = onLockScreen;
674     }
675 
setInNotificationIconShelf(boolean inShelf)676     public void setInNotificationIconShelf(boolean inShelf) {
677         mInNotificationIconShelf = inShelf;
678     }
679 
680     public class IconState extends ViewState {
681         public static final int NO_VALUE = NotificationIconContainer.NO_VALUE;
682         public float iconAppearAmount = 1.0f;
683         public float clampedAppearAmount = 1.0f;
684         public int visibleState;
685         public boolean justAdded = true;
686         private boolean justReplaced;
687         public boolean needsCannedAnimation;
688         public int iconColor = StatusBarIconView.NO_COLOR;
689         public boolean noAnimations;
690         private final View mView;
691 
692         private final Consumer<Property> mCannedAnimationEndListener;
693 
IconState(View child)694         public IconState(View child) {
695             mView = child;
696             mCannedAnimationEndListener = (property) -> {
697                 // If we finished animating out of the shelf
698                 if (property == View.TRANSLATION_Y && iconAppearAmount == 0.0f
699                         && mView.getVisibility() == VISIBLE) {
700                     mView.setVisibility(INVISIBLE);
701                 }
702             };
703         }
704 
705         @Override
applyToView(View view)706         public void applyToView(View view) {
707             if (view instanceof StatusBarIconView) {
708                 StatusBarIconView icon = (StatusBarIconView) view;
709                 boolean animate = false;
710                 AnimationProperties animationProperties = null;
711                 final boolean isLowPriorityIconChange =
712                         (visibleState == StatusBarIconView.STATE_HIDDEN
713                                 && icon.getVisibleState() == StatusBarIconView.STATE_DOT)
714                         || (visibleState == StatusBarIconView.STATE_DOT
715                             && icon.getVisibleState() == StatusBarIconView.STATE_HIDDEN);
716                 final boolean animationsAllowed = areAnimationsEnabled(icon)
717                         && !mDisallowNextAnimation
718                         && !noAnimations
719                         && !isLowPriorityIconChange;
720                 if (animationsAllowed) {
721                     if (justAdded || justReplaced) {
722                         super.applyToView(icon);
723                         if (justAdded && iconAppearAmount != 0.0f) {
724                             icon.setAlpha(0.0f);
725                             icon.setVisibleState(StatusBarIconView.STATE_HIDDEN,
726                                     false /* animate */);
727                             animationProperties = ADD_ICON_PROPERTIES;
728                             animate = true;
729                         }
730                     } else if (visibleState != icon.getVisibleState()) {
731                         animationProperties = DOT_ANIMATION_PROPERTIES;
732                         animate = true;
733                     }
734                     if (!animate && mAddAnimationStartIndex >= 0
735                             && indexOfChild(view) >= mAddAnimationStartIndex
736                             && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
737                             || visibleState != StatusBarIconView.STATE_HIDDEN)) {
738                         animationProperties = DOT_ANIMATION_PROPERTIES;
739                         animate = true;
740                     }
741                     if (needsCannedAnimation) {
742                         AnimationFilter animationFilter = sTempProperties.getAnimationFilter();
743                         animationFilter.reset();
744                         animationFilter.combineFilter(
745                                 ICON_ANIMATION_PROPERTIES.getAnimationFilter());
746                         sTempProperties.resetCustomInterpolators();
747                         sTempProperties.combineCustomInterpolators(ICON_ANIMATION_PROPERTIES);
748                         Interpolator interpolator;
749                         if (icon.showsConversation()) {
750                             interpolator = Interpolators.ICON_OVERSHOT_LESS;
751                         } else {
752                             interpolator = Interpolators.ICON_OVERSHOT;
753                         }
754                         sTempProperties.setCustomInterpolator(View.TRANSLATION_Y, interpolator);
755                         sTempProperties.setAnimationEndAction(mCannedAnimationEndListener);
756                         if (animationProperties != null) {
757                             animationFilter.combineFilter(animationProperties.getAnimationFilter());
758                             sTempProperties.combineCustomInterpolators(animationProperties);
759                         }
760                         animationProperties = sTempProperties;
761                         animationProperties.setDuration(CANNED_ANIMATION_DURATION);
762                         animate = true;
763                         mCannedAnimationStartIndex = indexOfChild(view);
764                     }
765                     if (!animate && mCannedAnimationStartIndex >= 0
766                             && indexOfChild(view) > mCannedAnimationStartIndex
767                             && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
768                             || visibleState != StatusBarIconView.STATE_HIDDEN)) {
769                         AnimationFilter animationFilter = sTempProperties.getAnimationFilter();
770                         animationFilter.reset();
771                         animationFilter.animateX();
772                         sTempProperties.resetCustomInterpolators();
773                         animationProperties = sTempProperties;
774                         animationProperties.setDuration(CANNED_ANIMATION_DURATION);
775                         animate = true;
776                     }
777                     if (mIsolatedIconForAnimation != null) {
778                         if (view == mIsolatedIconForAnimation) {
779                             animationProperties = UNISOLATION_PROPERTY;
780                             animationProperties.setDelay(
781                                     mIsolatedIcon != null ? CONTENT_FADE_DELAY : 0);
782                         } else {
783                             animationProperties = UNISOLATION_PROPERTY_OTHERS;
784                             animationProperties.setDelay(
785                                     mIsolatedIcon == null ? CONTENT_FADE_DELAY : 0);
786                         }
787                         animate = true;
788                     }
789                 }
790                 icon.setVisibleState(visibleState, animationsAllowed);
791                 icon.setIconColor(mInNotificationIconShelf ? mThemedTextColorPrimary : iconColor,
792                         needsCannedAnimation && animationsAllowed);
793                 if (animate) {
794                     animateTo(icon, animationProperties);
795                 } else {
796                     super.applyToView(view);
797                 }
798                 sTempProperties.setAnimationEndAction(null);
799                 boolean inShelf = iconAppearAmount == 1.0f;
800                 icon.setIsInShelf(inShelf);
801             }
802             justAdded = false;
803             justReplaced = false;
804             needsCannedAnimation = false;
805         }
806 
807         @Override
initFrom(View view)808         public void initFrom(View view) {
809             super.initFrom(view);
810             if (view instanceof StatusBarIconView) {
811                 iconColor = ((StatusBarIconView) view).getStaticDrawableColor();
812             }
813         }
814     }
815 }
816