优酷暗黑模式(五):暗黑模式的技术实现策略

正如本系列第一篇文章所介绍的,Google 是从 Android 10 正式发布了对暗黑模式的支持,之前随公众理解的深色氛围一跃而上成为系统平台级能力。暗黑模式带给了我们什么呢?

  • 深色界面在专注环境下与内容有更高的契合度,更凸显内容、缓解视觉疲劳;
  • 深色界面更易营造品质感与沉浸感;
  • 深色界面更易建立填充感。

一、暗黑模式项目背景

暗黑模式作为一种系统平台能力,它打造的是整个用户使用周期的全链路视觉体验,也就是要覆盖到用户能到达的每一个角落。

这包括了分发场景,搜索场景,消费场景等复杂的页面结构,也包括二级落地页,活动页等独立页面 ; 还包括了弹窗,Toast,播放器等常用组件。要全面覆盖如此复杂细碎的场景,需要实现整体性的视觉呈现效果和低成本的全局平台开关。

分析完暗黑模式的影响范围,我们再来看一下我们实践的对象 – 优酷 APP。优酷 App 发展到今天,已经从一个单体 App,进化成了一个承载集团众多业务出口的超级 App。所以优酷 App 的页面是由十几个完整建制的内部和外部团队共同开发和维护的。

暗黑模式这种全站范围的适配将涉及到上百位设计 / 开发 / 测试同学,管理成本,沟通和协调成本,最终落地成本都非常高。

对于这种全站规格的需求,以往优酷的经验是会大量占用业务需求的开发人力,甚至会因此 delay 部分业务需求。这种开发模式是很难延续,不可接受的,本次暗黑模式的适配,我们既希望在第一时间将暗黑模式呈现给客户的,也希望能以更低的成本来完成项目需求。

这给项目方案提出了很大的挑战,但是得益于优酷之前已经实施了设计标准化体系,因此我们有信心和底气完成这些挑战。

二、暗黑模式在优酷的实践

首先,得益于优酷设计标准化体系的落地,我们已经提炼出公共资源库,构建了多层的 DesignToken 体系 ; 并对大部分一级页面的业务组件进行了接入。所以我们的工作就变成了两部分,一部分是已经接入设计标准化的视觉元素,这部分可以通过修改公共资源库中的 Token 定义,直接添加对暗黑模式的支持,零成本完成适配。另一部分则是扩大设计标准化体系的覆盖范围,在适配暗黑模式的同时,完成更多业务的技术架构的基础建设。

其次,我们还对于暗黑模式进行了色彩分层,静态色层是全站使用的基础色值,它直接对应着一个具体的值。它不会随着视觉模式的变化而变化。在它之上的是动态色层,动态色在不同的视觉模式下,对应不同的静态色。这是通过 Android 原生的资源加载机制完成: 即暗黑模式对应关系在暗黑资源文件中,普通模式在普通资源文件夹中。

在动态色层之上,是代码编写的色彩管理器,它在合适的时间会去获取当前的所有静态 / 动态色值。设计这一层有两个原因: 一个是提高性能,提前缓存一份给更上层调用,另一个是形成中间层。

众所周知,XML 资源文件的动态性是不足的。XML 资源启动即加载,加载后就是只读的。有了这一层,我们可以支持服务端动态下发色值 Token 的定义,以达成一定程度的动态性。

在色彩管理器之上,是公共的控件和组件层。有了这样的层次关系,使最终的业务设计可以通过搭建完成,完全不需要从零写起,也不需要关注设计标注的细节,开发再也不用逐个元素的调整,设计也不需要逐个像素的校对。只要在第一次纳入的时候进行一次就可以完成,大幅提高了工作效率。

静态 Token:

动态 Token:


动态色对照表

在实际的适配中,布局文件中需要注意的是要使用 Token 来设置组件属性,而在代码中则可以通过公共资源库中提供的工具方法 UIMode.isDarkMode() 来读取当前是否是暗黑模式,通过 ColorConfigureManager.getInstance().getColorMap().get(token 名) 来获得色值。

具体的适配工作可以看后面的相关文章。08 暗黑模式在优酷分发场景的落地、09 暗黑模式在优酷消费场景的落地 Android。

下面是暗黑模式适配的示例代码:

复制代码




    



// 获取下拉刷新 DesignToken 色值
int refresBgColor = ColorConfigureManager.getInstance().getColorMap().get(YKN_DEEP_BLUE_GRADIENT_MIDDLE_POINT);
// 设置下拉刷新
mYkClassicsHeader.setBgColor(refresBgColor);

// 获取顶部导航背景资源,对应 android 来说都是一个命名,暗黑模式的资源单独放在 night 目录下
int defaultImage = R.drawable.yk_top_bg;
// 设置顶部导航背景
setPlaceHoldForeground(getResources().getDrawable(defaultImage));

// 获取页面背景色
int backGroundColor=ColorConfigureManager.getInstance().getColorMap().get(YKN_PRIMARY_BACKGROUND);
// 设置页面背景色
setFragmentBackGroundColor();

此外,我们在适配暗黑的过程中,还遇到了很多具体的问题,比如

1、低版本 Android 系统如何支持暗黑模式

虽然 Google 是在 Android 10 的版本中才默认添加了暗黑模式的切换开关,但是,在之前的系统版本中已经预埋了对暗黑资源文件夹的加载能力 ; 而且有一部分厂商如小米就在 Android 9 的 MIUI 定制版本中提供了切换 ” 暗黑模式 ” 的开关。

所以对于低版本的用户,我们也提供了适配方案。具体来说,我们是通过调用系统 API

复制代码

AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO/MODE_NIGHT_YES);

来触发模式切换的。

需要特别注意的是,使用这个函数是有一些坑的。比如,在使用它之后,如果没有在 Activity 的 manifest 文件中增加

android:configChanges=“UIMODE”

的话,在 Activity 转屏也会引起 Activity 重建。

一般的 Activity 开发逻辑可能不会考虑到这种场景,很多逻辑在这里是会引发 Bug 的。

2、如何监视暗黑模式的切换

可以通过 onConfigurationChanged 进行监听。

这里一般有两种情况:

一种是活动需要监听它,来进行手动刷新,这时需要在 manifest 文件增加上面的配置。才能够监听到系统回调。

另一种是某个控件或组件,或者我们的全局状态,可能需要独立监听,这种情况按下面的代码进行监听。

复制代码

getApplication().registerComponentCallbacks(new ComponentCallbacks() {
        @Override
        public void onConfigurationChanged(Configuration newConfig) {
        //do something
        }
}

3、系统刷新和手动刷新

暗黑模式的切换必然需要重新渲染页面,这里我们分两种刷新方式:

一种是直接交给系统,系统会在切换是重建 Activity 从而引发页面重新渲染。这种适合 ” 用户使用行为不需要记录状态 ” 的 Native 页面,和 Weex/H5 等动态页面。缺点就是会丢失用户之前的视觉锚点。

另一种, 则是自己监听 onConfigurationChanged 事件然后手动进行有目的范围的 ” 局部刷新 “。比如优酷 App 中的播放页。请参考《极致酷黑: 优酷暗黑模式实现系列 – (9) 暗黑模式在优酷消费场景的落地 Android》

4、设计体系未覆盖的老旧页面如何适配

在众多的页面中,有一些老旧的页面改造成本过高,甚至已无人维护。

如果不作任何处理的话,这些页面会因为同时使用已改造组件和未改造组件而造成不同视觉模式同时出现。为了了避免这种情况的发生,我们会在页面进入时,强制指定页面的视觉模式方法是:

复制代码

getDelegate().setLocalNightMode(MODE_NIGHT_YES/MODE_NIGHT_NO);

这个 API 的作用范围是所属的 Activity。

三、未来的展望

我们认为,设计标准化体系大大的提高了类似 ” 暗黑模式 ” 这种全站视觉变更项目的效率,DesignToken 的设计将开发从繁琐的视觉效果开发中解脱了出来。

优酷的暗黑模式适配在两周之内顺利完成,并且没有影响同期的产品需求。未来我们会继续深化和丰富标准化设计体系的能力,也希望这种开发方式可以在不同的 App 间变成通用的开发范式。

设计标准化体系的开发,对于公共组件池的跨应用使用,Weex/Flutter/ 小程序等的跨应用通投都有重要的参考意义。

作者简介:

涵父,阿里文娱无线开发专家。