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