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