对于一款具备视频播放功能的app产品来说,视频全屏是一个基本且重要的要求。虽然这个需求看起来很简单,但是我们已经在实现前后迭代了三套技术解决方案。本文将介绍这三种实施方案的优缺点和陷阱,以及在实施过程中积累的经验。
关键要求:
*在屏幕旋转的动画中,需要将界面布局保持在播放器之外(例如“First View”等几行字的布局不应该改变)。
*全屏切换到小屏,小屏需要回到原来的位置。
对于这三种实现方案,我分别写了一个演示。这三个方案在演示的三个选项卡中。
原方案:方案一。
从小屏移动到全屏时,将播放器所在的视图放置在窗口上,以变换的形式制作旋转动画,使视图完全覆盖窗口。
从全屏返回到小屏时,以变换的形式旋转动画,最后将玩家所在的视图返回到原来的parentView。
核心代码示例:
这个方法在实现上相对简单,因为它只旋转玩家所在的视图。
控制器和设备的方向总是纵向的。但最大的问题是全屏状态。
条形的方向仍然是垂直的。虽然之前通过全屏隐藏statusBar的方式掩盖了这个问题,但也导致用户无法在全屏视频中看到时间和网络情况,体验有待提升。
方案二假设
为了解决状态栏无法切换为横向的问题,我们决定更换全屏幕视频的实现方式。
业界最流行的屏幕转移方式应该是通过私有接口设置UIDevice的orientation属性。但是直接设置这个属性实现的转屏动画效果有些欠缺。例如,黑色会在旋转过程中漏出。
由于setStatusBarOrientation等方法已经被标记为折旧,使用它可能会带来风险,所以我们暂时没有考虑这个方法。
一种合理的技术方案是:
在只支持纵向的视图控制器上,显示的是只支持横向的视图控制器。通过重写视图控制器之间的过渡动画,可以高度定制全屏动画,并且当视频全屏时,可以水平显示状态栏。
该方案不使用任何私有接口或黑客手段,完全符合苹果的要求。理想情况下,它应该是一个稳定可靠的方案。
因此,我们选择了当前视图控制器作为第二个选项。
核心设计是:
添加ViewController的一个子类,在演示中是FullscreenViewController,并覆盖这个类的supportedInterfaceOrientations返回uiinterfaceorientmasclandscape。
当显示全屏时,系统会自动将状态栏转向横向。
同时,定制FullscreenViewController的过渡动画,形成符合产品要求的动画效果。
方案二坑点解。
在实现第二个方案的过程中,我们遇到了很多问题。
生意场上的坑。
*兼容viewWillDisppear等生命周期方法
默认情况下呈现一个视图控制器会导致presentingViewController的视图从视图层次结构中被移除,调用presentingViewController的viewing方法,这将极大地影响原有的业务逻辑。
经过调查发现,使用UIMap Representation OverFullsCreen来呈现不会影响到presentingViewController的生命周期。
*对iOS7的兼容
uiodalpresentationoverullscreen仅支持iOS8以上的系统。对于iOS7,我们使用UIModalPresentationCustom的当前模式。但是,在iOS7和iOS8中,视图级别。
结构有所不同,导致iOS7下需要进行特殊兼容:在iOS8及以上,present一个viewController时,view的层次结构是
在iOS7中,present一个viewController时,view的层次结构是
所以在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。
* 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)
在开发过程中发现,这种全屏方式会偶现手机半边黑屏的问题。在主线程忙碌时这个问题有较大的复现概率。
比如在这张图中,系统statusBar的宽度明显是横屏时的宽度,但是在渲染时整个界面都被旋转了90度,造成下方出现了半边黑屏。
但是在这种情形下,如果读取UIWindow,UIScreen以及各个层次的view的frame,得到的数值都符合预期,唯独屏幕上渲染出来的结果是bug的。
写了几个demo表明,这个即便没有转场动画,只要present一个只支持横屏方向的ViewController,半边黑屏的问题就有概率复现。
尝试了在全屏动画完成后再设置UIDevice的orientation,设置StatusBarOrientation等方法,但均没能解决这个问题。
* UIScreen长宽互换bug(iOS10)
当app在后台时,触发了present操作,再返回前台,会导致读取UIScreen时长宽被互换了,但此时UIWindow的长宽却是符合预期的。
如果其他业务中,有界面是通过读取UIScreen的长宽来布局的话,这时就会出现布局异常的bug,比如某一段时间的详情页:
对于这个问题,我们采用了两个walkaround的方案:
(1)当app在后台时,禁止触发全屏相关的代码; (2)各业务不依赖UIScreen布局,比较好的做法是仅依赖superView进行布局;
方案二放弃
屏幕渲染bug导致半边黑屏问题一直得不到解决,并且在腾讯视频、爱奇艺等app上也发现了类似的bug。
针对这个问题,我们尝试了苹果的Apple Developer Technical
Support,通过这个渠道可以接触到苹果的工程师,也许能给我们提供一些绕过这个bug的方法或者其他意见。在回信中,苹果承认这是他们的一个bug,但暂时没有给出解决方案。
无奈之下,我们只能放弃了方案二,开始寻求其他的方案。
方案三尝试
方案三尝试了一个看起来不太合理的方案:
> 在方案一的基础上,调用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:方法控制。
虽然方案三看起来有些出乎意料的简单,但使用这个方案,我们比较顺利的完成了视频全屏的需求。
参考资料
supportedInterfaceOrientations
setStatusBarOrientation:animated:
shouldAutorotate