优酷暗黑模式(十):消费场景落地(iOS)

一、概述

iOS 13 中苹果引入了暗黑模式,提供了全新的配色方案,非常适合暗光环境下使用,对眼睛的伤害更小。为了配合优酷 App 全站暗黑模式的适配,我们需要在优酷主客消费场景将暗黑模式全面落地。

值得庆幸的是,在这之前优酷主客消费场景已经完成了统一架构迁移和性能优化,并在这个过程中完成了大部分插件的治理,遵循统一架构、组件标准化、DesignToken 设计,使得消费场景可以快速高效地支持暗黑模式。

二、业务介绍

优酷主客消费场景即优酷 App 的播放页。播放页作为视频内容消费的落地页,主要提供视频播放、视频内容介绍、互动、视频推荐、视频花絮、视频周边等内容推荐,业务场景及页面内容都比较复杂。根据场景分为:剧集、电影、综艺、少儿、体育、新知等;其内容有:组件、半屏、Tab 等。

组件:即视频相关内容承载控件,包括简介,选集,周边视频,花絮视频,推荐视频等,通过这些内容,让用户了解更多的视频相关的信息。

半屏:包括 Native、Weex、H5 的半屏,通过半屏用户可以看到更多的视频相关内容,也可以承载视频互动。因为组件展示的内容还是有限,通过半屏可以更好更全地展示。

Tab:通过 Tab 让用户在不同的内容之间切换。

播放页架构图:

1)页面、组件、坑位结构图:

2)业务效果图:

三、适配策略

1、页面适配

页面展现主要分为几种状态:加载态 (Loading),展现态,异常态。主要工作可以分为加载态 LoadingView 适配,展现态背景色适配,异常态 ErrorView 适配。

按道理来说,背景色适配工作量会非常大,因为需要一层一层地去设置子 View 的背景色。但是由于优酷的视图中,大部分背景色是透明的,使得在进行暗黑模式适配的过程中非常便利,不用去一层一层的设置背景色,只需要适配容器 View 就可以。

复制代码

[self.containerView setBackgroundColor:UIColor.ykn_primaryBackground]; // 设置背景色,ykn_primaryBackground 为我们暗黑模式框架对外暴露的属性通过它可以取到对应的颜色
[self.view setBackgroundColor:UIColor.ykn_primaryBackground]; // 设置背景色

2、组件适配、坑位适配

因为播放页都已经拆分为组件和坑位,所以完成页面部分的适配之后,主要就是对组件和坑位的适配。由于播放页组件完成度很高,所以在进行组件适配的时候也是比较顺利的。举个例子,播放页很多的组件都有 Header View,即左面会有文字,右面有一个箭头的 image。

当替换完 Header View 组件的图片,并进行完文字的暗黑模式适配后,所有这些 UI 布局全部完成了适配工作,从这里可以看出组件标准化的完成程度对后期业务的维护有着深远的影响。

复制代码

_titleLabel.textColor = UIColor.ykn_primaryInfo;   // 设置组件头部标题颜色
_subtitleLabel.textColor = UIColor.ykn_secondaryInfo;  // 设置组件头部副标题颜色
_arrowImageView.image = UIImage.ykn_detail_comp_header_more;   // 设置组件头部箭头图片

3、半屏适配

半屏页面主要是分为 Native 半屏页面、H5 半屏页面、Weex 半屏页面。

由于所有的半屏页面都是由播放页半屏容器管理器来管理,所以只需要在里面统一适配即可;需要注意的是我们适配的只是 H5、Weex 的容器,容器里面的具体页面的适配详见: 暗黑模式的技术支撑 (Weex&H5)。

举例来说,H5 半屏容器页面适配:

复制代码

webViewController.view.backgroundColor = UIColor.ykn_primaryBackground;  // 设置容器背景色
webViewController.topBar.backgroundColor = UIColor.ykn_primaryBackground; // 设置顶部导航背景色
webViewController.titleLabel.textColor = UIColor.ykn_primaryInfo;  // 设置顶部导航字体颜色
[webViewController.closeBtn setImage:UIImage.ykn_detail_box_close forState:UIControlStateNormal];// 设置顶部导航关闭按钮图片

4、中间件处理,与沉浸式隔离

在适配暗黑模式之前,优酷播放页已经完成了沉浸式模式的开发。沉浸式使得用户可以快速地融入到内容本身中去,减少不必要的信息造成的视觉干扰,使用更有层次感的内容来引导用户聚焦,自然地实现播放器视觉 C 位呈现,其呈现效果与暗黑模式也有较大不同。

所以,在适配的过程中需要做一层颜色及资源管理层(架构图中的颜色资源管理层),来区分沉浸式氛围及暗黑模式。通过不同的开关控制背景处理及加载不同的资源、颜色。后期沉浸式模式将会接入优酷 App 的全局统一氛围配置,做到自动下发氛围相关样式,减少适配工作。

在进行与沉浸式隔离开发的时候我们遇到一个难点,在没有进行暗黑模式适配时候,沉浸式是通过函数

-(NSString *)immersionImageName:(NSString *)imgName

进行适配,该函数实现逻辑如下:

复制代码

-(NSString *)immersionImageName:(NSString *)imgName
{
   if (self.isImmersionMode) {
       NSString *name = [self.imgNameDic valueForKey:imgName] ? : imgName;
       return name;
   } else {
       return imgName;
   }
}

该函数首先会先判断是否为沉浸式模式,如果为沉浸式返回对应沉浸式的图片名称 ; 当找不到对应图片时,使用一个兜底图片。如果沉浸式没有生效则直接返回正常图片名称。

当适配暗黑模式时候遇到了难点,优酷设计标准化 SDK 是通过属性调用返回 UIImage 对象,所以需要改造该函数。

先贴上改造后结果:

复制代码

-(UIImage *)immersionImage:(NSString *)imgName
{
   if (self.isImmersionMode) {
       if ([self.imgNameDic stringForKey:imgName]) {
           return [UIImage imageNamed:[self.imgNameDic stringForKey:imgName]];
       }
   }
   SEL sel = NSSelectorFromString(imgName);
   if ([UIImage respondsToSelector:sel]) {
       return [UIImage performSelector:sel];
   } else {
       return nil;
   }
}

首先将函数返回结果改为 UIImage 对象。这么改造有两点好处:

第一: 在业务方调用时,可以直接使用该函数返回的结果,不需要再调用函数

(nullable UIImage *)imageNamed:(NSString *)name

便于业务方使用。

第二: 如果当前为暗黑模式,可以直接返回该属性值,不需要进行转换等操作,提高运行效率、降低出错概率。

最后: 因为传进来的是图片名称,并且属性名称是与图片名称一致的,所以可以通过其属性 get 方法拿到对应图片。

总结一下该函数的逻辑:如果为沉浸式模式,返回对应的沉浸式 UIImage 对象 ,因为沉浸式业务优先级大于暗黑模式。如果不是沉浸式,通过 NSSelectorFromString 生成该属性 get 方法对应的 selector,判断是否存在该方法。如果存在该方法说明存在对应图片,调用该属性 get 方法并返回对应 UIImage 对象。

5、网络图片的适配

暗黑 SDK 是直接支持本地图片适配的,但是网络图片适配需要业务方来做。在播放页,有些图片是支持后台配置的。即当后台配置了图片下发,优先展示后台配置的图片,如果后台没有配置图片则展示本地图片。

例如下图中的热评、收藏、缓存、分享对应图片都是支持后台配置的。

那么问题来了,这种情况下如何进行暗黑模式的适配?

解决方案为提前准备好从网络下载的图片,对应的图片有两套即正常模式和暗黑模式。使用时候判断对应模式来使用对应图片。

示例代码如下:

复制代码

UIImage *imageFromWeb1 ;// 从网络下载成功的 UIImage
UIImage *imageFromWeb2 ;// 从网络下载成功的 UIImage
UIImage *image = [UIImage ykn_imageWithThemeProvider:^UIImage * _Nonnull(__kindof YKNThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) {
             // 回调中不要做耗时操作, 如是网络图, 提前准备好图片
             if ([identifier isEqualToString:YKNThemeIdentifierDark]) {
                 return imageFromWeb1 ;  //dark 模式下的图
             }
             return imageFromWeb2; //light 模式下的图
         }];

四、适配效果

五、调试小技巧

在进行暗黑模式适配时候遇到这样一个问题,即每次改过 UI 我们都需要重新运行整个 App 才会生效。毫无疑问,这是十分浪费时间的,有没有什么方案可以做到不重新运行 App 即可生效的呢?答案是有的。

在模拟器上我们可以通过 InjectionIII 来进行热更新,具体方案不再赘述,网络上有很多介绍文档。这里只介绍真机实现方案。

实现方案:首先添加一个断点,接下来编辑断点并添加一个 Debugger Command 并输入表达式,最后选择 Automatically continue after evaluating actions。为什么要勾选这个呢?如果勾选上运行到该断点的时候不会停住,会自动执行表达式,并自动执行下一行代码。

六、项目总结

业务架构层面核心功能组件化的好处是巨大的,这使得代码按照功能模块高度聚合,只需要针对当前功能组件进行修改即可全局生效;UI 层级的扁平化及层级管理也是十分重要的。

通过暗黑模式的适配,大大的提升了优酷播放页的使用体验。这也是我们对 iOS 13 新特性的一个探索,并且在较短时间内拿出来一个成熟的暗黑模式适配方案,在优酷 iOS 端进行了尝试和使用,最终呈现的效果也是非常好的,通过这次的改造和实践也为我们以后对其它新技术的探索打了坚实的基础。

作者简介:

子荀、金籽,阿里文娱无线高级开发工程师。