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