SystemUI 導(dǎo)航欄Button顏色切換流程詳解

自8.0起,應(yīng)用可以通過View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR這個(gè)flag來自定義導(dǎo)航欄button顯示黑色還是白色,需要通過context.getWindow().getDecorView().setSystemUIVisibility()方法來設(shè)置。

一、響應(yīng)

上述方法調(diào)用后,CommandQueue會(huì)收到setSystemUiVisibility()的回調(diào),然后通過Handler回調(diào)所有實(shí)現(xiàn)了內(nèi)部接口Callbacks的類的setSystemUiVisibility()方法??v觀整個(gè)SystemUI,實(shí)現(xiàn)了CommandQueue.Callbacks接口的該方法的類只有兩個(gè):StatusBar 和NavigationBarFragment,導(dǎo)航欄的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后,修改導(dǎo)航欄的顯示模式和顏色,比如透明,半透明,省電模式的紅色等等,而最后的LightBarController則是負(fù)責(zé)修改button顏色的。

onNavigationVisibilityChanged()的具體實(shí)現(xiàn):

    // ------ 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;
    }

二、條件

在上面的方法中能進(jìn)行NavigationBar更新的條件有一個(gè)比較有意思的地方,首先先用newVis 與oldVis 進(jìn)行按位異或,再用得到的diffVis 與light flag進(jìn)行按位與運(yùn)算,這么做的好處是只要newVis 與oldVis 中某一個(gè)值有該flag位,另一個(gè)沒有,那么隨后的按位與運(yùn)算一定不為0,反之兩者都有或都沒有,后面的運(yùn)算就會(huì)得到0。

這樣一來就用比較精簡的寫法來對(duì)是否有添加或者移除 View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR操作進(jìn)行了判斷,當(dāng)判斷到有變化后,后續(xù)再來具體計(jì)算是要變成亮色的還是暗色的。

在isLight()這個(gè)方法中,判斷導(dǎo)航欄背景色是否是淺色有幾個(gè)并行條件:

  1. 導(dǎo)航欄必須是透明模式的,即顏色由系統(tǒng)默認(rèn)或應(yīng)用來決定;
  2. 不處于省電模式,因?yàn)槭‰娔J较聦?dǎo)航欄背景色被強(qiáng)制設(shè)為了紅色;
  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() 方法來計(jì)算的,指的是下拉通知欄背景的遮罩顏色及透明度。
    “ (mScrimAlphaBelowThreshold || !mInvertLightNavBarWithScrim) ” 這個(gè)算法指的是當(dāng)通知欄被下拉到不能支持黑色button的時(shí)候,必須再切回白色。

比如上圖這種情況,雖然Settings應(yīng)用設(shè)置了導(dǎo)航欄button為黑色,但是下拉通知欄后會(huì)出現(xiàn)黑色遮罩,此時(shí)的背景不再能支持黑色button了,于是將其切換為了白色,當(dāng)通知欄被收回時(shí)顏色會(huì)再切換回來。

  1. mQsCustomizing 則是QS處于編輯狀態(tài),這個(gè)時(shí)候QSCustomizer會(huì)在導(dǎo)航欄位置放一塊純黑的View蓋著,所以此時(shí)也不能使用黑色的button,否則會(huì)完全看不清。

三、元件初始化

更新操作則是在updateNavigation()里處理:

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

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

mNavigationBarController是LightBarTransitionsController類的實(shí)例,這里先來看下LightBarTransitionsController的初始化過程吧。

StatusBar初始化更新UI的makeStatusBarView()方法里會(huì)初始化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的構(gòu)造方法中,會(huì)初始化一個(gè)NavigationBarTransitions對(duì)象,后者又會(huì)初始化一個(gè)LightBarTransitionsController對(duì)象,參數(shù)為Context 和DarkIntensityApplier,DarkIntensityApplier是LightBarTransitionsController內(nèi)部的一個(gè)public接口,只有唯一的一個(gè)方法applyDarkIntensity(),這邊根據(jù)java 8的特性重寫了applyDarkIntensity()的實(shí)現(xiàn),并調(diào)用NavigationBarTransitions.this.applyDarkIntensity() 。
[ Android與Java8那些事 ]

以下是三個(gè)構(gòu)造方法:

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

到這里初始化過程就完成了,下面可以繼續(xù)本節(jié)開頭的updateNavigation()流程了。

四、更新

setIconsDark()方法有兩個(gè)參數(shù),第一個(gè)dark是第二節(jié)里計(jì)算出的mNavigationLight,true表示黑色icon,false則是白色icon;第二個(gè)animate通過animateChange()方法計(jì)算,主要與是否設(shè)置了指紋鎖有關(guān)。

    // ------ 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();
    }

在這個(gè)方法里,mTransitionPending和mTransitionDeferring主要與keyguard解鎖的一些流程有關(guān),且最后仍然會(huì)執(zhí)行到animateIconTint(),這里就不特別講了;可以看到,animate的區(qū)別就是有沒有經(jīng)過animateIconTint()去設(shè)置一個(gè)線性變化的Animator,最后都得靠setIconTintInternal()來改變顏色。

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

mApplier在前面已經(jīng)提到了,最后會(huì)回到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初始化后會(huì)把每個(gè)button布局里KeyButtonView的id封裝到一個(gè)ButtonDispatcher對(duì)象中,隨后在NavigationBarInflaterView里將這些KeyButtonView遍歷出來,再add進(jìn)ButtonDispatcher的mViews中。
[ SystemUI之NavigationBar加載流程 ]

KeyButtonView繼承自ImageView并且實(shí)現(xiàn)了ButtonInterface接口,所以需要給它設(shè)置相應(yīng)的drawable。在NavigationBarView中給它們?cè)O(shè)的是KeyButtonDrawable,這里涉及到導(dǎo)航欄的黑白色按鈕的設(shè)計(jì)(淺析Android 9.0導(dǎo)航欄的變化 一文中已經(jīng)有介紹過)。

    // ------ 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();
    }

因?yàn)镵eyButtonDrawable 是有一黑一白的drawable重疊放置的,所以setDarkIntensity()的最終是通過修改兩者的透明度,并通知View刷新,來實(shí)現(xiàn)黑色與白色顯示的瞬間切換或動(dòng)畫切換。


以上就是通過LightBarController切換導(dǎo)航欄黑白色主題button的大致流程,有些過程可能沒有完全說明,有興趣的朋友可以在Android 8.1 SystemUI的源碼里再深入的了解學(xué)習(xí)一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 把畫得不好的畫拿出來在反面做一些速涂練習(xí)??~ 自在的天空 自在的舞者
    塵里微光閱讀 213評(píng)論 0 4
  • 不管是上班還是休息,出門還是旅游;走過的每一條路,路過的每一片風(fēng)景,都有不一樣的美好。 山間的小野花 ,旅途上的小...
    囈語雪閱讀 748評(píng)論 4 9
  • 去看情圣沒有遲到,本來上午場的電影人就少,圖的就是安靜,結(jié)果為數(shù)不多的人還在里面吃瓜子,跟小松鼠一樣真的是很令人不...
    胖胖瑾閱讀 654評(píng)論 0 0
  • 原問題鏈接:駱和:邏輯思維、戰(zhàn)略思維、系統(tǒng)性思維、批判性思維、創(chuàng)造性思維、策略思維的區(qū)別是什么? 正文: 1,關(guān)于...
    駱和先生閱讀 969評(píng)論 0 0
  • 今天想寫一下口碑不錯(cuò)的電影《喜歡你》,小長假開始的前一天晚上,其實(shí)是為了打發(fā)時(shí)間,算是歪打正著,收獲了一些小小的感...
    深灰淺灰閱讀 553評(píng)論 0 0

友情鏈接更多精彩內(nèi)容