blob: 4538977f7a6fa2bc868354b081c48378dcf63508 [file] [log] [blame]
Evan Laird058c8ae2018-01-12 14:26:10 -05001/*
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
Evan Laird058c8ae2018-01-12 14:26:10 -050017package com.android.systemui.statusbar.phone;
18
Evan Laird20b87bf2018-04-12 09:54:11 -040019import 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
Evan Laird058c8ae2018-01-12 14:26:10 -050023import android.annotation.Nullable;
24import android.content.Context;
Evan Laird20b87bf2018-04-12 09:54:11 -040025import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.Paint;
28import android.graphics.Paint.Style;
Evan Laird058c8ae2018-01-12 14:26:10 -050029import android.util.AttributeSet;
Evan Lairde1d13c92018-03-20 16:58:01 -040030import android.util.Log;
Evan Laird058c8ae2018-01-12 14:26:10 -050031
32import android.view.View;
33import com.android.keyguard.AlphaOptimizedLinearLayout;
34import com.android.systemui.R;
Evan Lairde1d13c92018-03-20 16:58:01 -040035import com.android.systemui.statusbar.StatusIconDisplayable;
Evan Laird150701d42018-05-04 16:14:00 -040036import com.android.systemui.statusbar.stack.AnimationFilter;
37import com.android.systemui.statusbar.stack.AnimationProperties;
Evan Laird058c8ae2018-01-12 14:26:10 -050038import com.android.systemui.statusbar.stack.ViewState;
Evan Laird20b87bf2018-04-12 09:54:11 -040039import java.util.ArrayList;
Evan Laird058c8ae2018-01-12 14:26:10 -050040
Evan Laird937d9fa2018-02-08 10:12:16 -080041/**
42 * A container for Status bar system icons. Limits the number of system icons and handles overflow
Evan Laird20b87bf2018-04-12 09:54:11 -040043 * similar to {@link NotificationIconContainer}.
Evan Laird937d9fa2018-02-08 10:12:16 -080044 *
Evan Laird20b87bf2018-04-12 09:54:11 -040045 * Children are expected to implement {@link StatusIconDisplayable}
Evan Laird937d9fa2018-02-08 10:12:16 -080046 */
Evan Laird058c8ae2018-01-12 14:26:10 -050047public class StatusIconContainer extends AlphaOptimizedLinearLayout {
48
49 private static final String TAG = "StatusIconContainer";
Evan Lairde0fbc3e2018-02-13 11:41:00 -050050 private static final boolean DEBUG = false;
Evan Laird20b87bf2018-04-12 09:54:11 -040051 private static final boolean DEBUG_OVERFLOW = false;
52 // Max 5 status icons including battery
Evan Laird150701d42018-05-04 16:14:00 -040053 private static final int MAX_ICONS = 7;
54 private static final int MAX_DOTS = 1;
Evan Laird058c8ae2018-01-12 14:26:10 -050055
Evan Laird20b87bf2018-04-12 09:54:11 -040056 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
Evan Laird937d9fa2018-02-08 10:12:16 -080070 public StatusIconContainer(Context context) {
71 this(context, null);
72 }
73
Evan Laird058c8ae2018-01-12 14:26:10 -050074 public StatusIconContainer(Context context, AttributeSet attrs) {
75 super(context, attrs);
Evan Lairdfeec2ab2018-05-02 12:47:26 -040076 initDimens();
77 setWillNotDraw(!DEBUG_OVERFLOW);
Evan Laird058c8ae2018-01-12 14:26:10 -050078 }
79
80 @Override
Evan Laird20b87bf2018-04-12 09:54:11 -040081 protected void onFinishInflate() {
82 super.onFinishInflate();
Evan Laird20b87bf2018-04-12 09:54:11 -040083 }
84
85 public void setShouldRestrictIcons(boolean should) {
86 mShouldRestrictIcons = should;
87 }
88
Evan Lairdfeec2ab2018-05-02 12:47:26 -040089 public boolean isRestrictingIcons() {
90 return mShouldRestrictIcons;
91 }
92
Evan Laird20b87bf2018-04-12 09:54:11 -040093 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;
Evan Laird150701d42018-05-04 16:14:00 -0400100 mUnderflowWidth = mIconDotFrameWidth + (MAX_DOTS - 1) * (mStaticDotDiameter + mDotPadding);
Evan Laird20b87bf2018-04-12 09:54:11 -0400101 }
102
103 @Override
Evan Laird058c8ae2018-01-12 14:26:10 -0500104 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
Evan Laird20b87bf2018-04-12 09:54:11 -0400122 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
Evan Laird058c8ae2018-01-12 14:26:10 -0500140 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Evan Laird20b87bf2018-04-12 09:54:11 -0400141 mMeasureViews.clear();
142 int mode = MeasureSpec.getMode(widthMeasureSpec);
143 final int width = MeasureSpec.getSize(widthMeasureSpec);
Evan Laird058c8ae2018-01-12 14:26:10 -0500144 final int count = getChildCount();
Evan Laird20b87bf2018-04-12 09:54:11 -0400145 // Collect all of the views which want to be laid out
Evan Laird058c8ae2018-01-12 14:26:10 -0500146 for (int i = 0; i < count; i++) {
Evan Laird20b87bf2018-04-12 09:54:11 -0400147 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 = getPaddingStart() + getPaddingEnd();
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));
Evan Laird058c8ae2018-01-12 14:26:10 -0500189 }
190 }
191
192 @Override
193 public void onViewAdded(View child) {
194 super.onViewAdded(child);
Evan Laird20b87bf2018-04-12 09:54:11 -0400195 StatusIconState vs = new StatusIconState();
Evan Laird150701d42018-05-04 16:14:00 -0400196 vs.justAdded = true;
Evan Laird058c8ae2018-01-12 14:26:10 -0500197 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() {
Evan Laird20b87bf2018-04-12 09:54:11 -0400210 mLayoutStates.clear();
Evan Lairde1d13c92018-03-20 16:58:01 -0400211 float width = getWidth() - getPaddingEnd();
Evan Laird63036132018-02-02 16:30:41 -0500212 float translationX = width;
Evan Laird058c8ae2018-01-12 14:26:10 -0500213 float contentStart = getPaddingStart();
214 int childCount = getChildCount();
215 // Underflow === don't show content until that index
Evan Lairdfeec2ab2018-05-02 12:47:26 -0400216 if (DEBUG) android.util.Log.d(TAG, "calculateIconTranslations: start=" + translationX
217 + " width=" + width + " underflow=" + mNeedsUnderflow);
Evan Laird058c8ae2018-01-12 14:26:10 -0500218
Evan Laird20b87bf2018-04-12 09:54:11 -0400219 // Collect all of the states which want to be visible
Evan Laird058c8ae2018-01-12 14:26:10 -0500220 for (int i = childCount - 1; i >= 0; i--) {
221 View child = getChildAt(i);
Evan Lairde1d13c92018-03-20 16:58:01 -0400222 StatusIconDisplayable iconView = (StatusIconDisplayable) child;
Evan Laird20b87bf2018-04-12 09:54:11 -0400223 StatusIconState childState = getViewStateFromChild(child);
Evan Laird058c8ae2018-01-12 14:26:10 -0500224
Evan Lairde1d13c92018-03-20 16:58:01 -0400225 if (!iconView.isIconVisible() || iconView.isIconBlocked()) {
Evan Laird20b87bf2018-04-12 09:54:11 -0400226 childState.visibleState = STATE_HIDDEN;
Evan Lairde1d13c92018-03-20 16:58:01 -0400227 if (DEBUG) Log.d(TAG, "skipping child (" + iconView.getSlot() + ") not visible");
Evan Laird058c8ae2018-01-12 14:26:10 -0500228 continue;
229 }
230
Evan Laird20b87bf2018-04-12 09:54:11 -0400231 childState.visibleState = STATE_ICON;
232 childState.xTranslation = translationX - getViewTotalWidth(child);
233 mLayoutStates.add(0, childState);
Evan Laird058c8ae2018-01-12 14:26:10 -0500234
Evan Laird20b87bf2018-04-12 09:54:11 -0400235 translationX -= getViewTotalWidth(child);
236 }
237
Evan Laird150701d42018-05-04 16:14:00 -0400238 // Show either 1-MAX_ICONS icons, or (MAX_ICONS - 1) icons + overflow
Evan Laird20b87bf2018-04-12 09:54:11 -0400239 int totalVisible = mLayoutStates.size();
240 int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
241
242 mUnderflowStart = 0;
243 int visible = 0;
Evan Laird150701d42018-05-04 16:14:00 -0400244 int firstUnderflowIndex = -1;
Evan Laird20b87bf2018-04-12 09:54:11 -0400245 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;
Evan Laird058c8ae2018-01-12 14:26:10 -0500252 }
Evan Laird20b87bf2018-04-12 09:54:11 -0400253 mUnderflowStart = (int) Math.max(contentStart, state.xTranslation - mUnderflowWidth);
254 visible++;
Evan Laird058c8ae2018-01-12 14:26:10 -0500255 }
256
257 if (firstUnderflowIndex != -1) {
Evan Laird20b87bf2018-04-12 09:54:11 -0400258 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;
Evan Laird058c8ae2018-01-12 14:26:10 -0500270 }
271 }
272 }
Evan Laird63036132018-02-02 16:30:41 -0500273
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);
Evan Laird20b87bf2018-04-12 09:54:11 -0400278 StatusIconState state = getViewStateFromChild(child);
Evan Laird63036132018-02-02 16:30:41 -0500279 state.xTranslation = width - state.xTranslation - child.getWidth();
280 }
281 }
Evan Laird058c8ae2018-01-12 14:26:10 -0500282 }
283
284 private void applyIconStates() {
285 for (int i = 0; i < getChildCount(); i++) {
286 View child = getChildAt(i);
Evan Laird20b87bf2018-04-12 09:54:11 -0400287 StatusIconState vs = getViewStateFromChild(child);
Evan Laird058c8ae2018-01-12 14:26:10 -0500288 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);
Evan Laird20b87bf2018-04-12 09:54:11 -0400297 StatusIconState vs = getViewStateFromChild(child);
Evan Laird058c8ae2018-01-12 14:26:10 -0500298 if (vs == null) {
299 continue;
300 }
301
302 vs.initFrom(child);
303 vs.alpha = 1.0f;
Evan Lairde1d13c92018-03-20 16:58:01 -0400304 if (child instanceof StatusIconDisplayable) {
305 vs.hidden = !((StatusIconDisplayable)child).isIconVisible();
Evan Laird058c8ae2018-01-12 14:26:10 -0500306 } else {
307 vs.hidden = false;
308 }
309 }
310 }
311
Evan Laird20b87bf2018-04-12 09:54:11 -0400312 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;
Evan Laird150701d42018-05-04 16:14:00 -0400327 public boolean justAdded = true;
Evan Laird20b87bf2018-04-12 09:54:11 -0400328
329 @Override
330 public void applyToView(View view) {
Evan Laird150701d42018-05-04 16:14:00 -0400331 if (!(view instanceof StatusIconDisplayable)) {
332 return;
Evan Laird20b87bf2018-04-12 09:54:11 -0400333 }
Evan Laird150701d42018-05-04 16:14:00 -0400334 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 icon.setVisibleState(visibleState);
348 if (animate) {
349 animateTo(view, animationProperties);
350 } else {
351 super.applyToView(view);
352 }
353
354 justAdded = false;
Evan Laird20b87bf2018-04-12 09:54:11 -0400355 }
Evan Laird058c8ae2018-01-12 14:26:10 -0500356 }
Evan Laird150701d42018-05-04 16:14:00 -0400357
358 private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
359 private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
360
361 @Override
362 public AnimationFilter getAnimationFilter() {
363 return mAnimationFilter;
364 }
365 }.setDuration(200).setDelay(50);
366
367 private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() {
368 private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
369
370 @Override
371 public AnimationFilter getAnimationFilter() {
372 return mAnimationFilter;
373 }
374 }.setDuration(200);
Evan Laird058c8ae2018-01-12 14:26:10 -0500375}