StatusIconContainer.java revision 20b87bf0ae8c880a76d0de859b3665b7d4f2e144
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 android.view.ViewGroup;
34import com.android.keyguard.AlphaOptimizedLinearLayout;
35import com.android.systemui.R;
36import com.android.systemui.statusbar.StatusIconDisplayable;
37import com.android.systemui.statusbar.stack.ViewState;
38import java.lang.ref.WeakReference;
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 5 status icons including battery
53    private static final int MAX_ICONS = 4;
54    private static final int MAX_DOTS = 3;
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    }
77
78    @Override
79    protected void onFinishInflate() {
80        super.onFinishInflate();
81        setWillNotDraw(!DEBUG_OVERFLOW);
82        initDimens();
83    }
84
85    public void setShouldRestrictIcons(boolean should) {
86        mShouldRestrictIcons = should;
87    }
88
89    private void initDimens() {
90        // This is the same value that StatusBarIconView uses
91        mIconDotFrameWidth = getResources().getDimensionPixelSize(
92                com.android.internal.R.dimen.status_bar_icon_size);
93        mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
94        int radius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
95        mStaticDotDiameter = 2 * radius;
96        mUnderflowWidth = mIconDotFrameWidth + 2 * (mStaticDotDiameter + mDotPadding);
97    }
98
99    @Override
100    protected void onLayout(boolean changed, int l, int t, int r, int b) {
101        float midY = getHeight() / 2.0f;
102
103        // Layout all child views so that we can move them around later
104        for (int i = 0; i < getChildCount(); i++) {
105            View child = getChildAt(i);
106            int width = child.getMeasuredWidth();
107            int height = child.getMeasuredHeight();
108            int top = (int) (midY - height / 2.0f);
109            child.layout(0, top, width, top + height);
110        }
111
112        resetViewStates();
113        calculateIconTranslations();
114        applyIconStates();
115    }
116
117    @Override
118    protected void onDraw(Canvas canvas) {
119        super.onDraw(canvas);
120        if (DEBUG_OVERFLOW) {
121            Paint paint = new Paint();
122            paint.setStyle(Style.STROKE);
123            paint.setColor(Color.RED);
124
125            // Show bounding box
126            canvas.drawRect(getPaddingStart(), 0, getWidth() - getPaddingEnd(), getHeight(), paint);
127
128            // Show etc box
129            paint.setColor(Color.GREEN);
130            canvas.drawRect(
131                    mUnderflowStart, 0, mUnderflowStart + mUnderflowWidth, getHeight(), paint);
132        }
133    }
134
135    @Override
136    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
137        mMeasureViews.clear();
138        int mode = MeasureSpec.getMode(widthMeasureSpec);
139        final int width = MeasureSpec.getSize(widthMeasureSpec);
140        final int count = getChildCount();
141        // Collect all of the views which want to be laid out
142        for (int i = 0; i < count; i++) {
143            StatusIconDisplayable icon = (StatusIconDisplayable) getChildAt(i);
144            if (icon.isIconVisible() && !icon.isIconBlocked()) {
145                mMeasureViews.add((View) icon);
146            }
147        }
148
149        int visibleCount = mMeasureViews.size();
150        int maxVisible = visibleCount <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
151        int totalWidth = getPaddingStart() + getPaddingEnd();
152        boolean trackWidth = true;
153
154        // Measure all children so that they report the correct width
155        int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED);
156        mNeedsUnderflow = mShouldRestrictIcons && visibleCount > MAX_ICONS;
157        for (int i = 0; i < mMeasureViews.size(); i++) {
158            // Walking backwards
159            View child = mMeasureViews.get(visibleCount - i - 1);
160            measureChild(child, childWidthSpec, heightMeasureSpec);
161            if (mShouldRestrictIcons) {
162                if (i < maxVisible && trackWidth) {
163                    totalWidth += getViewTotalMeasuredWidth(child);
164                } else if (trackWidth) {
165                    // We've hit the icon limit; add space for dots
166                    totalWidth += mUnderflowWidth;
167                    trackWidth = false;
168                }
169            } else {
170                totalWidth += getViewTotalMeasuredWidth(child);
171            }
172        }
173
174        if (mode == MeasureSpec.EXACTLY) {
175            if (!mNeedsUnderflow && totalWidth > width) {
176                mNeedsUnderflow = true;
177            }
178            setMeasuredDimension(width, MeasureSpec.getSize(heightMeasureSpec));
179        } else {
180            if (mode == MeasureSpec.AT_MOST && totalWidth > width) {
181                mNeedsUnderflow = true;
182                totalWidth = width;
183            }
184            setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec));
185        }
186    }
187
188    @Override
189    public void onViewAdded(View child) {
190        super.onViewAdded(child);
191        StatusIconState vs = new StatusIconState();
192        child.setTag(R.id.status_bar_view_state_tag, vs);
193    }
194
195    @Override
196    public void onViewRemoved(View child) {
197        super.onViewRemoved(child);
198        child.setTag(R.id.status_bar_view_state_tag, null);
199    }
200
201    /**
202     * Layout is happening from end -> start
203     */
204    private void calculateIconTranslations() {
205        mLayoutStates.clear();
206        float width = getWidth() - getPaddingEnd();
207        float translationX = width;
208        float contentStart = getPaddingStart();
209        int childCount = getChildCount();
210        // Underflow === don't show content until that index
211        int firstUnderflowIndex = -1;
212        if (DEBUG) android.util.Log.d(TAG, "calculateIconTransitions: start=" + translationX
213                + " width=" + width);
214
215        // Collect all of the states which want to be visible
216        for (int i = childCount - 1; i >= 0; i--) {
217            View child = getChildAt(i);
218            StatusIconDisplayable iconView = (StatusIconDisplayable) child;
219            StatusIconState childState = getViewStateFromChild(child);
220
221            if (!iconView.isIconVisible() || iconView.isIconBlocked()) {
222                childState.visibleState = STATE_HIDDEN;
223                if (DEBUG) Log.d(TAG, "skipping child (" + iconView.getSlot() + ") not visible");
224                continue;
225            }
226
227            childState.visibleState = STATE_ICON;
228            childState.xTranslation = translationX - getViewTotalWidth(child);
229            mLayoutStates.add(0, childState);
230
231            translationX -= getViewTotalWidth(child);
232        }
233
234        // Show either 1-4 dots, or 3 dots + overflow
235        int totalVisible = mLayoutStates.size();
236        int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
237
238        mUnderflowStart = 0;
239        int visible = 0;
240        firstUnderflowIndex = -1;
241        for (int i = totalVisible - 1; i >= 0; i--) {
242            StatusIconState state = mLayoutStates.get(i);
243            // Allow room for underflow if we found we need it in onMeasure
244            if (mNeedsUnderflow && (state.xTranslation < (contentStart + mUnderflowWidth))||
245                    (mShouldRestrictIcons && visible >= maxVisible)) {
246                firstUnderflowIndex = i;
247                break;
248            }
249            mUnderflowStart = (int) Math.max(contentStart, state.xTranslation - mUnderflowWidth);
250            visible++;
251        }
252
253        if (firstUnderflowIndex != -1) {
254            int totalDots = 0;
255            int dotWidth = mStaticDotDiameter + mDotPadding;
256            int dotOffset = mUnderflowStart + mUnderflowWidth - mIconDotFrameWidth;
257            for (int i = firstUnderflowIndex; i >= 0; i--) {
258                StatusIconState state = mLayoutStates.get(i);
259                if (totalDots < MAX_DOTS) {
260                    state.xTranslation = dotOffset;
261                    state.visibleState = STATE_DOT;
262                    dotOffset -= dotWidth;
263                    totalDots++;
264                } else {
265                    state.visibleState = STATE_HIDDEN;
266                }
267            }
268        }
269
270        // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean
271        if (isLayoutRtl()) {
272            for (int i = 0; i < childCount; i++) {
273                View child = getChildAt(i);
274                StatusIconState state = getViewStateFromChild(child);
275                state.xTranslation = width - state.xTranslation - child.getWidth();
276            }
277        }
278    }
279
280    private void applyIconStates() {
281        for (int i = 0; i < getChildCount(); i++) {
282            View child = getChildAt(i);
283            StatusIconState vs = getViewStateFromChild(child);
284            if (vs != null) {
285                vs.applyToView(child);
286            }
287        }
288    }
289
290    private void resetViewStates() {
291        for (int i = 0; i < getChildCount(); i++) {
292            View child = getChildAt(i);
293            StatusIconState vs = getViewStateFromChild(child);
294            if (vs == null) {
295                continue;
296            }
297
298            vs.initFrom(child);
299            vs.alpha = 1.0f;
300            if (child instanceof StatusIconDisplayable) {
301                vs.hidden = !((StatusIconDisplayable)child).isIconVisible();
302            } else {
303                vs.hidden = false;
304            }
305        }
306    }
307
308    private static @Nullable StatusIconState getViewStateFromChild(View child) {
309        return (StatusIconState) child.getTag(R.id.status_bar_view_state_tag);
310    }
311
312    private static int getViewTotalMeasuredWidth(View child) {
313        return child.getMeasuredWidth() + child.getPaddingStart() + child.getPaddingEnd();
314    }
315
316    private static int getViewTotalWidth(View child) {
317        return child.getWidth() + child.getPaddingStart() + child.getPaddingEnd();
318    }
319
320    public static class StatusIconState extends ViewState {
321        /// StatusBarIconView.STATE_*
322        public int visibleState = STATE_ICON;
323
324        @Override
325        public void applyToView(View view) {
326            if (view instanceof  StatusIconDisplayable) {
327                StatusIconDisplayable icon = (StatusIconDisplayable) view;
328                icon.setVisibleState(visibleState);
329            }
330            super.applyToView(view);
331        }
332    }
333}
334