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
            
 }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。