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_DOT; 20 import static com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN; 21 import static com.android.systemui.statusbar.StatusBarIconView.STATE_ICON; 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 import android.view.View; 32 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.notification.stack.AnimationFilter; 37 import com.android.systemui.statusbar.notification.stack.AnimationProperties; 38 import com.android.systemui.statusbar.notification.stack.ViewState; 39 40 import java.util.ArrayList; 41 import java.util.List; 42 43 /** 44 * A container for Status bar system icons. Limits the number of system icons and handles overflow 45 * similar to {@link NotificationIconContainer}. 46 * 47 * Children are expected to implement {@link StatusIconDisplayable} 48 */ 49 public class StatusIconContainer extends AlphaOptimizedLinearLayout { 50 51 private static final String TAG = "StatusIconContainer"; 52 private static final boolean DEBUG = false; 53 private static final boolean DEBUG_OVERFLOW = false; 54 // Max 8 status icons including battery 55 private static final int MAX_ICONS = 7; 56 private static final int MAX_DOTS = 1; 57 58 private int mDotPadding; 59 private int mStaticDotDiameter; 60 private int mUnderflowWidth; 61 private int mUnderflowStart = 0; 62 // Whether or not we can draw into the underflow space 63 private boolean mNeedsUnderflow; 64 // Individual StatusBarIconViews draw their etc dots centered in this width 65 private int mIconDotFrameWidth; 66 private boolean mShouldRestrictIcons = true; 67 // Used to count which states want to be visible during layout 68 private ArrayList<StatusIconState> mLayoutStates = new ArrayList<>(); 69 // So we can count and measure properly 70 private ArrayList<View> mMeasureViews = new ArrayList<>(); 71 // Any ignored icon will never be added as a child 72 private ArrayList<String> mIgnoredSlots = new ArrayList<>(); 73 StatusIconContainer(Context context)74 public StatusIconContainer(Context context) { 75 this(context, null); 76 } 77 StatusIconContainer(Context context, AttributeSet attrs)78 public StatusIconContainer(Context context, AttributeSet attrs) { 79 super(context, attrs); 80 initDimens(); 81 setWillNotDraw(!DEBUG_OVERFLOW); 82 } 83 84 @Override onFinishInflate()85 protected void onFinishInflate() { 86 super.onFinishInflate(); 87 } 88 setShouldRestrictIcons(boolean should)89 public void setShouldRestrictIcons(boolean should) { 90 mShouldRestrictIcons = should; 91 } 92 isRestrictingIcons()93 public boolean isRestrictingIcons() { 94 return mShouldRestrictIcons; 95 } 96 initDimens()97 private void initDimens() { 98 // This is the same value that StatusBarIconView uses 99 mIconDotFrameWidth = getResources().getDimensionPixelSize( 100 com.android.internal.R.dimen.status_bar_icon_size); 101 mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding); 102 int radius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius); 103 mStaticDotDiameter = 2 * radius; 104 mUnderflowWidth = mIconDotFrameWidth + (MAX_DOTS - 1) * (mStaticDotDiameter + mDotPadding); 105 } 106 107 @Override onLayout(boolean changed, int l, int t, int r, int b)108 protected void onLayout(boolean changed, int l, int t, int r, int b) { 109 float midY = getHeight() / 2.0f; 110 111 // Layout all child views so that we can move them around later 112 for (int i = 0; i < getChildCount(); i++) { 113 View child = getChildAt(i); 114 int width = child.getMeasuredWidth(); 115 int height = child.getMeasuredHeight(); 116 int top = (int) (midY - height / 2.0f); 117 child.layout(0, top, width, top + height); 118 } 119 120 resetViewStates(); 121 calculateIconTranslations(); 122 applyIconStates(); 123 } 124 125 @Override onDraw(Canvas canvas)126 protected void onDraw(Canvas canvas) { 127 super.onDraw(canvas); 128 if (DEBUG_OVERFLOW) { 129 Paint paint = new Paint(); 130 paint.setStyle(Style.STROKE); 131 paint.setColor(Color.RED); 132 133 // Show bounding box 134 canvas.drawRect(getPaddingStart(), 0, getWidth() - getPaddingEnd(), getHeight(), paint); 135 136 // Show etc box 137 paint.setColor(Color.GREEN); 138 canvas.drawRect( 139 mUnderflowStart, 0, mUnderflowStart + mUnderflowWidth, getHeight(), paint); 140 } 141 } 142 143 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)144 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 145 mMeasureViews.clear(); 146 int mode = MeasureSpec.getMode(widthMeasureSpec); 147 final int width = MeasureSpec.getSize(widthMeasureSpec); 148 final int count = getChildCount(); 149 // Collect all of the views which want to be laid out 150 for (int i = 0; i < count; i++) { 151 StatusIconDisplayable icon = (StatusIconDisplayable) getChildAt(i); 152 if (icon.isIconVisible() && !icon.isIconBlocked() 153 && !mIgnoredSlots.contains(icon.getSlot())) { 154 mMeasureViews.add((View) icon); 155 } 156 } 157 158 int visibleCount = mMeasureViews.size(); 159 int maxVisible = visibleCount <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1; 160 int totalWidth = mPaddingLeft + mPaddingRight; 161 boolean trackWidth = true; 162 163 // Measure all children so that they report the correct width 164 int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED); 165 mNeedsUnderflow = mShouldRestrictIcons && visibleCount > MAX_ICONS; 166 for (int i = 0; i < mMeasureViews.size(); i++) { 167 // Walking backwards 168 View child = mMeasureViews.get(visibleCount - i - 1); 169 measureChild(child, childWidthSpec, heightMeasureSpec); 170 if (mShouldRestrictIcons) { 171 if (i < maxVisible && trackWidth) { 172 totalWidth += getViewTotalMeasuredWidth(child); 173 } else if (trackWidth) { 174 // We've hit the icon limit; add space for dots 175 totalWidth += mUnderflowWidth; 176 trackWidth = false; 177 } 178 } else { 179 totalWidth += getViewTotalMeasuredWidth(child); 180 } 181 } 182 183 if (mode == MeasureSpec.EXACTLY) { 184 if (!mNeedsUnderflow && totalWidth > width) { 185 mNeedsUnderflow = true; 186 } 187 setMeasuredDimension(width, MeasureSpec.getSize(heightMeasureSpec)); 188 } else { 189 if (mode == MeasureSpec.AT_MOST && totalWidth > width) { 190 mNeedsUnderflow = true; 191 totalWidth = width; 192 } 193 setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec)); 194 } 195 } 196 197 @Override onViewAdded(View child)198 public void onViewAdded(View child) { 199 super.onViewAdded(child); 200 StatusIconState vs = new StatusIconState(); 201 vs.justAdded = true; 202 child.setTag(R.id.status_bar_view_state_tag, vs); 203 } 204 205 @Override onViewRemoved(View child)206 public void onViewRemoved(View child) { 207 super.onViewRemoved(child); 208 child.setTag(R.id.status_bar_view_state_tag, null); 209 } 210 211 /** 212 * Add a name of an icon slot to be ignored. It will not show up nor be measured 213 * @param slotName name of the icon as it exists in 214 * frameworks/base/core/res/res/values/config.xml 215 */ addIgnoredSlot(String slotName)216 public void addIgnoredSlot(String slotName) { 217 addIgnoredSlotInternal(slotName); 218 requestLayout(); 219 } 220 221 /** 222 * Add a list of slots to be ignored 223 * @param slots names of the icons to ignore 224 */ addIgnoredSlots(List<String> slots)225 public void addIgnoredSlots(List<String> slots) { 226 for (String slot : slots) { 227 addIgnoredSlotInternal(slot); 228 } 229 230 requestLayout(); 231 } 232 addIgnoredSlotInternal(String slotName)233 private void addIgnoredSlotInternal(String slotName) { 234 if (!mIgnoredSlots.contains(slotName)) { 235 mIgnoredSlots.add(slotName); 236 } 237 } 238 239 /** 240 * Remove a slot from the list of ignored icon slots. It will then be shown when set to visible 241 * by the {@link StatusBarIconController}. 242 * @param slotName name of the icon slot to remove from the ignored list 243 */ removeIgnoredSlot(String slotName)244 public void removeIgnoredSlot(String slotName) { 245 if (mIgnoredSlots.contains(slotName)) { 246 mIgnoredSlots.remove(slotName); 247 } 248 249 requestLayout(); 250 } 251 252 /** 253 * Sets the list of ignored icon slots clearing the current list. 254 * @param slots names of the icons to ignore 255 */ setIgnoredSlots(List<String> slots)256 public void setIgnoredSlots(List<String> slots) { 257 mIgnoredSlots.clear(); 258 addIgnoredSlots(slots); 259 } 260 261 /** 262 * Layout is happening from end -> start 263 */ calculateIconTranslations()264 private void calculateIconTranslations() { 265 mLayoutStates.clear(); 266 float width = getWidth(); 267 float translationX = width - getPaddingEnd(); 268 float contentStart = getPaddingStart(); 269 int childCount = getChildCount(); 270 // Underflow === don't show content until that index 271 if (DEBUG) android.util.Log.d(TAG, "calculateIconTranslations: start=" + translationX 272 + " width=" + width + " underflow=" + mNeedsUnderflow); 273 274 // Collect all of the states which want to be visible 275 for (int i = childCount - 1; i >= 0; i--) { 276 View child = getChildAt(i); 277 StatusIconDisplayable iconView = (StatusIconDisplayable) child; 278 StatusIconState childState = getViewStateFromChild(child); 279 280 if (!iconView.isIconVisible() || iconView.isIconBlocked() 281 || mIgnoredSlots.contains(iconView.getSlot())) { 282 childState.visibleState = STATE_HIDDEN; 283 if (DEBUG) Log.d(TAG, "skipping child (" + iconView.getSlot() + ") not visible"); 284 continue; 285 } 286 287 childState.visibleState = STATE_ICON; 288 childState.xTranslation = translationX - getViewTotalWidth(child); 289 mLayoutStates.add(0, childState); 290 291 translationX -= getViewTotalWidth(child); 292 } 293 294 // Show either 1-MAX_ICONS icons, or (MAX_ICONS - 1) icons + overflow 295 int totalVisible = mLayoutStates.size(); 296 int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1; 297 298 mUnderflowStart = 0; 299 int visible = 0; 300 int firstUnderflowIndex = -1; 301 for (int i = totalVisible - 1; i >= 0; i--) { 302 StatusIconState state = mLayoutStates.get(i); 303 // Allow room for underflow if we found we need it in onMeasure 304 if (mNeedsUnderflow && (state.xTranslation < (contentStart + mUnderflowWidth))|| 305 (mShouldRestrictIcons && visible >= maxVisible)) { 306 firstUnderflowIndex = i; 307 break; 308 } 309 mUnderflowStart = (int) Math.max(contentStart, state.xTranslation - mUnderflowWidth); 310 visible++; 311 } 312 313 if (firstUnderflowIndex != -1) { 314 int totalDots = 0; 315 int dotWidth = mStaticDotDiameter + mDotPadding; 316 int dotOffset = mUnderflowStart + mUnderflowWidth - mIconDotFrameWidth; 317 for (int i = firstUnderflowIndex; i >= 0; i--) { 318 StatusIconState state = mLayoutStates.get(i); 319 if (totalDots < MAX_DOTS) { 320 state.xTranslation = dotOffset; 321 state.visibleState = STATE_DOT; 322 dotOffset -= dotWidth; 323 totalDots++; 324 } else { 325 state.visibleState = STATE_HIDDEN; 326 } 327 } 328 } 329 330 // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean 331 if (isLayoutRtl()) { 332 for (int i = 0; i < childCount; i++) { 333 View child = getChildAt(i); 334 StatusIconState state = getViewStateFromChild(child); 335 state.xTranslation = width - state.xTranslation - child.getWidth(); 336 } 337 } 338 } 339 applyIconStates()340 private void applyIconStates() { 341 for (int i = 0; i < getChildCount(); i++) { 342 View child = getChildAt(i); 343 StatusIconState vs = getViewStateFromChild(child); 344 if (vs != null) { 345 vs.applyToView(child); 346 } 347 } 348 } 349 resetViewStates()350 private void resetViewStates() { 351 for (int i = 0; i < getChildCount(); i++) { 352 View child = getChildAt(i); 353 StatusIconState vs = getViewStateFromChild(child); 354 if (vs == null) { 355 continue; 356 } 357 358 vs.initFrom(child); 359 vs.alpha = 1.0f; 360 vs.hidden = false; 361 } 362 } 363 getViewStateFromChild(View child)364 private static @Nullable StatusIconState getViewStateFromChild(View child) { 365 return (StatusIconState) child.getTag(R.id.status_bar_view_state_tag); 366 } 367 getViewTotalMeasuredWidth(View child)368 private static int getViewTotalMeasuredWidth(View child) { 369 return child.getMeasuredWidth() + child.getPaddingStart() + child.getPaddingEnd(); 370 } 371 getViewTotalWidth(View child)372 private static int getViewTotalWidth(View child) { 373 return child.getWidth() + child.getPaddingStart() + child.getPaddingEnd(); 374 } 375 376 public static class StatusIconState extends ViewState { 377 /// StatusBarIconView.STATE_* 378 public int visibleState = STATE_ICON; 379 public boolean justAdded = true; 380 381 // How far we are from the end of the view actually is the most relevant for animation 382 float distanceToViewEnd = -1; 383 384 @Override applyToView(View view)385 public void applyToView(View view) { 386 float parentWidth = 0; 387 if (view.getParent() instanceof View) { 388 parentWidth = ((View) view.getParent()).getWidth(); 389 } 390 391 float currentDistanceToEnd = parentWidth - xTranslation; 392 393 if (!(view instanceof StatusIconDisplayable)) { 394 return; 395 } 396 StatusIconDisplayable icon = (StatusIconDisplayable) view; 397 AnimationProperties animationProperties = null; 398 boolean animateVisibility = true; 399 400 // Figure out which properties of the state transition (if any) we need to animate 401 if (justAdded 402 || icon.getVisibleState() == STATE_HIDDEN && visibleState == STATE_ICON) { 403 // Icon is appearing, fade it in by putting it where it will be and animating alpha 404 super.applyToView(view); 405 view.setAlpha(0.f); 406 icon.setVisibleState(STATE_HIDDEN); 407 animationProperties = ADD_ICON_PROPERTIES; 408 } else if (icon.getVisibleState() != visibleState) { 409 if (icon.getVisibleState() == STATE_ICON && visibleState == STATE_HIDDEN) { 410 // Disappearing, don't do anything fancy 411 animateVisibility = false; 412 } else { 413 // all other transitions (to/from dot, etc) 414 animationProperties = ANIMATE_ALL_PROPERTIES; 415 } 416 } else if (visibleState != STATE_HIDDEN && distanceToViewEnd != currentDistanceToEnd) { 417 // Visibility isn't changing, just animate position 418 animationProperties = X_ANIMATION_PROPERTIES; 419 } 420 421 icon.setVisibleState(visibleState, animateVisibility); 422 if (animationProperties != null) { 423 animateTo(view, animationProperties); 424 } else { 425 super.applyToView(view); 426 } 427 428 justAdded = false; 429 distanceToViewEnd = currentDistanceToEnd; 430 431 } 432 } 433 434 private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() { 435 private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha(); 436 437 @Override 438 public AnimationFilter getAnimationFilter() { 439 return mAnimationFilter; 440 } 441 }.setDuration(200).setDelay(50); 442 443 private static final AnimationProperties X_ANIMATION_PROPERTIES = new AnimationProperties() { 444 private AnimationFilter mAnimationFilter = new AnimationFilter().animateX(); 445 446 @Override 447 public AnimationFilter getAnimationFilter() { 448 return mAnimationFilter; 449 } 450 }.setDuration(200); 451 452 private static final AnimationProperties ANIMATE_ALL_PROPERTIES = new AnimationProperties() { 453 private AnimationFilter mAnimationFilter = new AnimationFilter().animateX().animateY() 454 .animateAlpha().animateScale(); 455 456 @Override 457 public AnimationFilter getAnimationFilter() { 458 return mAnimationFilter; 459 } 460 }.setDuration(200); 461 } 462