一、概述
Apple 是从 iOS 13 正式发布了对暗黑模式的支持。
参考文档:
iOS:https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/dark-mode/
苹果在 iOS13 以前已经对自己的部分应用加入了深色模式的支持,比如 iBooks。iBooks 切换到深夜模式后,仍然保持美观的用户界面,对于长时间盯着屏幕的用户而言降低了眼睛的疲劳度。
适配暗黑模式要面临的问题,是如何在深色和浅色模式下界面同时保持应用的视觉效果。涉及各组件的颜色适配,图标的适配,整体的观感,还需要考虑开发的工作量,适配暗黑的方式,以及对现有业务的影响等。
适配暗黑模式和应用换肤的大有区别:对于 App 的部分界面本身具备换肤的能力,切换到一个深色的皮肤并不代表适配了暗黑模式。从用户的角度来看,如果换肤的时机与系统切换暗黑模式是一致的,两者看不出明显的区别。但是对于复杂的像优酷这样大型 App,很难对所有的界面,包括弹窗,提示,H5 页面,提供换肤的能力。从这点看,暗黑模式 SDK 必须是比换肤更简便,更轻量,覆盖更全面的方案。
在优酷 iOS 的暗黑模式开发过程中,我们自行开发了一个“暗黑 SDK”。我们希望达到的目的是,业务代码只需要最少的改动就能适配暗黑模式。
1、前期实践
在开发暗黑 SDK 之前,我们对优酷 APP 中常用的组件和基本控件构建了一套标准化组件库,对于间距,字号,颜色等基础属性,从设计维度提炼出一整套通过 DesignToken 来访问的属性库。标准组件库接入暗黑 SDK 之后,使用了标准组件库的业务代码无需修改就可以直接适配暗黑模式。对于其他使用非标准组件的业务,暗黑 SDK 提供了最简化的接入方案。
2、暗黑 SDK 在 App 中的位置
App 动态颜色的维护和切换由暗黑 SDK 来完成。下图是暗黑 SDK 在 App 层次结构中的位置。
1)暗黑 SDK 的初始化。暗黑 SDK 是在 App 启动时初始化的,主要完成以下工作 :
暗黑模式开关的设置
对各类 View 的相关方法进行注册
装载预置的主题集,比如深色和浅色两种主题
自定义动态颜色的初始化。
2)暗黑 SDK 的工作原理。暗黑 SDK 作为监听系统暗黑模式变更,并通知界面切换颜色以及图片的统一入口,暗黑 SDK 提供所有动态颜色/动态图片的 Token 接口,供业务方使用。暗黑 SDK 汇总了全部的动态颜色色值,集中维护,方便将来新增主题。为以后后台管理,下发主题提供了可能性。
当系统的暗黑模式切换时,暗黑 SDK 监听到模式变更,开始遍历 App 所有窗口,以及窗口下的各类注册过的 View, 然后遍历 View 的注册过的属性。
如果属性的值是动态颜色或者动态图片,则会根据当前的暗黑模式取对应的颜色或图片,然后重新赋值。业务代码不用主动刷新页面, 也不用监听当前是不是暗黑模式,不涉及服务端, 不需要关心当前 App 以哪一种模式运行。
3、暗黑 SDK 更新界面的工作流程:
4、暗黑 SDK 在视图链中搜寻标记了动态属性的节点:
5、暗黑 SDK 的开关以及支持的类型:
6、和苹果官方的暗黑切换的调用过程的对比
通过苹果官方提供的接口分析,当系统的暗黑模式变化时,任何层级的 UIView 的 traitCollectionDidChange 方法都可以被调用到,说明苹果暗黑内部的实现方法必然是一种遍历所有 UIWindow,UIVIewController 及 UIView 的机制,或者类似遍历的机制。
暗黑 SDK 同样使用了遍历所有 UIWindow 的方式,寻找设置了动态颜色或动态图片的节点,来达到在 Light 和 Dark 模式之间切换的效果。
为什么采用遍历的方式?
适配暗黑模式,是不是就是用不同的颜色和图片刷新下界面就可以了?答案是否定的。
刷新界面不能解决适配暗黑的所有问题,还会带来负面影响。
因为适配暗黑模式的工作,主要是对老的业务代码进行修改。如果沿用现有代码逻辑中的刷新逻辑的话,假如刷新中包含数据请求,会导致同时触发了不必要的代码和逻辑。如果刷新导致用户当前的操作现场丢失,是不太好的用户体验。
另外,不是所有代码都存在刷新逻辑,比如一个普通的弹窗,创建的时候数据是固定的,显示后不存在刷新的必要。对于这样的控件,如果为了接入暗黑模式需要新增一个专门的刷新函数的话,对于优酷这样的大型 App 其工作量不可想象。如果存在大量这样的改动,也会带来大量测试的回归工作量。
考虑到这些特殊情况,为了达到暗黑切换的效果且不影响到业务逻辑,最直接和高效的办法是直接修改界面元素的相关属性,比如直接修改 UILabel 的 textColor。遍历整个视图层次链,看似费时费力,实则最大程度地减轻了业务方的工作量,覆盖面相对完整。
二、动态颜色的支持
从最简单的案例开始:
1、上图中的暗黑模式切换涉及到以下两种情况:
1)这个蓝色的按钮在深色和浅色下没有任何变化,文字颜色是白色,背景图保持不变
2)容器的背景色,浅色模式下是白色,深色模式下是黑色,其他按钮,比如,
在浅色模式下文字是黑色,背景是白色,在深色模式下相反。
2、解决方案:
第一种情况:在深色和浅色没有任何变化的代码,保持不变。
第二种情况:实现暗黑模式的切换,需要做如下改动:
涉及到图片的案例
1、上图中的暗黑切换涉及到以下两种情况:
1)按钮的颜色变化,上个示例已经讲过
2)弹窗的背景图的变化,浅色模式下是一张浅红色的图片,深色模式下是一种透明的图片。
解决方案:
实现暗黑模式的切换,需要做如下改动:
从上面的两个例子可以看出,适配暗黑只需要将现有代码中的颜色和图片替换成带 Token 的颜色和图片即可。可以看出 Token 是暗黑切换的关键。
2、Token 的设计
这个表格定义了几种常见的动态颜色的 token。
比如 primaryBackground 是一个背景色的 token,对应两种颜色,在浅色模式下是 #FFFFFF 白色,在深色模式下是 #16161A 浅白色。
带 token 的动态颜色可以适应暗黑模式的变化。
3、全局暗黑模式开关
考虑到暗黑模式的支持的早期阶段,可能存在解决方案支持不完整,存在一些 bug 的情况,我们添加了全局暗黑模式开关。
这个开关支持
在特定的机型和版本上可以关闭暗黑开关
在测试用例中可以选择开启或关闭暗黑开关
3、暗黑 SDK 和组件标准化的关系
暗黑 SDK 处于标准组件库的下一级,为标准组件库提供暗黑方案的支持,同时也为其他自定义组件提供支持。
四、为什么不使用苹果官方的暗黑方案?
为什么我们要独立开发一个“暗黑 SDK”,而不是直接使用苹果的官方方案呢?
苹果官方暗黑方案的几处不太便利的点
1、CGColor 的暗黑适配
iOS 系统原生的方案:
必须监听广播,取得当前暗黑模式下的 UIColor 的值,然后根据 UIColor 提取 CGColor,代码如下:
暗黑 SDK 的方案:
无需监听,直接赋值,CGColor 也是动态颜色,可以适应暗黑变化,代码如下:
2、支持自定义属性
在 iOS 原生的解决方案中:
此动态颜色无法适应暗黑变化
如果需要完成暗黑的适配,必须监听暗黑模式变化广播,具体代码如案例 1,增加代码的复杂度和可读性。
在暗黑 SDK 中
此动态颜色可以适应暗黑变化,代码简洁。
3、支持自定义类:
在 iOS 原生的解决方案中:
在 iOS 13 中,UIView、UIViewController,UIWindow 及其子类支持暗黑模式。其他类,比如 NSObject 则不支持暗黑。如果需要完成暗黑的适配,需要在与 MyObject 有关联的 UIView、UIViewController,UIWindow 中监听暗黑模式的变化广播,破坏了代码的模块化设计。
具体代码如案例 1,在回调函数中操作 MyObject,增加了代码的复杂度。
在暗黑 SDK 中:
此动态颜色可以适应暗黑变化,代码简洁。
4、iOS 系统的动态颜色只在 iOS13 以上被支持,iOS13 以下无法直接使用:
在 iOS 原生的解决方案中:
需要针对当前机器的 OS 版本区别对待,或者额外提供另一个函数,封装所有的动态颜色。
在暗黑 SDK 中
此动态颜色可以直接书写,代码简洁。
5、支持 iOS13 以下系统
在 iOS 原生的解决方案中:
不支持 iOS13 以下系统。
在暗黑 SDK 中
支持 iOS13 以下系统。
苹果官方暗黑方案的几处不太便利的点一共五点。
特别是前三点,并不是功能的缺失,只涉及到了代码改动的复杂度,需要判断 OS 版本。
而暗黑 SDK 将代码的改动简化到一行代码,这对于大规模的业务快速接入的优势是显而易见的。
另外,在业务代码加入大量的监听代码,并在监听中判断当前模式是否 UIUserInterfaceStyleLight,可维护性比较差。
五、优酷 APP 在深色模式下的效果图
六、总结
设计标准化的逻辑为暗黑模式的快速接入奠定了基础,暗黑模式的快速实现和上线更凸显了设计标准化的重大意义。
将更多的界面元素统一到标准组件库中,实现组件集中化开发,组件的使用就能够实现跨业务,跨应用,将极大提高业务开发效率和视觉换新的成本。
作者简介:
大甘,阿里文娱无线开发专家。
相关阅读:
评论