1 /*
2  * Copyright (C) 2017 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.StatusBarIconView.STATE_DOT;
20 import static com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN;
21 import static com.android.systemui.statusbar.StatusBarIconView.STATE_ICON;
22 
23 import android.annotation.Nullable;
24 import android.content.Context;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.Paint;
28 import android.graphics.Paint.Style;
29 import android.util.AttributeSet;
30 import android.util.Log;
31 import android.view.View;
32 
33 import com.android.keyguard.AlphaOptimizedLinearLayout;
34 import com.android.systemui.R;
35 import com.android.systemui.statusbar.StatusIconDisplayable;
36 import com.android.systemui.statusbar.notification.stack.AnimationFilter;
37 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
38 import com.android.systemui.statusbar.notification.stack.ViewState;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 /**
44  * A container for Status bar system icons. Limits the number of system icons and handles overflow
45  * similar to {@link NotificationIconContainer}.
46  *
47  * Children are expected to implement {@link StatusIconDisplayable}
48  */
49 public class StatusIconContainer extends AlphaOptimizedLinearLayout {
50 
51     private static final String TAG = "StatusIconContainer";
52     private static final boolean DEBUG = false;
53     private static final boolean DEBUG_OVERFLOW = false;
54     // Max 8 status icons including battery
55     private static final int MAX_ICONS = 7;
56     private static final int MAX_DOTS = 1;
57 
58     private int mDotPadding;
59     private int mIconSpacing;
60     private int mStaticDotDiameter;
61     private int mUnderflowWidth;
62     private int mUnderflowStart = 0;
63     // Whether or not we can draw into the underflow space
64     private boolean mNeedsUnderflow;
65     // Individual StatusBarIconViews draw their etc dots centered in this width
66     private int mIconDotFrameWidth;
67     private boolean mShouldRestrictIcons = true;
68     // Used to count which states want to be visible during layout
69     private ArrayList<StatusIconState> mLayoutStates = new ArrayList<>();
70     // So we can count and measure properly
71     private ArrayList<View> mMeasureViews = new ArrayList<>();
72     // Any ignored icon will never be added as a child
73     private ArrayList<String> mIgnoredSlots = new ArrayList<>();
74 
StatusIconContainer(Context context)75     public StatusIconContainer(Context context) {
76         this(context, null);
77     }
78 
StatusIconContainer(Context context, AttributeSet attrs)79     public StatusIconContainer(Context context, AttributeSet attrs) {
80         super(context, attrs);
81         initDimens();
82         setWillNotDraw(!DEBUG_OVERFLOW);
83     }
84 
85     @Override
onFinishInflate()86     protected void onFinishInflate() {
87         super.onFinishInflate();
88     }
89 
setShouldRestrictIcons(boolean should)90     public void setShouldRestrictIcons(boolean should) {
91         mShouldRestrictIcons = should;
92     }
93 
isRestrictingIcons()94     public boolean isRestrictingIcons() {
95         return mShouldRestrictIcons;
96     }
97 
initDimens()98     private void initDimens() {
99         // This is the same value that StatusBarIconView uses
100         mIconDotFrameWidth = getResources().getDimensionPixelSize(
101                 com.android.internal.R.dimen.status_bar_icon_size);
102         mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
103         mIconSpacing = getResources().getDimensionPixelSize(R.dimen.status_bar_system_icon_spacing);
104         int radius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
105         mStaticDotDiameter = 2 * radius;
106         mUnderflowWidth = mIconDotFrameWidth + (MAX_DOTS - 1) * (mStaticDotDiameter + mDotPadding);
107     }
108 
109     @Override
onLayout(boolean changed, int l, int t, int r, int b)110     protected void onLayout(boolean changed, int l, int t, int r, int b) {
111         float midY = getHeight() / 2.0f;
112 
113         // Layout all child views so that we can move them around later
114         for (int i = 0; i < getChildCount(); i++) {
115             View child = getChildAt(i);
116             int width = child.getMeasuredWidth();
117             int height = child.getMeasuredHeight();
118             int top = (int) (midY - height / 2.0f);
119             child.layout(0, top, width, top + height);
120         }
121 
122         resetViewStates();
123         calculateIconTranslations();
124         applyIconStates();
125     }
126 
127     @Override
onDraw(Canvas canvas)128     protected void onDraw(Canvas canvas) {
129         super.onDraw(canvas);
130         if (DEBUG_OVERFLOW) {
131             Paint paint = new Paint();
132             paint.setStyle(Style.STROKE);
133             paint.setColor(Color.RED);
134 
135             // Show bounding box
136             canvas.drawRect(getPaddingStart(), 0, getWidth() - getPaddingEnd(), getHeight(), paint);
137 
138             // Show etc box
139             paint.setColor(Color.GREEN);
140             canvas.drawRect(
141                     mUnderflowStart, 0, mUnderflowStart + mUnderflowWidth, getHeight(), paint);
142         }
143     }
144 
145     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)146     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
147         mMeasureViews.clear();
148         int mode = MeasureSpec.getMode(widthMeasureSpec);
149         final int width = MeasureSpec.getSize(widthMeasureSpec);
150         final int count = getChildCount();
151         // Collect all of the views which want to be laid out
152         for (int i = 0; i < count; i++) {
153             StatusIconDisplayable icon = (StatusIconDisplayable) getChildAt(i);
154             if (icon.isIconVisible() && !icon.isIconBlocked()
155                     && !mIgnoredSlots.contains(icon.getSlot())) {
156                 mMeasureViews.add((View) icon);
157             }
158         }
159 
160         int visibleCount = mMeasureViews.size();
161         int maxVisible = visibleCount <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
162         int totalWidth = mPaddingLeft + mPaddingRight;
163         boolean trackWidth = true;
164 
165         // Measure all children so that they report the correct width
166         int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED);
167         mNeedsUnderflow = mShouldRestrictIcons && visibleCount > MAX_ICONS;
168         for (int i = 0; i < visibleCount; i++) {
169             // Walking backwards
170             View child = mMeasureViews.get(visibleCount - i - 1);
171             measureChild(child, childWidthSpec, heightMeasureSpec);
172             int spacing = i == visibleCount - 1 ? 0 : mIconSpacing;
173             if (mShouldRestrictIcons) {
174                 if (i < maxVisible && trackWidth) {
175                     totalWidth += getViewTotalMeasuredWidth(child) + spacing;
176                 } else if (trackWidth) {
177                     // We've hit the icon limit; add space for dots
178                     totalWidth += mUnderflowWidth;
179                     trackWidth = false;
180                 }
181             } else {
182                 totalWidth += getViewTotalMeasuredWidth(child) + spacing;
183             }
184         }
185 
186         if (mode == MeasureSpec.EXACTLY) {
187             if (!mNeedsUnderflow && totalWidth > width) {
188                 mNeedsUnderflow = true;
189             }
190             setMeasuredDimension(width, MeasureSpec.getSize(heightMeasureSpec));
191         } else {
192             if (mode == MeasureSpec.AT_MOST && totalWidth > width) {
193                 mNeedsUnderflow = true;
194                 totalWidth = width;
195             }
196             setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec));
197         }
198     }
199 
200     @Override
onViewAdded(View child)201     public void onViewAdded(View child) {
202         super.onViewAdded(child);
203         StatusIconState vs = new StatusIconState();
204         vs.justAdded = true;
205         child.setTag(R.id.status_bar_view_state_tag, vs);
206     }
207 
208     @Override
onViewRemoved(View child)209     public void onViewRemoved(View child) {
210         super.onViewRemoved(child);
211         child.setTag(R.id.status_bar_view_state_tag, null);
212     }
213 
214     /**
215      * Add a name of an icon slot to be ignored. It will not show up nor be measured
216      * @param slotName name of the icon as it exists in
217      * frameworks/base/core/res/res/values/config.xml
218      */
addIgnoredSlot(String slotName)219     public void addIgnoredSlot(String slotName) {
220         addIgnoredSlotInternal(slotName);
221         requestLayout();
222     }
223 
224     /**
225      * Add a list of slots to be ignored
226      * @param slots names of the icons to ignore
227      */
addIgnoredSlots(List<String> slots)228     public void addIgnoredSlots(List<String> slots) {
229         for (String slot : slots) {
230             addIgnoredSlotInternal(slot);
231         }
232 
233         requestLayout();
234     }
235 
addIgnoredSlotInternal(String slotName)236     private void addIgnoredSlotInternal(String slotName) {
237         if (!mIgnoredSlots.contains(slotName)) {
238             mIgnoredSlots.add(slotName);
239         }
240     }
241 
242     /**
243      * Remove a slot from the list of ignored icon slots. It will then be shown when set to visible
244      * by the {@link StatusBarIconController}.
245      * @param slotName name of the icon slot to remove from the ignored list
246      */
removeIgnoredSlot(String slotName)247     public void removeIgnoredSlot(String slotName) {
248         if (mIgnoredSlots.contains(slotName)) {
249             mIgnoredSlots.remove(slotName);
250         }
251 
252         requestLayout();
253     }
254 
255     /**
256      * Sets the list of ignored icon slots clearing the current list.
257      * @param slots names of the icons to ignore
258      */
setIgnoredSlots(List<String> slots)259     public void setIgnoredSlots(List<String> slots) {
260         mIgnoredSlots.clear();
261         addIgnoredSlots(slots);
262     }
263 
264     /**
265      * Layout is happening from end -> start
266      */
calculateIconTranslations()267     private void calculateIconTranslations() {
268         mLayoutStates.clear();
269         float width = getWidth();
270         float translationX = width - getPaddingEnd();
271         float contentStart = getPaddingStart();
272         int childCount = getChildCount();
273         // Underflow === don't show content until that index
274         if (DEBUG) android.util.Log.d(TAG, "calculateIconTranslations: start=" + translationX
275                 + " width=" + width + " underflow=" + mNeedsUnderflow);
276 
277         // Collect all of the states which want to be visible
278         for (int i = childCount - 1; i >= 0; i--) {
279             View child = getChildAt(i);
280             StatusIconDisplayable iconView = (StatusIconDisplayable) child;
281             StatusIconState childState = getViewStateFromChild(child);
282 
283             if (!iconView.isIconVisible() || iconView.isIconBlocked()
284                     || mIgnoredSlots.contains(iconView.getSlot())) {
285                 childState.visibleState = STATE_HIDDEN;
286                 if (DEBUG) Log.d(TAG, "skipping child (" + iconView.getSlot() + ") not visible");
287                 continue;
288             }
289 
290             // Move translationX to the spot within StatusIconContainer's layout to add the view
291             // without cutting off the child view.
292             translationX -= getViewTotalWidth(child);
293             childState.visibleState = STATE_ICON;
294             childState.xTranslation = translationX;
295             mLayoutStates.add(0, childState);
296 
297             // Shift translationX over by mIconSpacing for the next view.
298             translationX -= mIconSpacing;
299         }
300 
301         // Show either 1-MAX_ICONS icons, or (MAX_ICONS - 1) icons + overflow
302         int totalVisible = mLayoutStates.size();
303         int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
304 
305         mUnderflowStart = 0;
306         int visible = 0;
307         int firstUnderflowIndex = -1;
308         for (int i = totalVisible - 1; i >= 0; i--) {
309             StatusIconState state = mLayoutStates.get(i);
310             // Allow room for underflow if we found we need it in onMeasure
311             if (mNeedsUnderflow && (state.xTranslation < (contentStart + mUnderflowWidth))||
312                     (mShouldRestrictIcons && visible >= maxVisible)) {
313                 firstUnderflowIndex = i;
314                 break;
315             }
316             mUnderflowStart = (int) Math.max(
317                     contentStart, state.xTranslation - mUnderflowWidth - mIconSpacing);
318             visible++;
319         }
320 
321         if (firstUnderflowIndex != -1) {
322             int totalDots = 0;
323             int dotWidth = mStaticDotDiameter + mDotPadding;
324             int dotOffset = mUnderflowStart + mUnderflowWidth - mIconDotFrameWidth;
325             for (int i = firstUnderflowIndex; i >= 0; i--) {
326                 StatusIconState state = mLayoutStates.get(i);
327                 if (totalDots < MAX_DOTS) {
328                     state.xTranslation = dotOffset;
329                     state.visibleState = STATE_DOT;
330                     dotOffset -= dotWidth;
331                     totalDots++;
332                 } else {
333                     state.visibleState = STATE_HIDDEN;
334                 }
335             }
336         }
337 
338         // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean
339         if (isLayoutRtl()) {
340             for (int i = 0; i < childCount; i++) {
341                 View child = getChildAt(i);
342                 StatusIconState state = getViewStateFromChild(child);
343                 state.xTranslation = width - state.xTranslation - child.getWidth();
344             }
345         }
346     }
347 
applyIconStates()348     private void applyIconStates() {
349         for (int i = 0; i < getChildCount(); i++) {
350             View child = getChildAt(i);
351             StatusIconState vs = getViewStateFromChild(child);
352             if (vs != null) {
353                 vs.applyToView(child);
354             }
355         }
356     }
357 
resetViewStates()358     private void resetViewStates() {
359         for (int i = 0; i < getChildCount(); i++) {
360             View child = getChildAt(i);
361             StatusIconState vs = getViewStateFromChild(child);
362             if (vs == null) {
363                 continue;
364             }
365 
366             vs.initFrom(child);
367             vs.alpha = 1.0f;
368             vs.hidden = false;
369         }
370     }
371 
getViewStateFromChild(View child)372     private static @Nullable StatusIconState getViewStateFromChild(View child) {
373         return (StatusIconState) child.getTag(R.id.status_bar_view_state_tag);
374     }
375 
getViewTotalMeasuredWidth(View child)376     private static int getViewTotalMeasuredWidth(View child) {
377         return child.getMeasuredWidth() + child.getPaddingStart() + child.getPaddingEnd();
378     }
379 
getViewTotalWidth(View child)380     private static int getViewTotalWidth(View child) {
381         return child.getWidth() + child.getPaddingStart() + child.getPaddingEnd();
382     }
383 
384     public static class StatusIconState extends ViewState {
385         /// StatusBarIconView.STATE_*
386         public int visibleState = STATE_ICON;
387         public boolean justAdded = true;
388 
389         // How far we are from the end of the view actually is the most relevant for animation
390         float distanceToViewEnd = -1;
391 
392         @Override
applyToView(View view)393         public void applyToView(View view) {
394             float parentWidth = 0;
395             if (view.getParent() instanceof View) {
396                 parentWidth = ((View) view.getParent()).getWidth();
397             }
398 
399             float currentDistanceToEnd = parentWidth - xTranslation;
400 
401             if (!(view instanceof StatusIconDisplayable)) {
402                 return;
403             }
404             StatusIconDisplayable icon = (StatusIconDisplayable) view;
405             AnimationProperties animationProperties = null;
406             boolean animateVisibility = true;
407 
408             // Figure out which properties of the state transition (if any) we need to animate
409             if (justAdded
410                     || icon.getVisibleState() == STATE_HIDDEN && visibleState == STATE_ICON) {
411                 // Icon is appearing, fade it in by putting it where it will be and animating alpha
412                 super.applyToView(view);
413                 view.setAlpha(0.f);
414                 icon.setVisibleState(STATE_HIDDEN);
415                 animationProperties = ADD_ICON_PROPERTIES;
416             } else if (icon.getVisibleState() != visibleState) {
417                 if (icon.getVisibleState() == STATE_ICON && visibleState == STATE_HIDDEN) {
418                     // Disappearing, don't do anything fancy
419                     animateVisibility = false;
420                 } else {
421                     // all other transitions (to/from dot, etc)
422                     animationProperties = ANIMATE_ALL_PROPERTIES;
423                 }
424             } else if (visibleState != STATE_HIDDEN && distanceToViewEnd != currentDistanceToEnd) {
425                 // Visibility isn't changing, just animate position
426                 animationProperties = X_ANIMATION_PROPERTIES;
427             }
428 
429             icon.setVisibleState(visibleState, animateVisibility);
430             if (animationProperties != null) {
431                 animateTo(view, animationProperties);
432             } else {
433                 super.applyToView(view);
434             }
435 
436             justAdded = false;
437             distanceToViewEnd = currentDistanceToEnd;
438 
439         }
440     }
441 
442     private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
443         private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
444 
445         @Override
446         public AnimationFilter getAnimationFilter() {
447             return mAnimationFilter;
448         }
449     }.setDuration(200).setDelay(50);
450 
451     private static final AnimationProperties X_ANIMATION_PROPERTIES = new AnimationProperties() {
452         private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
453 
454         @Override
455         public AnimationFilter getAnimationFilter() {
456             return mAnimationFilter;
457         }
458     }.setDuration(200);
459 
460     private static final AnimationProperties ANIMATE_ALL_PROPERTIES = new AnimationProperties() {
461         private AnimationFilter mAnimationFilter = new AnimationFilter().animateX().animateY()
462                 .animateAlpha().animateScale();
463 
464         @Override
465         public AnimationFilter getAnimationFilter() {
466             return mAnimationFilter;
467         }
468     }.setDuration(200);
469 }
470