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 17 package com.android.systemui.statusbar.phone; 18 19 import static com.android.systemui.statusbar.StatusBarIconView.STATE_ICON; 20 import static com.android.systemui.statusbar.StatusBarIconView.STATE_DOT; 21 import static com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN; 22 23 import android.annotation.Nullable; 24 import android.content.Context; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.Paint; 28 import android.graphics.Paint.Style; 29 import android.util.AttributeSet; 30 import android.util.Log; 31 32 import android.view.View; 33 import com.android.keyguard.AlphaOptimizedLinearLayout; 34 import com.android.systemui.R; 35 import com.android.systemui.statusbar.StatusIconDisplayable; 36 import com.android.systemui.statusbar.stack.AnimationFilter; 37 import com.android.systemui.statusbar.stack.AnimationProperties; 38 import com.android.systemui.statusbar.stack.ViewState; 39 import 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 */ 47 public 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