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