最近随着ios13对DarkMode的支持,DarkMode的热度一下子高涨了起来。很多App都对DarkMode进行了支持。我们自然也要立于潮头。
什么是DarkMode?先上一路我司的视频 player.youku.com/embed/XNDIx…
我的理解是黑暗模式绝对不仅仅是颜色翻转版本,而是对每个界面中使用的颜色的完全重写,一开始就从暗处开始设计和配色。这个模式可以使我们真正与之交互和操作的内容可以被凸显出来,给用户沉浸式的体验。另外,它还很酷炫,省眼又省电:)
既然它是一整套的设计理念,对应而来的,就是对现有App设计元素的全盘重构,这在开发侧来讲,实在是一个庞杂繁琐的工程。
对此,我进行了一些思考,也实践了一些方案,我把它们成为DarkMode适配的七种武器。这些武器之前并不是独立的,可以根据App现有的生态环境配合使用。
- 使用 setTheme 的方法让 Activity 重新设置主题; 定义两种主题,主题中定义控件的属性对应不同的色值。在编写布局文件时,引用定义好的属性。
<resources>
<attr name="mainBackground" format="color|reference"></attr>
</resources>
<style name="白色主题">
<item name="android:textColor">@color/白色</item> //系统控件属性
<item name="mainBackground">@color/黑色</item> //自定义属性
</style>
<style name="深色主题">
<item name="android:textColor">@color/黑色</item>
<item name="mainBackground">@color/白色</item>
</style>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/mainBackground">
<TextView
android:id="@+id/tv"
android:layout_below="@id/btn_theme"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="通过setTheme()的方法" />
</RelativeLayout>
这个方案还有一个变种,就是setTheme之后不recreate activity,而是发送一个广播。收到广播后,操作视图更新UI。
Resources.Theme theme = getTheme();
theme.resolveAttribute(R.attr.mainBackground, background, true);
mLayout.setBackgroundResource(background.resourceId);
- 设置 Android Support Library 中的 UiMode 来支持日间/夜间模式的切换; 使用 UiMode 的方法也很简单,我们需要把 colors.xml 定义为日间/夜间两种。之后根据不同的模式会去选择不同的 colors.xml 。在 Activity 调用 recreate() 之后,就实现了切换日/夜间模式的功能。 下面是 values/colors.xml :
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="textColor">#FF000000</color>
<color name="backgroundColor">#FFFFFF</color>
</resources>
除了 values/colors.xml 之外,我们还要创建一个 values-night/colors.xml 文件,用来设置夜间模式的颜色,其中 的 name 必须要和 values/colors.xml 中的相对应:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3b3b3b</color>
<color name="colorPrimaryDark">#383838</color>
<color name="colorAccent">#a72b55</color>
<color name="textColor">#FFFFFF</color>
<color name="backgroundColor">#3b3b3b</color>
</resources>
在 styles.xml 中去引用我们在 colors.xml 中定义好的颜色:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:textColor">@color/textColor</item>
<item name="mainBackground">@color/backgroundColor</item>
</style>
</resources>
在页面创建时先选择一个默认的 Mode,在需要的时候切换,
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES)
- Android通过代码进行多语言切换的套路,完成模式切换
这里有我的一个脑洞,是基于UiMode方案的变种,既然有value-night,也可以有其他的value-xxx,但是安卓原生只能识别预定义的后缀,这里我们可以借用与locale类型,新增value-aa,value-ab等资源文件夹,每一套对应一个模式。然后在代码中,用这种方式切换
@Override
protected void attachBaseContext(Context newBase) {
Locale locale = new Locale("aa"); // aa:normal,ab:dark,....
final Resources res = newBase.getResources();
final Configuration config = res.getConfiguration();
config.setLocale(locale); // getLocale() should return a Locale
final Context newContext = newBase.createConfigurationContext(config);
super.attachBaseContext(newContext);
}
- 通过资源 id 映射,回调自定义 ThemeChangeListener 接口来处理日间/夜间模式的切换。
这种方法的思路就是根据设置的主题去动态地获取资源 id 的映射,然后使用回调接口的方式让 UI 去设置相关的属性值。我们在这里先规定一下:夜间模式的资源在命名上都要加上后缀 “_night” ,比如日间模式的背景色命名为 color_background ,那么相对应的夜间模式的背景资源就要命名为 color_background_night 例如,我们可以提供一个工具类ThemeManager,这个工具类会维护资源id和主题的组合,并最终返回一个资源值。
//设置主题
ThemeManager.setThemeMode(ThemeManager.getThemeMode() == ThemeManager.ThemeMode.DAY ? ThemeManager.ThemeMode.NIGHT : ThemeManager.ThemeMode.DAY);
//获取资源的规范
textview.setTextColor(getResources().getColor(
ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.textColor)));
- 仿照AppCompat类,将原生控件替换成自定义控件,统一控制
当我们引入appcompat-v7,有了AppCompatActivity的时候,我们发现我们渲染的TextView/Button等组件分别变成了AppCompatTextView和AppCompatButton。同理,我们可以借助于LayoutInflaterCompat设置我们自己的LayoutInflaterFactory。然后,参考google的方式,将xml里的原生控件,转化成我们的自定义控件。
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
// by using the parent's context
if (inheritContext && parent != null) {
context = parent.getContext();
}
if (readAndroidTheme || readAppTheme) {
// We then apply the theme on the context, if specified
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}
if (wrapContext) {
context = TintContextWrapper.wrap(context);
}
View view = null;
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
//view = new AppCompatTextView(context, attrs);
view = new SkinnableTextView(context, attrs); //自定义view
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
......
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check it's android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
每次需要换肤的时候就调用setDayNightMode函数, 它会去通知View层级中所有实现了Skinnable接口的对象. 调用他们的applyDayNight方法, 来切换他们的样式.
我们在View生成的时候, 记录下了它引用的一些资源id, 然后因为切换了UiMode后, 获取相同资源 id 得到的实质资源不一样的特性来完成夜间模式切换的方案.
- 类似CSS的视图控制方案
服务端下发一份类似CSS的样式表,key由客户端定义,对应客户端某一类的视图的某一个属性,value则是一个样式值。客户端在渲染时,则按照预定的规则读取对于的样式值。这种方式的关键在于这个读取--设置的过程,是集中化的,还是每个视图都需要自己控制。它取决了协议的设计水平,也决定了开发的成本。这个我们在后面优酷实践的部分会详细介绍一下。
- 通过开关设置不同的参数 服务端下发当前希望的模式,组件开发者把这个模式当作是普通的参数一样,对不同的参数,进行不同的渲染。这把所有的工作量都放到了组件开发的部分。设计最简单。这个在优酷也有对应的场景,我们后面会介绍一下。
具体到优酷来说,目前已经进行了这样一些实践。
- 方案:下发模式开关 代表:播放页沉浸式 截图:
-
方案:CSS样式表下发 代表:首页高清频道 截图:
-
方案:UIMode 我个人实践 截图: