End

深色模式适配

本文地址


目录

深色模式适配

深色模式简介

深色模式是 Google 在 Android 10(Android QAPI 29) 版本中提供的核心功能。

实际上,在之前的 Android 9(Android PAPI 28) 系统中,就已经在设置项中提供了一个「深色主题」,不过这个主题并非全局,而是仅适用于下拉通知栏的开关面板和文件夹等少部分界面。

而从 Android Q 开始,Google 开始提供系统级的黑暗模式,大部分预装应用、抽屉、设置菜单和 Google Feed 资讯流等界面和按钮,都会变成以黑色为主色调。

因此,我们只需要适配 Android 9(含) 及以后的系统版本即可。

几种适配方案

常规方案一:不重启模式 + 手动适配

给需要适配的 Activity 添加android:configChanges="uiMode"

  • 切换深色模式不会导致 Activity 重启,而是会回调onConfigurationChanged()方法
  • 系统只是通知你显示模式改变了,所有的适配工作都需要自行处理
  • 如果不做任何处理,就等价于不适配深色模式(显示模式切换后 Activity 不会有任何改变)
  • 不建议采用此方案
    • 优点:不会导致 Activity 重启,甚至可以自定义过渡动效,体验较好
    • 缺点:
      • 适配工作量较大,所有 UI 的状态都需要手动一个一个的刷新
      • 需要处理各种奇奇怪怪的缓存、渲染、布局、事件分发等问题
      • 也不是 Google 建议采用的方案

常规方案二:重启模式 + 自动适配【建议】

不做任何设置时的默认效果(即没有添加android:configChanges="uiMode")

  • 切换深色模式会导致 Activity 重启
    • 仅当前显示的页面(栈顶 Activity)会重启,会重走 onCreate 方法
    • 未显示的 Activity 会在推到栈顶时重启(而不是立即重启)
    • Activity 重启时可能会保留一些状态信息(例如横竖屏状态)
    • 进程不会重启
  • 切换深色模式后会自动使用values-nightdrawable-night等深色模式专用目录下的资源
  • 建议采用此方案
    • 优点:不会导致状态异常,不会出现各种奇奇怪怪的缓存、渲染、布局、事件分发等问题
    • 缺点:当前页面会回到启动时的初始状态,导致信息丢失,体验不好
      • 浏览的位置会重置:当前屏幕内可见的应用会被划走
      • 接口可能会重新拉取:接口返回的内容也可能会改变
      • 输入框中的内容会清空、弹窗会消失、会占用引导提示次数等

其他方案三:使用 Force Dark 功能一键适配

Android 10 中也提供了 Force Dark 功能,可让开发者快速实现深色主题背景。

  • 使用条件:
    • 安卓10.0及以上版本
    • 应用的主题中包含<item name="android:forceDarkAllowed">true</item>属性
      • 或者在onCreate中、setContentView之前,调用getWindow().getDecorView().setForceDarkAllowed(true);
      • 此属性只有在 API 29 及以上才有,只能放在values-v29目录内
      • 要求compileSdkVersion必须在29及以上,否则编译失败
    • 父主题中需要有<item name="android:isLightTheme">true</item>属性
      • 一般只有带.Light的浅色主题中才有此属性,比如Theme.LightTheme.AppCompat.LightTheme.Material.Light
      • 对于继承自.DayNight的主题,系统不会应用 Force Dark,因为这个主题本身有自己的适配方案(详见方案四)
      • 对于深色主题(例如DarkTheme.Material),系统也不会应用 Force Dark
  • 原理:系统会分析浅色主题应用下的每一层 View,并且在这些 View 绘制到屏幕之前,自动将它们的颜色转换成更加适合深色主题的颜色
  • 不建议采用此方案
    • 优点:代码量少,自动替换
    • 缺点:
      • 不精致:是一种简单粗暴的转换方式,转换效果通常是不尽如人意的
      • 存在很多bug:网上有很多反馈颜色反色异常的bug
      • 有局限性:不兼容API-29之前的系统,只能使用浅色主题,WebView 支持不完善

也可以混合使用 Force Dark 和前两种方案,也即默认采用 Force Dark,在需要精致适配的场景使用方案一或方案二对效果进行微调。

其他方案四:继承 DayNight 主题一键适配

这种方案和 Force Dark 功能类似,也是在切换到深色模式时让系统自动帮你适配文字颜色、背景色,个人感觉整体效果还不如 Force Dark

  • 使用条件:继承自Theme.AppCompat.DayNightTheme.MaterialComponents.DayNight
  • 继承后,如果当前开启了深色主题,系统会自动帮你适配很多文字颜色、背景色内容,当然你可以通过指定night-qualified中的资源来人工干涉

其实这不能算是一种方案,因为这和前几个方案没啥特殊的地方,可以算是一个杂糅体。

其他方案五:很多阅读类用应用的自研方案

  • 比如很多阅读类用应用都有一个换肤的功能
    • 原理:简单来讲,就是在换肤(切换主题)的时候,把原来的 Context 和 Resource 换掉,切换到对应主题的资源,每一个主题下都有同名的资源文件,在主题中没有找到对应 id 的资源文件,就取默认的
    • 由于需要适配图片主题等等很多功能,所以实现这个功能要复杂一些
  • 原本的换肤功能和深色模式是没有关系的
    • 先有换肤后有深色模式
    • 后来这些阅读类用应用为了适配深色模式,所以就在监听到深色模式切换时切换到深色皮肤,让你误以为是按照 Google 规范适配了深色模式,实际上并不是
    • 此方案由于是完全自定义的,所以可以实现很多系统深色模式无法实现的功能,在和产品沟通时要明确:别人能实现的,我们不一定能实现,因为方案完全不一样
  • 此方案改动太大,不适合普通应用,不建议采用

其他方案N:开源的 MultipleTheme 等方案

MultipleTheme:真正的支持无缝换肤

夜间模式的 Android 框架,配合 theme 和换肤控件框架可以做到无缝切换换肤(无需重启应用和当前页面)。

该应用框架可以实现无缝换肤/切换夜间模式的需求,需要在换肤/切换夜间模式的界面只需要使用框架里的自封装控件,其他界面的控件使用原生android控件即可。

核心功能

判断当前是否处于深色模式

public static boolean isNightMode(@NonNull Context context) {
    int uiMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
    return uiMode == Configuration.UI_MODE_NIGHT_YES;
}

非 Activity 也可以通过如下方式判断状态
同一个进程内的不同 Context 获取的值可能是不一样的

手动切换深色模式

public static void switchNightMode(@NonNull Context context) {
    //进行正常模式、暗黑模式切换
    int uiMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
    int nightMode = uiMode == Configuration.UI_MODE_NIGHT_NO ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO;
    AppCompatDelegate.setDefaultNightMode(nightMode); //AppCompatActivity 中也可使用 getDelegate().setLocalNightMode(nightMode);
}

注意:Activity 必须继承自 AppCompatActivity 才能正常切换

  • 如果 Activity 继承自 AppCompatActivity
    • 【核心】必须给其设置一个Theme.AppCompat(或其后代)的主题
      • 例如:android:theme="@style/Theme.AppCompat"Theme.AppCompat.Light"
      • 否则,运行时崩溃:IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity
    • 手动切换模式后
      • 仍会导致重启模式下 Activity 重启,不重启模式下触发 onConfigurationChanged
      • 当前应用所有 Activity 均会受影响,但不影响其他应用,也不会改变系统设置中模式开关的状态
      • 手动切换模式后,当前应用仍会正常响应系统设置中的模式切换,但也仅仅是事件响应而已
        • 仍会导致不重启模式下触发 onConfigurationChanged,当然也可以手动更新 UI
        • 【核心】仍会导致重启模式下 Activity 重启,但重启后仍然保持原先手动设置的模式,而不会匹配系统设置中的模式
        • Force Dark 模式、.DayNight等主题也不会在匹配系统设置中的模式
        • 总之一句话:手动设置的优先级 > 系统设置的优先级
  • 如果 Activity 不继承自 AppCompatActivity
    • 使用当前 Context 调用setDefaultNightMode方法,对自身及对其他 Activity 都无任何效果
    • 在其他 Activity 中调用setDefaultNightMode方法切换模式后,对自身也无任何效果

命令行切换深色模式

adb shell cmd uimode night yes(开)
adb shell cmd uimode night no(关)
adb shell cmd uimode night custom(定时)
adb shell cmd uimode night auto(日出日落)

监听深色模式状态改变

onConfigurationChanged

Activity 可以通过android:configChanges="uiMode"监听uiMode改变,在onConfigurationChanged()中处理状态改变

此种监听方式会导致 Activity 不会重启,需要自行确保(手动处理)切换深色模式后页面反色是否正常
所有 View、Fragment 也都可以监听onConfigurationChanged

通过监听深色模式字段改变

Uri uri = Settings.System.getUriFor("xxxx"); //不同 ROM 下的 Uri 是不一样的
getContentResolver().registerContentObserver(uri, true, new ContentObserver(new Handler()) {

    @Override
    public boolean deliverSelfNotifications() {
        return super.deliverSelfNotifications();
    }

    @Override
    public void onChange(boolean selfChange) {
        super.onChange(selfChange);
    }

    @Override
    public void onChange(boolean selfChange, Uri uri) {
        super.onChange(selfChange, uri);
    }
});

资源适配

可以将深色模式资源放置在values-nightdrawable-nightdrawable-night-xxx中,当深色模式切换时,会使用对应深色模式的资源。

assets 目录中的资源不能采用这种方式

给图片指定透明度

可以通过指定透明度 alpha 来实现对深色模式的适配

android:alpha="@dimen/pic_alpha_dimen"
  • 透明度 alpha 不仅可以用在图片上,也可以用在任意 View 上
  • 在父 View 上加 alpha 和在子 View 上加 alpha 的效果没有什么区别
  • 父布局和子 View 上都加 alpha 时,效果会叠加

在代码中可以通过以下三种方案设置透明度:

<resources>
    <integer name="pic_alpha_integer">204</integer>
    <item name="pic_alpha_fraction" type="fraction">80%</item>
    <item name="pic_alpha_dimen" format="float" type="dimen">0.8</item>
</resources>
//float:范围为 0.0- 1.0
float alpha = getResources().getFraction(R.fraction.pic_alpha_fraction, 1, 1);
imageView.setAlpha(alpha);

//int:范围为 0- 255
float alpha = getResources().getFraction(R.fraction.pic_alpha_fraction, 255, 1);
imageView.setAlpha((int) alpha); //必须强转为 int,否则调用的是另一个重载方法
imageView.setAlpha(getResources().getInteger(R.integer.pic_alpha_integer));

给图片加一个蒙层

public static void setImageViewColorFilter(@NonNull ImageView imageView) {
    int color = imageView.getContext().getResources().getColor(R.color.night_color_tint);
    imageView.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_OVER));
}

颜色 night_color_tint 在浅色模式下是全透明颜色,在深色模式下是你需要的蒙层颜色

给 Drawable 设置透明度

public static void setTextViewDrawableAlpha(@NonNull TextView textView) {
    Drawable[] drawables = textView.getCompoundDrawables();
    for (Drawable drawable : drawables) {
        if (drawable != null) {
            drawable.setAlpha(0.8*255); //给 Drawable 设置透明度
        }
    }
}

Vector 图片适配

可以通过指定画笔颜色来实现对深色模式的适配

android:tint="@color/clear_red"

Lottie 动画适配

对于 Lottie 动画,我们可以使用其Dynamic Properties特性来针对深色模式进行颜色变化。

  • 调用LottieAnimationView.resolveKeyPath()方法获取动画的路径
  • 定义深色模式和明亮模式的色值,在深色模式切换时,Lottie 动画的颜色会随着深色模式切换而变化
  • 同样的对于播放动画,我们也可以设置描边颜色,来达到深色模式切换的效果

可能存在的问题

2021-03-07

posted @ 2021-03-07 19:10  白乾涛  阅读(2873)  评论(0编辑  收藏  举报