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.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.content.Context;
25 import android.content.res.Configuration;
26 import android.graphics.Canvas;
27 import android.graphics.Color;
28 import android.graphics.Paint;
29 import android.graphics.Rect;
30 import android.graphics.drawable.Icon;
31 import android.util.AttributeSet;
32 import android.util.Property;
33 import android.view.View;
34 import android.view.animation.Interpolator;
35 
36 import androidx.collection.ArrayMap;
37 
38 import com.android.internal.statusbar.StatusBarIcon;
39 import com.android.systemui.Interpolators;
40 import com.android.systemui.R;
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 mChangingViewPositions;
154     private int mAddAnimationStartIndex = -1;
155     private int mCannedAnimationStartIndex = -1;
156     private int mSpeedBumpIndex = -1;
157     private int mIconSize;
158     private float mOpenedAmount = 0.0f;
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 
NotificationIconContainer(Context context, AttributeSet attrs)173     public NotificationIconContainer(Context context, AttributeSet attrs) {
174         super(context, attrs);
175         initDimens();
176         setWillNotDraw(!(DEBUG || DEBUG_OVERFLOW));
177     }
178 
initDimens()179     private void initDimens() {
180         mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
181         mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
182         mStaticDotDiameter = 2 * mStaticDotRadius;
183     }
184 
185     @Override
onDraw(Canvas canvas)186     protected void onDraw(Canvas canvas) {
187         super.onDraw(canvas);
188         Paint paint = new Paint();
189         paint.setColor(Color.RED);
190         paint.setStyle(Paint.Style.STROKE);
191         canvas.drawRect(getActualPaddingStart(), 0, getLayoutEnd(), getHeight(), paint);
192 
193         if (DEBUG_OVERFLOW) {
194             if (mLastVisibleIconState == null) {
195                 return;
196             }
197 
198             int height = getHeight();
199             int end = getFinalTranslationX();
200 
201             // Visualize the "end" of the layout
202             paint.setColor(Color.BLUE);
203             canvas.drawLine(end, 0, end, height, paint);
204 
205             paint.setColor(Color.GREEN);
206             int lastIcon = (int) mLastVisibleIconState.xTranslation;
207             canvas.drawLine(lastIcon, 0, lastIcon, height, paint);
208 
209             if (mFirstVisibleIconState != null) {
210                 int firstIcon = (int) mFirstVisibleIconState.xTranslation;
211                 canvas.drawLine(firstIcon, 0, firstIcon, height, paint);
212             }
213 
214             paint.setColor(Color.RED);
215             canvas.drawLine(mVisualOverflowStart, 0, mVisualOverflowStart, height, paint);
216 
217             paint.setColor(Color.YELLOW);
218             float overflow = getMaxOverflowStart();
219             canvas.drawLine(overflow, 0, overflow, height, paint);
220         }
221     }
222 
223     @Override
onConfigurationChanged(Configuration newConfig)224     protected void onConfigurationChanged(Configuration newConfig) {
225         super.onConfigurationChanged(newConfig);
226         initDimens();
227     }
228 
229     @Override
onLayout(boolean changed, int l, int t, int r, int b)230     protected void onLayout(boolean changed, int l, int t, int r, int b) {
231         float centerY = getHeight() / 2.0f;
232         // we layout all our children on the left at the top
233         mIconSize = 0;
234         for (int i = 0; i < getChildCount(); i++) {
235             View child = getChildAt(i);
236             // We need to layout all children even the GONE ones, such that the heights are
237             // calculated correctly as they are used to calculate how many we can fit on the screen
238             int width = child.getMeasuredWidth();
239             int height = child.getMeasuredHeight();
240             int top = (int) (centerY - height / 2.0f);
241             child.layout(0, top, width, top + height);
242             if (i == 0) {
243                 setIconSize(child.getWidth());
244             }
245         }
246         getLocationOnScreen(mAbsolutePosition);
247         if (mIsStaticLayout) {
248             updateState();
249         }
250     }
251 
setIconSize(int size)252     private void setIconSize(int size) {
253         mIconSize = size;
254         mOverflowWidth = mIconSize + (MAX_DOTS - 1) * (mStaticDotDiameter + mDotPadding);
255     }
256 
updateState()257     private void updateState() {
258         resetViewStates();
259         calculateIconTranslations();
260         applyIconStates();
261     }
262 
applyIconStates()263     public void applyIconStates() {
264         for (int i = 0; i < getChildCount(); i++) {
265             View child = getChildAt(i);
266             ViewState childState = mIconStates.get(child);
267             if (childState != null) {
268                 childState.applyToView(child);
269             }
270         }
271         mAddAnimationStartIndex = -1;
272         mCannedAnimationStartIndex = -1;
273         mDisallowNextAnimation = false;
274         mIsolatedIconForAnimation = null;
275     }
276 
277     @Override
onViewAdded(View child)278     public void onViewAdded(View child) {
279         super.onViewAdded(child);
280         boolean isReplacingIcon = isReplacingIcon(child);
281         if (!mChangingViewPositions) {
282             IconState v = new IconState(child);
283             if (isReplacingIcon) {
284                 v.justAdded = false;
285                 v.justReplaced = true;
286             }
287             mIconStates.put(child, v);
288         }
289         int childIndex = indexOfChild(child);
290         if (childIndex < getChildCount() - 1 && !isReplacingIcon
291             && mIconStates.get(getChildAt(childIndex + 1)).iconAppearAmount > 0.0f) {
292             if (mAddAnimationStartIndex < 0) {
293                 mAddAnimationStartIndex = childIndex;
294             } else {
295                 mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, childIndex);
296             }
297         }
298         if (child instanceof StatusBarIconView) {
299             ((StatusBarIconView) child).setDozing(mDozing, false, 0);
300         }
301     }
302 
isReplacingIcon(View child)303     private boolean isReplacingIcon(View child) {
304         if (mReplacingIcons == null) {
305             return false;
306         }
307         if (!(child instanceof StatusBarIconView)) {
308             return false;
309         }
310         StatusBarIconView iconView = (StatusBarIconView) child;
311         Icon sourceIcon = iconView.getSourceIcon();
312         String groupKey = iconView.getNotification().getGroupKey();
313         ArrayList<StatusBarIcon> statusBarIcons = mReplacingIcons.get(groupKey);
314         if (statusBarIcons != null) {
315             StatusBarIcon replacedIcon = statusBarIcons.get(0);
316             if (sourceIcon.sameAs(replacedIcon.icon)) {
317                 return true;
318             }
319         }
320         return false;
321     }
322 
323     @Override
onViewRemoved(View child)324     public void onViewRemoved(View child) {
325         super.onViewRemoved(child);
326 
327         if (child instanceof StatusBarIconView) {
328             boolean isReplacingIcon = isReplacingIcon(child);
329             final StatusBarIconView icon = (StatusBarIconView) child;
330             if (areAnimationsEnabled(icon) && icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
331                     && child.getVisibility() == VISIBLE && isReplacingIcon) {
332                 int animationStartIndex = findFirstViewIndexAfter(icon.getTranslationX());
333                 if (mAddAnimationStartIndex < 0) {
334                     mAddAnimationStartIndex = animationStartIndex;
335                 } else {
336                     mAddAnimationStartIndex = Math.min(mAddAnimationStartIndex, animationStartIndex);
337                 }
338             }
339             if (!mChangingViewPositions) {
340                 mIconStates.remove(child);
341                 if (areAnimationsEnabled(icon) && !isReplacingIcon) {
342                     addTransientView(icon, 0);
343                     boolean isIsolatedIcon = child == mIsolatedIcon;
344                     icon.setVisibleState(StatusBarIconView.STATE_HIDDEN, true /* animate */,
345                             () -> removeTransientView(icon),
346                             isIsolatedIcon ? CONTENT_FADE_DURATION : 0);
347                 }
348             }
349         }
350     }
351 
areAnimationsEnabled(StatusBarIconView icon)352     private boolean areAnimationsEnabled(StatusBarIconView icon) {
353         return mAnimationsEnabled || icon == mIsolatedIcon;
354     }
355 
356     /**
357      * Finds the first view with a translation bigger then a given value
358      */
findFirstViewIndexAfter(float translationX)359     private int findFirstViewIndexAfter(float translationX) {
360         for (int i = 0; i < getChildCount(); i++) {
361             View view = getChildAt(i);
362             if (view.getTranslationX() > translationX) {
363                 return i;
364             }
365         }
366         return getChildCount();
367     }
368 
resetViewStates()369     public void resetViewStates() {
370         for (int i = 0; i < getChildCount(); i++) {
371             View view = getChildAt(i);
372             ViewState iconState = mIconStates.get(view);
373             iconState.initFrom(view);
374             iconState.alpha = mIsolatedIcon == null || view == mIsolatedIcon ? 1.0f : 0.0f;
375             iconState.hidden = false;
376         }
377     }
378 
379     /**
380      * Calculate the horizontal translations for each notification based on how much the icons
381      * are inserted into the notification container.
382      * If this is not a whole number, the fraction means by how much the icon is appearing.
383      */
calculateIconTranslations()384     public void calculateIconTranslations() {
385         float translationX = getActualPaddingStart();
386         int firstOverflowIndex = -1;
387         int childCount = getChildCount();
388         int maxVisibleIcons = mOnLockScreen ? MAX_VISIBLE_ICONS_ON_LOCK :
389                 mIsStaticLayout ? MAX_STATIC_ICONS : childCount;
390         float layoutEnd = getLayoutEnd();
391         float overflowStart = getMaxOverflowStart();
392         mVisualOverflowStart = 0;
393         mFirstVisibleIconState = null;
394         boolean hasAmbient = mSpeedBumpIndex != -1 && mSpeedBumpIndex < getChildCount();
395         for (int i = 0; i < childCount; i++) {
396             View view = getChildAt(i);
397             IconState iconState = mIconStates.get(view);
398             if (iconState.iconAppearAmount == 1.0f) {
399                 // We only modify the xTranslation if it's fully inside of the container
400                 // since during the transition to the shelf, the translations are controlled
401                 // from the outside
402                 iconState.xTranslation = translationX;
403             }
404             if (mFirstVisibleIconState == null) {
405                 mFirstVisibleIconState = iconState;
406             }
407             boolean forceOverflow = mSpeedBumpIndex != -1 && i >= mSpeedBumpIndex
408                     && iconState.iconAppearAmount > 0.0f || i >= maxVisibleIcons;
409             boolean noOverflowAfter = i == childCount - 1;
410             float drawingScale = mOnLockScreen && view instanceof StatusBarIconView
411                     ? ((StatusBarIconView) view).getIconScaleIncreased()
412                     : 1f;
413             if (mOpenedAmount != 0.0f) {
414                 noOverflowAfter = noOverflowAfter && !hasAmbient && !forceOverflow;
415             }
416             iconState.visibleState = StatusBarIconView.STATE_ICON;
417 
418             boolean isOverflowing =
419                     (translationX > (noOverflowAfter ? layoutEnd - mIconSize
420                             : overflowStart - mIconSize));
421             if (firstOverflowIndex == -1 && (forceOverflow || isOverflowing)) {
422                 firstOverflowIndex = noOverflowAfter && !forceOverflow ? i - 1 : i;
423                 mVisualOverflowStart = layoutEnd - mOverflowWidth;
424                 if (forceOverflow || mIsStaticLayout) {
425                     mVisualOverflowStart = Math.min(translationX, mVisualOverflowStart);
426                 }
427             }
428             translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
429         }
430         mNumDots = 0;
431         if (firstOverflowIndex != -1) {
432             translationX = mVisualOverflowStart;
433             for (int i = firstOverflowIndex; i < childCount; i++) {
434                 View view = getChildAt(i);
435                 IconState iconState = mIconStates.get(view);
436                 int dotWidth = mStaticDotDiameter + mDotPadding;
437                 iconState.xTranslation = translationX;
438                 if (mNumDots < MAX_DOTS) {
439                     if (mNumDots == 0 && iconState.iconAppearAmount < 0.8f) {
440                         iconState.visibleState = StatusBarIconView.STATE_ICON;
441                     } else {
442                         iconState.visibleState = StatusBarIconView.STATE_DOT;
443                         mNumDots++;
444                     }
445                     translationX += (mNumDots == MAX_DOTS ? MAX_DOTS * dotWidth : dotWidth)
446                             * iconState.iconAppearAmount;
447                     mLastVisibleIconState = iconState;
448                 } else {
449                     iconState.visibleState = StatusBarIconView.STATE_HIDDEN;
450                 }
451             }
452         } else if (childCount > 0) {
453             View lastChild = getChildAt(childCount - 1);
454             mLastVisibleIconState = mIconStates.get(lastChild);
455             mFirstVisibleIconState = mIconStates.get(getChildAt(0));
456         }
457 
458         boolean center = mOnLockScreen;
459         if (center && translationX < getLayoutEnd()) {
460             float initialTranslation =
461                     mFirstVisibleIconState == null ? 0 : mFirstVisibleIconState.xTranslation;
462 
463             float contentWidth = 0;
464             if (mLastVisibleIconState != null) {
465                 contentWidth = mLastVisibleIconState.xTranslation + mIconSize;
466                 contentWidth = Math.min(getWidth(), contentWidth) - initialTranslation;
467             }
468             float availableSpace = getLayoutEnd() - getActualPaddingStart();
469             float delta = (availableSpace - contentWidth) / 2;
470 
471             if (firstOverflowIndex != -1) {
472                 // If we have an overflow, only count those half for centering because the dots
473                 // don't have a lot of visual weight.
474                 float deltaIgnoringOverflow = (getLayoutEnd() - mVisualOverflowStart) / 2;
475                 delta = (deltaIgnoringOverflow + delta) / 2;
476             }
477             for (int i = 0; i < childCount; i++) {
478                 View view = getChildAt(i);
479                 IconState iconState = mIconStates.get(view);
480                 iconState.xTranslation += delta;
481             }
482         }
483 
484         if (isLayoutRtl()) {
485             for (int i = 0; i < childCount; i++) {
486                 View view = getChildAt(i);
487                 IconState iconState = mIconStates.get(view);
488                 iconState.xTranslation = getWidth() - iconState.xTranslation - view.getWidth();
489             }
490         }
491         if (mIsolatedIcon != null) {
492             IconState iconState = mIconStates.get(mIsolatedIcon);
493             if (iconState != null) {
494                 // Most of the time the icon isn't yet added when this is called but only happening
495                 // later
496                 iconState.xTranslation = mIsolatedIconLocation.left - mAbsolutePosition[0]
497                         - (1 - mIsolatedIcon.getIconScale()) * mIsolatedIcon.getWidth() / 2.0f;
498                 iconState.visibleState = StatusBarIconView.STATE_ICON;
499             }
500         }
501     }
502 
getLayoutEnd()503     private float getLayoutEnd() {
504         return getActualWidth() - getActualPaddingEnd();
505     }
506 
getActualPaddingEnd()507     private float getActualPaddingEnd() {
508         if (mActualPaddingEnd == NO_VALUE) {
509             return getPaddingEnd();
510         }
511         return mActualPaddingEnd;
512     }
513 
514     /**
515      * @return the actual startPadding of this view
516      */
getActualPaddingStart()517     public float getActualPaddingStart() {
518         if (mActualPaddingStart == NO_VALUE) {
519             return getPaddingStart();
520         }
521         return mActualPaddingStart;
522     }
523 
524     /**
525      * Sets whether the layout should always show the same number of icons.
526      * If this is true, the icon positions will be updated on layout.
527      * If this if false, the layout is managed from the outside and layouting won't trigger a
528      * repositioning of the icons.
529      */
setIsStaticLayout(boolean isStaticLayout)530     public void setIsStaticLayout(boolean isStaticLayout) {
531         mIsStaticLayout = isStaticLayout;
532     }
533 
setActualLayoutWidth(int actualLayoutWidth)534     public void setActualLayoutWidth(int actualLayoutWidth) {
535         mActualLayoutWidth = actualLayoutWidth;
536         if (DEBUG) {
537             invalidate();
538         }
539     }
540 
setActualPaddingEnd(float paddingEnd)541     public void setActualPaddingEnd(float paddingEnd) {
542         mActualPaddingEnd = paddingEnd;
543         if (DEBUG) {
544             invalidate();
545         }
546     }
547 
setActualPaddingStart(float paddingStart)548     public void setActualPaddingStart(float paddingStart) {
549         mActualPaddingStart = paddingStart;
550         if (DEBUG) {
551             invalidate();
552         }
553     }
554 
getActualWidth()555     public int getActualWidth() {
556         if (mActualLayoutWidth == NO_VALUE) {
557             return getWidth();
558         }
559         return mActualLayoutWidth;
560     }
561 
getFinalTranslationX()562     public int getFinalTranslationX() {
563         if (mLastVisibleIconState == null) {
564             return 0;
565         }
566 
567         int translation = (int) (isLayoutRtl() ? getWidth() - mLastVisibleIconState.xTranslation
568                 : mLastVisibleIconState.xTranslation + mIconSize);
569         // There's a chance that last translation goes beyond the edge maybe
570         return Math.min(getWidth(), translation);
571     }
572 
getMaxOverflowStart()573     private float getMaxOverflowStart() {
574         return getLayoutEnd() - mOverflowWidth;
575     }
576 
setChangingViewPositions(boolean changingViewPositions)577     public void setChangingViewPositions(boolean changingViewPositions) {
578         mChangingViewPositions = changingViewPositions;
579     }
580 
setDozing(boolean dozing, boolean fade, long delay)581     public void setDozing(boolean dozing, boolean fade, long delay) {
582         mDozing = dozing;
583         mDisallowNextAnimation |= !fade;
584         for (int i = 0; i < getChildCount(); i++) {
585             View view = getChildAt(i);
586             if (view instanceof StatusBarIconView) {
587                 ((StatusBarIconView) view).setDozing(dozing, fade, delay);
588             }
589         }
590     }
591 
getIconState(StatusBarIconView icon)592     public IconState getIconState(StatusBarIconView icon) {
593         return mIconStates.get(icon);
594     }
595 
setSpeedBumpIndex(int speedBumpIndex)596     public void setSpeedBumpIndex(int speedBumpIndex) {
597         mSpeedBumpIndex = speedBumpIndex;
598     }
599 
setOpenedAmount(float expandAmount)600     public void setOpenedAmount(float expandAmount) {
601         mOpenedAmount = expandAmount;
602     }
603 
hasOverflow()604     public boolean hasOverflow() {
605         return mNumDots > 0;
606     }
607 
608     /**
609      * If the overflow is in the range [1, max_dots - 1) (basically 1 or 2 dots), then
610      * extra padding will have to be accounted for
611      *
612      * This method has no meaning for non-static containers
613      */
hasPartialOverflow()614     public boolean hasPartialOverflow() {
615         return mNumDots > 0 && mNumDots < MAX_DOTS;
616     }
617 
618     /**
619      * Get padding that can account for extra dots up to the max. The only valid values for
620      * this method are for 1 or 2 dots.
621      * @return only extraDotPadding or extraDotPadding * 2
622      */
getPartialOverflowExtraPadding()623     public int getPartialOverflowExtraPadding() {
624         if (!hasPartialOverflow()) {
625             return 0;
626         }
627 
628         int partialOverflowAmount = (MAX_DOTS - mNumDots) * (mStaticDotDiameter + mDotPadding);
629 
630         int adjustedWidth = getFinalTranslationX() + partialOverflowAmount;
631         // In case we actually give too much padding...
632         if (adjustedWidth > getWidth()) {
633             partialOverflowAmount = getWidth() - getFinalTranslationX();
634         }
635 
636         return partialOverflowAmount;
637     }
638 
639     // Give some extra room for btw notifications if we can
getNoOverflowExtraPadding()640     public int getNoOverflowExtraPadding() {
641         if (mNumDots != 0) {
642             return 0;
643         }
644 
645         int collapsedPadding = mOverflowWidth;
646 
647         if (collapsedPadding + getFinalTranslationX() > getWidth()) {
648             collapsedPadding = getWidth() - getFinalTranslationX();
649         }
650 
651         return collapsedPadding;
652     }
653 
getIconSize()654     public int getIconSize() {
655         return mIconSize;
656     }
657 
setAnimationsEnabled(boolean enabled)658     public void setAnimationsEnabled(boolean enabled) {
659         if (!enabled && mAnimationsEnabled) {
660             for (int i = 0; i < getChildCount(); i++) {
661                 View child = getChildAt(i);
662                 ViewState childState = mIconStates.get(child);
663                 if (childState != null) {
664                     childState.cancelAnimations(child);
665                     childState.applyToView(child);
666                 }
667             }
668         }
669         mAnimationsEnabled = enabled;
670     }
671 
setReplacingIcons(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons)672     public void setReplacingIcons(ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons) {
673         mReplacingIcons = replacingIcons;
674     }
675 
showIconIsolated(StatusBarIconView icon, boolean animated)676     public void showIconIsolated(StatusBarIconView icon, boolean animated) {
677         if (animated) {
678             mIsolatedIconForAnimation = icon != null ? icon : mIsolatedIcon;
679         }
680         mIsolatedIcon = icon;
681         updateState();
682     }
683 
setIsolatedIconLocation(Rect isolatedIconLocation, boolean requireUpdate)684     public void setIsolatedIconLocation(Rect isolatedIconLocation, boolean requireUpdate) {
685         mIsolatedIconLocation = isolatedIconLocation;
686         if (requireUpdate) {
687             updateState();
688         }
689     }
690 
setOnLockScreen(boolean onLockScreen)691     public void setOnLockScreen(boolean onLockScreen) {
692         mOnLockScreen = onLockScreen;
693     }
694 
695     public class IconState extends ViewState {
696         public static final int NO_VALUE = NotificationIconContainer.NO_VALUE;
697         public float iconAppearAmount = 1.0f;
698         public float clampedAppearAmount = 1.0f;
699         public int visibleState;
700         public boolean justAdded = true;
701         private boolean justReplaced;
702         public boolean needsCannedAnimation;
703         public boolean useFullTransitionAmount;
704         public boolean useLinearTransitionAmount;
705         public boolean translateContent;
706         public int iconColor = StatusBarIconView.NO_COLOR;
707         public boolean noAnimations;
708         public boolean isLastExpandIcon;
709         public int customTransformHeight = NO_VALUE;
710         private final View mView;
711 
712         private final Consumer<Property> mCannedAnimationEndListener;
713 
IconState(View child)714         public IconState(View child) {
715             mView = child;
716             mCannedAnimationEndListener = (property) -> {
717                 // If we finished animating out of the shelf
718                 if (property == View.TRANSLATION_Y && iconAppearAmount == 0.0f
719                         && mView.getVisibility() == VISIBLE) {
720                     mView.setVisibility(INVISIBLE);
721                 }
722             };
723         }
724 
725         @Override
applyToView(View view)726         public void applyToView(View view) {
727             if (view instanceof StatusBarIconView) {
728                 StatusBarIconView icon = (StatusBarIconView) view;
729                 boolean animate = false;
730                 AnimationProperties animationProperties = null;
731                 boolean animationsAllowed = areAnimationsEnabled(icon) && !mDisallowNextAnimation
732                         && !noAnimations;
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(iconColor, needsCannedAnimation && animationsAllowed);
805                 if (animate) {
806                     animateTo(icon, animationProperties);
807                 } else {
808                     super.applyToView(view);
809                 }
810                 boolean inShelf = iconAppearAmount == 1.0f;
811                 icon.setIsInShelf(inShelf);
812             }
813             justAdded = false;
814             justReplaced = false;
815             needsCannedAnimation = false;
816         }
817 
hasCustomTransformHeight()818         public boolean hasCustomTransformHeight() {
819             return isLastExpandIcon && customTransformHeight != NO_VALUE;
820         }
821 
822         @Override
initFrom(View view)823         public void initFrom(View view) {
824             super.initFrom(view);
825             if (view instanceof StatusBarIconView) {
826                 iconColor = ((StatusBarIconView) view).getStaticDrawableColor();
827             }
828         }
829     }
830 }
831