对于一个带有视频播放功能的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. nginx 通过proxy_next_upstream实现容灾和重复处理问题

    ngx_http_proxy_module 模块中包括proxy_next_upstream指令 语法: proxy_next_upstream error | timeout | invalid_h ...

  2. 互联网的keyvalue处理

    今天在和许伟讨论系统配置页面得时候,许伟提到了“打通页面”的概念,当时我没太明白,后来才知道是指类似于cloudera里面的配置页面那种,不是列表页,而是展示+编辑在一个页面.刚才想了一下,其实对于这 ...

  3. 常见企业IT支撑【8、端口回流问题】

    端口回流故障场景,常见于内网启用了Server服务器,使用出口路由设备的外网口NAT映射了一个公网地址,域内内网主机访问了这个公网地址,访问不通. 故障容易出现在办公网内的带有对外Server测试环境 ...

  4. Proxool抛出的警告 was active for 365172 milliseconds and has been removed automaticaly

    WARN cetDB:149 - #0005 was active for 365172 milliseconds and has been removed automaticaly. The Thr ...

  5. 阻塞队列之四:ArrayBlockingQueue

    一.ArrayBlockingQueue简介 一个由循环数组支持的有界阻塞队列.它的本质是一个基于数组的BlockingQueue的实现. 它的容纳大小是固定的.此队列按 FIFO(先进先出)原则对元 ...

  6. Java-Runoob-面向对象:Java 包(Package)

    ylbtech-Java-Runoob-面向对象:Java 包(Package) 1.返回顶部 1. Java 包(package) 为了更好地组织类,Java 提供了包机制,用于区别类名的命名空间. ...

  7. Redis:目录

    ylbtech-Redis:目录 1.返回顶部   2.返回顶部   3.返回顶部   4.返回顶部   5.返回顶部     6.返回顶部   7.返回顶部   8.返回顶部   9.返回顶部   ...

  8. Java Web不能不懂的知识

    1.传说中java的class文件可以一次编译到处运行,那么源代码采用GBK还是UTF-8会有影响么? 不会有影响. 因为Java源代码通过编译后,生成的class文件为字节码.它可以被看作是包含一个 ...

  9. 【学步者日记】实现破碎效果 Fracturing & Destruction 插件使用

    全文见原始链接:http://note.youdao.com/noteshare?id=ef5ef90b71da4e960e5bc0da4f3f17ec 下面是预览 示例工程链接:https://pa ...

  10. 004:MySQL数据库体系结构

    目录 一. MySQL数据库体系结构 1.MySQL数据库体系结构介绍 1 数据库定义 2 数据库实例 2. MySQL体系结构 1 单进程多线程结构 2 存储引擎的概念 3 体系结构图 4 逻辑存储 ...