iOS播放器全屏旋转实现

代码地址:HSPlayerFullScreenDemo 2020年6月19日更新

我们的客户端主要功能就是看电影,所以我们经常要与视频播放器打交道,用到视频播放器就需要满足用户全屏观看需求,视频播放器全屏需求往往需要App界面的旋转来实现,但是界面旋转就会带来一系列的兼容问题,比如系统弹窗方向与播放器方向不一致、导航栏和状态栏的方向大小飘回不定、App页面跳转与横屏播放器冲突、前贴片广告与播放器兼容等等。随着iOS系统的更新以及业务的迭代,我们全屏方案也跟着改进了好几版,在这过程中我们碰到了很多问题也积累了一些经验,现在将部分内容分享出来,供大家参考。

完善播放器的全屏一般需要满足以下功能点:

  • 能够正常切换到横屏。
  • 必要的过渡动画。
  • 返回竖屏播放器大小位置未发生变化。
  • 竖屏界面的内容不能发生变化。
  • 兼容系统弹窗。
  • 跟随设备方向旋转播放器方向。

常见方案介绍:

先来看看主流视频App使用的旋转方案吧:

1、原生页面旋转。

使用苹果原生支持的页面旋转,竖屏状态下强制旋转设备旋转,播放器和所在的页面一同旋转为横屏状态。腾讯视频和芒果TV就是使用了这种方案,这也是苹果支持的方案。

优点:逻辑简单,易用,兼容性好
缺点: 过渡动画稍显生硬,需要使用私有方法。

原生旋转

2、播放器View旋转。

使用UIView的transform属性,让播放器View旋转90度,然后通过一些方法把状态栏旋转到对应的方向,达到播放器旋转的目的。今日头条的短视频在使用这种方案,他们技术团队也做了分享 文章直达

优点:动画简单高效,过渡自然
缺点:播放器不是真正的横屏,播放器全屏状态下无法使用正常使用AlertView等系统弹窗。

播放器View旋转.gif

3、播放器View旋转+竖屏Window

这种方案是在第二种方案的基础上添加一个竖屏Window而来,全屏播放器的Window和主界面Window不是同一个Window,这样我们就可以通过全屏window的rootViewcontroller控制状态栏的显示隐藏了。据我所知,新版的zfplayer正在使用此方案。

优点:动画简单高效,过渡自然;全屏播放器和主界面不是同一个Window,可以方便的控制状态栏显隐。
缺点:播放器不是真正的横屏,播放器全屏状态下无法使用正常使用AlertView等系统弹窗;播放器从一个Window转移到另个Window上,较大概率能够看到闪屏。

第三种方案的图层

4、播放器View旋转动画+横屏Window

这种方案我们在主界面Window上,使用播放器的旋转动画做过渡动画,动画完成后把播放器View正过来,添加到提前生成的横屏Window上。经分析发现爱奇艺和优酷在使用当前方案。

优点:动画简单高效,过渡自然;全屏播放器和主界面不是同一个Window,可以方便的控制状态栏显隐;播放器是真正的横屏,播放器全屏状态可以完全兼容系统弹窗;
缺点:播放器从一个Window转移到另个Window上,较大概率能够看到闪屏;界面;因为涉及到横竖屏切换,横屏状态下将App切换到后台然后切换到前台可能看到竖屏界面尺寸发生异常改变。

通过xcode查看视图层级发现第四种的图层是正过来,第三种方案的图片是旋转90度的。

第四种方案的图层

知识点:

如何控制界面的旋转?

如果应用内所有页面都只支持同一方向或者每个页面都支持所有多个方向,那么在项目中的 info.plist里通过设置UIInterfaceOrientation的值或者在xcode里面勾选Device Orientation的选项值来配置应用支持的设备方向。

在info.plis设置Supported interface orientations的值
在xcode里面设置App支持的设备方向

如果应用内大部分页面只支持横屏,部分页面支持多个设备反向,那么需要动态灵活的配置应用支持的设备方向。
这时我们就可以实现AppDelegate的supportedInterfaceOrientationsForWindow方法来动态指定某个Window可以旋转的方向。如果我们没有实现这个方法,应用的支持的方向由info.plist的UIInterfaceOrientation值确定。如果实现了这个方法那么info.plist里面的值就无效了。

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        if 横屏条件 {
            return UIInterfaceOrientationMask.allButUpsideDown
        }
        return UIInterfaceOrientationMask.portrait
}

当设备方向发生改变时,系统就会调用包括上面方法等一系列的方法来确定当前页面的方向。

  • 首先会调用supportedInterfaceOrientationsForWindow方法来确定应用支持的方向,当前window的值为nil。(iOS13 window不会出现nil值)
  • 然后会再次调用supportedInterfaceOrientationsForWindow方法来确定当前页面的Window支持的方向,此时window的值为当前页面的window
  • 最后调用Window的rootViewcontroller的supportedInterfaceOrientationsshouldAutorotate方向来确定当前页面支持的方向。

然而我们的rootViewController大多是TabbarController或者NavigationController,而我们需要旋转的页面一般属于它们的ChildViewController,所以当我们要配置某一个ViewController可以旋转的方向时,需要将当前ViewController的supportedInterfaceOrientationsshouldAutorotate值传递给ViewController所在的window的rootViewcontroller。以下是用到的代码:

import UIKit

extension UITabBarController{
    open override var shouldAutorotate: Bool{
        let selected = self.selectedViewController;
        if selected?.isKind(of: UITabBarController.classForCoder()) ?? false{
            let nav = selected as! UINavigationController
            return nav.topViewController!.shouldAutorotate
        }else{
            return selected!.shouldAutorotate
        }
    }
    
    open override var supportedInterfaceOrientations: UIInterfaceOrientationMask{
        let selected = self.selectedViewController;
        if selected?.isKind(of: UITabBarController.classForCoder()) ?? false{
            let nav = selected as! UINavigationController
            return nav.topViewController!.supportedInterfaceOrientations
        }else{
            return selected!.supportedInterfaceOrientations
        }
    }
    
    open override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation{
        let selected = self.selectedViewController;
        if selected?.isKind(of: UITabBarController.classForCoder()) ?? false{
            let nav = selected as! UINavigationController
            return nav.topViewController!.preferredInterfaceOrientationForPresentation
        }else{
            return selected!.preferredInterfaceOrientationForPresentation
        }
    }
}

extension UINavigationController{
    open override var shouldAutorotate: Bool{
        return self.topViewController!.shouldAutorotate
    }
    
    open override var supportedInterfaceOrientations: UIInterfaceOrientationMask{
        return self.topViewController!.supportedInterfaceOrientations
    }
    
    open override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation{
        return self.topViewController!.preferredInterfaceOrientationForPresentation
    }
    
    open override var childForStatusBarStyle: UIViewController?{
        return self.topViewController
    }
    
    open override var childForStatusBarHidden: UIViewController?{
        return self.topViewController
    }
    
    open override var childForHomeIndicatorAutoHidden: UIViewController?{
        return self.topViewController
    }
}

强制设备旋转

使用如下方法强制设备旋转,不过UIDevice.current.setValue(value, forKey: key)方法属于私有方法,苹果审核有被拒的风险。

let orientationRawValue = UIInterfaceOrientation.landscapeRight.rawValue
UIDevice.current.setValue(orientationRawValue, forKey: "orientation")

self.setNeedsStatusBarAppearanceUpdate() //在当前ViewController里面调用
UIViewController.attemptRotationToDeviceOrientation()

如何更改状态栏的方向?

如方案2和方案3如何在界面不旋转的情况下更改状态栏的方向,在iOS13之前我们使用了今日头条分享的那种方案:

方法一
  • 调用UIApplication的setStatusBarOrientation:animated:方法改变statusBar的方向
  • 当前的ViewController的shouldAutorotate方法,返回NO

比如我们项目中就使用了如下代码:

- (void)setStatusBarOrientation:(UIInterfaceOrientation)interfaceOrientation {
    [[UIApplication sharedApplication] setStatusBarOrientation:interfaceOrientation animated:NO];
}

但是这个方法已经被苹果depreciate了,在iOS12以及iOS12之前这个方法没有问题,但是到了iOS13,我们使用xcode11编译发布项目,这个方法就无效了,这个方法无法改变状态栏的方向。iOS13发布后,我们试图寻找别的方法更改状态栏的方向,最终我们找到了一个方法。

方法二

我们可以通过创建相应方向的window,然后调用当前ViewController的setNeedsStatusBarAppearanceUpdate方法和UIViewControllerattemptRotationToDeviceOrientation达到状态栏旋转的目的。(具体代码可以下载Demo查看)

我们先自定义一个UIViewController的子类HSPlayerSceneController,代码如下:

import UIKit
class HSPlayerSceneController: UIViewController {    
    var interfaceOrientationMask:UIInterfaceOrientationMask? = nil
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override var shouldAutorotate:Bool{
        return !self.shouldNotAutorotate
    }
    
    override var supportedInterfaceOrientations:UIInterfaceOrientationMask{
        if self.interfaceOrientationMask != nil {
            return self.interfaceOrientationMask!
        }
        return .landscape
    }
}

然后创建有方向的window:

let sceneVC = HSPlayerSceneController()
sceneVC.interfaceOrientationMask = (orientation == UIInterfaceOrientation.landscapeLeft) ? UIInterfaceOrientationMask.landscapeLeft : UIInterfaceOrientationMask.landscapeRight
let sceneWnd = UIWindow(frame: UIScreen.main.bounds)
sceneWnd.rootViewController = sceneVC

最后通过以下代码改变状态栏方向:

func updateStatusBarAppearance() {
    let window = (UIApplication.shared.delegate as! AppDelegate).window
    var top: UIViewController? = window?.rootViewController
    
    while true {
        if top?.presentingViewController != nil {
            top = top?.presentingViewController
        } else if top is UINavigationController {
            if let nav: UINavigationController = top as? UINavigationController {
                top = nav.topViewController
            } else {
                break
            }
        } else if top is UITabBarController {
            if let tab: UITabBarController = top as? UITabBarController {
                top = tab.selectedViewController
            } else {
                break
            }
        } else {
            break
        }
    }
    
    top?.setNeedsStatusBarAppearanceUpdate()
    UIViewController.attemptRotationToDeviceOrientation()
}

如何解决播放器的自动布局约束和transform动画冲突?

我们的播放器使用自动布局约束控件,使用播放器的View的transform动画做翻转,这一切在iOS10以上的系统比较正常,但是到iOS10或者iOS10以下系统会出现旋转后界面异常。我们当时查了一些资料,发现播放器的自动布局和transform动画冲突,那么如何解决这个问题呢?
很简单,我们在播放器View上再套一个View,在这个View上做transform动画,就可以解决这个问题。这就是在Demo中我们使用playerTransitionView 的原因。

在旋转动画中playerTransitionView的SubView会出现拖白现象:SubView和动画playerTransitionView的变化不同步。解决这个问题只需要设置动画选项UIView.KeyframeAnimationOptions.layoutSubviews即可。

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