Home | History | Annotate | Download | only in phone
      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_ICON;
     20 import static com.android.systemui.statusbar.StatusBarIconView.STATE_DOT;
     21 import static com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN;
     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 
     32 import android.view.View;
     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.stack.AnimationFilter;
     37 import com.android.systemui.statusbar.stack.AnimationProperties;
     38 import com.android.systemui.statusbar.stack.ViewState;
     39 import 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  */
     47 public 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 8 status icons including battery
     53     private static final int MAX_ICONS = 7;
     54     private static final int MAX_DOTS = 1;
     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         initDimens();
     77         setWillNotDraw(!DEBUG_OVERFLOW);
     78     }
     79 
     80     @Override
     81     protected void onFinishInflate() {
     82         super.onFinishInflate();
     83     }
     84 
     85     public void setShouldRestrictIcons(boolean should) {
     86         mShouldRestrictIcons = should;
     87     }
     88 
     89     public boolean isRestrictingIcons() {
     90         return mShouldRestrictIcons;
     91     }
     92 
     93     private void initDimens() {
     94         // This is the same value that StatusBarIconView uses
     95         mIconDotFrameWidth = getResources().getDimensionPixelSize(
     96                 com.android.internal.R.dimen.status_bar_icon_size);
     97         mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
     98         int radius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
     99         mStaticDotDiameter = 2 * radius;
    100         mUnderflowWidth = mIconDotFrameWidth + (MAX_DOTS - 1) * (mStaticDotDiameter + mDotPadding);
    101     }
    102 
    103     @Override
    104     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    105         float midY = getHeight() / 2.0f;
    106 
    107         // Layout all child views so that we can move them around later
    108         for (int i = 0; i < getChildCount(); i++) {
    109             View child = getChildAt(i);
    110             int width = child.getMeasuredWidth();
    111             int height = child.getMeasuredHeight();
    112             int top = (int) (midY - height / 2.0f);
    113             child.layout(0, top, width, top + height);
    114         }
    115 
    116         resetViewStates();
    117         calculateIconTranslations();
    118         applyIconStates();
    119     }
    120 
    121     @Override
    122     protected void onDraw(Canvas canvas) {
    123         super.onDraw(canvas);
    124         if (DEBUG_OVERFLOW) {
    125             Paint paint = new Paint();
    126             paint.setStyle(Style.STROKE);
    127             paint.setColor(Color.RED);
    128 
    129             // Show bounding box
    130             canvas.drawRect(getPaddingStart(), 0, getWidth() - getPaddingEnd(), getHeight(), paint);
    131 
    132             // Show etc box
    133             paint.setColor(Color.GREEN);
    134             canvas.drawRect(
    135                     mUnderflowStart, 0, mUnderflowStart + mUnderflowWidth, getHeight(), paint);
    136         }
    137     }
    138 
    139     @Override
    140     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    141         mMeasureViews.clear();
    142         int mode = MeasureSpec.getMode(widthMeasureSpec);
    143         final int width = MeasureSpec.getSize(widthMeasureSpec);
    144         final int count = getChildCount();
    145         // Collect all of the views which want to be laid out
    146         for (int i = 0; i < count; i++) {
    147             StatusIconDisplayable icon = (StatusIconDisplayable) getChildAt(i);
    148             if (icon.isIconVisible() && !icon.isIconBlocked()) {
    149                 mMeasureViews.add((View) icon);
    150             }
    151         }
    152 
    153         int visibleCount = mMeasureViews.size();
    154         int maxVisible = visibleCount <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
    155         int totalWidth = mPaddingLeft + mPaddingRight;
    156         boolean trackWidth = true;
    157 
    158         // Measure all children so that they report the correct width
    159         int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED);
    160         mNeedsUnderflow = mShouldRestrictIcons && visibleCount > MAX_ICONS;
    161         for (int i = 0; i < mMeasureViews.size(); i++) {
    162             // Walking backwards
    163             View child = mMeasureViews.get(visibleCount - i - 1);
    164             measureChild(child, childWidthSpec, heightMeasureSpec);
    165             if (mShouldRestrictIcons) {
    166                 if (i < maxVisible && trackWidth) {
    167                     totalWidth += getViewTotalMeasuredWidth(child);
    168                 } else if (trackWidth) {
    169                     // We've hit the icon limit; add space for dots
    170                     totalWidth += mUnderflowWidth;
    171                     trackWidth = false;
    172                 }
    173             } else {
    174                 totalWidth += getViewTotalMeasuredWidth(child);
    175             }
    176         }
    177 
    178         if (mode == MeasureSpec.EXACTLY) {
    179             if (!mNeedsUnderflow && totalWidth > width) {
    180                 mNeedsUnderflow = true;
    181             }
    182             setMeasuredDimension(width, MeasureSpec.getSize(heightMeasureSpec));
    183         } else {
    184             if (mode == MeasureSpec.AT_MOST && totalWidth > width) {
    185                 mNeedsUnderflow = true;
    186                 totalWidth = width;
    187             }
    188             setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec));
    189         }
    190     }
    191 
    192     @Override
    193     public void onViewAdded(View child) {
    194         super.onViewAdded(child);
    195         StatusIconState vs = new StatusIconState();
    196         vs.justAdded = true;
    197         child.setTag(R.id.status_bar_view_state_tag, vs);
    198     }
    199 
    200     @Override
    201     public void onViewRemoved(View child) {
    202         super.onViewRemoved(child);
    203         child.setTag(R.id.status_bar_view_state_tag, null);
    204     }
    205 
    206     /**
    207      * Layout is happening from end -> start
    208      */
    209     private void calculateIconTranslations() {
    210         mLayoutStates.clear();
    211         float width = getWidth();
    212         float translationX = width - getPaddingEnd();
    213         float contentStart = getPaddingStart();
    214         int childCount = getChildCount();
    215         // Underflow === don't show content until that index
    216         if (DEBUG) android.util.Log.d(TAG, "calculateIconTranslations: start=" + translationX
    217                 + " width=" + width + " underflow=" + mNeedsUnderflow);
    218 
    219         // Collect all of the states which want to be visible
    220         for (int i = childCount - 1; i >= 0; i--) {
    221             View child = getChildAt(i);
    222             StatusIconDisplayable iconView = (StatusIconDisplayable) child;
    223             StatusIconState childState = getViewStateFromChild(child);
    224 
    225             if (!iconView.isIconVisible() || iconView.isIconBlocked()) {
    226                 childState.visibleState = STATE_HIDDEN;
    227                 if (DEBUG) Log.d(TAG, "skipping child (" + iconView.getSlot() + ") not visible");
    228                 continue;
    229             }
    230 
    231             childState.visibleState = STATE_ICON;
    232             childState.xTranslation = translationX - getViewTotalWidth(child);
    233             mLayoutStates.add(0, childState);
    234 
    235             translationX -= getViewTotalWidth(child);
    236         }
    237 
    238         // Show either 1-MAX_ICONS icons, or (MAX_ICONS - 1) icons + overflow
    239         int totalVisible = mLayoutStates.size();
    240         int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;
    241 
    242         mUnderflowStart = 0;
    243         int visible = 0;
    244         int firstUnderflowIndex = -1;
    245         for (int i = totalVisible - 1; i >= 0; i--) {
    246             StatusIconState state = mLayoutStates.get(i);
    247             // Allow room for underflow if we found we need it in onMeasure
    248             if (mNeedsUnderflow && (state.xTranslation < (contentStart + mUnderflowWidth))||
    249                     (mShouldRestrictIcons && visible >= maxVisible)) {
    250                 firstUnderflowIndex = i;
    251                 break;
    252             }
    253             mUnderflowStart = (int) Math.max(contentStart, state.xTranslation - mUnderflowWidth);
    254             visible++;
    255         }
    256 
    257         if (firstUnderflowIndex != -1) {
    258             int totalDots = 0;
    259             int dotWidth = mStaticDotDiameter + mDotPadding;
    260             int dotOffset = mUnderflowStart + mUnderflowWidth - mIconDotFrameWidth;
    261             for (int i = firstUnderflowIndex; i >= 0; i--) {
    262                 StatusIconState state = mLayoutStates.get(i);
    263                 if (totalDots < MAX_DOTS) {
    264                     state.xTranslation = dotOffset;
    265                     state.visibleState = STATE_DOT;
    266                     dotOffset -= dotWidth;
    267                     totalDots++;
    268                 } else {
    269                     state.visibleState = STATE_HIDDEN;
    270                 }
    271             }
    272         }
    273 
    274         // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean
    275         if (isLayoutRtl()) {
    276             for (int i = 0; i < childCount; i++) {
    277                 View child = getChildAt(i);
    278                 StatusIconState state = getViewStateFromChild(child);
    279                 state.xTranslation = width - state.xTranslation - child.getWidth();
    280             }
    281         }
    282     }
    283 
    284     private void applyIconStates() {
    285         for (int i = 0; i < getChildCount(); i++) {
    286             View child = getChildAt(i);
    287             StatusIconState vs = getViewStateFromChild(child);
    288             if (vs != null) {
    289                 vs.applyToView(child);
    290             }
    291         }
    292     }
    293 
    294     private void resetViewStates() {
    295         for (int i = 0; i < getChildCount(); i++) {
    296             View child = getChildAt(i);
    297             StatusIconState vs = getViewStateFromChild(child);
    298             if (vs == null) {
    299                 continue;
    300             }
    301 
    302             vs.initFrom(child);
    303             vs.alpha = 1.0f;
    304             if (child instanceof StatusIconDisplayable) {
    305                 vs.hidden = !((StatusIconDisplayable)child).isIconVisible();
    306             } else {
    307                 vs.hidden = false;
    308             }
    309         }
    310     }
    311 
    312     private static @Nullable StatusIconState getViewStateFromChild(View child) {
    313         return (StatusIconState) child.getTag(R.id.status_bar_view_state_tag);
    314     }
    315 
    316     private static int getViewTotalMeasuredWidth(View child) {
    317         return child.getMeasuredWidth() + child.getPaddingStart() + child.getPaddingEnd();
    318     }
    319 
    320     private static int getViewTotalWidth(View child) {
    321         return child.getWidth() + child.getPaddingStart() + child.getPaddingEnd();
    322     }
    323 
    324     public static class StatusIconState extends ViewState {
    325         /// StatusBarIconView.STATE_*
    326         public int visibleState = STATE_ICON;
    327         public boolean justAdded = true;
    328 
    329         @Override
    330         public void applyToView(View view) {
    331             if (!(view instanceof StatusIconDisplayable)) {
    332                 return;
    333             }
    334             StatusIconDisplayable icon = (StatusIconDisplayable) view;
    335             AnimationProperties animationProperties = null;
    336             boolean animate = false;
    337 
    338             if (justAdded) {
    339                 super.applyToView(view);
    340                 animationProperties = ADD_ICON_PROPERTIES;
    341                 animate = true;
    342             } else if (icon.getVisibleState() != visibleState) {
    343                 animationProperties = DOT_ANIMATION_PROPERTIES;
    344                 animate = true;
    345             }
    346 
    347             if (animate) {
    348                 animateTo(view, animationProperties);
    349                 icon.setVisibleState(visibleState);
    350             } else {
    351                 icon.setVisibleState(visibleState);
    352                 super.applyToView(view);
    353             }
    354 
    355             justAdded = false;
    356         }
    357     }
    358 
    359     private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
    360         private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
    361 
    362         @Override
    363         public AnimationFilter getAnimationFilter() {
    364             return mAnimationFilter;
    365         }
    366     }.setDuration(200).setDelay(50);
    367 
    368     private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() {
    369         private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
    370 
    371         @Override
    372         public AnimationFilter getAnimationFilter() {
    373             return mAnimationFilter;
    374         }
    375     }.setDuration(200);
    376 }
    377