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