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