Android底层摸索改BUG(一):Android系统状态栏显示不下Wifi图标

这是我入职的第一个BUG,头疼,隔壁实习生一周解决了,我多花了几天

其中最大的原因就是我思考复杂了,在公司系统上,此BUG标题为:

请确认Wifi优先级,状态栏Wifi被忽略

BUG意思就是:当前安卓系统状态栏图标有显示尺寸的测量,如果比如需要显示8个图标,已经在状态栏绘制不下,则显示一个点表示省略,而不希望wifi被省略

我思考了一下一直以为就是优先级问题,是不是Android底层状态栏有各个图标优先级,图标过多的时候优先级高的就不会被隐藏,而其实最后参考了公司以前相关BUG的修改,其实只是状态栏的图标大小修改即可,将图标改小,状态栏就可以多一个显示图标,Wifi就不会被隐藏。(点向后一个图标挪了一位)

在尺寸文件中进行修改:对这两个尺寸进行改小(可以调一下刷机看一下)

 <dimen name="status_bar_icon_size">14dip</dimen>
 <dimen name="status_bar_system_icon_size">11.5dp</dimen>

值得一提的是上面的逻辑:超出的图标隐藏、绘制、测量相关的Java类是:StatusIconContainer.Java(虽然和BUG不在这改,记录一下)

java 复制代码
/**
 * StatusIconContainer 是 StatusBarMobileView 内部的一个自定义视图容器,用于管理状态图标的显示。
 * 它负责在 StatusBarMobileView 中管理状态图标的布局和可见性。
 */
public class StatusIconContainer extends AlphaOptimizedLinearLayout {
    //TAG    
    private static final String TAG = "StatusIconContainer";
    //是否打开DEBUG模式,会Log相关信息
    private static final boolean DEBUG = false;
    //是否打开DEBUG_OVERFLOW,会在状态栏绘制边框用来显示区域什么的
    private static final boolean DEBUG_OVERFLOW = false;
    // 最多可以显示 8 个状态图标,包括电池图标
    private static final int MAX_ICONS = 7;
    //    private static final int MAX_ICONS = 8;
    //点的个数        
    private static final int MAX_DOTS = 1;
    //Dot  点  Icon图标 相关属性
    private int mDotPadding;
    private int mIconSpacing;
    private int mStaticDotDiameter;
    private int mUnderflowWidth;
    private int mUnderflowStart = 0;
    // 是否可以在溢出空间中绘制
    private boolean mNeedsUnderflow;
    // 单个 StatusBarIconView 的点图标在此宽度内居中显示
    private int mIconDotFrameWidth;
    private boolean mShouldRestrictIcons = true;
    // 用于计算哪些状态要在布局期间可见
    private ArrayList<StatusIconState> mLayoutStates = new ArrayList<>();
    // 为了正确计数和测量
    private ArrayList<View> mMeasureViews = new ArrayList<>();
    // 任何被忽略的图标将不会被添加为子视图
    private ArrayList<String> mIgnoredSlots = new ArrayList<>();

    /**
     * 创建 StatusIconContainer 的构造函数。
     *
     * @param context Android 上下文对象
     */
    public StatusIconContainer(Context context) {
        this(context, null);
    }

    /**
     * 创建 StatusIconContainer 的构造函数。
     *
     * @param context Android 上下文对象
     * @param attrs   属性集
     */
    public StatusIconContainer(Context context, AttributeSet attrs) {
        super(context, attrs);
        initDimens();
        setWillNotDraw(!DEBUG_OVERFLOW);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
    }

    /**
     * 设置是否限制状态图标的显示。
     *
     * @param should 是否应该限制状态图标的显示
     */
    public void setShouldRestrictIcons(boolean should) {
        mShouldRestrictIcons = should;
    }

    /**
     * 检查是否正在限制状态图标的显示。
     *
     * @return 如果正在限制状态图标的显示,则返回 true;否则返回 false。
     */
    public boolean isRestrictingIcons() {
        return mShouldRestrictIcons;
    }

    /**
     * 初始化尺寸参数。
     */
    private void initDimens() {
        // 这是与 StatusBarIconView 使用相同的值
        mIconDotFrameWidth = getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.status_bar_icon_size);
        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);
        // 计算点的直径 R.dimen.overflow_dot_radius 2dp
        mStaticDotDiameter = 2 * radius;
        // 不能超过宽度 mIconDotFrameWidth 图标宽度 最大图标个数-1(可能还有个点,所以-1) 每个宽高
        mUnderflowWidth = mIconDotFrameWidth + (MAX_DOTS - 1) * (mStaticDotDiameter + mDotPadding);
        android.util.Log.d(TAG, "initDimens: " + mUnderflowStart);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        float midY = getHeight() / 2.0f;

        // 首先对所有子视图进行布局,以便稍后移动它们
        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) {
            //如果开启了DEBUG_OVERFLOW模式画框框
            Paint paint = new Paint();
            paint.setStyle(Style.STROKE);
            paint.setColor(Color.RED);

            // 显示边界框
            canvas.drawRect(getPaddingStart(), 0, getWidth() - getPaddingEnd(), getHeight(), paint);

            // 显示溢出框
            paint.setColor(Color.GREEN);
            canvas.drawRect(
                    mUnderflowStart, 0, mUnderflowStart + mUnderflowWidth, getHeight(), paint);
        }
    }
    //测量
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mMeasureViews.clear();
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        final int width = MeasureSpec.getSize(widthMeasureSpec);
        final int count = getChildCount();
        // 收集所有希望进行布局的视图
        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;

        // 测量所有子视图,以便它们报告正确的宽度
        int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED);
        mNeedsUnderflow = mShouldRestrictIcons && visibleCount > MAX_ICONS;
        for (int i = 0; i < visibleCount; i++) {
            View child = mMeasureViews.get(i);
            measureChild(child, childWidthSpec, heightMeasureSpec);
            int spacing = i == visibleCount - 1 ? 0 : mIconSpacing;
            if (mShouldRestrictIcons) {
                if (i < maxVisible && trackWidth) {
                    totalWidth += getViewTotalMeasuredWidth(child) + spacing;
                } else if (trackWidth) {
                    // 达到图标限制;为点图标添加空间
                    totalWidth += mUnderflowWidth;
                    trackWidth = false;
                }
            } else {
                totalWidth += getViewTotalMeasuredWidth(child) + spacing;
            }
        }

        if (mode == MeasureSpec.EXACTLY) {
            if (!mNeedsUnderflow && totalWidth > width) {
                mNeedsUnderflow = true;
            }
            setMeasuredDimension(width, MeasureSpec.getSize(heightMeasureSpec));
        } else {
            if (mode == MeasureSpec.AT_MOST && totalWidth > width) {
                mNeedsUnderflow = true;
                totalWidth = width;
            }
            setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec));
        }
    }

    @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);
    }

    /**
     * 添加要忽略的图标槽的名称。它将不会显示在布局中也不会被测量。
     *
     * @param slotName 图标的名称,就像在 frameworks/base/core/res/res/values/config.xml 中定义的那样
     */
    public void addIgnoredSlot(String slotName) {
        android.util.Log.d(TAG, "addIgnoredSlot: " + slotName);
        boolean added = addIgnoredSlotInternal(slotName);
        if (added) {
            requestLayout();
        }
    }

    /**
     * 添加要忽略的图标槽的名称的列表。
     *
     * @param slots 要忽略的图标的名称列表
     */
    public void addIgnoredSlots(List<String> slots) {
        for (String slot : slots) {
            android.util.Log.d(TAG, "addIgnoredSlots: " + slot);
        }

        boolean willAddAny = false;
        for (String slot : slots) {
            willAddAny |= addIgnoredSlotInternal(slot);
        }

        if (willAddAny) {
            requestLayout();
        }
    }

    /**
     * 内部添加要忽略的图标槽名称。
     *
     * @param slotName 图标的名称,就像在 frameworks/base/core/res/res/values/config.xml 中定义的那样
     * @return 如果成功添加,则返回 true,否则返回 false
     */
    private boolean addIgnoredSlotInternal(String slotName) {
        android.util.Log.d(TAG, "addIgnoredSlotInternal: " + slotName);
        if (mIgnoredSlots.contains(slotName)) {
            return false;
        }
        mIgnoredSlots.add(slotName);
        return true;
    }

    /**
     * 从忽略的图标槽中移除一个名称。
     *
     * @param slotName 要移除的图标槽的名称
     */
    public void removeIgnoredSlot(String slotName) {
        android.util.Log.d(TAG, "removeIgnoredSlot: " + slotName);
        boolean removed = mIgnoredSlots.remove(slotName);
        if (removed) {
            requestLayout();
        }
    }

    /**
     * 从忽略的图标槽中移除名称的列表。
     *
     * @param slots 要移除的图标槽的名称列表
     */
    public void removeIgnoredSlots(List<String> slots) {
        for (String slot: slots) {
            android.util.Log.d(TAG, "removeIgnoredSlot: " + slot);
        }
        boolean removedAny = false;
        for (String slot : slots) {
            removedAny |= mIgnoredSlots.remove(slot);
        }

        if (removedAny) {
            requestLayout();
        }
    }

    /**
     * 设置要忽略的图标槽的列表,清除当前的列表。
     *
     * @param slots 要忽略的图标槽的名称列表
     */
    public void setIgnoredSlots(List<String> slots) {
        mIgnoredSlots.clear();
        addIgnoredSlots(slots);
    }

    /**
     * 返回与特定槽名称相对应的视图。
     * 仅用于操作它如何呈现。
     *
     * @param slot 槽名称,与 com.android.internal.R.config_statusBarIcons 中定义的名称相对应
     * @return 如果此容器拥有相应的视图,则返回该视图,否则返回 null
     */
    public View getViewForSlot(String slot) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child instanceof StatusIconDisplayable
                    && ((StatusIconDisplayable) child).getSlot().equals(slot)) {
                return child;
            }
        }
        return null;
    }

    /**
     * 布局从右到左发生。
     */
    private void calculateIconTranslations() {
        mLayoutStates.clear();
        float width = getWidth();
        float translationX = width - getPaddingEnd();
        float contentStart = getPaddingStart();
        int childCount = getChildCount();
        // Underflow === 不显示内容直到此索引
        if (DEBUG) Log.d(TAG, "calculateIconTranslations: start=" + translationX
                + " width=" + width + " underflow=" + mNeedsUnderflow);

        // 收集所有希望可见的状态
        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;
            }

            // 移动 translationX 到布局的位置,以添加视图而不截断子视图
            translationX -= getViewTotalWidth(child);
            childState.visibleState = STATE_ICON;
            childState.xTranslation = translationX;
            mLayoutStates.add(0, childState);

            // 为下一个视图移动 translationX 以腾出间隔
            translationX -= mIconSpacing;
        }

        // 显示 1 至 MAX_ICONS 个图标,或 (MAX_ICONS - 1) 个图标 + 溢出
        int totalVisible = mLayoutStates.size();
        int maxVisible = totalVisible <= MAX_ICONS ? MAX_ICONS : MAX_ICONS - 1;

        mUnderflowStart = 0;
        int visible = 0;
        int firstUnderflowIndex = -1;
        for (int i = totalVisible - 1; i >= 0; i--) {
            StatusIconState state = mLayoutStates.get(i);
            // 如果在测量时发现需要溢出,则允许在此之前腾出空间
            if (mNeedsUnderflow && (state.xTranslation < (contentStart + mUnderflowWidth)) ||
                    (mShouldRestrictIcons && visible >= maxVisible)) {
                firstUnderflowIndex = i;
                break;
            }
            mUnderflowStart = (int) Math.max(
                    contentStart, state.xTranslation - 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.xTranslation = dotOffset;
                    state.visibleState = STATE_DOT;
                    dotOffset -= dotWidth;
                    totalDots++;
                } else {
                    state.visibleState = STATE_HIDDEN;
                }
            }
        }

        // 从 NotificationIconContainer 中拿来的,不是最优解,但保持布局逻辑简洁
        if (isLayoutRtl()) {
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                StatusIconState state = getViewStateFromChild(child);
                state.xTranslation = width - state.xTranslation - 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);
            }
        }
    }

    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.alpha = 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;

        // 从视图末尾的距离是最相关的,用于动画
        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 - xTranslation;

            if (!(view instanceof StatusIconDisplayable)) {
                return;
            }
            StatusIconDisplayable icon = (StatusIconDisplayable) view;
            AnimationProperties animationProperties = null;
            boolean animateVisibility = true;

            // 用于计算哪些状态在布局过程中是可见的,找出哪些属性的状态转换(如果有的话)我们需要动画
            // 确定要动画的状态转换的属性(如果有)
            if (justAdded
                    || icon.getVisibleState() == STATE_HIDDEN && visibleState == STATE_ICON) {
                // 图标正在出现,通过将其放在它将出现的位置并动画 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) {
                    // 消失,不要执行任何复杂的操作
                    animateVisibility = false;
                } else {
                    // 所有其他转换(到/从点等)
                    animationProperties = ANIMATE_ALL_PROPERTIES;
                }
            } else if (visibleState != STATE_HIDDEN && distanceToViewEnd != currentDistanceToEnd) {
                // 可见性不在发生变化,只需动画位置
                animationProperties = X_ANIMATION_PROPERTIES;
            }

            icon.setVisibleState(visibleState, animateVisibility);
            if (animationProperties != null) {
                view.animate().cancel();
                icon.addTransformationToViewGroup(view, animationProperties, null);
            }
            icon.applyInShelfTransformation(view, this, animationProperties, animationProperties);
        }
    }
}
相关推荐
今天我又学废了8 分钟前
Scala学习记录,List
学习
王俊山IT32 分钟前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
阿伟来咯~1 小时前
一些 uniapp相关bug
uni-app·bug
Mephisto.java1 小时前
【大数据学习 | kafka高级部分】kafka中的选举机制
大数据·学习·kafka
南宫生2 小时前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法
武子康3 小时前
大数据-212 数据挖掘 机器学习理论 - 无监督学习算法 KMeans 基本原理 簇内误差平方和
大数据·人工智能·学习·算法·机器学习·数据挖掘
使者大牙3 小时前
【大语言模型学习笔记】第一篇:LLM大规模语言模型介绍
笔记·学习·语言模型
As977_3 小时前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
ajsbxi3 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
Rattenking3 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js