前言
Android 夜间模式早在API 23的时候就可以使用了,不过那时候还有些限制,仅对新入栈的Activity生效,已在栈中的Activity不生效。但现在大家的App一般都是API28,甚至29,所以这个限制已经没有了。设置了夜间模式,对已入栈的Activity也生效,相当完美。
配置
配置起来很无脑
首先基类Activity必须继承
AppCompatActivity
,基于现在国内应用商店sdk最低要求28,绝大部分应用都升级到AndroidX,所以基本上来说,基类都是AppCompatActivity
。如果之前用的FragmentActivity
,那现在就要换成AppCompatActivity
其次,修改默认主题。
Theme.AppCompat.DayNight.xx
之前用Light的,只要中间这部分改成DayNight
就可以了。这块可以直接修改,没啥隐患。
然后,建立对应的夜间资源文件,与
values
对应的是values-night
,图片也一样,在对应目录后面加上-night
即可。
再然后,同步更新values,一般夜间模式也只是颜色发生变化,所以主要修改的就是colors.xml文件。将日间模式下的资源copy一份到夜间模式下的colors.xml下,然后对应修改夜间模式的values值就可以了,注意,colors的key要保持一致。
最后,代码切换夜间模式。
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
原理
细扒起来代码配置只有一句:AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
深入了解下:
/**
* Sets the default night mode. This is the default value used for all components, but can
* be overridden locally via {@link #setLocalNightMode(int)}.
*
* This is the primary method to control the DayNight functionality, since it allows
* the delegates to avoid unnecessary recreations when possible.
*
* If this method is called after any host components with attached
* {@link AppCompatDelegate}s have been 'created', a {@code uiMode} configuration change
* will occur in each. This may result in those components being recreated, depending
* on their manifest configuration.
*
* Defaults to {@link #MODE_NIGHT_FOLLOW_SYSTEM}.
*
* @see #setLocalNightMode(int)
* @see #getDefaultNightMode()
*/
public static void setDefaultNightMode(@NightMode int mode) {
if (DEBUG) {
Log.d(TAG, String.format("setDefaultNightMode. New:%d, Current:%d",
mode, sDefaultNightMode));
}
switch (mode) {
case MODE_NIGHT_NO:
case MODE_NIGHT_YES:
case MODE_NIGHT_FOLLOW_SYSTEM:
case MODE_NIGHT_AUTO_TIME:
case MODE_NIGHT_AUTO_BATTERY:
if (sDefaultNightMode != mode) {
sDefaultNightMode = mode;
applyDayNightToActiveDelegates();
}
break;
default:
Log.d(TAG, "setDefaultNightMode() called with an unknown mode");
break;
}
}
代码上面有一句注释:
This is the primary method to control the DayNight functionality, since it allows the delegates to avoid unnecessary recreations when possible.
翻译过来大概的意思是:如果此方法调用在已经创建完成的页面,那么这些页面可能会被重建。
简单的理解为,已经入栈的Activity,系统会自动完成重建过程。无需我们再操作。这就非常爽了,我只需要配置,其他的交给系统来完成。那到底是怎么实现的呢?
继续扒源码:
private static void applyDayNightToActiveDelegates() {
synchronized (sActivityDelegatesLock) {
for (WeakReference activeDelegate : sActivityDelegates) {
final AppCompatDelegate delegate = activeDelegate.get();
if (delegate != null) {
if (DEBUG) {
Log.d(TAG, "applyDayNightToActiveDelegates. Applying to " + delegate);
}
delegate.applyDayNight();
}
}
}
}
这很好理解,通过一个for循环,对循环内的Activity应用新的模式。 有遍历就必然有新增和删除,很容易就能找出对应的代码:
static void addActiveDelegate(@NonNull AppCompatDelegate delegate) {
synchronized (sActivityDelegatesLock) {
// Remove any existing records pointing to the delegate.
// There should not be any, but we'll make sure
removeDelegateFromActives(delegate);
// Add a new record to the set
sActivityDelegates.add(new WeakReference<>(delegate));
}
}
static void removeActivityDelegate(@NonNull AppCompatDelegate delegate) {
synchronized (sActivityDelegatesLock) {
// Remove any WeakRef records pointing to the delegate in the set
removeDelegateFromActives(delegate);
}
}
调用的地方盲猜也是在 onCreate
和onDestory
,继续追代码:
@Override
public void onCreate(Bundle savedInstanceState) {
// attachBaseContext will only be called from an Activity, so make sure we switch this for
// Dialogs, etc
mBaseContextAttached = true;
// Our implicit call to applyDayNight() should not recreate until after the Activity is
// created
applyDayNight(false);
// We lazily fetch the Window for Activities, to allow DayNight to apply in
// attachBaseContext
ensureWindow();
if (mHost instanceof Activity) {
String parentActivityName = null;
try {
parentActivityName = NavUtils.getParentActivityName((Activity) mHost);
} catch (IllegalArgumentException iae) {
// Ignore in this case
}
if (parentActivityName != null) {
// Peek at the Action Bar and update it if it already exists
ActionBar ab = peekSupportActionBar();
if (ab == null) {
mEnableDefaultActionBarUp = true;
} else {
ab.setDefaultDisplayHomeAsUpEnabled(true);
}
}
// Only activity-hosted delegates should apply night mode changes.
addActiveDelegate(this);
}
mCreated = true;
}
首先 onCreate
会先配置下默认的日夜间模式,然后会把当前Activity添加到弱引用中
@Override
public void onDestroy() {
if (mHost instanceof Activity) {
removeActivityDelegate(this);
}
if (mInvalidatePanelMenuPosted) {
mWindow.getDecorView().removeCallbacks(mInvalidatePanelMenuRunnable);
}
mStarted = false;
mIsDestroyed = true;
if (mLocalNightMode != MODE_NIGHT_UNSPECIFIED
&& mHost instanceof Activity
&& ((Activity) mHost).isChangingConfigurations()) {
// If we have a local night mode set, save it
sLocalNightModes.put(mHost.getClass().getName(), mLocalNightMode);
} else {
sLocalNightModes.remove(mHost.getClass().getName());
}
if (mActionBar != null) {
mActionBar.onDestroy();
}
// Make sure we clean up any receivers setup for AUTO mode
cleanupAutoManagers();
}
onDestory
中也会移除已添加的Activity,跟我们盲猜的一样。
然后继续看下如何应用配置的:
@SuppressWarnings("deprecation")
private boolean applyDayNight(final boolean allowRecreation) {
if (mIsDestroyed) {
if (DEBUG) {
Log.d(TAG, "applyDayNight. Skipping because host is destroyed");
}
// If we're destroyed, ignore the call
return false;
}
@NightMode final int nightMode = calculateNightMode();
@ApplyableNightMode final int modeToApply = mapNightMode(mContext, nightMode);
final boolean applied = updateForNightMode(modeToApply, allowRecreation);
if (nightMode == MODE_NIGHT_AUTO_TIME) {
getAutoTimeNightModeManager(mContext).setup();
} else if (mAutoTimeNightModeManager != null) {
// Make sure we clean up the existing manager
mAutoTimeNightModeManager.cleanup();
}
if (nightMode == MODE_NIGHT_AUTO_BATTERY) {
getAutoBatteryNightModeManager(mContext).setup();
} else if (mAutoBatteryNightModeManager != null) {
// Make sure we clean up the existing manager
mAutoBatteryNightModeManager.cleanup();
}
return applied;
}
其他的不用管,有一个比较显眼的方法:updateForNightMode
继续看:
private boolean updateForNightMode(@ApplyableNightMode final int mode,
final boolean allowRecreation) {
... //省略
if (currentNightMode != newNightMode
&& allowRecreation
&& !activityHandlingUiMode
&& mBaseContextAttached
&& (sCanReturnDifferentContext || mCreated)
&& mHost instanceof Activity
&& !((Activity) mHost).isChild()) {
// If we're an attached, standalone Activity, we can recreate() to apply using the
// attachBaseContext() + createConfigurationContext() code path.
// Else, we need to use updateConfiguration() before we're 'created' (below)
if (DEBUG) {
Log.d(TAG, "updateForNightMode. Recreating Activity: " + mHost);
}
// 重启代码
ActivityCompat.recreate((Activity) mHost);
handled = true;
}
if (!handled && currentNightMode != newNightMode) {
// Else we need to use the updateConfiguration path
if (DEBUG) {
Log.d(TAG, "updateForNightMode. Updating resources config on host: " + mHost);
}
//更新模式资源
updateResourcesConfigurationForNightMode(newNightMode, activityHandlingUiMode, null);
handled = true;
}
if (DEBUG && !handled) {
Log.d(TAG, "updateForNightMode. Skipping. Night mode: " + mode + " for host:" + mHost);
}
// Notify the activity of the night mode. We only notify if we handled the change,
// or the Activity is set to handle uiMode changes
// 模式更换监听
if (handled && mHost instanceof AppCompatActivity) {
((AppCompatActivity) mHost).onNightModeChanged(mode);
}
return handled;
}
具体关键代码,已经在上面加了注释。已入栈的Activity,会走 recreate
重新完成模式替换,然后更新资源,以及回调模式改版状态。
至此,夜间模式分析完毕。
结语
配置起来就这些,很简单,对于一个新app来说,完全没有任何工作量可言。但对于一个已经成型的商业app来说,新增夜间模式的工作量是巨大的。平常代码书写的规范与否,间接的影响夜间模式的工作量。如果书写规范,所有的color都写在了xml文件里,那改起来还能接受,如果书写随意,直接在布局或者代码里写色值,那就有的哭了。所以,一个良好的代码规范,还是非常重要的。