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閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379