iOS端一次视频全屏需求的实现

对于一个带有视频播放功能的app产品来说,视频全屏是一个基本且重要的需求。虽然这个需求看起来很简单,但是在实现上,我们前后迭代了三套技术方案。这篇文章将介绍这三种实现方案中的利弊和坑点,以及实现过程中积累的经验。

需求要点:

  • 在屏幕旋转的动画中,需要保持播放器之外的界面布局(比如“First View”等几行字的布局不应该发生变化)

  • 全屏切换到小屏,小屏需要回到原先位置

image

对于这三种实现方案,我写了个demo分别示意。三个方案分别在demo的三个tab中。

原始方案:方案一

从小屏进入全屏时,将播放器所在的view放置到window上,用transform的方式做一个旋转动画,最终让view完全覆盖window。 从全屏回到小屏时,用transform的方式做旋转动画,最终让播放器所在的view回到原先的parentView上

image

这种方式在实现上相对简单,因为仅仅旋转了播放器所在的view,view controller和device的方向均始终为竖直(portrait)。但最大的问题就是全屏时status bar的方向依然是竖直的,虽然之前通过全屏时隐藏statusBar来掩盖了这个问题,但这同时导致了用户无法在视频全屏时看到时间、网络情况等,体验有待改善。

image

方案二设想

为了解决status bar不能转至横向的问题,我们决定替换视频全屏的实现方式。

业界比较流行的转屏方式应该是通过私有接口设置UIDevice的orientation属性。但直接设置这一属性的实现出来的转屏动画效果有些欠缺。比如旋转过程中会漏出黑色。

由于setStatusBarOrientation等方法已经被标记为depreciated了,使用它可能会带来风险,于是我们暂时也没有考虑这种方式

一个顺理成章的技术方案是:

在一个只支持Portrait的ViewController上,present一个只支持Landscape的ViewController,通过改写ViewController之间的转场动画,既能高度自定义全屏动画,也能让StatusBar在视频全屏时横向显示。

这个方案没有用任何私有接口或hack的方式,完全符合苹果的要求,理想中它应该会是一个稳定可靠的方案。

于是我们选用了present一个ViewController的方式作为方案二进行了下去。

核心设计为:

新增一个ViewController的子类,demo中为FullscreenViewController,重写这个类的supportedInterfaceOrientations方法,返回UIInterfaceOrientationMaskLandscape。

全屏时,present这个FullscreenViewController,系统会自动将statusBar转至Landscape方向。 同时自定义这个FullscreenViewController的转场动画,形成一个符合产品需求的动画效果。

方案二坑点&解决

在方案二的实现过程中,我们遇到了不少问题。

业务上的坑点

  • 兼容viewWillDisppear等生命周期方法

用默认方式present一个viewController,会导致presentingViewController的view被从视图层次中移除,同时presentingViewController的viewWillDisappear方法被调用,这对原有业务逻辑有较大影响。

调研后发现使用UIModalPresentationOverFullScreen的方式来present,presentingViewController的生命周期将不受影响。

  • 对iOS7的兼容

UIModalPresentationOverFullScreen只支持iOS8以上系统,对于iOS7系统,我们使用UIModalPresentationCustom的present方式。然而iOS7和iOS8中,view的层次结构有所不同,导致iOS7下需要进行特殊兼容:

在iOS8及以上,present一个viewController时,view的层次结构是

image

在iOS7中,present一个viewController时,view的层次结构是

image

所以在iOS7中,需要自行将presentedViewController.view应用transform变形,让它旋转90度达到横屏的效果。 在demo中,进入全屏的动画对iOS7和iOS8及以上系统做了分别处理:

iOS7:进入全屏的动画开始前,设置presentedViewController.view.transform = CGAffineTransformIdentity,为的是让presentedViewController.view覆盖在播放器view的位置上,形成动画起始的布局;在全屏动画的过程中,设置presentedViewController.view应用transform变形,让它旋转90度达到横屏的效果;

iOS8及以上:进去全屏的动画开始前,由于presentedViewController.view已经被系统旋转了90度,所以我们也让presentedViewController.view旋转90度,才能覆盖在播放器view的位置上;在全屏动画的过程中,设置presentedViewController.view.transform = CGAffineTransformIdentity,由于它的父视图已经是横向状态,所以此时presentedViewController.view看起来也称为了横屏状态。

具体代码可以参考demo中的EnterFullscreenTransition和ExitFullscreenTransition两个类。

  • 部分控件依靠window尺寸布局,导致全屏动画过程中布局错乱

在iOS8及以上系统中,present的动画过程中,iOS对presentingViewController的view的frame经过了两次变化:

第一次变化:由于window的bounds从竖直(height > width)的状态变化为了横向(width > height)的状态,由于autoresizing的作用,presentingViewController.view的frame也变成了横向状态

第二次变化:系统给presentingViewController.view增加了transform使其旋转了90度,让presentingViewController.view看起来还是竖直方向的

如果一个presentingViewController.view的一个子视图通过读取window的宽高来布局,那么在第一次变化的时候,window的宽高已经对调,导致第二次变化时这个子视图的布局错乱。

demo中,方案二内的红色小字展示了这个bug。

image
  • Window横竖屏的切换导致tableView被reloadData

上一个问题中讲到,在present的过程中,iOS对presentingViewController的view的frame经过了两次变化,这很可能会导致presentingViewController中的tableView被触发reloadData。

原本,为了让一个视频在退出全屏时回到原来的位置上,我们只需要记录movieView的superView以及movieView小屏状态下的frame,退出全屏时将movieView重新添加到superView上即可(如demo中的实现方式)。但是如果这个superView是一个tableViewCell的话,reloadData会导致cell的重用。退出全屏时将movieView添加到superView上,反而会导致视频视图回到了错误的位置。在这种情况下,我们只能改为记录movieView所在cell的index来弥补这个问题。

另外,由于我们的app对tableView做了高度缓存等优化,在一些极端情况下,这两次出乎意料的reloadData导致了一些业务上的bug,比如存入了错误的高度缓存。

系统级的坑点

如果说业务上的坑点都能通过修改代码逻辑来依次解决,但系统级的坑点却很难有有效的解决方案。

  • 屏幕渲染bug导致半边黑屏问题(iOS10)

在开发过程中发现,这种全屏方式会偶现手机半边黑屏的问题。在主线程忙碌时这个问题有较大的复现概率。

image

比如在这张图中,系统statusBar的宽度明显是横屏时的宽度,但是在渲染时整个界面都被旋转了90度,造成下方出现了半边黑屏。 但是在这种情形下,如果读取UIWindow,UIScreen以及各个层次的view的frame,得到的数值都符合预期,唯独屏幕上渲染出来的结果是bug的。

image

写了几个demo表明,这个即便没有转场动画,只要present一个只支持横屏方向的ViewController,半边黑屏的问题就有概率复现。 尝试了在全屏动画完成后再设置UIDevice的orientation,设置StatusBarOrientation等方法,但均没能解决这个问题。

  • UIScreen长宽互换bug(iOS10)

当app在后台时,触发了present操作,再返回前台,会导致读取UIScreen时长宽被互换了,但此时UIWindow的长宽却是符合预期的。

如果其他业务中,有界面是通过读取UIScreen的长宽来布局的话,这时就会出现布局异常的bug,比如某一段时间的详情页:
image
image

对于这个问题,我们采用了两个walkaround的方案:

(1)当app在后台时,禁止触发全屏相关的代码; (2)各业务不依赖UIScreen布局,比较好的做法是仅依赖superView进行布局;

方案二放弃

屏幕渲染bug导致半边黑屏问题一直得不到解决,并且在腾讯视频、爱奇艺等app上也发现了类似的bug。

image
image

针对这个问题,我们尝试了苹果的Apple Developer Technical Support,通过这个渠道可以接触到苹果的工程师,也许能给我们提供一些绕过这个bug的方法或者其他意见。在回信中,苹果承认这是他们的一个bug,但暂时没有给出解决方案。

image

无奈之下,我们只能放弃了方案二,开始寻求其他的方案。

方案三尝试

方案三尝试了一个看起来不太合理的方案:

在方案一的基础上,调用UIApplication的setStatusBarOrientation:animated:方法来改变statusBar的方向 同时重写当前的ViewController的shouldAutorotate方法,返回NO

官方文档对setStatusBarOrientation:animated:方法的描述是这样的:

Sets the app's status bar to the specified orientation, optionally animating the transition. Calling this method changes the value of the statusBarOrientation property and rotates the status bar, animating the transition if animated is YES . If your app has rotatable window content, however, you should not arbitrarily set status-bar orientation using this method. The status-bar orientation set by this method does not change if the device changes orientation.

这个方法已经被depreciate了,并且文档中也透露出不希望开发者调用的意思,然而神奇的是,使用这个方法并配合shouldAutorotate返回NO,竟然能旋转statusBar,并且让动画效果符合产品需求。

在supportedInterfaceOrientations的文档中,有这样的说明:

When the user changes the device orientation, the system calls this method on the root view controller or the topmost presented view controller that fills the window. If the view controller supports the new orientation, the window and view controller are rotated to the new orientation. This method is only called if the view controller'��s shouldAutorotate method returns true.

也就是说,当shouldAutorotate为NO的时候,supportedInterfaceOrientations方法将不再被调用。由于无法窥探UIKit的内部实现,我们只能猜测,当shouldAutorotate为NO的时候,界面的方向将不受supportedInterfaceOrientations控制,转而被setStatusBarOrientation:animated:方法控制。

虽然方案三看起来有些出乎意料的简单,但使用这个方案,我们比较顺利的完成了视频全屏的需求。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容