• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         boolean added = addIgnoredSlotInternal(slotName);
221         if (added) {
222             requestLayout();
223         }
224     }
225 
226     /**
227      * Add a list of slots to be ignored
228      * @param slots names of the icons to ignore
229      */
addIgnoredSlots(List<String> slots)230     public void addIgnoredSlots(List<String> slots) {
231         boolean willAddAny = false;
232         for (String slot : slots) {
233             willAddAny |= addIgnoredSlotInternal(slot);
234         }
235 
236         if (willAddAny) {
237             requestLayout();
238         }
239     }
240 
241     /**
242      *
243      * @param slotName
244      * @return
245      */
addIgnoredSlotInternal(String slotName)246     private boolean addIgnoredSlotInternal(String slotName) {
247         if (mIgnoredSlots.contains(slotName)) {
248             return false;
249         }
250         mIgnoredSlots.add(slotName);
251         return true;
252     }
253 
254     /**
255      * Remove a slot from the list of ignored icon slots. It will then be shown when set to visible
256      * by the {@link StatusBarIconController}.
257      * @param slotName name of the icon slot to remove from the ignored list
258      */
removeIgnoredSlot(String slotName)259     public void removeIgnoredSlot(String slotName) {
260         boolean removed = mIgnoredSlots.remove(slotName);
261         if (removed) {
262             requestLayout();
263         }
264     }
265 
266     /**
267      * Remove a list of slots from the list of ignored icon slots.
268      * It will then be shown when set to visible by the {@link StatusBarIconController}.
269      * @param slots name of the icon slots to remove from the ignored list
270      */
removeIgnoredSlots(List<String> slots)271     public void removeIgnoredSlots(List<String> slots) {
272         boolean removedAny = false;
273         for (String slot : slots) {
274             removedAny |= mIgnoredSlots.remove(slot);
275         }
276 
277         if (removedAny) {
278             requestLayout();
279         }
280     }
281 
282     /**
283      * Sets the list of ignored icon slots clearing the current list.
284      * @param slots names of the icons to ignore
285      */
setIgnoredSlots(List<String> slots)286     public void setIgnoredSlots(List<String> slots) {
287         mIgnoredSlots.clear();
288         addIgnoredSlots(slots);
289     }
290 
291     /**
292      * Returns the view corresponding to a particular slot.
293      *
294      * Use it solely to manipulate how it is presented.
295      * @param slot name of the slot to find. Names are defined in
296      *            {@link com.android.internal.R.config_statusBarIcons}
297      * @return a view for the slot if this container has it, else {@code null}
298      */
getViewForSlot(String slot)299     public View getViewForSlot(String slot) {
300         for (int i = 0; i < getChildCount(); i++) {
301             View child = getChildAt(i);
302             if (child instanceof StatusIconDisplayable
303                     && ((StatusIconDisplayable) child).getSlot().equals(slot)) {
304                 return child;
305             }
306         }
307         return null;
308     }
309 
310     /**
311      * Layout is happening from end -> start
312      */
calculateIconTranslations()313     private void calculateIconTranslations() {
314         mLayoutStates.clear();
315         float width = getWidth();
316         float translationX = width - getPaddingEnd();
317         float contentStart = getPaddingStart();
318         int childCount = getChildCount();
319         // Underflow === don't show content until that index
320         if (DEBUG) Log.d(TAG, "calculateIconTranslations: start=" + translationX
321                 + " width=" + width + " underflow=" + mNeedsUnderflow);
322 
323         // Collect all of the states which want to be visible
324         for (int i = childCount - 1; i >= 0; i--) {
325             View child = getChildAt(i);
326             StatusIconDisplayable iconView = (StatusIconDisplayable) child;
327             StatusIconState childState = getViewStateFromChild(child);
328 
329             if (!iconView.isIconVisible() || iconView.isIconBlocked()
330                     || mIgnoredSlots.contains(iconView.getSlot())) {
331                 childState.visibleState = STATE_HIDDEN;
332                 if (DEBUG) Log.d(TAG, "skipping child (" + iconView.getSlot() + ") not visible");
333                 continue;
334             }
335 
336             // Move translationX to the spot within StatusIconContainer's layout to add the view
337             // without cutting off the child view.
338             translationX -= getViewTotalWidth(child);
339             childState.visibleState = STATE_ICON;
340             childState.setXTranslation(translationX);
341             mLayoutStates.add(0, childState);
342 
343             // Shift translationX over by mIconSpacing for the next view.
344             translationX -= mIconSpacing;
345         }
346 
347         // Show either 1-MAX_ICONS icons, or (MAX_ICONS - 1) icons + overflow
348         int totalVisible = mLayoutStates.size();
349         int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
350 
351         mUnderflowStart = 0;
352         int visible = 0;
353         int firstUnderflowIndex = -1;
354         for (int i = totalVisible - 1; i >= 0; i--) {
355             StatusIconState state = mLayoutStates.get(i);
356             // Allow room for underflow if we found we need it in onMeasure
357             if (mNeedsUnderflow && (state.getXTranslation() < (contentStart + mUnderflowWidth))
358                     || (mShouldRestrictIcons && (visible >= maxVisible))) {
359                 firstUnderflowIndex = i;
360                 break;
361             }
362             mUnderflowStart = (int) Math.max(
363                     contentStart, state.getXTranslation() - mUnderflowWidth - mIconSpacing);
364             visible++;
365         }
366 
367         if (firstUnderflowIndex != -1) {
368             int totalDots = 0;
369             int dotWidth = mStaticDotDiameter + mDotPadding;
370             int dotOffset = mUnderflowStart + mUnderflowWidth - mIconDotFrameWidth;
371             for (int i = firstUnderflowIndex; i >= 0; i--) {
372                 StatusIconState state = mLayoutStates.get(i);
373                 if (totalDots < MAX_DOTS) {
374                     state.setXTranslation(dotOffset);
375                     state.visibleState = STATE_DOT;
376                     dotOffset -= dotWidth;
377                     totalDots++;
378                 } else {
379                     state.visibleState = STATE_HIDDEN;
380                 }
381             }
382         }
383 
384         // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean
385         if (isLayoutRtl()) {
386             for (int i = 0; i < childCount; i++) {
387                 View child = getChildAt(i);
388                 StatusIconState state = getViewStateFromChild(child);
389                 state.setXTranslation(width - state.getXTranslation() - child.getWidth());
390             }
391         }
392     }
393 
applyIconStates()394     private void applyIconStates() {
395         for (int i = 0; i < getChildCount(); i++) {
396             View child = getChildAt(i);
397             StatusIconState vs = getViewStateFromChild(child);
398             if (vs != null) {
399                 vs.applyToView(child);
400             }
401         }
402     }
403 
resetViewStates()404     private void resetViewStates() {
405         for (int i = 0; i < getChildCount(); i++) {
406             View child = getChildAt(i);
407             StatusIconState vs = getViewStateFromChild(child);
408             if (vs == null) {
409                 continue;
410             }
411 
412             vs.initFrom(child);
413             vs.setAlpha(1.0f);
414             vs.hidden = false;
415         }
416     }
417 
getViewStateFromChild(View child)418     private static @Nullable StatusIconState getViewStateFromChild(View child) {
419         return (StatusIconState) child.getTag(R.id.status_bar_view_state_tag);
420     }
421 
getViewTotalMeasuredWidth(View child)422     private static int getViewTotalMeasuredWidth(View child) {
423         return child.getMeasuredWidth() + child.getPaddingStart() + child.getPaddingEnd();
424     }
425 
getViewTotalWidth(View child)426     private static int getViewTotalWidth(View child) {
427         return child.getWidth() + child.getPaddingStart() + child.getPaddingEnd();
428     }
429 
430     public static class StatusIconState extends ViewState {
431         /// StatusBarIconView.STATE_*
432         public int visibleState = STATE_ICON;
433         public boolean justAdded = true;
434 
435         // How far we are from the end of the view actually is the most relevant for animation
436         float distanceToViewEnd = -1;
437 
438         @Override
applyToView(View view)439         public void applyToView(View view) {
440             float parentWidth = 0;
441             if (view.getParent() instanceof View) {
442                 parentWidth = ((View) view.getParent()).getWidth();
443             }
444 
445             float currentDistanceToEnd = parentWidth - getXTranslation();
446 
447             if (!(view instanceof StatusIconDisplayable)) {
448                 return;
449             }
450             StatusIconDisplayable icon = (StatusIconDisplayable) view;
451             AnimationProperties animationProperties = null;
452             boolean animateVisibility = true;
453 
454             // Figure out which properties of the state transition (if any) we need to animate
455             if (justAdded
456                     || icon.getVisibleState() == STATE_HIDDEN && visibleState == STATE_ICON) {
457                 // Icon is appearing, fade it in by putting it where it will be and animating alpha
458                 super.applyToView(view);
459                 view.setAlpha(0.f);
460                 icon.setVisibleState(STATE_HIDDEN);
461                 animationProperties = ADD_ICON_PROPERTIES;
462             } else if (icon.getVisibleState() != visibleState) {
463                 if (icon.getVisibleState() == STATE_ICON && visibleState == STATE_HIDDEN) {
464                     // Disappearing, don't do anything fancy
465                     animateVisibility = false;
466                 } else {
467                     // all other transitions (to/from dot, etc)
468                     animationProperties = ANIMATE_ALL_PROPERTIES;
469                 }
470             } else if (visibleState != STATE_HIDDEN && distanceToViewEnd != currentDistanceToEnd) {
471                 // Visibility isn't changing, just animate position
472                 animationProperties = X_ANIMATION_PROPERTIES;
473             }
474 
475             icon.setVisibleState(visibleState, animateVisibility);
476             if (animationProperties != null) {
477                 animateTo(view, animationProperties);
478             } else {
479                 super.applyToView(view);
480             }
481 
482             justAdded = false;
483             distanceToViewEnd = currentDistanceToEnd;
484 
485         }
486     }
487 
488     private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
489         private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
490 
491         @Override
492         public AnimationFilter getAnimationFilter() {
493             return mAnimationFilter;
494         }
495     }.setDuration(200).setDelay(50);
496 
497     private static final AnimationProperties X_ANIMATION_PROPERTIES = new AnimationProperties() {
498         private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
499 
500         @Override
501         public AnimationFilter getAnimationFilter() {
502             return mAnimationFilter;
503         }
504     }.setDuration(200);
505 
506     private static final AnimationProperties ANIMATE_ALL_PROPERTIES = new AnimationProperties() {
507         private AnimationFilter mAnimationFilter = new AnimationFilter().animateX().animateY()
508                 .animateAlpha().animateScale();
509 
510         @Override
511         public AnimationFilter getAnimationFilter() {
512             return mAnimationFilter;
513         }
514     }.setDuration(200);
515 }
516