SystemUI 导航栏Button颜色切换流程详解

自8.0起,应用可以通过View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR这个flag来自定义导航栏button显示黑色还是白色,需要通过context.getWindow().getDecorView().setSystemUIVisibility()方法来设置。

一、响应

上述方法调用后,CommandQueue会收到setSystemUiVisibility()的回调,然后通过Handler回调所有实现了内部接口Callbacks的类的setSystemUiVisibility()方法。纵观整个SystemUI,实现了CommandQueue.Callbacks接口的该方法的类只有两个:StatusBar 和NavigationBarFragment,导航栏的button颜色变化肯定是放在NavigationBarFragment里面处理的。

    // ------ NavigationBarFragment ------

    @Override
    public void setSystemUiVisibility(int vis, int fullscreenStackVis, int dockedStackVis,
            int mask, Rect fullscreenStackBounds, Rect dockedStackBounds) {
        final int oldVal = mSystemUiVisibility;
        final int newVal = (oldVal & ~mask) | (vis & mask);
        final int diff = newVal ^ oldVal;
        boolean nbModeChanged = false;
        if (diff != 0) {
            mSystemUiVisibility = newVal;

            // update navigation bar mode
            final int nbMode = getView() == null
                    ? -1 : mStatusBar.computeBarMode(oldVal, newVal,
                    View.NAVIGATION_BAR_TRANSIENT, View.NAVIGATION_BAR_TRANSLUCENT,
                    View.NAVIGATION_BAR_TRANSPARENT);
            nbModeChanged = nbMode != -1;
            if (nbModeChanged) {
                if (mNavigationBarMode != nbMode) {
                    mNavigationBarMode = nbMode;
                    checkNavBarModes();
                }
                mStatusBar.touchAutoHide();
            }
        }

        mLightBarController.onNavigationVisibilityChanged(vis, mask, nbModeChanged,
                mNavigationBarMode);
    }

这段代码中间部分主要是在比较新旧vis后,修改导航栏的显示模式和颜色,比如透明,半透明,省电模式的红色等等,而最后的LightBarController则是负责修改button颜色的。

onNavigationVisibilityChanged()的具体实现:

    // ------ LightBarController ------

    public void onNavigationVisibilityChanged(int vis, int mask, boolean nbModeChanged, int navigationBarMode) {
        int oldVis = mSystemUiVisibility;
        int newVis = (oldVis & ~mask) | (vis & mask);
        int diffVis = newVis ^ oldVis;
        if ((diffVis & View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0
                || nbModeChanged) {
            boolean last = mNavigationLight;
            mHasLightNavigationBar = isLight(vis, navigationBarMode,
                    View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
            mNavigationLight = mHasLightNavigationBar
                    && (mScrimAlphaBelowThreshold || !mInvertLightNavBarWithScrim)
                    && !mQsCustomizing;
            if (mNavigationLight != last) {
                updateNavigation();
            }
        }
        mSystemUiVisibility = newVis;
        mLastNavigationBarMode = navigationBarMode;
    }

二、条件

在上面的方法中能进行NavigationBar更新的条件有一个比较有意思的地方,首先先用newVis 与oldVis 进行按位异或,再用得到的diffVis 与light flag进行按位与运算,这么做的好处是只要newVis 与oldVis 中某一个值有该flag位,另一个没有,那么随后的按位与运算一定不为0,反之两者都有或都没有,后面的运算就会得到0。

这样一来就用比较精简的写法来对是否有添加或者移除 View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR操作进行了判断,当判断到有变化后,后续再来具体计算是要变成亮色的还是暗色的。

在isLight()这个方法中,判断导航栏背景色是否是浅色有几个并行条件:

  1. 导航栏必须是透明模式的,即颜色由系统默认或应用来决定;
  2. 不处于省电模式,因为省电模式下导航栏背景色被强制设为了红色;
  3. 新的visibility必须带有View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR这一flag,否则则为暗色;
    // ------ LightBarController ------

    private boolean isLight(int vis, int barMode, int flag) {
        boolean isTransparentBar = (barMode == MODE_TRANSPARENT
                || barMode == MODE_LIGHTS_OUT_TRANSPARENT);
        boolean allowLight = isTransparentBar && !mBatteryController.isPowerSave();
        boolean light = (vis & flag) != 0;
        return allowLight && light;
    }

就算上面判断满足isLight,后面仍有一些硬性条件:mScrimAlphaBelowThreshold ,mInvertLightNavBarWithScrim,mQsCustomizing。

  1. mScrimAlphaBelowThreshold 和mInvertLightNavBarWithScrim 分别是通过setScrimAlpha() 和setScrimColor() 方法来计算的,指的是下拉通知栏背景的遮罩颜色及透明度。
    “ (mScrimAlphaBelowThreshold || !mInvertLightNavBarWithScrim) ” 这个算法指的是当通知栏被下拉到不能支持黑色button的时候,必须再切回白色。

比如上图这种情况,虽然Settings应用设置了导航栏button为黑色,但是下拉通知栏后会出现黑色遮罩,此时的背景不再能支持黑色button了,于是将其切换为了白色,当通知栏被收回时颜色会再切换回来。

  1. mQsCustomizing 则是QS处于编辑状态,这个时候QSCustomizer会在导航栏位置放一块纯黑的View盖着,所以此时也不能使用黑色的button,否则会完全看不清。

三、元件初始化

更新操作则是在updateNavigation()里处理:

    // ------ LightBarController ------

    private void updateNavigation() {
        if (mNavigationBarController != null) {
            mNavigationBarController.setIconsDark(
                    mNavigationLight, animateChange());
        }
    }

mNavigationBarController是LightBarTransitionsController类的实例,这里先来看下LightBarTransitionsController的初始化过程吧。

StatusBar初始化更新UI的makeStatusBarView()方法里会初始化NavigationBarView,然后把LightBarController传递给NavigationBarFragment:

    // ------ StatusBar ------

    public void makeStatusBarView() {
        ...
        try {
            boolean showNav = mWindowManagerService.hasNavigationBar();
            if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav);
            if (showNav) {
                //初始化NavigationBarView
                createNavigationBar();
            }
        } catch (RemoteException ex) {
            // no window manager? good luck with that
        }
        ...
        mLightBarController = Dependency.get(LightBarController.class);
        if (mNavigationBar != null) {
            mNavigationBar.setLightBarController(mLightBarController);
        }
    }

首先,在NavigationBarView的构造方法中,会初始化一个NavigationBarTransitions对象,后者又会初始化一个LightBarTransitionsController对象,参数为Context 和DarkIntensityApplier,DarkIntensityApplier是LightBarTransitionsController内部的一个public接口,只有唯一的一个方法applyDarkIntensity(),这边根据java 8的特性重写了applyDarkIntensity()的实现,并调用NavigationBarTransitions.this.applyDarkIntensity() 。
[ Android与Java8那些事 ]

以下是三个构造方法:

    public NavigationBarView(Context context, AttributeSet attrs) {
        super(context, attrs);
        ...
        mBarTransitions = new NavigationBarTransitions(this);
        ...
    }


    public NavigationBarTransitions(NavigationBarView view) {
        super(view, R.drawable.nav_background);
        mView = view;
        mBarService = IStatusBarService.Stub.asInterface(
                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
        mLightTransitionsController = new LightBarTransitionsController(view.getContext(),
                this::applyDarkIntensity);
        ...
    }


    public LightBarTransitionsController(Context context, DarkIntensityApplier applier) {
        mApplier = applier;
        mHandler = new Handler();
        mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
        SysUiServiceProvider.getComponent(context, CommandQueue.class)
                .addCallbacks(this);
    }

然后,在NavigationBarFragment的setLightBarController()中,通过NavigationBarView获得NavigationBarTransitions后,再得到LightBarTransitionsController:

    // ------ NavigationBarFragment ------
    public void setLightBarController(LightBarController lightBarController) {
        mLightBarController = lightBarController;
        mLightBarController.setNavigationBar(mNavigationBarView.getLightTransitionsController());
    }

    // ------ NavigationBarView ------
    public LightBarTransitionsController getLightTransitionsController() {
        return mBarTransitions.getLightTransitionsController();
    }

    // ------ NavigationBarTransitions ------
    public LightBarTransitionsController getLightTransitionsController() {
        return mLightTransitionsController;
    }

再赋值给LightBarController:

    // ------ LightBarController ------

    public void setNavigationBar(LightBarTransitionsController navigationBar) {
        mNavigationBarController = navigationBar;
        updateNavigation();
    }

到这里初始化过程就完成了,下面可以继续本节开头的updateNavigation()流程了。

四、更新

setIconsDark()方法有两个参数,第一个dark是第二节里计算出的mNavigationLight,true表示黑色icon,false则是白色icon;第二个animate通过animateChange()方法计算,主要与是否设置了指纹锁有关。

    // ------ LightBarTransitionsController ------

    public void setIconsDark(boolean dark, boolean animate) {
        if (!animate) {
            setIconTintInternal(dark ? 1.0f : 0.0f);
            mNextDarkIntensity = dark ? 1.0f : 0.0f;
        } else if (mTransitionPending) {
            deferIconTintChange(dark ? 1.0f : 0.0f);
        } else if (mTransitionDeferring) {
            animateIconTint(dark ? 1.0f : 0.0f,
                    Math.max(0, mTransitionDeferringStartTime - SystemClock.uptimeMillis()),
                    mTransitionDeferringDuration);
        } else {
            animateIconTint(dark ? 1.0f : 0.0f, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION);
        }
    }

    private void animateIconTint(float targetDarkIntensity, long delay,
            long duration) {
        ...
        mNextDarkIntensity = targetDarkIntensity;
        mTintAnimator = ValueAnimator.ofFloat(mDarkIntensity, targetDarkIntensity);
        mTintAnimator.addUpdateListener(
                animation -> setIconTintInternal((Float) animation.getAnimatedValue()));
        mTintAnimator.setDuration(duration);
        mTintAnimator.setStartDelay(delay);
        mTintAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
        mTintAnimator.start();
    }

在这个方法里,mTransitionPending和mTransitionDeferring主要与keyguard解锁的一些流程有关,且最后仍然会执行到animateIconTint(),这里就不特别讲了;可以看到,animate的区别就是有没有经过animateIconTint()去设置一个线性变化的Animator,最后都得靠setIconTintInternal()来改变颜色。

    private void setIconTintInternal(float darkIntensity) {
        mDarkIntensity = darkIntensity;
        mApplier.applyDarkIntensity(darkIntensity);
    }

mApplier在前面已经提到了,最后会回到NavigationBarTransitions.applyDarkIntensity()处理:

    // ------ NavigationBarTransitions ------
    public void applyDarkIntensity(float darkIntensity) {
        SparseArray<ButtonDispatcher> buttonDispatchers = mView.getButtonDispatchers();
        for (int i = buttonDispatchers.size() - 1; i >= 0; i--) {
            buttonDispatchers.valueAt(i).setDarkIntensity(darkIntensity);
        }
        ...
    }

    // ------ ButtonDispatcher ------
    public void setDarkIntensity(float darkIntensity) {
        mDarkIntensity = darkIntensity;
        final int N = mViews.size();
        for (int i = 0; i < N; i++) {
            ((ButtonInterface) mViews.get(i)).setDarkIntensity(darkIntensity);
        }
    }

看到这里疑问来了,mViews是从哪来的呢?
在NavigationBarView初始化后会把每个button布局里KeyButtonView的id封装到一个ButtonDispatcher对象中,随后在NavigationBarInflaterView里将这些KeyButtonView遍历出来,再add进ButtonDispatcher的mViews中。
[ SystemUI之NavigationBar加载流程 ]

KeyButtonView继承自ImageView并且实现了ButtonInterface接口,所以需要给它设置相应的drawable。在NavigationBarView中给它们设的是KeyButtonDrawable,这里涉及到导航栏的黑白色按钮的设计(浅析Android 9.0导航栏的变化 一文中已经有介绍过)。

    // ------ KeyButtonView ------
    public void setDarkIntensity(float darkIntensity) {
        Drawable drawable = getDrawable();
        if (drawable != null) {
            ((KeyButtonDrawable) getDrawable()).setDarkIntensity(darkIntensity);
            ...
        }
    }

    // ------ KeyButtonDrawable ------
    public void setDarkIntensity(float intensity) {
        if (!mHasDarkDrawable) {
            return;
        }
        getDrawable(0).setAlpha((int) ((1 - intensity) * 255f));
        getDrawable(1).setAlpha((int) (intensity * 255f));
        invalidateSelf();
    }

因为KeyButtonDrawable 是有一黑一白的drawable重叠放置的,所以setDarkIntensity()的最终是通过修改两者的透明度,并通知View刷新,来实现黑色与白色显示的瞬间切换或动画切换。


以上就是通过LightBarController切换导航栏黑白色主题button的大致流程,有些过程可能没有完全说明,有兴趣的朋友可以在Android 8.1 SystemUI的源码里再深入的了解学习一下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容