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