blob: f8f698131f4eb5e234502802273fc88323cabd5c [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;
Evan Laird97ae20c2018-05-10 13:13:27 -040052 // Max 8 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;
Evan Laird97ae20c2018-05-10 13:13:27 -0400155 int totalWidth = mPaddingLeft + mPaddingRight;
Evan Laird20b87bf2018-04-12 09:54:11 -0400156 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 Laird97ae20c2018-05-10 13:13:27 -0400211 float width = getWidth();
212 float translationX = width - getPaddingEnd();
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 Laird620ea2c2018-06-21 11:18:19 -0400304 vs.hidden = false;
Evan Laird058c8ae2018-01-12 14:26:10 -0500305 }
306 }
307
Evan Laird20b87bf2018-04-12 09:54:11 -0400308 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;
Evan Laird150701d42018-05-04 16:14:00 -0400323 public boolean justAdded = true;
Evan Laird20b87bf2018-04-12 09:54:11 -0400324
325 @Override
326 public void applyToView(View view) {
Evan Laird150701d42018-05-04 16:14:00 -0400327 if (!(view instanceof StatusIconDisplayable)) {
328 return;
Evan Laird20b87bf2018-04-12 09:54:11 -0400329 }
Evan Laird150701d42018-05-04 16:14:00 -0400330 StatusIconDisplayable icon = (StatusIconDisplayable) view;
331 AnimationProperties animationProperties = null;
Evan Laird620ea2c2018-06-21 11:18:19 -0400332 boolean animateVisibility = true;
Evan Laird150701d42018-05-04 16:14:00 -0400333
Evan Laird620ea2c2018-06-21 11:18:19 -0400334 // Figure out which properties of the state transition (if any) we need to animate
335 if (justAdded
336 || icon.getVisibleState() == STATE_HIDDEN && visibleState == STATE_ICON) {
337 // Icon is appearing, fade it in by putting it where it will be and animating alpha
Evan Laird150701d42018-05-04 16:14:00 -0400338 super.applyToView(view);
Evan Laird620ea2c2018-06-21 11:18:19 -0400339 view.setAlpha(0.f);
340 icon.setVisibleState(STATE_HIDDEN);
Evan Laird150701d42018-05-04 16:14:00 -0400341 animationProperties = ADD_ICON_PROPERTIES;
Evan Laird150701d42018-05-04 16:14:00 -0400342 } else if (icon.getVisibleState() != visibleState) {
Evan Laird620ea2c2018-06-21 11:18:19 -0400343 if (icon.getVisibleState() == STATE_ICON && visibleState == STATE_HIDDEN) {
344 // Disappearing, don't do anything fancy
345 animateVisibility = false;
346 } else {
347 // all other transitions (to/from dot, etc)
348 animationProperties = ANIMATE_ALL_PROPERTIES;
349 }
350 } else if (visibleState != STATE_HIDDEN && xTranslation != view.getTranslationX()) {
351 // Visibility isn't changing, just animate position
352 animationProperties = X_ANIMATION_PROPERTIES;
Evan Laird150701d42018-05-04 16:14:00 -0400353 }
354
Evan Laird620ea2c2018-06-21 11:18:19 -0400355 icon.setVisibleState(visibleState, animateVisibility);
356 if (animationProperties != null) {
Evan Laird150701d42018-05-04 16:14:00 -0400357 animateTo(view, animationProperties);
358 } else {
359 super.applyToView(view);
360 }
361
362 justAdded = false;
Evan Laird20b87bf2018-04-12 09:54:11 -0400363 }
Evan Laird058c8ae2018-01-12 14:26:10 -0500364 }
Evan Laird150701d42018-05-04 16:14:00 -0400365
366 private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
367 private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
368
369 @Override
370 public AnimationFilter getAnimationFilter() {
371 return mAnimationFilter;
372 }
373 }.setDuration(200).setDelay(50);
374
Evan Laird620ea2c2018-06-21 11:18:19 -0400375 private static final AnimationProperties X_ANIMATION_PROPERTIES = new AnimationProperties() {
Evan Laird150701d42018-05-04 16:14:00 -0400376 private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
377
378 @Override
379 public AnimationFilter getAnimationFilter() {
380 return mAnimationFilter;
381 }
382 }.setDuration(200);
Evan Laird620ea2c2018-06-21 11:18:19 -0400383
384 private static final AnimationProperties ANIMATE_ALL_PROPERTIES = new AnimationProperties() {
385 private AnimationFilter mAnimationFilter = new AnimationFilter().animateX().animateY()
386 .animateAlpha().animateScale();
387
388 @Override
389 public AnimationFilter getAnimationFilter() {
390 return mAnimationFilter;
391 }
392 }.setDuration(200);
Evan Laird058c8ae2018-01-12 14:26:10 -0500393}