自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()这个方法中,判断导航栏背景色是否是浅色有几个并行条件:
- 导航栏必须是透明模式的,即颜色由系统默认或应用来决定;
- 不处于省电模式,因为省电模式下导航栏背景色被强制设为了红色;
- 新的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。
- mScrimAlphaBelowThreshold 和mInvertLightNavBarWithScrim 分别是通过setScrimAlpha() 和setScrimColor() 方法来计算的,指的是下拉通知栏背景的遮罩颜色及透明度。
“ (mScrimAlphaBelowThreshold || !mInvertLightNavBarWithScrim) ” 这个算法指的是当通知栏被下拉到不能支持黑色button的时候,必须再切回白色。
比如上图这种情况,虽然Settings应用设置了导航栏button为黑色,但是下拉通知栏后会出现黑色遮罩,此时的背景不再能支持黑色button了,于是将其切换为了白色,当通知栏被收回时颜色会再切换回来。
- 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的源码里再深入的了解学习一下。