StatusIconContainer.java revision 20b87bf0ae8c880a76d0de859b3665b7d4f2e144
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 17package com.android.systemui.statusbar.phone; 18 19import static com.android.systemui.statusbar.StatusBarIconView.STATE_ICON; 20import static com.android.systemui.statusbar.StatusBarIconView.STATE_DOT; 21import static com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN; 22 23import android.annotation.Nullable; 24import android.content.Context; 25import android.graphics.Canvas; 26import android.graphics.Color; 27import android.graphics.Paint; 28import android.graphics.Paint.Style; 29import android.util.AttributeSet; 30import android.util.Log; 31 32import android.view.View; 33import android.view.ViewGroup; 34import com.android.keyguard.AlphaOptimizedLinearLayout; 35import com.android.systemui.R; 36import com.android.systemui.statusbar.StatusIconDisplayable; 37import com.android.systemui.statusbar.stack.ViewState; 38import java.lang.ref.WeakReference; 39import java.util.ArrayList; 40 41/** 42 * A container for Status bar system icons. Limits the number of system icons and handles overflow 43 * similar to {@link NotificationIconContainer}. 44 * 45 * Children are expected to implement {@link StatusIconDisplayable} 46 */ 47public class StatusIconContainer extends AlphaOptimizedLinearLayout { 48 49 private static final String TAG = "StatusIconContainer"; 50 private static final boolean DEBUG = false; 51 private static final boolean DEBUG_OVERFLOW = false; 52 // Max 5 status icons including battery 53 private static final int MAX_ICONS = 4; 54 private static final int MAX_DOTS = 3; 55 56 private int mDotPadding; 57 private int mStaticDotDiameter; 58 private int mUnderflowWidth; 59 private int mUnderflowStart = 0; 60 // Whether or not we can draw into the underflow space 61 private boolean mNeedsUnderflow; 62 // Individual StatusBarIconViews draw their etc dots centered in this width 63 private int mIconDotFrameWidth; 64 private boolean mShouldRestrictIcons = true; 65 // Used to count which states want to be visible during layout 66 private ArrayList<StatusIconState> mLayoutStates = new ArrayList<>(); 67 // So we can count and measure properly 68 private ArrayList<View> mMeasureViews = new ArrayList<>(); 69 70 public StatusIconContainer(Context context) { 71 this(context, null); 72 } 73 74 public StatusIconContainer(Context context, AttributeSet attrs) { 75 super(context, attrs); 76 } 77 78 @Override 79 protected void onFinishInflate() { 80 super.onFinishInflate(); 81 setWillNotDraw(!DEBUG_OVERFLOW); 82 initDimens(); 83 } 84 85 public void setShouldRestrictIcons(boolean should) { 86 mShouldRestrictIcons = should; 87 } 88 89 private void initDimens() { 90 // This is the same value that StatusBarIconView uses 91 mIconDotFrameWidth = getResources().getDimensionPixelSize( 92 com.android.internal.R.dimen.status_bar_icon_size); 93 mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding); 94 int radius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius); 95 mStaticDotDiameter = 2 * radius; 96 mUnderflowWidth = mIconDotFrameWidth + 2 * (mStaticDotDiameter + mDotPadding); 97 } 98 99 @Override 100 protected void onLayout(boolean changed, int l, int t, int r, int b) { 101 float midY = getHeight() / 2.0f; 102 103 // Layout all child views so that we can move them around later 104 for (int i = 0; i < getChildCount(); i++) { 105 View child = getChildAt(i); 106 int width = child.getMeasuredWidth(); 107 int height = child.getMeasuredHeight(); 108 int top = (int) (midY - height / 2.0f); 109 child.layout(0, top, width, top + height); 110 } 111 112 resetViewStates(); 113 calculateIconTranslations(); 114 applyIconStates(); 115 } 116 117 @Override 118 protected void onDraw(Canvas canvas) { 119 super.onDraw(canvas); 120 if (DEBUG_OVERFLOW) { 121 Paint paint = new Paint(); 122 paint.setStyle(Style.STROKE); 123 paint.setColor(Color.RED); 124 125 // Show bounding box 126 canvas.drawRect(getPaddingStart(), 0, getWidth() - getPaddingEnd(), getHeight(), paint); 127 128 // Show etc box 129 paint.setColor(Color.GREEN); 130 canvas.drawRect( 131 mUnderflowStart, 0, mUnderflowStart + mUnderflowWidth, getHeight(), paint); 132 } 133 } 134 135 @Override 136 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 137 mMeasureViews.clear(); 138 int mode = MeasureSpec.getMode(widthMeasureSpec); 139 final int width = MeasureSpec.getSize(widthMeasureSpec); 140 final int count = getChildCount(); 141 // Collect all of the views which want to be laid out 142 for (int i = 0; i < count; i++) { 143 StatusIconDisplayable icon = (StatusIconDisplayable) getChildAt(i); 144 if (icon.isIconVisible() && !icon.isIconBlocked()) { 145 mMeasureViews.add((View) icon); 146 } 147 } 148 149 int visibleCount = mMeasureViews.size(); 150 int maxVisible = visibleCount <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1; 151 int totalWidth = getPaddingStart() + getPaddingEnd(); 152 boolean trackWidth = true; 153 154 // Measure all children so that they report the correct width 155 int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED); 156 mNeedsUnderflow = mShouldRestrictIcons && visibleCount > MAX_ICONS; 157 for (int i = 0; i < mMeasureViews.size(); i++) { 158 // Walking backwards 159 View child = mMeasureViews.get(visibleCount - i - 1); 160 measureChild(child, childWidthSpec, heightMeasureSpec); 161 if (mShouldRestrictIcons) { 162 if (i < maxVisible && trackWidth) { 163 totalWidth += getViewTotalMeasuredWidth(child); 164 } else if (trackWidth) { 165 // We've hit the icon limit; add space for dots 166 totalWidth += mUnderflowWidth; 167 trackWidth = false; 168 } 169 } else { 170 totalWidth += getViewTotalMeasuredWidth(child); 171 } 172 } 173 174 if (mode == MeasureSpec.EXACTLY) { 175 if (!mNeedsUnderflow && totalWidth > width) { 176 mNeedsUnderflow = true; 177 } 178 setMeasuredDimension(width, MeasureSpec.getSize(heightMeasureSpec)); 179 } else { 180 if (mode == MeasureSpec.AT_MOST && totalWidth > width) { 181 mNeedsUnderflow = true; 182 totalWidth = width; 183 } 184 setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec)); 185 } 186 } 187 188 @Override 189 public void onViewAdded(View child) { 190 super.onViewAdded(child); 191 StatusIconState vs = new StatusIconState(); 192 child.setTag(R.id.status_bar_view_state_tag, vs); 193 } 194 195 @Override 196 public void onViewRemoved(View child) { 197 super.onViewRemoved(child); 198 child.setTag(R.id.status_bar_view_state_tag, null); 199 } 200 201 /** 202 * Layout is happening from end -> start 203 */ 204 private void calculateIconTranslations() { 205 mLayoutStates.clear(); 206 float width = getWidth() - getPaddingEnd(); 207 float translationX = width; 208 float contentStart = getPaddingStart(); 209 int childCount = getChildCount(); 210 // Underflow === don't show content until that index 211 int firstUnderflowIndex = -1; 212 if (DEBUG) android.util.Log.d(TAG, "calculateIconTransitions: start=" + translationX 213 + " width=" + width); 214 215 // Collect all of the states which want to be visible 216 for (int i = childCount - 1; i >= 0; i--) { 217 View child = getChildAt(i); 218 StatusIconDisplayable iconView = (StatusIconDisplayable) child; 219 StatusIconState childState = getViewStateFromChild(child); 220 221 if (!iconView.isIconVisible() || iconView.isIconBlocked()) { 222 childState.visibleState = STATE_HIDDEN; 223 if (DEBUG) Log.d(TAG, "skipping child (" + iconView.getSlot() + ") not visible"); 224 continue; 225 } 226 227 childState.visibleState = STATE_ICON; 228 childState.xTranslation = translationX - getViewTotalWidth(child); 229 mLayoutStates.add(0, childState); 230 231 translationX -= getViewTotalWidth(child); 232 } 233 234 // Show either 1-4 dots, or 3 dots + overflow 235 int totalVisible = mLayoutStates.size(); 236 int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1; 237 238 mUnderflowStart = 0; 239 int visible = 0; 240 firstUnderflowIndex = -1; 241 for (int i = totalVisible - 1; i >= 0; i--) { 242 StatusIconState state = mLayoutStates.get(i); 243 // Allow room for underflow if we found we need it in onMeasure 244 if (mNeedsUnderflow && (state.xTranslation < (contentStart + mUnderflowWidth))|| 245 (mShouldRestrictIcons && visible >= maxVisible)) { 246 firstUnderflowIndex = i; 247 break; 248 } 249 mUnderflowStart = (int) Math.max(contentStart, state.xTranslation - mUnderflowWidth); 250 visible++; 251 } 252 253 if (firstUnderflowIndex != -1) { 254 int totalDots = 0; 255 int dotWidth = mStaticDotDiameter + mDotPadding; 256 int dotOffset = mUnderflowStart + mUnderflowWidth - mIconDotFrameWidth; 257 for (int i = firstUnderflowIndex; i >= 0; i--) { 258 StatusIconState state = mLayoutStates.get(i); 259 if (totalDots < MAX_DOTS) { 260 state.xTranslation = dotOffset; 261 state.visibleState = STATE_DOT; 262 dotOffset -= dotWidth; 263 totalDots++; 264 } else { 265 state.visibleState = STATE_HIDDEN; 266 } 267 } 268 } 269 270 // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean 271 if (isLayoutRtl()) { 272 for (int i = 0; i < childCount; i++) { 273 View child = getChildAt(i); 274 StatusIconState state = getViewStateFromChild(child); 275 state.xTranslation = width - state.xTranslation - child.getWidth(); 276 } 277 } 278 } 279 280 private void applyIconStates() { 281 for (int i = 0; i < getChildCount(); i++) { 282 View child = getChildAt(i); 283 StatusIconState vs = getViewStateFromChild(child); 284 if (vs != null) { 285 vs.applyToView(child); 286 } 287 } 288 } 289 290 private void resetViewStates() { 291 for (int i = 0; i < getChildCount(); i++) { 292 View child = getChildAt(i); 293 StatusIconState vs = getViewStateFromChild(child); 294 if (vs == null) { 295 continue; 296 } 297 298 vs.initFrom(child); 299 vs.alpha = 1.0f; 300 if (child instanceof StatusIconDisplayable) { 301 vs.hidden = !((StatusIconDisplayable)child).isIconVisible(); 302 } else { 303 vs.hidden = false; 304 } 305 } 306 } 307 308 private static @Nullable StatusIconState getViewStateFromChild(View child) { 309 return (StatusIconState) child.getTag(R.id.status_bar_view_state_tag); 310 } 311 312 private static int getViewTotalMeasuredWidth(View child) { 313 return child.getMeasuredWidth() + child.getPaddingStart() + child.getPaddingEnd(); 314 } 315 316 private static int getViewTotalWidth(View child) { 317 return child.getWidth() + child.getPaddingStart() + child.getPaddingEnd(); 318 } 319 320 public static class StatusIconState extends ViewState { 321 /// StatusBarIconView.STATE_* 322 public int visibleState = STATE_ICON; 323 324 @Override 325 public void applyToView(View view) { 326 if (view instanceof StatusIconDisplayable) { 327 StatusIconDisplayable icon = (StatusIconDisplayable) view; 328 icon.setVisibleState(visibleState); 329 } 330 super.applyToView(view); 331 } 332 } 333} 334