SystemUI状态栏之你所不知道的overflow icon

引言

今天我们主要讲的是SystemUI状态栏里面notification icons中的overflow icon,众所周知,notification icon整个view的宽度是有限制的,当内容区宽度大于最大宽度时,notification icon将无法再显示,此时今天我们的主角overflow icon就出现了,overflow icon以"..."的形式出现,表示当前通知过多被折叠起来了,以达到提示用户的目的。overflow icon截图如下红框所示


SystemUI状态栏之你所不知道的overflow icon_第1张图片
Screenshot_20181114-094822.png

正文

本文主要讲述下源码里面overflow icon显示的逻辑和加载的流程,话不多说,我们开始吧。

流程图

overflow icon显示的逻辑和加载的流程图大致如下

SystemUI状态栏之你所不知道的overflow icon_第2张图片
systemui overflow 条件流程图.png
显示的逻辑和加载流程

关于notification icon加载的流程我上一篇已经讲过,SystemUI之状态栏notification icon加载流程
本文不再赘述,我们只要知道notification的添加,实际上就是在NotificationIconContainer中进行的,下面我们就从这个NotificationIconContainer讲起。

首先我们看下NotificationIconContainer是何方神圣

/**
 * A container for notification icons. It handles overflowing icons properly and positions them
 * correctly on the screen.
 */
public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
}

/**
 * A frame layout which does not have overlapping renderings commands and therefore does not need a
 * layer when alpha is changed.
 */
public class AlphaOptimizedFrameLayout extends FrameLayout{
}

我们能够看到,NotificationIconContainer其实是一个自定义的FrameLayout,心细的你是不是有疑问了,
WTF? FrameLayout不是重叠显示的吗? 怎么会显示成状态栏上面从左到到右的类似于LinearLayout效果呢?

别急,既然它是一个自定义view,那我们就看下自定义view的四大护法

SystemUI状态栏之你所不知道的overflow icon_第3张图片
自定义view.png
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        canvas.drawRect(getActualPaddingStart(), 0, getLayoutEnd(), getHeight(), paint);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        float centerY = getHeight() / 2.0f;
        // we layout all our children on the left at the top
        mIconSize = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            // We need to layout all children even the GONE ones, such that the heights are
            // calculated correctly as they are used to calculate how many we can fit on the screen
            int width = child.getMeasuredWidth();
            int height = child.getMeasuredHeight();
            int top = (int) (centerY - height / 2.0f);
            child.layout(0, top, width, top + height);
            if (i == 0) {
                mIconSize = child.getWidth();
            }
        }
        if (mShowAllIcons) {
            resetViewStates();
            calculateIconTranslations();
            applyIconStates();
        }
    }

    public void resetViewStates() {
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            ViewState iconState = mIconStates.get(view);
            iconState.initFrom(view);
            iconState.alpha = 1.0f;
            iconState.hidden = false;
        }
    }

onDraw这边比较简单,和本文应该没啥必然联系,剩下的就是这个onLayout了,重点的就是最后一段逻辑
if (mShowAllIcons) {}中的函数,我们先看下resetViewStates这个函数,该函数主要是每次onLayout的时候把状态重置,非本文重点,剩下的两个就是本文的重中之重了。

  • calculateIconTranslations 计算TranslationsX等信息,存入IconState
/**
     * Calulate the horizontal translations for each notification based on how much the icons
     * are inserted into the notification container.
     * If this is not a whole number, the fraction means by how much the icon is appearing.
     */
    public void calculateIconTranslations() {
        float translationX = getActualPaddingStart();
        int firstOverflowIndex = -1;//  第一个需要显示dot的index
        int childCount = getChildCount();
        int maxVisibleIcons = mDark ? MAX_VISIBLE_ICONS_WHEN_DARK : childCount;
        float layoutEnd = getLayoutEnd();
        float overflowStart = layoutEnd - mIconSize * (2 + OVERFLOW_EARLY_AMOUNT);
        boolean hasAmbient = mSpeedBumpIndex != -1 && mSpeedBumpIndex < getChildCount();
        float visualOverflowStart = 0;
        
        for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);
            IconState iconState = mIconStates.get(view);
            iconState.xTranslation = translationX;
            boolean forceOverflow = mSpeedBumpIndex != -1 && i >= mSpeedBumpIndex
                    && iconState.iconAppearAmount > 0.0f || i >= maxVisibleIcons;
            boolean noOverflowAfter = i == childCount - 1;
            float drawingScale = mDark && view instanceof StatusBarIconView
                    ? ((StatusBarIconView) view).getIconScaleFullyDark()
                    : 1f;
            if (mOpenedAmount != 0.0f) {
                noOverflowAfter = noOverflowAfter && !hasAmbient && !forceOverflow;
            }
            iconState.visibleState = StatusBarIconView.STATE_ICON;
            //  当translationX >= overflowStart时说明需要显示dot
            if (firstOverflowIndex == -1 && (forceOverflow
                    || (translationX >= (noOverflowAfter ? layoutEnd - mIconSize : overflowStart)))) {
                //  找到需要显示dot的index
                firstOverflowIndex = noOverflowAfter && !forceOverflow ? i - 1 : i;
                int totalDotLength = mStaticDotRadius * 6 + 2 * mDotPadding;
                visualOverflowStart = overflowStart + mIconSize * (1 + OVERFLOW_EARLY_AMOUNT)
                        - totalDotLength / 2
                        - mIconSize * 0.5f + mStaticDotRadius;

                if (forceOverflow) {
                    visualOverflowStart = Math.min(translationX, visualOverflowStart
                            + mStaticDotRadius * 2 + mDotPadding);
                } else {
                    visualOverflowStart += (translationX - overflowStart) / mIconSize
                            * (mStaticDotRadius * 2 + mDotPadding);
                }
                if (mShowAllIcons) {
                    // We want to perfectly position the overflow in the static state, such that
                    // it's perfectly centered instead of measuring it from the end.
                    mVisualOverflowAdaption = 0;
                    if (firstOverflowIndex != -1) {
                        View firstOverflowView = getChildAt(i);
                        IconState overflowState = mIconStates.get(firstOverflowView);
                        float totalAmount = layoutEnd - overflowState.xTranslation;
                        float newPosition = overflowState.xTranslation + totalAmount / 2
                                - totalDotLength / 2
                                - mIconSize * 0.5f + mStaticDotRadius;
                        mVisualOverflowAdaption = newPosition - visualOverflowStart;
                        visualOverflowStart = newPosition;
                    }
                } else {
                    visualOverflowStart += mVisualOverflowAdaption * (1f - mOpenedAmount);
                }
            }            
            translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
        }
       
        if (firstOverflowIndex != -1 ) {
            int numDots = 1;
            translationX = visualOverflowStart;
            for (int i = firstOverflowIndex; i < childCount; i++) {
                View view = getChildAt(i);
                IconState iconState = mIconStates.get(view);
                int dotWidth = mStaticDotRadius * 2 + mDotPadding;
                iconState.xTranslation = translationX;
                if (numDots <= 3) {//  最多显示3个dot
                    if (numDots == 1 && iconState.iconAppearAmount < 0.8f) {
                        iconState.visibleState = StatusBarIconView.STATE_ICON;
                        numDots--;
                    } else {
                        //  把所有firstOverflowIndex之后的iconState设置成STATE_DOT
                        iconState.visibleState = StatusBarIconView.STATE_DOT;
                    }
                    //  根据numDots和dotWidth算出translationX
                    translationX += (numDots == 3 ? 3 * dotWidth : dotWidth)
                            * iconState.iconAppearAmount;
                } else {//  多于三个的iconState设置成hidden,就不显示了
                    iconState.visibleState = StatusBarIconView.STATE_HIDDEN;
                }
                numDots++;
            }
        }
        
        }

函数比较长,主要分为两个阶段,我们拆分下慢慢看,先看下面几个变量

        float translationX = getActualPaddingStart();//初始位置translationX一般==0

        int firstOverflowIndex = -1;//  第一个需要显示overflow dot的index

        int childCount = getChildCount();//子view数量也即所有通知icon数量
        int maxVisibleIcons = mDark ? MAX_VISIBLE_ICONS_WHEN_DARK : childCount;//状态栏部分==childCount

        float layoutEnd = getLayoutEnd();//  整个view的最大宽度坐标
        
        //  用layoutEnd 减去偏移量算出第一个overflow的显示坐标,
        //也就是说正常icon能显示的最大translationX的值为overflowStart,后面会讲到
        float overflowStart = layoutEnd - mIconSize * (2 + OVERFLOW_EARLY_AMOUNT);

        //  第一个overflow显示的坐标
        float visualOverflowStart = 0;

知道了上面变量的意义,我们就可以接着往下看了

1 . 算出每个icon的xTranslation、firstOverflowIndex 和visualOverflowStart

      for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);
            IconState iconState = mIconStates.get(view);
            iconState.xTranslation = translationX;//  设置xTranslation
            //  forceOverflow是否需要强制显示overflow,statusbar中不需要,这个主要是用于NotificationShelf,本文不关注
            boolean forceOverflow = mSpeedBumpIndex != -1 && i >= mSpeedBumpIndex
                    && iconState.iconAppearAmount > 0.0f || i >= maxVisibleIcons;
            //  最后一个icon显示不下,需要overflow
            boolean noOverflowAfter = i == childCount - 1;
          //  缩放比例,主要是用于NotificationShelf,statusbar == 1f
            float drawingScale = mDark && view instanceof StatusBarIconView
                    ? ((StatusBarIconView) view).getIconScaleFullyDark()
                    : 1f;
            if (mOpenedAmount != 0.0f) {
                noOverflowAfter = noOverflowAfter && !hasAmbient && !forceOverflow;
            }
            iconState.visibleState = StatusBarIconView.STATE_ICON;
            //  当translationX >= overflowStart时说明需要显示overflow
            if (firstOverflowIndex == -1 && (forceOverflow
                    || (translationX >= (noOverflowAfter ? layoutEnd - mIconSize : overflowStart)))) {
                firstOverflowIndex = noOverflowAfter && !forceOverflow ? i - 1 : i;//  找到需要显示overflow的index
                int totalDotLength = mStaticDotRadius * 6 + 2 * mDotPadding;
                visualOverflowStart = overflowStart + mIconSize * (1 + OVERFLOW_EARLY_AMOUNT)
                        - totalDotLength / 2
                        - mIconSize * 0.5f + mStaticDotRadius;

                if (forceOverflow) {
                    visualOverflowStart = Math.min(translationX, visualOverflowStart
                            + mStaticDotRadius * 2 + mDotPadding);
                } else {
                    visualOverflowStart += (translationX - overflowStart) / mIconSize
                            * (mStaticDotRadius * 2 + mDotPadding);
                }
                if (mShowAllIcons) {
                    // We want to perfectly position the overflow in the static state, such that
                    // it's perfectly centered instead of measuring it from the end.
                    mVisualOverflowAdaption = 0;
                    if (firstOverflowIndex != -1) {
                        View firstOverflowView = getChildAt(i);
                        IconState overflowState = mIconStates.get(firstOverflowView);
                        float totalAmount = layoutEnd - overflowState.xTranslation;
                        float newPosition = overflowState.xTranslation + totalAmount / 2
                                - totalDotLength / 2
                                - mIconSize * 0.5f + mStaticDotRadius;
                        mVisualOverflowAdaption = newPosition - visualOverflowStart;
                        visualOverflowStart = newPosition;
                    }
                } else {
                    visualOverflowStart += mVisualOverflowAdaption * (1f - mOpenedAmount);
                }
            }            
            //  根据当前icon的Width算出下一个icon的translationX
            translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
        }

上面的函数中,主要是遍历每一个icon,经过运算后设置状态
iconState.xTranslation = translationX,
iconState.visibleState = StatusBarIconView.STATE_ICON;
根据当前icon的Width算出下一个icon的translationX
translationX += iconState.iconAppearAmount * view.getWidth() * drawingScale;
当translationX >= overflowStart时说明需要显示不下了,需要显示overflow icon,
此时算出firstOverflowIndex = noOverflowAfter && !forceOverflow ? i - 1 : i;
找到需要显示overflow icon的index
然后算出visualOverflowStart第一个overflow icon 出现的坐标
到此第一阶段的逻辑就结束了。

2 . 计算overflow的translationX和iconState.visibleState

      if (firstOverflowIndex != -1 ) {//  说明需要显示overflow icon
            int numDots = 1;
            translationX = visualOverflowStart;
            for (int i = firstOverflowIndex; i < childCount; i++) {//  遍历firstOverflowIndex之后的icon
                View view = getChildAt(i);
                IconState iconState = mIconStates.get(view);
                int dotWidth = mStaticDotRadius * 2 + mDotPadding;
                iconState.xTranslation = translationX;
                if (numDots <= 3) {//  最多显示3个dot
                    if (numDots == 1 && iconState.iconAppearAmount < 0.8f) {//  用于NotificationShelf,无视
                        iconState.visibleState = StatusBarIconView.STATE_ICON;
                        numDots--;
                    } else {
                        //  把所有firstOverflowIndex之后的iconState设置成STATE_DOT
                        iconState.visibleState = StatusBarIconView.STATE_DOT;
                    }
                    //  根据numDots和dotWidth算出translationX
                    translationX += (numDots == 3 ? 3 * dotWidth : dotWidth)
                            * iconState.iconAppearAmount;
                } else {//  多于三个的iconState设置成hidden,就不显示了
                    iconState.visibleState = StatusBarIconView.STATE_HIDDEN;
                }
                numDots++;
            }
        }

第二阶段稍微简单点,就是遍历firstOverflowIndex之后的icon,算出translationX,
把numDots <= 3的iconState设置成STATE_DOT
多于三个的iconState设置成hidden,就不显示了

到这里calculateIconTranslations流程就介绍完毕了,逻辑虽然比较复杂,但是按照两个阶段理清条理后,思路清晰了许多。

  • applyIconStates 根据IconState,刷新界面进行显示
public void applyIconStates() {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            ViewState childState = mIconStates.get(child);
            if (childState != null) {
                childState.applyToView(child);
            }
        }
        mAddAnimationStartIndex = -1;
        mCannedAnimationStartIndex = -1;
        mDisallowNextAnimation = false;
    }
        @Override
        public void applyToView(View view) {
            if (view instanceof StatusBarIconView) {
                StatusBarIconView icon = (StatusBarIconView) view;
                boolean animate = false;
                AnimationProperties animationProperties = null;
                boolean animationsAllowed = mAnimationsEnabled && !mDisallowNextAnimation
                        && !noAnimations;
                if (animationsAllowed) {
                    if (justAdded || justReplaced) {
                        super.applyToView(icon);
                        if (justAdded && iconAppearAmount != 0.0f) {//  新加notification icon
                            icon.setAlpha(0.0f);
                            icon.setVisibleState(StatusBarIconView.STATE_HIDDEN,
                                    false /* animate */);
                            animationProperties = ADD_ICON_PROPERTIES;
                            animate = true;
                        }
                    } else if (visibleState != icon.getVisibleState()) {
                        //  visibleState变化的icon,比如从STATE变成DOT
                        animationProperties = DOT_ANIMATION_PROPERTIES;
                        animate = true;
                    }
                    if (!animate && mAddAnimationStartIndex >= 0
                            && indexOfChild(view) >= mAddAnimationStartIndex
                            && (icon.getVisibleState() != StatusBarIconView.STATE_HIDDEN
                            || visibleState != StatusBarIconView.STATE_HIDDEN)) {//  常规的notification icon
                        animationProperties = DOT_ANIMATION_PROPERTIES;
                        animate = true;
                    }
                }
                icon.setVisibleState(visibleState, animationsAllowed);
                icon.setIconColor(iconColor, needsCannedAnimation && animationsAllowed);
                if (animate) {
                    animateTo(icon, animationProperties);
                } else {
                    super.applyToView(view);
                }                
        }

applyToView逻辑比较简单,主要根据visibleState区分new icon, overflow icon和常规notification icon,设置不同的animationProperties,最后通过icon.setVisibleState(visibleState, animationsAllowed)触发StatusBarIconView.java中的刷新,我们重点看下StatusBarIconView.java。

public void setVisibleState(int visibleState, boolean animate, Runnable endRunnable) {

        if (animate) {
                float targetAmount = 0.0f;//  把所有icon设置成0.0f
                Interpolator interpolator = Interpolators.FAST_OUT_LINEAR_IN;
                if (visibleState == STATE_ICON) {//  常规notification icon
                    targetAmount = 1.0f;
                    interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
                }
                float currentAmount = getIconAppearAmount();//  current == 0.0f
                if (targetAmount != currentAmount) {
                    mIconAppearAnimator = ObjectAnimator.ofFloat(this, ICON_APPEAR_AMOUNT,
                            currentAmount, targetAmount);
                    mIconAppearAnimator.setInterpolator(interpolator);
                    mIconAppearAnimator.setDuration(ANIMATION_DURATION_FAST);
                    mIconAppearAnimator.addListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            mIconAppearAnimator = null;
                            runRunnable(endRunnable);
                        }
                    });
                    mIconAppearAnimator.start();
                    runnableAdded = true;
                }

                //  常规STATE_ICON, targetAmount=2.0, STATE_DOT 和 STATE_HIDDEN targetAmount = 0.0f
                targetAmount = visibleState == STATE_ICON ? 2.0f : 0.0f;
                interpolator = Interpolators.FAST_OUT_LINEAR_IN;
                if (visibleState == STATE_DOT) {
                    targetAmount = 1.0f;//  overflow icon
                    interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
                }
                currentAmount = getDotAppearAmount();//  currentAmount == 0.0f
                if (targetAmount != currentAmount) {
                    mDotAnimator = ObjectAnimator.ofFloat(this, DOT_APPEAR_AMOUNT,
                            currentAmount, targetAmount);
                    mDotAnimator.setInterpolator(interpolator);
                    mDotAnimator.setDuration(ANIMATION_DURATION_FAST);
                    final boolean runRunnable = !runnableAdded;
                    mDotAnimator.addListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            mDotAnimator = null;
                            if (runRunnable) {
                                runRunnable(endRunnable);
                            }
                        }
                    });
                    mDotAnimator.start();
                    runnableAdded = true;
                }
            }
}

这个函数主要根据visibleState设置targetAmount,然后通过ObjectAnimator更新UI,主要分为两个阶段,我们拆分下慢慢看

1 . mIconAppearAnimator更新常规的notification icon

private static final Property ICON_APPEAR_AMOUNT
            = new FloatProperty("iconAppearAmount") {

        @Override
        public void setValue(StatusBarIconView object, float value) {
            object.setIconAppearAmount(value);
        }

        @Override
        public Float get(StatusBarIconView object) {
            return object.getIconAppearAmount();
        }
    };

public void setIconAppearAmount(float iconAppearAmount) {
        if (mIconAppearAmount != iconAppearAmount) {
            mIconAppearAmount = iconAppearAmount;
            invalidate();
        }
    }

在setVisibleState函数里面,STATE_ICON的常规notification icon,则最终通过ObjectAnimator把mIconAppearAmount 从0.0 设置到1.0,而STATE_DOT的overflow icon,则把mIconAppearAmount设置成0.0f

2 . mDotAnimator更新overflow icon

private static final Property DOT_APPEAR_AMOUNT
            = new FloatProperty("dot_appear_amount") {

        @Override
        public void setValue(StatusBarIconView object, float value) {
            object.setDotAppearAmount(value);
        }

        @Override
        public Float get(StatusBarIconView object) {
            return object.getDotAppearAmount();
        }
    };

public void setDotAppearAmount(float dotAppearAmount) {
        if (mDotAppearAmount != dotAppearAmount) {
            mDotAppearAmount = dotAppearAmount;
            invalidate();
        }
    }

在setVisibleState函数里面,STATE_ICON的常规notification icon,则最终通过mDotAnimator把mDotAppearAmount 从0.0 设置到2.0,而STATE_DOT的overflow icon,则把mDotAppearAmount设置成1.0f

从上面的两个阶段,我们可以总结如下,对于单个icon当visibleState对应如下值时:
STATE_ICON:
mIconAppearAmount 0.0f--->1.0f
mDotAppearAmount 0.0f--->2.0f

STATE_DOT:
mIconAppearAmount 0.0f--->0.0f
mDotAppearAmount 0.0f--->1.0f

STATE_HIDDEN:
mIconAppearAmount 0.0f--->0.0f
mDotAppearAmount 0.0f--->0.0f

到这里applyIconStates到setVisibleState流程也介绍完毕了,分两个阶段理清条理后,思路也相对比较清晰了。

  • StatusBarIconView 界面更新
    前面提到,setVisibleState根据不同的visibleState,最终设置了mIconAppearAmount和mDotAppearAmount,那么StatusBarIconView是怎么完成更新的呢?

我们回顾下刚才的ICON_APPEAR_AMOUNT和DOT_APPEAR_AMOUNT

public void setIconAppearAmount(float iconAppearAmount) {
        if (mIconAppearAmount != iconAppearAmount) {
            mIconAppearAmount = iconAppearAmount;
            invalidate();
        }
    }

public void setDotAppearAmount(float dotAppearAmount) {
        if (mDotAppearAmount != dotAppearAmount) {
            mDotAppearAmount = dotAppearAmount;
            invalidate();
        }
    }

就是这里,在动画更新的时候,最终调用了invalidate(),从而触发了StatusBarIconView的更新,又回到了自定义view上面

@Override
    protected void onDraw(Canvas canvas) {
        if (mIconAppearAmount > 0.0f) {
            canvas.save();
            canvas.scale(mIconScale * mIconAppearAmount, mIconScale * mIconAppearAmount,
                    getWidth() / 2, getHeight() / 2);
            super.onDraw(canvas);
            canvas.restore();
        }

        if (mNumberBackground != null) {//  暂时不用,或已经废弃
            mNumberBackground.draw(canvas);
            canvas.drawText(mNumberText, mNumberX, mNumberY, mNumberPain);
        }
        if (mDotAppearAmount != 0.0f) {
            float radius;
            float alpha;
            if (mDotAppearAmount <= 1.0f) {
                radius = mDotRadius * mDotAppearAmount;
                alpha = 1.0f;
            } else {
                float fadeOutAmount = mDotAppearAmount - 1.0f;//  fadeOutAmount == 1.0f
                alpha = 1.0f - fadeOutAmount;//  alpha== 0.0
                radius = NotificationUtils.interpolate(mDotRadius, getWidth() / 4, fadeOutAmount);
            }
            mDotPaint.setAlpha((int) (alpha * 255));
            canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mDotPaint);
        }
    }

从前面我们总结的STATE_ICON、STATE_DOT和STATE_HIDDEN三种不同的状态对应的mIconAppearAmount和mDotAppearAmount值,逻辑就很清晰了:
STATE_ICON: draw了常规的notification icon和canvas.drawCircle全透明的overflow icon
STATE_DOT: 不draw常规的notification icon和canvas.drawCircle半径为radius的圆点
STATE_HIDDEN: 两个都不draw,所以我们啥也看不见了。

到这里,overflow icon显示的逻辑和加载的流程已经讲完,如有什么问题欢迎指正。

本文章已经独家授权ApeClub公众号使用。

你可能感兴趣的:(SystemUI状态栏之你所不知道的overflow icon)