StatusIconContainer.java revision 97ae20c5d841a244369dba5decaa3ab994cdad33
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
17package com.android.systemui.statusbar.phone;
18
19import static com.android.systemui.statusbar.StatusBarIconView.STATE_ICON;
20import static com.android.systemui.statusbar.StatusBarIconView.STATE_DOT;
21import static com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN;
22
23import android.annotation.Nullable;
24import android.content.Context;
25import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.Paint;
28import android.graphics.Paint.Style;
29import android.util.AttributeSet;
30import android.util.Log;
31
32import android.view.View;
33import com.android.keyguard.AlphaOptimizedLinearLayout;
34import com.android.systemui.R;
35import com.android.systemui.statusbar.StatusIconDisplayable;
36import com.android.systemui.statusbar.stack.AnimationFilter;
37import com.android.systemui.statusbar.stack.AnimationProperties;
38import com.android.systemui.statusbar.stack.ViewState;
39import java.util.ArrayList;
40
41/**
42 * A container for Status bar system icons. Limits the number of system icons and handles overflow
43 * similar to {@link NotificationIconContainer}.
44 *
45 * Children are expected to implement {@link StatusIconDisplayable}
46 */
47public class StatusIconContainer extends AlphaOptimizedLinearLayout {
48
49    private static final String TAG = "StatusIconContainer";
50    private static final boolean DEBUG = false;
51    private static final boolean DEBUG_OVERFLOW = false;
52    // Max 8 status icons including battery
53    private static final int MAX_ICONS = 7;
54    private static final int MAX_DOTS = 1;
55
56    private int mDotPadding;
57    private int mStaticDotDiameter;
58    private int mUnderflowWidth;
59    private int mUnderflowStart = 0;
60    // Whether or not we can draw into the underflow space
61    private boolean mNeedsUnderflow;
62    // Individual StatusBarIconViews draw their etc dots centered in this width
63    private int mIconDotFrameWidth;
64    private boolean mShouldRestrictIcons = true;
65    // Used to count which states want to be visible during layout
66    private ArrayList<StatusIconState> mLayoutStates = new ArrayList<>();
67    // So we can count and measure properly
68    private ArrayList<View> mMeasureViews = new ArrayList<>();
69
70    public StatusIconContainer(Context context) {
71        this(context, null);
72    }
73
74    public StatusIconContainer(Context context, AttributeSet attrs) {
75        super(context, attrs);
76        initDimens();
77        setWillNotDraw(!DEBUG_OVERFLOW);
78    }
79
80    @Override
81    protected void onFinishInflate() {
82        super.onFinishInflate();
83    }
84
85    public void setShouldRestrictIcons(boolean should) {
86        mShouldRestrictIcons = should;
87    }
88
89    public boolean isRestrictingIcons() {
90        return mShouldRestrictIcons;
91    }
92
93    private void initDimens() {
94        // This is the same value that StatusBarIconView uses
95        mIconDotFrameWidth = getResources().getDimensionPixelSize(
96                com.android.internal.R.dimen.status_bar_icon_size);
97        mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
98        int radius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
99        mStaticDotDiameter = 2 * radius;
100        mUnderflowWidth = mIconDotFrameWidth + (MAX_DOTS - 1) * (mStaticDotDiameter + mDotPadding);
101    }
102
103    @Override
104    protected void onLayout(boolean changed, int l, int t, int r, int b) {
105        float midY = getHeight() / 2.0f;
106
107        // Layout all child views so that we can move them around later
108        for (int i = 0; i < getChildCount(); i++) {
109            View child = getChildAt(i);
110            int width = child.getMeasuredWidth();
111            int height = child.getMeasuredHeight();
112            int top = (int) (midY - height / 2.0f);
113            child.layout(0, top, width, top + height);
114        }
115
116        resetViewStates();
117        calculateIconTranslations();
118        applyIconStates();
119    }
120
121    @Override
122    protected void onDraw(Canvas canvas) {
123        super.onDraw(canvas);
124        if (DEBUG_OVERFLOW) {
125            Paint paint = new Paint();
126            paint.setStyle(Style.STROKE);
127            paint.setColor(Color.RED);
128
129            // Show bounding box
130            canvas.drawRect(getPaddingStart(), 0, getWidth() - getPaddingEnd(), getHeight(), paint);
131
132            // Show etc box
133            paint.setColor(Color.GREEN);
134            canvas.drawRect(
135                    mUnderflowStart, 0, mUnderflowStart + mUnderflowWidth, getHeight(), paint);
136        }
137    }
138
139    @Override
140    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
141        mMeasureViews.clear();
142        int mode = MeasureSpec.getMode(widthMeasureSpec);
143        final int width = MeasureSpec.getSize(widthMeasureSpec);
144        final int count = getChildCount();
145        // Collect all of the views which want to be laid out
146        for (int i = 0; i < count; i++) {
147            StatusIconDisplayable icon = (StatusIconDisplayable) getChildAt(i);
148            if (icon.isIconVisible() && !icon.isIconBlocked()) {
149                mMeasureViews.add((View) icon);
150            }
151        }
152
153        int visibleCount = mMeasureViews.size();
154        int maxVisible = visibleCount <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
155        int totalWidth = mPaddingLeft + mPaddingRight;
156        boolean trackWidth = true;
157
158        // Measure all children so that they report the correct width
159        int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED);
160        mNeedsUnderflow = mShouldRestrictIcons && visibleCount > MAX_ICONS;
161        for (int i = 0; i < mMeasureViews.size(); i++) {
162            // Walking backwards
163            View child = mMeasureViews.get(visibleCount - i - 1);
164            measureChild(child, childWidthSpec, heightMeasureSpec);
165            if (mShouldRestrictIcons) {
166                if (i < maxVisible && trackWidth) {
167                    totalWidth += getViewTotalMeasuredWidth(child);
168                } else if (trackWidth) {
169                    // We've hit the icon limit; add space for dots
170                    totalWidth += mUnderflowWidth;
171                    trackWidth = false;
172                }
173            } else {
174                totalWidth += getViewTotalMeasuredWidth(child);
175            }
176        }
177
178        if (mode == MeasureSpec.EXACTLY) {
179            if (!mNeedsUnderflow && totalWidth > width) {
180                mNeedsUnderflow = true;
181            }
182            setMeasuredDimension(width, MeasureSpec.getSize(heightMeasureSpec));
183        } else {
184            if (mode == MeasureSpec.AT_MOST && totalWidth > width) {
185                mNeedsUnderflow = true;
186                totalWidth = width;
187            }
188            setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec));
189        }
190    }
191
192    @Override
193    public void onViewAdded(View child) {
194        super.onViewAdded(child);
195        StatusIconState vs = new StatusIconState();
196        vs.justAdded = true;
197        child.setTag(R.id.status_bar_view_state_tag, vs);
198    }
199
200    @Override
201    public void onViewRemoved(View child) {
202        super.onViewRemoved(child);
203        child.setTag(R.id.status_bar_view_state_tag, null);
204    }
205
206    /**
207     * Layout is happening from end -> start
208     */
209    private void calculateIconTranslations() {
210        mLayoutStates.clear();
211        float width = getWidth();
212        float translationX = width - getPaddingEnd();
213        float contentStart = getPaddingStart();
214        int childCount = getChildCount();
215        // Underflow === don't show content until that index
216        if (DEBUG) android.util.Log.d(TAG, "calculateIconTranslations: start=" + translationX
217                + " width=" + width + " underflow=" + mNeedsUnderflow);
218
219        // Collect all of the states which want to be visible
220        for (int i = childCount - 1; i >= 0; i--) {
221            View child = getChildAt(i);
222            StatusIconDisplayable iconView = (StatusIconDisplayable) child;
223            StatusIconState childState = getViewStateFromChild(child);
224
225            if (!iconView.isIconVisible() || iconView.isIconBlocked()) {
226                childState.visibleState = STATE_HIDDEN;
227                if (DEBUG) Log.d(TAG, "skipping child (" + iconView.getSlot() + ") not visible");
228                continue;
229            }
230
231            childState.visibleState = STATE_ICON;
232            childState.xTranslation = translationX - getViewTotalWidth(child);
233            mLayoutStates.add(0, childState);
234
235            translationX -= getViewTotalWidth(child);
236        }
237
238        // Show either 1-MAX_ICONS icons, or (MAX_ICONS - 1) icons + overflow
239        int totalVisible = mLayoutStates.size();
240        int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
241
242        mUnderflowStart = 0;
243        int visible = 0;
244        int firstUnderflowIndex = -1;
245        for (int i = totalVisible - 1; i >= 0; i--) {
246            StatusIconState state = mLayoutStates.get(i);
247            // Allow room for underflow if we found we need it in onMeasure
248            if (mNeedsUnderflow && (state.xTranslation < (contentStart + mUnderflowWidth))||
249                    (mShouldRestrictIcons && visible >= maxVisible)) {
250                firstUnderflowIndex = i;
251                break;
252            }
253            mUnderflowStart = (int) Math.max(contentStart, state.xTranslation - mUnderflowWidth);
254            visible++;
255        }
256
257        if (firstUnderflowIndex != -1) {
258            int totalDots = 0;
259            int dotWidth = mStaticDotDiameter + mDotPadding;
260            int dotOffset = mUnderflowStart + mUnderflowWidth - mIconDotFrameWidth;
261            for (int i = firstUnderflowIndex; i >= 0; i--) {
262                StatusIconState state = mLayoutStates.get(i);
263                if (totalDots < MAX_DOTS) {
264                    state.xTranslation = dotOffset;
265                    state.visibleState = STATE_DOT;
266                    dotOffset -= dotWidth;
267                    totalDots++;
268                } else {
269                    state.visibleState = STATE_HIDDEN;
270                }
271            }
272        }
273
274        // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean
275        if (isLayoutRtl()) {
276            for (int i = 0; i < childCount; i++) {
277                View child = getChildAt(i);
278                StatusIconState state = getViewStateFromChild(child);
279                state.xTranslation = width - state.xTranslation - child.getWidth();
280            }
281        }
282    }
283
284    private void applyIconStates() {
285        for (int i = 0; i < getChildCount(); i++) {
286            View child = getChildAt(i);
287            StatusIconState vs = getViewStateFromChild(child);
288            if (vs != null) {
289                vs.applyToView(child);
290            }
291        }
292    }
293
294    private void resetViewStates() {
295        for (int i = 0; i < getChildCount(); i++) {
296            View child = getChildAt(i);
297            StatusIconState vs = getViewStateFromChild(child);
298            if (vs == null) {
299                continue;
300            }
301
302            vs.initFrom(child);
303            vs.alpha = 1.0f;
304            if (child instanceof StatusIconDisplayable) {
305                vs.hidden = !((StatusIconDisplayable)child).isIconVisible();
306            } else {
307                vs.hidden = false;
308            }
309        }
310    }
311
312    private static @Nullable StatusIconState getViewStateFromChild(View child) {
313        return (StatusIconState) child.getTag(R.id.status_bar_view_state_tag);
314    }
315
316    private static int getViewTotalMeasuredWidth(View child) {
317        return child.getMeasuredWidth() + child.getPaddingStart() + child.getPaddingEnd();
318    }
319
320    private static int getViewTotalWidth(View child) {
321        return child.getWidth() + child.getPaddingStart() + child.getPaddingEnd();
322    }
323
324    public static class StatusIconState extends ViewState {
325        /// StatusBarIconView.STATE_*
326        public int visibleState = STATE_ICON;
327        public boolean justAdded = true;
328
329        @Override
330        public void applyToView(View view) {
331            if (!(view instanceof StatusIconDisplayable)) {
332                return;
333            }
334            StatusIconDisplayable icon = (StatusIconDisplayable) view;
335            AnimationProperties animationProperties = null;
336            boolean animate = false;
337
338            if (justAdded) {
339                super.applyToView(view);
340                animationProperties = ADD_ICON_PROPERTIES;
341                animate = true;
342            } else if (icon.getVisibleState() != visibleState) {
343                animationProperties = DOT_ANIMATION_PROPERTIES;
344                animate = true;
345            }
346
347            if (animate) {
348                animateTo(view, animationProperties);
349                icon.setVisibleState(visibleState);
350            } else {
351                icon.setVisibleState(visibleState);
352                super.applyToView(view);
353            }
354
355            justAdded = false;
356        }
357    }
358
359    private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
360        private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
361
362        @Override
363        public AnimationFilter getAnimationFilter() {
364            return mAnimationFilter;
365        }
366    }.setDuration(200).setDelay(50);
367
368    private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() {
369        private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
370
371        @Override
372        public AnimationFilter getAnimationFilter() {
373            return mAnimationFilter;
374        }
375    }.setDuration(200);
376}
377