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