/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.statusbar.phone; import static com.android.systemui.statusbar.StatusBarIconView.STATE_DOT; import static com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN; import static com.android.systemui.statusbar.StatusBarIconView.STATE_ICON; import android.annotation.Nullable; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.util.AttributeSet; import android.util.Log; import android.view.View; import com.android.keyguard.AlphaOptimizedLinearLayout; import com.android.systemui.R; import com.android.systemui.statusbar.StatusIconDisplayable; import com.android.systemui.statusbar.notification.stack.AnimationFilter; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.ViewState; import java.util.ArrayList; import java.util.List; /** * A container for Status bar system icons. Limits the number of system icons and handles overflow * similar to {@link NotificationIconContainer}. * * Children are expected to implement {@link StatusIconDisplayable} */ public class StatusIconContainer extends AlphaOptimizedLinearLayout { private static final String TAG = "StatusIconContainer"; private static final boolean DEBUG = false; private static final boolean DEBUG_OVERFLOW = false; // Max 8 status icons including battery private static final int MAX_ICONS = 7; private static final int MAX_DOTS = 1; private int mDotPadding; private int mIconSpacing; private int mStaticDotDiameter; private int mUnderflowWidth; private int mUnderflowStart = 0; // Whether or not we can draw into the underflow space private boolean mNeedsUnderflow; // Individual StatusBarIconViews draw their etc dots centered in this width private int mIconDotFrameWidth; private boolean mQsExpansionTransitioning; private boolean mShouldRestrictIcons = true; // Used to count which states want to be visible during layout private ArrayList mLayoutStates = new ArrayList<>(); // So we can count and measure properly private ArrayList mMeasureViews = new ArrayList<>(); // Any ignored icon will never be added as a child private ArrayList mIgnoredSlots = new ArrayList<>(); private Configuration mConfiguration; public StatusIconContainer(Context context) { this(context, null); } public StatusIconContainer(Context context, AttributeSet attrs) { super(context, attrs); mConfiguration = new Configuration(context.getResources().getConfiguration()); reloadDimens(); setWillNotDraw(!DEBUG_OVERFLOW); } @Override protected void onFinishInflate() { super.onFinishInflate(); } public void setQsExpansionTransitioning(boolean expansionTransitioning) { mQsExpansionTransitioning = expansionTransitioning; } public void setShouldRestrictIcons(boolean should) { mShouldRestrictIcons = should; } public boolean isRestrictingIcons() { return mShouldRestrictIcons; } private void reloadDimens() { // This is the same value that StatusBarIconView uses mIconDotFrameWidth = getResources().getDimensionPixelSize( com.android.internal.R.dimen.status_bar_icon_size_sp); mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding); mIconSpacing = getResources().getDimensionPixelSize(R.dimen.status_bar_system_icon_spacing); int radius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius); mStaticDotDiameter = 2 * radius; mUnderflowWidth = mIconDotFrameWidth + (MAX_DOTS - 1) * (mStaticDotDiameter + mDotPadding); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { float midY = getHeight() / 2.0f; // Layout all child views so that we can move them around later for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); int width = child.getMeasuredWidth(); int height = child.getMeasuredHeight(); int top = (int) (midY - height / 2.0f); child.layout(0, top, width, top + height); } resetViewStates(); calculateIconTranslations(); applyIconStates(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (DEBUG_OVERFLOW) { Paint paint = new Paint(); paint.setStyle(Style.STROKE); paint.setColor(Color.RED); // Show bounding box canvas.drawRect(getPaddingStart(), 0, getWidth() - getPaddingEnd(), getHeight(), paint); // Show etc box paint.setColor(Color.GREEN); canvas.drawRect( mUnderflowStart, 0, mUnderflowStart + mUnderflowWidth, getHeight(), paint); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mMeasureViews.clear(); int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int specWidth = MeasureSpec.getSize(widthMeasureSpec); final int count = getChildCount(); // Collect all of the views which want to be laid out for (int i = 0; i < count; i++) { StatusIconDisplayable icon = (StatusIconDisplayable) getChildAt(i); if (icon.isIconVisible() && !icon.isIconBlocked() && !mIgnoredSlots.contains(icon.getSlot())) { mMeasureViews.add((View) icon); } } int visibleCount = mMeasureViews.size(); int maxVisible = visibleCount <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1; int totalWidth = mPaddingLeft + mPaddingRight; boolean trackWidth = true; // Measure all children so that they report the correct width int childWidthSpec = MeasureSpec.makeMeasureSpec(specWidth, MeasureSpec.UNSPECIFIED); mNeedsUnderflow = mShouldRestrictIcons && visibleCount > MAX_ICONS; for (int i = 0; i < visibleCount; i++) { // Walking backwards View child = mMeasureViews.get(visibleCount - i - 1); measureChild(child, childWidthSpec, heightMeasureSpec); int spacing = i == visibleCount - 1 ? 0 : mIconSpacing; if (mShouldRestrictIcons) { if (i < maxVisible && trackWidth) { totalWidth += getViewTotalMeasuredWidth(child) + spacing; } else if (trackWidth) { // We've hit the icon limit; add space for dots totalWidth += mUnderflowWidth; trackWidth = false; } } else { totalWidth += getViewTotalMeasuredWidth(child) + spacing; } } setMeasuredDimension( getMeasuredWidth(widthMode, specWidth, totalWidth), getMeasuredHeight(heightMeasureSpec, mMeasureViews)); } private int getMeasuredHeight(int heightMeasureSpec, List measuredChildren) { if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) { return MeasureSpec.getSize(heightMeasureSpec); } else { int highest = 0; for (View child : measuredChildren) { highest = Math.max(child.getMeasuredHeight(), highest); } return highest + getPaddingTop() + getPaddingBottom(); } } private int getMeasuredWidth(int widthMode, int specWidth, int totalWidth) { if (widthMode == MeasureSpec.EXACTLY) { if (!mNeedsUnderflow && totalWidth > specWidth) { mNeedsUnderflow = true; } return specWidth; } else { if (widthMode == MeasureSpec.AT_MOST && totalWidth > specWidth) { mNeedsUnderflow = true; totalWidth = specWidth; } return totalWidth; } } @Override public void onViewAdded(View child) { super.onViewAdded(child); StatusIconState vs = new StatusIconState(); vs.justAdded = true; child.setTag(R.id.status_bar_view_state_tag, vs); } @Override public void onViewRemoved(View child) { super.onViewRemoved(child); child.setTag(R.id.status_bar_view_state_tag, null); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); final int configDiff = newConfig.diff(mConfiguration); mConfiguration.setTo(newConfig); if ((configDiff & (ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_FONT_SCALE)) != 0) { reloadDimens(); } } /** * Add a name of an icon slot to be ignored. It will not show up nor be measured * @param slotName name of the icon as it exists in * frameworks/base/core/res/res/values/config.xml */ public void addIgnoredSlot(String slotName) { boolean added = addIgnoredSlotInternal(slotName); if (added) { requestLayout(); } } /** * Add a list of slots to be ignored * @param slots names of the icons to ignore */ public void addIgnoredSlots(List slots) { boolean willAddAny = false; for (String slot : slots) { willAddAny |= addIgnoredSlotInternal(slot); } if (willAddAny) { requestLayout(); } } /** * * @param slotName * @return */ private boolean addIgnoredSlotInternal(String slotName) { if (mIgnoredSlots.contains(slotName)) { return false; } mIgnoredSlots.add(slotName); return true; } /** * Remove a slot from the list of ignored icon slots. It will then be shown when set to visible * by the {@link StatusBarIconController}. * @param slotName name of the icon slot to remove from the ignored list */ public void removeIgnoredSlot(String slotName) { boolean removed = mIgnoredSlots.remove(slotName); if (removed) { requestLayout(); } } /** * Remove a list of slots from the list of ignored icon slots. * It will then be shown when set to visible by the {@link StatusBarIconController}. * @param slots name of the icon slots to remove from the ignored list */ public void removeIgnoredSlots(List slots) { boolean removedAny = false; for (String slot : slots) { removedAny |= mIgnoredSlots.remove(slot); } if (removedAny) { requestLayout(); } } /** * Layout is happening from end -> start */ private void calculateIconTranslations() { mLayoutStates.clear(); float width = getWidth(); float translationX = width - getPaddingEnd(); float contentStart = getPaddingStart(); int childCount = getChildCount(); // Underflow === don't show content until that index if (DEBUG) Log.d(TAG, "calculateIconTranslations: start=" + translationX + " width=" + width + " underflow=" + mNeedsUnderflow); // Collect all of the states which want to be visible for (int i = childCount - 1; i >= 0; i--) { View child = getChildAt(i); StatusIconDisplayable iconView = (StatusIconDisplayable) child; StatusIconState childState = getViewStateFromChild(child); if (!iconView.isIconVisible() || iconView.isIconBlocked() || mIgnoredSlots.contains(iconView.getSlot())) { childState.visibleState = STATE_HIDDEN; if (DEBUG) Log.d(TAG, "skipping child (" + iconView.getSlot() + ") not visible"); continue; } // Move translationX to the spot within StatusIconContainer's layout to add the view // without cutting off the child view. translationX -= getViewTotalWidth(child); childState.visibleState = STATE_ICON; childState.setXTranslation(translationX); mLayoutStates.add(0, childState); // Shift translationX over by mIconSpacing for the next view. translationX -= mIconSpacing; } // Show either 1-MAX_ICONS icons, or (MAX_ICONS - 1) icons + overflow int totalVisible = mLayoutStates.size(); int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1; // Init mUnderflowStart value with the offset to let the dot be placed next to battery icon. // This is to prevent if the underflow happens at rightest(totalVisible - 1) child then // break the for loop with mUnderflowStart staying 0(initial value), causing the dot be // placed at the leftest side. mUnderflowStart = (int) Math.max(contentStart, width - getPaddingEnd() - mUnderflowWidth); int visible = 0; int firstUnderflowIndex = -1; for (int i = totalVisible - 1; i >= 0; i--) { StatusIconState state = mLayoutStates.get(i); // Allow room for underflow if we found we need it in onMeasure if ((mNeedsUnderflow && (state.getXTranslation() < (contentStart + mUnderflowWidth))) || (mShouldRestrictIcons && (visible >= maxVisible))) { firstUnderflowIndex = i; break; } mUnderflowStart = (int) Math.max( contentStart, state.getXTranslation() - mUnderflowWidth - mIconSpacing); visible++; } if (firstUnderflowIndex != -1) { int totalDots = 0; int dotWidth = mStaticDotDiameter + mDotPadding; int dotOffset = mUnderflowStart + mUnderflowWidth - mIconDotFrameWidth; for (int i = firstUnderflowIndex; i >= 0; i--) { StatusIconState state = mLayoutStates.get(i); if (totalDots < MAX_DOTS) { state.setXTranslation(dotOffset); state.visibleState = STATE_DOT; dotOffset -= dotWidth; totalDots++; } else { state.visibleState = STATE_HIDDEN; } } } // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean if (isLayoutRtl()) { for (int i = 0; i < childCount; i++) { View child = getChildAt(i); StatusIconState state = getViewStateFromChild(child); state.setXTranslation(width - state.getXTranslation() - child.getWidth()); } } } private void applyIconStates() { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); StatusIconState vs = getViewStateFromChild(child); if (vs != null) { vs.applyToView(child); vs.qsExpansionTransitioning = mQsExpansionTransitioning; } } } private void resetViewStates() { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); StatusIconState vs = getViewStateFromChild(child); if (vs == null) { continue; } vs.initFrom(child); vs.setAlpha(1.0f); vs.hidden = false; } } private static @Nullable StatusIconState getViewStateFromChild(View child) { return (StatusIconState) child.getTag(R.id.status_bar_view_state_tag); } private static int getViewTotalMeasuredWidth(View child) { return child.getMeasuredWidth() + child.getPaddingStart() + child.getPaddingEnd(); } private static int getViewTotalWidth(View child) { return child.getWidth() + child.getPaddingStart() + child.getPaddingEnd(); } public static class StatusIconState extends ViewState { /// StatusBarIconView.STATE_* public int visibleState = STATE_ICON; public boolean justAdded = true; public boolean qsExpansionTransitioning = false; // How far we are from the end of the view actually is the most relevant for animation float distanceToViewEnd = -1; @Override public void applyToView(View view) { float parentWidth = 0; if (view.getParent() instanceof View) { parentWidth = ((View) view.getParent()).getWidth(); } float currentDistanceToEnd = parentWidth - getXTranslation(); if (!(view instanceof StatusIconDisplayable)) { return; } StatusIconDisplayable icon = (StatusIconDisplayable) view; AnimationProperties animationProperties = null; boolean animateVisibility = true; // Figure out which properties of the state transition (if any) we need to animate if (justAdded || icon.getVisibleState() == STATE_HIDDEN && visibleState == STATE_ICON) { // Icon is appearing, fade it in by putting it where it will be and animating alpha super.applyToView(view); view.setAlpha(0.f); icon.setVisibleState(STATE_HIDDEN); animationProperties = ADD_ICON_PROPERTIES; } else if (icon.getVisibleState() != visibleState) { if (icon.getVisibleState() == STATE_ICON && visibleState == STATE_HIDDEN) { // Disappearing, don't do anything fancy animateVisibility = false; } else { // all other transitions (to/from dot, etc) animationProperties = ANIMATE_ALL_PROPERTIES; } } else if (visibleState != STATE_HIDDEN && distanceToViewEnd != currentDistanceToEnd) { // Visibility isn't changing, just animate position animationProperties = X_ANIMATION_PROPERTIES; } icon.setVisibleState(visibleState, animateVisibility); if (animationProperties != null && !qsExpansionTransitioning) { animateTo(view, animationProperties); } else { super.applyToView(view); } qsExpansionTransitioning = false; justAdded = false; distanceToViewEnd = currentDistanceToEnd; } } private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() { private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha(); @Override public AnimationFilter getAnimationFilter() { return mAnimationFilter; } }.setDuration(200).setDelay(50); private static final AnimationProperties X_ANIMATION_PROPERTIES = new AnimationProperties() { private AnimationFilter mAnimationFilter = new AnimationFilter().animateX(); @Override public AnimationFilter getAnimationFilter() { return mAnimationFilter; } }.setDuration(200); private static final AnimationProperties ANIMATE_ALL_PROPERTIES = new AnimationProperties() { private AnimationFilter mAnimationFilter = new AnimationFilter().animateX().animateY() .animateAlpha().animateScale(); @Override public AnimationFilter getAnimationFilter() { return mAnimationFilter; } }.setDuration(200); }