引言
今天我们主要讲的是SystemUI状态栏里面notification icons中的overflow icon,众所周知,notification icon整个view的宽度是有限制的,当内容区宽度大于最大宽度时,notification icon将无法再显示,此时今天我们的主角overflow icon就出现了,overflow icon以"..."的形式出现,表示当前通知过多被折叠起来了,以达到提示用户的目的。overflow icon截图如下红框所示
正文
本文主要讲述下源码里面overflow icon显示的逻辑和加载的流程,话不多说,我们开始吧。
流程图
overflow icon显示的逻辑和加载的流程图大致如下
显示的逻辑和加载流程
关于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的四大护法
@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公众号使用。