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 addIgnoredSlotInternal(slotName); 221 requestLayout(); 222 } 223 224 /** 225 * Add a list of slots to be ignored 226 * @param slots names of the icons to ignore 227 */ addIgnoredSlots(List<String> slots)228 public void addIgnoredSlots(List<String> slots) { 229 for (String slot : slots) { 230 addIgnoredSlotInternal(slot); 231 } 232 233 requestLayout(); 234 } 235 addIgnoredSlotInternal(String slotName)236 private void addIgnoredSlotInternal(String slotName) { 237 if (!mIgnoredSlots.contains(slotName)) { 238 mIgnoredSlots.add(slotName); 239 } 240 } 241 242 /** 243 * Remove a slot from the list of ignored icon slots. It will then be shown when set to visible 244 * by the {@link StatusBarIconController}. 245 * @param slotName name of the icon slot to remove from the ignored list 246 */ removeIgnoredSlot(String slotName)247 public void removeIgnoredSlot(String slotName) { 248 mIgnoredSlots.remove(slotName); 249 250 requestLayout(); 251 } 252 253 /** 254 * Remove a list of slots from the list of ignored icon slots. 255 * It will then be shown when set to visible by the {@link StatusBarIconController}. 256 * @param slots name of the icon slots to remove from the ignored list 257 */ removeIgnoredSlots(List<String> slots)258 public void removeIgnoredSlots(List<String> slots) { 259 for (String slot : slots) { 260 mIgnoredSlots.remove(slot); 261 } 262 263 requestLayout(); 264 } 265 266 /** 267 * Sets the list of ignored icon slots clearing the current list. 268 * @param slots names of the icons to ignore 269 */ setIgnoredSlots(List<String> slots)270 public void setIgnoredSlots(List<String> slots) { 271 mIgnoredSlots.clear(); 272 addIgnoredSlots(slots); 273 } 274 275 /** 276 * Returns the view corresponding to a particular slot. 277 * 278 * Use it solely to manipulate how it is presented. 279 * @param slot name of the slot to find. Names are defined in 280 * {@link com.android.internal.R.config_statusBarIcons} 281 * @return a view for the slot if this container has it, else {@code null} 282 */ getViewForSlot(String slot)283 public View getViewForSlot(String slot) { 284 for (int i = 0; i < getChildCount(); i++) { 285 View child = getChildAt(i); 286 if (child instanceof StatusIconDisplayable 287 && ((StatusIconDisplayable) child).getSlot().equals(slot)) { 288 return child; 289 } 290 } 291 return null; 292 } 293 294 /** 295 * Layout is happening from end -> start 296 */ calculateIconTranslations()297 private void calculateIconTranslations() { 298 mLayoutStates.clear(); 299 float width = getWidth(); 300 float translationX = width - getPaddingEnd(); 301 float contentStart = getPaddingStart(); 302 int childCount = getChildCount(); 303 // Underflow === don't show content until that index 304 if (DEBUG) Log.d(TAG, "calculateIconTranslations: start=" + translationX 305 + " width=" + width + " underflow=" + mNeedsUnderflow); 306 307 // Collect all of the states which want to be visible 308 for (int i = childCount - 1; i >= 0; i--) { 309 View child = getChildAt(i); 310 StatusIconDisplayable iconView = (StatusIconDisplayable) child; 311 StatusIconState childState = getViewStateFromChild(child); 312 313 if (!iconView.isIconVisible() || iconView.isIconBlocked() 314 || mIgnoredSlots.contains(iconView.getSlot())) { 315 childState.visibleState = STATE_HIDDEN; 316 if (DEBUG) Log.d(TAG, "skipping child (" + iconView.getSlot() + ") not visible"); 317 continue; 318 } 319 320 // Move translationX to the spot within StatusIconContainer's layout to add the view 321 // without cutting off the child view. 322 translationX -= getViewTotalWidth(child); 323 childState.visibleState = STATE_ICON; 324 childState.xTranslation = translationX; 325 mLayoutStates.add(0, childState); 326 327 // Shift translationX over by mIconSpacing for the next view. 328 translationX -= mIconSpacing; 329 } 330 331 // Show either 1-MAX_ICONS icons, or (MAX_ICONS - 1) icons + overflow 332 int totalVisible = mLayoutStates.size(); 333 int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1; 334 335 mUnderflowStart = 0; 336 int visible = 0; 337 int firstUnderflowIndex = -1; 338 for (int i = totalVisible - 1; i >= 0; i--) { 339 StatusIconState state = mLayoutStates.get(i); 340 // Allow room for underflow if we found we need it in onMeasure 341 if (mNeedsUnderflow && (state.xTranslation < (contentStart + mUnderflowWidth))|| 342 (mShouldRestrictIcons && visible >= maxVisible)) { 343 firstUnderflowIndex = i; 344 break; 345 } 346 mUnderflowStart = (int) Math.max( 347 contentStart, state.xTranslation - mUnderflowWidth - mIconSpacing); 348 visible++; 349 } 350 351 if (firstUnderflowIndex != -1) { 352 int totalDots = 0; 353 int dotWidth = mStaticDotDiameter + mDotPadding; 354 int dotOffset = mUnderflowStart + mUnderflowWidth - mIconDotFrameWidth; 355 for (int i = firstUnderflowIndex; i >= 0; i--) { 356 StatusIconState state = mLayoutStates.get(i); 357 if (totalDots < MAX_DOTS) { 358 state.xTranslation = dotOffset; 359 state.visibleState = STATE_DOT; 360 dotOffset -= dotWidth; 361 totalDots++; 362 } else { 363 state.visibleState = STATE_HIDDEN; 364 } 365 } 366 } 367 368 // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean 369 if (isLayoutRtl()) { 370 for (int i = 0; i < childCount; i++) { 371 View child = getChildAt(i); 372 StatusIconState state = getViewStateFromChild(child); 373 state.xTranslation = width - state.xTranslation - child.getWidth(); 374 } 375 } 376 } 377 applyIconStates()378 private void applyIconStates() { 379 for (int i = 0; i < getChildCount(); i++) { 380 View child = getChildAt(i); 381 StatusIconState vs = getViewStateFromChild(child); 382 if (vs != null) { 383 vs.applyToView(child); 384 } 385 } 386 } 387 resetViewStates()388 private void resetViewStates() { 389 for (int i = 0; i < getChildCount(); i++) { 390 View child = getChildAt(i); 391 StatusIconState vs = getViewStateFromChild(child); 392 if (vs == null) { 393 continue; 394 } 395 396 vs.initFrom(child); 397 vs.alpha = 1.0f; 398 vs.hidden = false; 399 } 400 } 401 getViewStateFromChild(View child)402 private static @Nullable StatusIconState getViewStateFromChild(View child) { 403 return (StatusIconState) child.getTag(R.id.status_bar_view_state_tag); 404 } 405 getViewTotalMeasuredWidth(View child)406 private static int getViewTotalMeasuredWidth(View child) { 407 return child.getMeasuredWidth() + child.getPaddingStart() + child.getPaddingEnd(); 408 } 409 getViewTotalWidth(View child)410 private static int getViewTotalWidth(View child) { 411 return child.getWidth() + child.getPaddingStart() + child.getPaddingEnd(); 412 } 413 414 public static class StatusIconState extends ViewState { 415 /// StatusBarIconView.STATE_* 416 public int visibleState = STATE_ICON; 417 public boolean justAdded = true; 418 419 // How far we are from the end of the view actually is the most relevant for animation 420 float distanceToViewEnd = -1; 421 422 @Override applyToView(View view)423 public void applyToView(View view) { 424 float parentWidth = 0; 425 if (view.getParent() instanceof View) { 426 parentWidth = ((View) view.getParent()).getWidth(); 427 } 428 429 float currentDistanceToEnd = parentWidth - xTranslation; 430 431 if (!(view instanceof StatusIconDisplayable)) { 432 return; 433 } 434 StatusIconDisplayable icon = (StatusIconDisplayable) view; 435 AnimationProperties animationProperties = null; 436 boolean animateVisibility = true; 437 438 // Figure out which properties of the state transition (if any) we need to animate 439 if (justAdded 440 || icon.getVisibleState() == STATE_HIDDEN && visibleState == STATE_ICON) { 441 // Icon is appearing, fade it in by putting it where it will be and animating alpha 442 super.applyToView(view); 443 view.setAlpha(0.f); 444 icon.setVisibleState(STATE_HIDDEN); 445 animationProperties = ADD_ICON_PROPERTIES; 446 } else if (icon.getVisibleState() != visibleState) { 447 if (icon.getVisibleState() == STATE_ICON && visibleState == STATE_HIDDEN) { 448 // Disappearing, don't do anything fancy 449 animateVisibility = false; 450 } else { 451 // all other transitions (to/from dot, etc) 452 animationProperties = ANIMATE_ALL_PROPERTIES; 453 } 454 } else if (visibleState != STATE_HIDDEN && distanceToViewEnd != currentDistanceToEnd) { 455 // Visibility isn't changing, just animate position 456 animationProperties = X_ANIMATION_PROPERTIES; 457 } 458 459 icon.setVisibleState(visibleState, animateVisibility); 460 if (animationProperties != null) { 461 animateTo(view, animationProperties); 462 } else { 463 super.applyToView(view); 464 } 465 466 justAdded = false; 467 distanceToViewEnd = currentDistanceToEnd; 468 469 } 470 } 471 472 private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() { 473 private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha(); 474 475 @Override 476 public AnimationFilter getAnimationFilter() { 477 return mAnimationFilter; 478 } 479 }.setDuration(200).setDelay(50); 480 481 private static final AnimationProperties X_ANIMATION_PROPERTIES = new AnimationProperties() { 482 private AnimationFilter mAnimationFilter = new AnimationFilter().animateX(); 483 484 @Override 485 public AnimationFilter getAnimationFilter() { 486 return mAnimationFilter; 487 } 488 }.setDuration(200); 489 490 private static final AnimationProperties ANIMATE_ALL_PROPERTIES = new AnimationProperties() { 491 private AnimationFilter mAnimationFilter = new AnimationFilter().animateX().animateY() 492 .animateAlpha().animateScale(); 493 494 @Override 495 public AnimationFilter getAnimationFilter() { 496 return mAnimationFilter; 497 } 498 }.setDuration(200); 499 } 500