Android 10 深色模式适配
简介
Android 10(API级别29)版本开始,Google提供了深色模式。
官方介绍详见:https://developer.android.com/guide/topics/ui/look-and-feel/darktheme
深色模式的优势
- 更好的用户体验
- 减少耗电量
- 为弱视以及对强光敏感的用户提高可视性
深色模式的启用
- 设置 -> 显示 -> 深色模式
- 下拉通知栏中开启
- Pixel 手机开启省电模式时会自动激活
适配
通过应用主题设配
要直接支持深色主题背景,可以将应用的主题设置为
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
或者
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
DayNight主题会将应用主要的主题背景与系统控制的深色模式相关联,跟随系统的深色模式开启关闭而变化。
Theme.AppCompat.DayNight
res\values\values.xml
res\values-night-v8\values-night-v8.xml
以上截图可以看出,DayNight主题就是在values和values-night中分别定义了浅色和深色的主题,如果应用主题是直接继承DayNight主题,就不需要重复地声明对应的night
主题资源了。
验证如下
主题继承DayNight,没有设置颜色(包括背景颜色及字体颜色),开启深色模式,系统会强制将背景及字体设置为深色模式,例如:
tips:控件如果设置了背景颜色或者字体颜色,不管系统是否已经开启深色模式,均显示为代码里设置的颜色。
应用内资源文件适配
常见的需要设置的资源有anim
、drawable
、layout
、values
等,对于这些资源,可以使用限定符来提供一些备用资源;例如深色模式可以使用限定符-night
来表示在深色模式中使用的资源,如下图:
tips:-night文件夹里只放置在深色模式中需要改变的资源,若浅色主题与深色主题使用的资源一样,则不需要在-night中新增;只在-night文件夹定义的资源而普通模式文件夹内没有的资源,能编译通过但是运行会崩溃。
网络图片适配
适配网络图片可以通过判断当前是否是应用深色主题,加载显示不同的图片。代码如下:
/*** 判断当前是否是深色模式 * @param mContext 上下文* @return true:深色,false:浅色*/public static boolean isNightMode(Context mContext){return (mContext.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;}
Configuration.uiMode有三种Night的模式
- UI_MODE_NIGHT_NO 表示当前使用的是
notnight
模式资源 - UI_MODE_NIGHT_YES 表示当前使用的是
night
模式资源 - UI_MODE_NIGHT_UNDEFINED 表示当前没有设置模式
直接设置使用深色模式
应用内可以直接调用AppCompatDelegate.setDefaultNightMode(),设置使用深色模式;共有以下几种场景:
- MODE_NIGHT_AUTO_BATTERY 低电量模式自动开启深色模式
- MODE_NIGHT_FOLLOW_SYSTEM 跟随系统开启和关闭深色模式(默认)
- MODE_NIGHT_NO 强制使用
notnight
资源,表示非深色模式 - MODE_NIGHT_YES 强制使用
night
资源 - MODE_NIGHT_UNSPECIFIED 配合 setLocalNightMode(int) 使用,表示由Activity通过AppCompactActivity.getDelegate()来单独设置页面的深色模式,不设置全局模式
深色模式设置可以从三个层级设置,分别是系统层、Applcation层以及Activity层;底层的设置会覆盖上层的设置。
系统层:是指在系统设置中开启深色模式
Applcation层:通过调用AppCompatDelegate.setDefaultNightMode()设置深色模式
static {AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); }
Activity层:通过调用getDelegate().setLocalNightMode()设置当前页面的深色模式。
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
setDefaultNightMode源码如下
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;}
}
其中主要是调用了applyDayNightToActiveDelegates()这个方法
private static void applyDayNightToActiveDelegates() {synchronized (sActivityDelegatesLock) {for (WeakReference<AppCompatDelegate> activeDelegate : sActivityDelegates) {final AppCompatDelegate delegate = activeDelegate.get();if (delegate != null) {if (DEBUG) {Log.d(TAG, "applyDayNightToActiveDelegates. Applying to " + delegate);}delegate.applyDayNight();}}}
}
通过遍历sActivityDelegates,调用了AppCompatDelegate的applyDayNight()方法
getDelegate().setLocalNightMode()主要也是调用了applyDayNight()方法
@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 callreturn 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 managermAutoTimeNightModeManager.cleanup();}if (nightMode == MODE_NIGHT_AUTO_BATTERY) {getAutoBatteryNightModeManager(mContext).setup();} else if (mAutoBatteryNightModeManager != null) {// Make sure we clean up the existing managermAutoBatteryNightModeManager.cleanup();}return applied;
}
这里很多都是状态的重置计算等,其中有个updateForNightMode()的方法需要注意
/*** Updates the {@link Resources} configuration {@code uiMode} with the* chosen {@code UI_MODE_NIGHT} value.** @param mode The new night mode to apply* @param allowRecreation whether to attempt activity recreate* @return true if an action has been taken (recreation, resources updating, etc)*/
private boolean updateForNightMode(@ApplyableNightMode final int mode,final boolean allowRecreation) {boolean handled = false;final Configuration overrideConfig =createOverrideConfigurationForDayNight(mContext, mode, null);final boolean activityHandlingUiMode = isActivityManifestHandlingUiMode();final int currentNightMode = mContext.getResources().getConfiguration().uiMode& Configuration.UI_MODE_NIGHT_MASK;final int newNightMode = overrideConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;if (DEBUG) {Log.d(TAG, String.format("updateForNightMode [allowRecreation:%s, currentNightMode:%d, "+ "newNightMode:%d, activityHandlingUiMode:%s, baseContextAttached:%s, "+ "created:%s, canReturnDifferentContext:%s, host:%s]",allowRecreation, currentNightMode, newNightMode, activityHandlingUiMode,mBaseContextAttached, mCreated, sCanReturnDifferentContext, mHost));}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 pathif (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 changesif (handled && mHost instanceof AppCompatActivity) {((AppCompatActivity) mHost).onNightModeChanged(mode);}return handled;
}
- 根据传入的mode调用createOverrideConfigurationForDayNight()方法创建新的Configuration;
- 如果新模式!=旧模式、manifest中没有处理uiMode变化等条件满足时,调用ActivityCompat.recreate()重建activity;
- 如果没有处理第2点,并且新模式!=旧模式,调用updateResourcesConfigurationForNightMode()方法,主要是为了更新Configuration;并且判断是否需要回调onConfigurationChanged()方法通知activity;
- 如果处理了第2点或者第3点,此时深色模式有改动,会回调onNightModeChanged()方法通知activity。
综上可知
- manifest中没有配置android:configChanges=“uiMode”,
直接调用AppCompatDelegate.setDefaultNightMode()或者getDelegate().setLocalNightMode(),
activity会重建,会回调onNightModeChanged()方法。
- manifest中配置了android:configChanges=“uiMode”,
直接调用AppCompatDelegate.setDefaultNightMode()或者getDelegate().setLocalNightMode(),
activity不会重建,会回调onConfigurationChanged()和onNightModeChanged()方法。
forceDarkAllowed
针对已有的项目,除了对所有的颜色和图片定义night
模式资源的自定义适配方法外,还可以使用forceDarkAllowed
进行强制转换,但是效果比较生硬,可能没有自己适配那么完美。
可以分别对Theme及View设置强制深色模式,具体如下:
Theme:在主题中设置
android:forceDarkAllowed
为true
,这样系统在深色模式时会强制改变应用控件颜色,自动进行适配;<style name="AppStyle" parent="@android:style/Theme.Light.NoTitleBar"><item name="android:forceDarkAllowed">true</item> </style>
view:如果不希望某个view被强制夜间模式,可以直接调用
view.setForceDarkAllowed(false)
或者在xml布局中给view添加android:forceDarkAllowed="false"
<LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:forceDarkAllowed="false"/>
需要注意的是:
- 要强制深色模式生效必须开启硬件加速(默认开启);
- 在主题添加
android:forceDarkAllowed
仅对Light
主题生效,对非Light
的主题无效; - 主题中若声明
android:forceDarkAllowed
为false
的话,无论view是否开启forceDarkAllowed
都不会显示深色模式; - 主题中若声明
android:forceDarkAllowed
为true
的话,view可以通过设置forceDarkAllowed
为false
来关闭view的深色模式; - 父view如果设置了
forceDarkAllowed
为false
的话,子view就算是设置forceDarkAllowed
为true
,也无法开启深色模式; - view中的
forceDarkAllowed
属性默认为true
。
综上可得出:forceDarkAllowed
设置为false
的优先
直接使用forceDarkAllowed
强制开启深色模式效果较生硬,forceDarkAllowed
一般在view上使用,避免已经适配好深色模式的view被系统强制修改了。
总结
深色模式适配总体上来说,并不复杂,主要是细致,并且前期就确定好适配方案。以下是个人总结的三种适配方案:
直接跟随系统开启或关闭深色模式
优点
- 直接跟随系统切换模式,只需要配置好
notnight
模式及night
模式对应的资源即可,适配性较好,工作量小; - 在系统层做颜色处理,比Java效率高。
缺点:
- 若应用在运行中,栈内所有activity会重建;
- 针对用户已操作的比如选择框、列表滑动等,activity重建相当于重新渲染此页面,用户之前操作内容不会保存,用户体验不好;
- 应用运行中切换深色模式状态,activity重建,可能会导致意想不到的崩溃,需要每个页面都进行排查,工作量较大。
- 直接跟随系统切换模式,只需要配置好
manifest配置android:configChanges=“uiMode”,通过监听onConfigurationChanged(),直接通过代码逻辑更改显示样式
优点:
- 页面保持当前状态,activity不会重建,用户体验较好。
缺点:
- 不会跟随系统切换显示样式,需要在
onConfigurationChanged
回调中通过一系列代码逻辑更改显示样式,工作量较大。
manifest配置android:configChanges=“uiMode”,通过onConfigurationChanged()监听在深色模式改变时给予提示,类似弹窗告知用户模式改变,需要重启才能生效,用户点击同意了再重启app
优点:
- 具有跟随系统切换模式的全部优点;
- 能避免activity重建带来的不可预料的崩溃以及减少重新渲染页面深/浅色的代码量。
缺点:
- 每次深色模式切换时,都需要用户重启应用才能生效。
Android 10 深色模式适配相关推荐
- Android 10深色主题适配踩坑记录
1. 问题简述 Android 10 推出了深色主题,便于用户根据白天和夜晚自由切换合适的主题.在适配的过程中,要特别注意,切换主题会导致当前activity被重建,也就是会重新走一遍Activity ...
- android自动切换暗色,超实用!Android 深色模式适配(可定时开启的APP内主题切换管理工具)...
前言 前面分享了一篇"黑白化主题"的文,主要适用场景是不久就要到来的"清明"等时节或者是其他的国家公祭日什么的(一名成熟的程序员,要学会自己提产品需求). 今天 ...
- Android深色模式适配原理分析,android应用开发
return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI ...
- Android Q暗色模式适配踩坑—状态栏
暗色模式已经不是什么新鲜玩意了,大家最近看到关于暗色模式最多的内容可能就是iOS版本微信未适配暗色模式面临被AppStore下架的风险.然后今天早上一醒来,发现Android的微信也黑了(因为我手机一 ...
- AndroidQ(10)黑暗模式适配
前言:作为一个Android程序员,每年最期待就是Google的发布会啦!!这不,今年的AndroidQ如期而至.这里简单介绍一下Android的新特性: AndroidQ全局暗黑模式 隐私权限的更新 ...
- H5页面系统深色模式适配
1. 在css中设置CSS 媒体查询 PC web模式适配,在css中通过media查询进行适配即可生效. /*日间模式*/ @media (prefers-color-scheme: light) ...
- Web前端深色模式适配方案
众所周知,GitHub 网站在前不久支持了深色模式.我在看 GitHub 的时候发现浏览器默认的滚动条也变成了深色样式: 我当时猜测应该有一个属性可以声明配色方案,然后浏览器根据声明的配色方案采用相应 ...
- Android切换深色模式导致布局字体变小的解决方案
切换深色模式导致布局字体变小问题困扰了我很久,一直排查自身代码问题却没发现并非自身代码导致,而是使用了今日头条屏幕适配方案AndroidAutoSize导致的,目前暂时在小米手机安卓11系统发现,切换 ...
- android 分屏模式适配,安卓适配分屏注意事项
分屏模式下可强制应用横屏,所以注意应用内强制竖屏的页面适配横屏显示 分屏模式下,获取应用所占用的宽高 getResources().getConfiguration().screenWidthDp g ...
- Android 10 录屏适配
Android 8.0以后android得权限有所更改,但是影响录屏得得权限目前只影响Android Q版本,具体原理请看下面这篇博客,是他人所写,很是详细. https://blog.csdn.ne ...
最新文章
- php的封装继承多态,PHP面向对象深入理解之二(封装、继承、多态、克隆)
- android 自定义频谱,android – 如何从实时音频开发频谱分析仪?
- 本人使用abapgit遇到的一些错误
- 【转】系统缓存全解析二:动态缓存(4)-第三方分布式缓存解决方案 Memcache(2)...
- kotlin函数式编程_我最喜欢的Kotlin函数式编程示例
- git 修改默认分支为main_Git:基本操作
- mysql 安全问题_浅谈MySQL数据库的Web安全问题
- 在fedora20下面手动为自己的安装程序创建桌面图标
- 搜狗开源srpc:自研高性能通用RPC框架
- 手机wifi java_Android中使用WIFI来连接ADB
- Profinet Commander下载方法
- 小心!第三方支付最常遇到的 6 大骗局!
- 人工智能成热门 苹果谷歌等科技公司竞相涌入改变神经科学研究
- 知识 | 四种渲染到底是啥?终于有人讲明白了(下)
- uni-app 杂记
- 【DuerOS开发日记】2.打造属于自己的小度(1):使用PythonSDK
- Windows 10 IDM 下载play.kth.se上面的网课视频
- 综述 | 最新双曲深度神经网络综述论文
- matplotlib基础绘图命令之pie
- 论文投稿指南——中文核心期刊推荐(原子能技术)