blob: 6495910359bbd5ead833a547f338a9969345c55c [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_DOT;
20import static com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN;
Gus Prevasab336792018-11-14 13:52:20 -050021import static com.android.systemui.statusbar.StatusBarIconView.STATE_ICON;
Evan Laird20b87bf2018-04-12 09:54:11 -040022
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 -050031import android.view.View;
Gus Prevasab336792018-11-14 13:52:20 -050032
Evan Laird058c8ae2018-01-12 14:26:10 -050033import com.android.keyguard.AlphaOptimizedLinearLayout;
34import com.android.systemui.R;
Evan Lairde1d13c92018-03-20 16:58:01 -040035import com.android.systemui.statusbar.StatusIconDisplayable;
Rohan Shah20790b82018-07-02 17:21:04 -070036import com.android.systemui.statusbar.notification.stack.AnimationFilter;
37import com.android.systemui.statusbar.notification.stack.AnimationProperties;
38import com.android.systemui.statusbar.notification.stack.ViewState;
Gus Prevasab336792018-11-14 13:52:20 -050039
Evan Laird20b87bf2018-04-12 09:54:11 -040040import java.util.ArrayList;
Evan Laird058c8ae2018-01-12 14:26:10 -050041
Evan Laird937d9fa2018-02-08 10:12:16 -080042/**
43 * A container for Status bar system icons. Limits the number of system icons and handles overflow
Evan Laird20b87bf2018-04-12 09:54:11 -040044 * similar to {@link NotificationIconContainer}.
Evan Laird937d9fa2018-02-08 10:12:16 -080045 *
Evan Laird20b87bf2018-04-12 09:54:11 -040046 * Children are expected to implement {@link StatusIconDisplayable}
Evan Laird937d9fa2018-02-08 10:12:16 -080047 */
Evan Laird058c8ae2018-01-12 14:26:10 -050048public class StatusIconContainer extends AlphaOptimizedLinearLayout {
49
50 private static final String TAG = "StatusIconContainer";
Evan Lairde0fbc3e2018-02-13 11:41:00 -050051 private static final boolean DEBUG = false;
Evan Laird20b87bf2018-04-12 09:54:11 -040052 private static final boolean DEBUG_OVERFLOW = false;
Evan Laird97ae20c2018-05-10 13:13:27 -040053 // Max 8 status icons including battery
Evan Laird150701d42018-05-04 16:14:00 -040054 private static final int MAX_ICONS = 7;
55 private static final int MAX_DOTS = 1;
Evan Laird058c8ae2018-01-12 14:26:10 -050056
Evan Laird20b87bf2018-04-12 09:54:11 -040057 private int mDotPadding;
58 private int mStaticDotDiameter;
59 private int mUnderflowWidth;
60 private int mUnderflowStart = 0;
61 // Whether or not we can draw into the underflow space
62 private boolean mNeedsUnderflow;
63 // Individual StatusBarIconViews draw their etc dots centered in this width
64 private int mIconDotFrameWidth;
65 private boolean mShouldRestrictIcons = true;
66 // Used to count which states want to be visible during layout
67 private ArrayList<StatusIconState> mLayoutStates = new ArrayList<>();
68 // So we can count and measure properly
69 private ArrayList<View> mMeasureViews = new ArrayList<>();
70
Evan Laird937d9fa2018-02-08 10:12:16 -080071 public StatusIconContainer(Context context) {
72 this(context, null);
73 }
74
Evan Laird058c8ae2018-01-12 14:26:10 -050075 public StatusIconContainer(Context context, AttributeSet attrs) {
76 super(context, attrs);
Evan Lairdfeec2ab2018-05-02 12:47:26 -040077 initDimens();
78 setWillNotDraw(!DEBUG_OVERFLOW);
Evan Laird058c8ae2018-01-12 14:26:10 -050079 }
80
81 @Override
Evan Laird20b87bf2018-04-12 09:54:11 -040082 protected void onFinishInflate() {
83 super.onFinishInflate();
Evan Laird20b87bf2018-04-12 09:54:11 -040084 }
85
86 public void setShouldRestrictIcons(boolean should) {
87 mShouldRestrictIcons = should;
88 }
89
Evan Lairdfeec2ab2018-05-02 12:47:26 -040090 public boolean isRestrictingIcons() {
91 return mShouldRestrictIcons;
92 }
93
Evan Laird20b87bf2018-04-12 09:54:11 -040094 private void initDimens() {
95 // This is the same value that StatusBarIconView uses
96 mIconDotFrameWidth = getResources().getDimensionPixelSize(
97 com.android.internal.R.dimen.status_bar_icon_size);
98 mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
99 int radius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
100 mStaticDotDiameter = 2 * radius;
Evan Laird150701d42018-05-04 16:14:00 -0400101 mUnderflowWidth = mIconDotFrameWidth + (MAX_DOTS - 1) * (mStaticDotDiameter + mDotPadding);
Evan Laird20b87bf2018-04-12 09:54:11 -0400102 }
103
104 @Override
Evan Laird058c8ae2018-01-12 14:26:10 -0500105 protected void onLayout(boolean changed, int l, int t, int r, int b) {
106 float midY = getHeight() / 2.0f;
107
108 // Layout all child views so that we can move them around later
109 for (int i = 0; i < getChildCount(); i++) {
110 View child = getChildAt(i);
111 int width = child.getMeasuredWidth();
112 int height = child.getMeasuredHeight();
113 int top = (int) (midY - height / 2.0f);
114 child.layout(0, top, width, top + height);
115 }
116
117 resetViewStates();
118 calculateIconTranslations();
119 applyIconStates();
120 }
121
122 @Override
Evan Laird20b87bf2018-04-12 09:54:11 -0400123 protected void onDraw(Canvas canvas) {
124 super.onDraw(canvas);
125 if (DEBUG_OVERFLOW) {
126 Paint paint = new Paint();
127 paint.setStyle(Style.STROKE);
128 paint.setColor(Color.RED);
129
130 // Show bounding box
131 canvas.drawRect(getPaddingStart(), 0, getWidth() - getPaddingEnd(), getHeight(), paint);
132
133 // Show etc box
134 paint.setColor(Color.GREEN);
135 canvas.drawRect(
136 mUnderflowStart, 0, mUnderflowStart + mUnderflowWidth, getHeight(), paint);
137 }
138 }
139
140 @Override
Evan Laird058c8ae2018-01-12 14:26:10 -0500141 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Evan Laird20b87bf2018-04-12 09:54:11 -0400142 mMeasureViews.clear();
143 int mode = MeasureSpec.getMode(widthMeasureSpec);
144 final int width = MeasureSpec.getSize(widthMeasureSpec);
Evan Laird058c8ae2018-01-12 14:26:10 -0500145 final int count = getChildCount();
Evan Laird20b87bf2018-04-12 09:54:11 -0400146 // Collect all of the views which want to be laid out
Evan Laird058c8ae2018-01-12 14:26:10 -0500147 for (int i = 0; i < count; i++) {
Evan Laird20b87bf2018-04-12 09:54:11 -0400148 StatusIconDisplayable icon = (StatusIconDisplayable) getChildAt(i);
149 if (icon.isIconVisible() && !icon.isIconBlocked()) {
150 mMeasureViews.add((View) icon);
151 }
152 }
153
154 int visibleCount = mMeasureViews.size();
155 int maxVisible = visibleCount <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
Evan Laird97ae20c2018-05-10 13:13:27 -0400156 int totalWidth = mPaddingLeft + mPaddingRight;
Evan Laird20b87bf2018-04-12 09:54:11 -0400157 boolean trackWidth = true;
158
159 // Measure all children so that they report the correct width
160 int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED);
161 mNeedsUnderflow = mShouldRestrictIcons && visibleCount > MAX_ICONS;
162 for (int i = 0; i < mMeasureViews.size(); i++) {
163 // Walking backwards
164 View child = mMeasureViews.get(visibleCount - i - 1);
165 measureChild(child, childWidthSpec, heightMeasureSpec);
166 if (mShouldRestrictIcons) {
167 if (i < maxVisible && trackWidth) {
168 totalWidth += getViewTotalMeasuredWidth(child);
169 } else if (trackWidth) {
170 // We've hit the icon limit; add space for dots
171 totalWidth += mUnderflowWidth;
172 trackWidth = false;
173 }
174 } else {
175 totalWidth += getViewTotalMeasuredWidth(child);
176 }
177 }
178
179 if (mode == MeasureSpec.EXACTLY) {
180 if (!mNeedsUnderflow && totalWidth > width) {
181 mNeedsUnderflow = true;
182 }
183 setMeasuredDimension(width, MeasureSpec.getSize(heightMeasureSpec));
184 } else {
185 if (mode == MeasureSpec.AT_MOST && totalWidth > width) {
186 mNeedsUnderflow = true;
187 totalWidth = width;
188 }
189 setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec));
Evan Laird058c8ae2018-01-12 14:26:10 -0500190 }
191 }
192
193 @Override
194 public void onViewAdded(View child) {
195 super.onViewAdded(child);
Evan Laird20b87bf2018-04-12 09:54:11 -0400196 StatusIconState vs = new StatusIconState();
Evan Laird150701d42018-05-04 16:14:00 -0400197 vs.justAdded = true;
Evan Laird058c8ae2018-01-12 14:26:10 -0500198 child.setTag(R.id.status_bar_view_state_tag, vs);
199 }
200
201 @Override
202 public void onViewRemoved(View child) {
203 super.onViewRemoved(child);
204 child.setTag(R.id.status_bar_view_state_tag, null);
205 }
206
207 /**
208 * Layout is happening from end -> start
209 */
210 private void calculateIconTranslations() {
Evan Laird20b87bf2018-04-12 09:54:11 -0400211 mLayoutStates.clear();
Evan Laird97ae20c2018-05-10 13:13:27 -0400212 float width = getWidth();
213 float translationX = width - getPaddingEnd();
Evan Laird058c8ae2018-01-12 14:26:10 -0500214 float contentStart = getPaddingStart();
215 int childCount = getChildCount();
216 // Underflow === don't show content until that index
Evan Lairdfeec2ab2018-05-02 12:47:26 -0400217 if (DEBUG) android.util.Log.d(TAG, "calculateIconTranslations: start=" + translationX
218 + " width=" + width + " underflow=" + mNeedsUnderflow);
Evan Laird058c8ae2018-01-12 14:26:10 -0500219
Evan Laird20b87bf2018-04-12 09:54:11 -0400220 // Collect all of the states which want to be visible
Evan Laird058c8ae2018-01-12 14:26:10 -0500221 for (int i = childCount - 1; i >= 0; i--) {
222 View child = getChildAt(i);
Evan Lairde1d13c92018-03-20 16:58:01 -0400223 StatusIconDisplayable iconView = (StatusIconDisplayable) child;
Evan Laird20b87bf2018-04-12 09:54:11 -0400224 StatusIconState childState = getViewStateFromChild(child);
Evan Laird058c8ae2018-01-12 14:26:10 -0500225
Evan Lairde1d13c92018-03-20 16:58:01 -0400226 if (!iconView.isIconVisible() || iconView.isIconBlocked()) {
Evan Laird20b87bf2018-04-12 09:54:11 -0400227 childState.visibleState = STATE_HIDDEN;
Evan Lairde1d13c92018-03-20 16:58:01 -0400228 if (DEBUG) Log.d(TAG, "skipping child (" + iconView.getSlot() + ") not visible");
Evan Laird058c8ae2018-01-12 14:26:10 -0500229 continue;
230 }
231
Evan Laird20b87bf2018-04-12 09:54:11 -0400232 childState.visibleState = STATE_ICON;
233 childState.xTranslation = translationX - getViewTotalWidth(child);
234 mLayoutStates.add(0, childState);
Evan Laird058c8ae2018-01-12 14:26:10 -0500235
Evan Laird20b87bf2018-04-12 09:54:11 -0400236 translationX -= getViewTotalWidth(child);
237 }
238
Evan Laird150701d42018-05-04 16:14:00 -0400239 // Show either 1-MAX_ICONS icons, or (MAX_ICONS - 1) icons + overflow
Evan Laird20b87bf2018-04-12 09:54:11 -0400240 int totalVisible = mLayoutStates.size();
241 int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
242
243 mUnderflowStart = 0;
244 int visible = 0;
Evan Laird150701d42018-05-04 16:14:00 -0400245 int firstUnderflowIndex = -1;
Evan Laird20b87bf2018-04-12 09:54:11 -0400246 for (int i = totalVisible - 1; i >= 0; i--) {
247 StatusIconState state = mLayoutStates.get(i);
248 // Allow room for underflow if we found we need it in onMeasure
249 if (mNeedsUnderflow && (state.xTranslation < (contentStart + mUnderflowWidth))||
250 (mShouldRestrictIcons && visible >= maxVisible)) {
251 firstUnderflowIndex = i;
252 break;
Evan Laird058c8ae2018-01-12 14:26:10 -0500253 }
Evan Laird20b87bf2018-04-12 09:54:11 -0400254 mUnderflowStart = (int) Math.max(contentStart, state.xTranslation - mUnderflowWidth);
255 visible++;
Evan Laird058c8ae2018-01-12 14:26:10 -0500256 }
257
258 if (firstUnderflowIndex != -1) {
Evan Laird20b87bf2018-04-12 09:54:11 -0400259 int totalDots = 0;
260 int dotWidth = mStaticDotDiameter + mDotPadding;
261 int dotOffset = mUnderflowStart + mUnderflowWidth - mIconDotFrameWidth;
262 for (int i = firstUnderflowIndex; i >= 0; i--) {
263 StatusIconState state = mLayoutStates.get(i);
264 if (totalDots < MAX_DOTS) {
265 state.xTranslation = dotOffset;
266 state.visibleState = STATE_DOT;
267 dotOffset -= dotWidth;
268 totalDots++;
269 } else {
270 state.visibleState = STATE_HIDDEN;
Evan Laird058c8ae2018-01-12 14:26:10 -0500271 }
272 }
273 }
Evan Laird63036132018-02-02 16:30:41 -0500274
275 // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean
276 if (isLayoutRtl()) {
277 for (int i = 0; i < childCount; i++) {
278 View child = getChildAt(i);
Evan Laird20b87bf2018-04-12 09:54:11 -0400279 StatusIconState state = getViewStateFromChild(child);
Evan Laird63036132018-02-02 16:30:41 -0500280 state.xTranslation = width - state.xTranslation - child.getWidth();
281 }
282 }
Evan Laird058c8ae2018-01-12 14:26:10 -0500283 }
284
285 private void applyIconStates() {
286 for (int i = 0; i < getChildCount(); i++) {
287 View child = getChildAt(i);
Evan Laird20b87bf2018-04-12 09:54:11 -0400288 StatusIconState vs = getViewStateFromChild(child);
Evan Laird058c8ae2018-01-12 14:26:10 -0500289 if (vs != null) {
290 vs.applyToView(child);
291 }
292 }
293 }
294
295 private void resetViewStates() {
296 for (int i = 0; i < getChildCount(); i++) {
297 View child = getChildAt(i);
Evan Laird20b87bf2018-04-12 09:54:11 -0400298 StatusIconState vs = getViewStateFromChild(child);
Evan Laird058c8ae2018-01-12 14:26:10 -0500299 if (vs == null) {
300 continue;
301 }
302
303 vs.initFrom(child);
304 vs.alpha = 1.0f;
Evan Laird620ea2c2018-06-21 11:18:19 -0400305 vs.hidden = false;
Evan Laird058c8ae2018-01-12 14:26:10 -0500306 }
307 }
308
Evan Laird20b87bf2018-04-12 09:54:11 -0400309 private static @Nullable StatusIconState getViewStateFromChild(View child) {
310 return (StatusIconState) child.getTag(R.id.status_bar_view_state_tag);
311 }
312
313 private static int getViewTotalMeasuredWidth(View child) {
314 return child.getMeasuredWidth() + child.getPaddingStart() + child.getPaddingEnd();
315 }
316
317 private static int getViewTotalWidth(View child) {
318 return child.getWidth() + child.getPaddingStart() + child.getPaddingEnd();
319 }
320
321 public static class StatusIconState extends ViewState {
322 /// StatusBarIconView.STATE_*
323 public int visibleState = STATE_ICON;
Evan Laird150701d42018-05-04 16:14:00 -0400324 public boolean justAdded = true;
Evan Laird20b87bf2018-04-12 09:54:11 -0400325
326 @Override
327 public void applyToView(View view) {
Evan Laird150701d42018-05-04 16:14:00 -0400328 if (!(view instanceof StatusIconDisplayable)) {
329 return;
Evan Laird20b87bf2018-04-12 09:54:11 -0400330 }
Evan Laird150701d42018-05-04 16:14:00 -0400331 StatusIconDisplayable icon = (StatusIconDisplayable) view;
332 AnimationProperties animationProperties = null;
Evan Laird620ea2c2018-06-21 11:18:19 -0400333 boolean animateVisibility = true;
Evan Laird150701d42018-05-04 16:14:00 -0400334
Evan Laird620ea2c2018-06-21 11:18:19 -0400335 // Figure out which properties of the state transition (if any) we need to animate
336 if (justAdded
337 || icon.getVisibleState() == STATE_HIDDEN && visibleState == STATE_ICON) {
338 // Icon is appearing, fade it in by putting it where it will be and animating alpha
Evan Laird150701d42018-05-04 16:14:00 -0400339 super.applyToView(view);
Evan Laird620ea2c2018-06-21 11:18:19 -0400340 view.setAlpha(0.f);
341 icon.setVisibleState(STATE_HIDDEN);
Evan Laird150701d42018-05-04 16:14:00 -0400342 animationProperties = ADD_ICON_PROPERTIES;
Evan Laird150701d42018-05-04 16:14:00 -0400343 } else if (icon.getVisibleState() != visibleState) {
Evan Laird620ea2c2018-06-21 11:18:19 -0400344 if (icon.getVisibleState() == STATE_ICON && visibleState == STATE_HIDDEN) {
345 // Disappearing, don't do anything fancy
346 animateVisibility = false;
347 } else {
348 // all other transitions (to/from dot, etc)
349 animationProperties = ANIMATE_ALL_PROPERTIES;
350 }
351 } else if (visibleState != STATE_HIDDEN && xTranslation != view.getTranslationX()) {
352 // Visibility isn't changing, just animate position
353 animationProperties = X_ANIMATION_PROPERTIES;
Evan Laird150701d42018-05-04 16:14:00 -0400354 }
355
Evan Laird620ea2c2018-06-21 11:18:19 -0400356 icon.setVisibleState(visibleState, animateVisibility);
357 if (animationProperties != null) {
Evan Laird150701d42018-05-04 16:14:00 -0400358 animateTo(view, animationProperties);
359 } else {
360 super.applyToView(view);
361 }
362
363 justAdded = false;
Evan Laird20b87bf2018-04-12 09:54:11 -0400364 }
Evan Laird058c8ae2018-01-12 14:26:10 -0500365 }
Evan Laird150701d42018-05-04 16:14:00 -0400366
367 private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
368 private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
369
370 @Override
371 public AnimationFilter getAnimationFilter() {
372 return mAnimationFilter;
373 }
374 }.setDuration(200).setDelay(50);
375
Evan Laird620ea2c2018-06-21 11:18:19 -0400376 private static final AnimationProperties X_ANIMATION_PROPERTIES = new AnimationProperties() {
Evan Laird150701d42018-05-04 16:14:00 -0400377 private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
378
379 @Override
380 public AnimationFilter getAnimationFilter() {
381 return mAnimationFilter;
382 }
383 }.setDuration(200);
Evan Laird620ea2c2018-06-21 11:18:19 -0400384
385 private static final AnimationProperties ANIMATE_ALL_PROPERTIES = new AnimationProperties() {
386 private AnimationFilter mAnimationFilter = new AnimationFilter().animateX().animateY()
387 .animateAlpha().animateScale();
388
389 @Override
390 public AnimationFilter getAnimationFilter() {
391 return mAnimationFilter;
392 }
393 }.setDuration(200);
Evan Laird058c8ae2018-01-12 14:26:10 -0500394}