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

需求要点:

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

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

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

原始方案:方案一

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

核心代码示例:

 - (void)enterFullscreen {

     if (self.movieView.state != MovieViewStateSmall) {
return;
} self.movieView.state = MovieViewStateAnimating; /*
* 记录进入全屏前的parentView和frame
*/
self.movieView.movieViewParentView = self.movieView.superview;
self.movieView.movieViewFrame = self.movieView.frame; /*
* movieView移到window上
*/
CGRect rectInWindow = [self.movieView convertRect:self.movieView.bounds toView:[UIApplication sharedApplication].keyWindow];
[self.movieView removeFromSuperview];
self.movieView.frame = rectInWindow;
[[UIApplication sharedApplication].keyWindow addSubview:self.movieView]; /*
* 执行动画
*/
[UIView animateWithDuration:0.5 animations:^{
self.movieView.transform = CGAffineTransformMakeRotation(M_PI_2);
self.movieView.bounds = CGRectMake(, , CGRectGetHeight(self.movieView.superview.bounds), CGRectGetWidth(self.movieView.superview.bounds));
self.movieView.center = CGPointMake(CGRectGetMidX(self.movieView.superview.bounds), CGRectGetMidY(self.movieView.superview.bounds));
} completion:^(BOOL finished) {
self.movieView.state = MovieViewStateFullscreen;
}];
} - (void)exitFullscreen { if (self.movieView.state != MovieViewStateFullscreen) {
return;
} self.movieView.state = MovieViewStateAnimating; CGRect frame = [self.movieView.movieViewParentView convertRect:self.movieView.movieViewFrame toView:[UIApplication sharedApplication].keyWindow];
[UIView animateWithDuration:0.5 animations:^{
self.movieView.transform = CGAffineTransformIdentity;
self.movieView.frame = frame;
} completion:^(BOOL finished) {
/*
* movieView回到小屏位置
*/
[self.movieView removeFromSuperview];
self.movieView.frame = self.movieView.movieViewFrame;
[self.movieView.movieViewParentView addSubview:self.movieView];
self.movieView.state = MovieViewStateSmall;
}];
}
这种方式在实现上相对简单,因为仅仅旋转了播放器所在的view,view controller和device的方向均始终为竖直(portrait)。但最大的问题就是全屏时status bar的方向依然是竖直的,虽然之前通过全屏时隐藏statusBar来掩盖了这个问题,但这同时导致了用户无法在视频全屏时看到时间、网络情况等,体验有待改善。

方案二设想

为了解决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的层次结构是

 UIWindow frame = ( ;  )
| presentingViewController.view frame = ( ; ); transform = [, , -, , , ]
| UITransitionView frame = ( ; )
| presentedViewController.view frame = ( ; )
在iOS7中,present一个viewController时,view的层次结构是 UIWindow frame = ( ; )
| UITransitionView frame = ( ; )
| presentingViewController.view frame = ( ; )
| presentedViewController.view frame = ( ; ) transform = [, -, , , , ]

所以在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

原文点击这里​

iOS端一次视频全屏需求的实现(转)的更多相关文章

  1. iOS 视频全屏功能 学习

    项目中,也写过类似"视频全屏"的功能, 前一阵子读到今日头条 的一篇技术文章,详细介绍三种旋转方法差异优劣最终择取.文章从技术角度看写的非常好,从用户角度看,也用过多家有视频功能的 ...

  2. H5项目常见问题及注意事项,视频全屏,定位,屏幕旋转和触摸,偏页面重构向 来源joacycode的github

    Meta基础知识: H5页面窗口自动调整到设备宽度,并禁止用户缩放页面 //一.HTML页面结构 <meta name="viewport" content="wi ...

  3. WebView中的视频全屏的相关操作

    近期工作中,基本一直在用WebView,今天就把它整理下: WebView 顾名思义,就是放一个网页,一个看起来十分简单,可是用起来不是那么简单的控件. 首先你肯定要定义,初始化一个webview,事 ...

  4. HTML5在手机端实现视频全屏展示

    最近做项目,遇到一个问题,在手机上要实现视频的全屏播放功能.测试了很久,终于找到解决办法. 第一种:将视频放大来控制. 视频在播放的时候,全屏是根据高度来的,如果设置视频 video 标签的宽度是 1 ...

  5. Android webview 退出时关闭声音 4.视频全屏 添加cookie

    全屏问题,可以参考 http://bbs.csdn.net/topics/390839259,点击 webView = (WebView) findViewById(R.id.webView); vi ...

  6. android--------自定义视频控件(视频全屏竖屏自动切换)

    android播放视频也是常用的技术,今天分享一个自定义视频控件,支持自定义控制 UI,全屏播放, 可以实现自动横竖屏切换的控件,跟随手机的位置而,重力感应自动切换横竖屏. 效果图:   代码下载Gi ...

  7. ppt转flash kindeditor上传视频全屏问题

    最近要增加页面的ppt显示功能,于是考虑把ppt转成flash,在网上搜到了ispingfree,链接: https://pan.baidu.com/s/1QZzx6qmdsnwzWCuULXzUOw ...

  8. video标签的视频全屏

    按钮: <div class="fullScreen" @click="fullScreen"><i class="el-icon- ...

  9. iOS开发——点击图片全屏显示

    点击图片,全屏显示,然后再点击屏幕一下,返回. 没啥难的,直接上代码: // //  ViewController.m //  Demo-hehe // //  Created by yyt on 1 ...

随机推荐

  1. UNIX简化路径

    Given an absolute path for a file (Unix-style), simplify it. For example, path = “/home/”, => “/h ...

  2. Python——while、continue、break、while-else、or、and、not

    1. while 终止while循环: (1) 改变条件,使其不成立 (2) break 应用实例1:计算1+2+3+...+100 #1.使用两个变量 count = 1 sum = 0 while ...

  3. MIT App Inventor使用与入门教程

    前言:随着信息技术新课标的提出,移动app应用设计被加入到新课标,程序与算法得到体现,学生的创造性,计算思维与信息意识也可以在其中得到体现. 安卓app开发目前比较火热的是Eclipse和Androi ...

  4. python3 内存管理

    怎么查找哪里存在内存泄露呢?武器就是两个库:gc.objgraph pip install psutil pip install objgraphpip install -U memory_profi ...

  5. Netty--JDK序列化编解码传输对象

    使用JDK序列化不需要额外的类库,只需要实现Serializable即可,但是序列化之后的码流只有Java才能反序列化,所以它不是跨语言的,另外由于Java序列化后码流比较大,效率也不高,所以在RPC ...

  6. Bootstrap组件系列之福利篇几款好用的组件(推荐)

    引用 :http://www.jb51.net/article/87189.htm 一.时间组件 bootstrap风格的时间组件非常多,你可以在github上面随便搜索“datepicker”关键字 ...

  7. Struts2的fliter与interceptor

    struts2   fliter与interceptor的区别 1.拦截器是基于java反射机制的,而过滤器是基于函数回调的.2.过滤器依赖于servlet容器,而拦截器不依赖于servlet容器.3 ...

  8. 03:TPCC 基准压测my.cnf

    line: V1.3 mail: gczheng@139.com date: 2017-11-09 一.TPCC测试前准备 1.压测环境 配置 信息 主机 Dell PowerEdge R730xd ...

  9. web 服务基础

    用户通过网站访问浏览器都发生了什么 如图,用户请求www.joker.com发生 1. 用户访问网站流程框架2. dns解析原理3. tcp/ip三次握手过程原理4. http协议原理(www服务的请 ...

  10. python 源代码保护 之 xx.py -> xx.so

    前情提要 之前由于项目的需要,需要我们将一部分“关键代码”隐藏起来. 虽然Python 先天支持 将源代码 编译后 生成 xxx.pyc 文件,但是破解起来相当容易 -_-!! 于是搜罗到了另外一种方 ...