你們的項目都是隻支持豎屏的吧?大多數朋友(這其中固然也包括博主),都沒有作過橫屏開發,此次項目恰好有這個需求,所以把橫豎屏相關的心得寫成一遍文章供諸位參考。html
大多數公司的項目都只支持豎屏,只有一兩個界面須要同時支持橫屏,就像視頻 APP 同樣,只有視頻播放的時候須要橫屏,其餘時候都只容許豎屏。給出的 demo 中處理兩種須要橫屏的情形:git
具體使用演示請前往優酷視頻查看:BLLandscape Demo。github
通常可能只須要播放視頻時橫屏,錄製橫屏通常用不到,可是若是有朋友須要作橫屏視頻錄製,這時候就須要錄製橫屏處理,就像下面這樣的。web
這個思路是這樣的:markdown
橫屏的時候,首先把要橫屏的 view
從原先的 superView
中移除,添加到當前的 keyWindow
上,而後作 frame
動畫,將窗口的高設爲 view
的寬,窗口的寬設置爲 view
的高,而後將 view
的旋轉 90°,執行動畫,就能獲得當前的效果。架構
豎屏的時候,是一個相反的過程,先在窗口上作完動畫,再將 view
插入到橫屏以前的 superView
中的對應位置上。app
我把這些實現都抽成一個 UIView
的分類,看一下實現:ide
// frame 轉換.
- (void)landscapeExecute{
self.transform = CGAffineTransformMakeRotation(M_PI_2);
CGRect bounds = CGRectMake(0, 0, CGRectGetHeight(self.superview.bounds), CGRectGetWidth(self.superview.bounds));
CGPoint center = CGPointMake(CGRectGetMidX(self.superview.bounds), CGRectGetMidY(self.superview.bounds));
self.bounds = bounds;
self.center = center;
}
- (void)bl_landscapeAnimated:(BOOL)animated animations:(BLScreenEventsAnimations)animations complete:(BLScreenEventsComplete)complete{
if (self.viewStatus != BLLandscapeViewStatusPortrait) {
return;
}
self.viewStatus = BLLandscapeViewStatusAnimating;
self.parentViewBeforeFullScreenSubstitute.anyObject = self.superview;
self.frame_beforeFullScreen = [NSValue valueWithCGRect:self.frame];
NSArray *subviews = self.superview.subviews;
if (subviews.count == 1) {
self.indexBeforeAnimation = 0;
}
else{
for (int i = 0; i < subviews.count; i++) {
id object = subviews[i];
if (object == self) {
self.indexBeforeAnimation = i;
break;
}
}
}
CGRect rectInWindow = [self.superview convertRect:self.frame toView:nil];
[self removeFromSuperview];
self.frame = rectInWindow;
[[UIApplication sharedApplication].keyWindow addSubview:self];
if (animated) {
[UIView animateWithDuration:0.35 animations:^{
[self landscapeExecute];
if (animations) {
animations();
}
} completion:^(BOOL finished) {
[self landscpeFinishedComplete:complete];
}];
}
else{
[self landscapeExecute];
[self landscpeFinishedComplete:complete];
}
self.viewStatus = BLLandscapeViewStatusLandscape;
[self refreshStatusBarOrientation:UIInterfaceOrientationLandscapeRight];
}
複製代碼
豎屏和橫屏就是一個相反的過程,這裏不貼代碼也不作解釋了。不懂的去看源碼就知道了。oop
weak
源碼沒什麼難度,可是有一個細節須要注意,咱們要在分類中以 weak
的內存管理策略去引用動畫以前的 superView
,以便咱們回來作豎屏動畫完成之後將當前 view
添加到動畫以前的 superView
上。可是在分類中添加屬性的內存管理策略中沒有 weak
屬性,可是有一個 OBJC_ASSOCIATION_ASSIGN
,它相似咱們經常使用的 assign
,assign
策略的特色就是在對象釋放之後,不會主動將應用的對象置爲 nil
,這樣會有訪問殭屍對象致使應用奔潰的風險。佈局
爲了解決這個問題,咱們能夠建立一個替身對象,咱們能夠在分類中以 OBJC_ASSOCIATION_RETAIN_NONATOMIC
的策略來強引用替身對象,而後在替身對象中以 weak
的策略去引用咱們真實須要保存的對象。這樣就能解決這個可能致使奔潰的問題了。
最近在知乎上有一個朋友說起了另一種方式,咱們能夠建立一個 block
,在 block
中引用咱們須要使用 weak
內存管理的對象,而後咱們強引用這個 block
,就像下面這樣:
#import <Foundation/Foundation.h>
@interface NSObject (Weak)
/**
* weak
*/
@property(nonatomic) id weakObject;
@end
#import "NSObject+Weak.h"
#import <objc/runtime.h>
@implementation NSObject (Weak)
- (void)setWeakObject:(id)weakObject {
id __weak __weak_object = weakObject;
id (^__weak_block)() = ^{
return __weak_object;
};
objc_setAssociatedObject(self, @selector(weakObject), __weak_block, OBJC_ASSOCIATION_COPY);
}
- (id)weakObject {
id (^__weak_block)() = objc_getAssociatedObject(self, _cmd);
return __weak_block();
}
@end
複製代碼
因爲咱們作的是 frame
動畫,因此以後在這個 view
上再添加子控件的時候必須使用 frame
佈局,Autolayout
佈局在當前的 view
上將不會被更新,致使 UI 錯亂。
大多數場景都是播放視頻的時候橫屏,好比下面這樣的:
若是你在網上搜 iOS 橫豎屏切換
能搜到的也就是播放視頻的時候的橫屏了,而這些文章彷佛都是抄的某一篇文章,你們說的都同樣。雖然你們抄來抄去,彷佛他們在文章中寫的都能解決問題,但實際上他們的文章是不能解決實際問題的。
咱們來看一下控制屏幕旋轉的兩個方法:
@interface UIViewController (UIViewControllerRotation)
...
- (BOOL)shouldAutorotate NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
- (UIInterfaceOrientationMask)supportedInterfaceOrientations NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
...
@end
複製代碼
能夠看到能夠控制屏幕方向的方法是定義在 UIViewController
裏的,第一個 -shouldAutorotate
方法,系統會詢問當前控制器是否支持旋轉,第二個方法 -supportedInterfaceOrientations
告訴系統當前控制器支持那幾個方向的旋轉。
真實項目中,咱們的 UI 架構多是這樣的:
咱們的項目中從窗口開始,依次是一個根控制器,而後再是 UITabbarController
而後再是 UINavigationController
,最後纔到咱們的 UIViewController
,咱們是某些界面須要橫屏,因此必需要把系統的詢問細化到每一個控制器的方法才行。
結合上圖,咱們看下一個橫豎屏事件的傳遞過程:
UITabbarController
UITabbarController
,須要把事件傳遞到 UINavigationController
UINavigationController
,須要把事件傳遞到咱們本身的控制器等等,你個人項目是一個已經可能有上千個控制器的大工程了,若是按照這個邏輯走下去,咱們要在每一個控制器寫這個兩個方法,不敢想象。
此時咱們第一要考慮的就是藉助分類來實現,既簡單又優雅,並且維護起來集中乾淨,何樂而不爲?
#import <UIKit/UIKit.h>
@interface UIViewController (Landscape)
/**
* 是否須要橫屏(默認 NO, 即當前 viewController 不支持橫屏).
*/
@property(nonatomic) BOOL bl_shouldAutoLandscape;
@end
#import "UIViewController+Landscape.h"
#import <objc/runtime.h>
#import <JRSwizzle.h>
@implementation UIViewController (Landscape)
+ (void)load{
[self jr_swizzleMethod:@selector(shouldAutorotate) withMethod:@selector(bl_shouldAutorotate) error:nil];
[self jr_swizzleMethod:@selector(supportedInterfaceOrientations) withMethod:@selector(bl_supportedInterfaceOrientations) error:nil];
}
- (BOOL)bl_shouldAutorotate{ // 是否支持旋轉.
if ([self isKindOfClass:NSClassFromString(@"BLAppRootViewController")]) {
return self.childViewControllers.firstObject.shouldAutorotate;
}
if ([self isKindOfClass:NSClassFromString(@"UITabBarController")]) {
return ((UITabBarController *)self).selectedViewController.shouldAutorotate;
}
if ([self isKindOfClass:NSClassFromString(@"UINavigationController")]) {
return ((UINavigationController *)self).viewControllers.lastObject.shouldAutorotate;
}
if ([self checkSelfNeedLandscape]) {
return YES;
}
if (self.bl_shouldAutoLandscape) {
return YES;
}
return NO;
}
- (UIInterfaceOrientationMask)bl_supportedInterfaceOrientations{ // 支持旋轉的方向.
if ([self isKindOfClass:NSClassFromString(@"BLAppRootViewController")]) {
return [self.childViewControllers.firstObject supportedInterfaceOrientations];
}
if ([self isKindOfClass:NSClassFromString(@"UITabBarController")]) {
return [((UITabBarController *)self).selectedViewController supportedInterfaceOrientations];
}
if ([self isKindOfClass:NSClassFromString(@"UINavigationController")]) {
return [((UINavigationController *)self).viewControllers.lastObject supportedInterfaceOrientations];
}
if ([self checkSelfNeedLandscape]) {
return UIInterfaceOrientationMaskAllButUpsideDown;
}
if (self.bl_shouldAutoLandscape) {
return UIInterfaceOrientationMaskAllButUpsideDown;
}
return UIInterfaceOrientationMaskPortrait;
}
- (void)setBl_shouldAutoLandscape:(BOOL)bl_shouldAutoLandscape{
objc_setAssociatedObject(self, @selector(bl_shouldAutoLandscape), @(bl_shouldAutoLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)bl_shouldAutoLandscape{
return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (BOOL)checkSelfNeedLandscape{
NSProcessInfo *processInfo = [NSProcessInfo processInfo];
NSOperatingSystemVersion operatingSytemVersion = processInfo.operatingSystemVersion;
if (operatingSytemVersion.majorVersion == 8) {
NSString *className = NSStringFromClass(self.class);
if ([@[@"AVPlayerViewController", @"AVFullScreenViewController", @"AVFullScreenPlaybackControlsViewController"
] containsObject:className]) {
return YES;
}
if ([self isKindOfClass:[UIViewController class]] && [self childViewControllers].count && [self.childViewControllers.firstObject isKindOfClass:NSClassFromString(@"AVPlayerViewController")]) {
return YES;
}
}
else if (operatingSytemVersion.majorVersion == 9){
NSString *className = NSStringFromClass(self.class);
if ([@[@"WebFullScreenVideoRootViewController", @"AVPlayerViewController", @"AVFullScreenViewController"
] containsObject:className]) {
return YES;
}
if ([self isKindOfClass:[UIViewController class]] && [self childViewControllers].count && [self.childViewControllers.firstObject isKindOfClass:NSClassFromString(@"AVPlayerViewController")]) {
return YES;
}
}
else if (operatingSytemVersion.majorVersion == 10){
if ([self isKindOfClass:NSClassFromString(@"AVFullScreenViewController")]) {
return YES;
}
}
return NO;
}
@end
複製代碼
JPWarpViewController
當前 demo 中使用了 JPNavigationController
,由於 JPNavigationController
結構的特殊性,因此這裏加了一個
if ([self isKindOfClass:NSClassFromString(@"JPWarpViewController")]) {
return [self.childViewControllers.firstObject supportedInterfaceOrientations];
}
複製代碼
若是你項目中有爲每一個界面定製導航條的需求,你或許能夠前往個人 GitHub 查看。
AVFullScreenViewController
當網頁中有 video 標籤的時候,iPhone 打開這個網頁的的時候會把 video 標籤替換爲對應的系統的播放器,當咱們點擊這個視頻的時候,系統會全屏進入一個視頻播放界面,經過打印這個控制器咱們能夠看到這個控制器的類名是 AVFullScreenViewController
,因此,這個界面須要橫屏,就返回橫屏對應的屬性就能夠實現這個控制器橫屏。
並非全部的網頁都須要橫屏,可是若是這個網頁有視頻,每每須要橫屏,那咱們怎麼知道某個頁面是否須要橫屏,是否有視頻呢?
一種方式是和 h5 約定一個事件,若是有視頻就告訴原生 APP 作一個標記,將 bl_shouldAutoLandscape
置爲 YES
。
可是我這裏提供一種更加簡便優雅的方式,咱們的 UIWebView
是能夠經過 -stringByEvaluatingJavaScriptFromString:
方法和咱們交互的,因此咱們能夠嘗試下面的方法:
#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView{
NSString *result = [webView stringByEvaluatingJavaScriptFromString:@"if(document.getElementsByTagName('video').length>0)document.getElementsByTagName('video').length;"];
if (result.length && result.integerValue != 0) {
self.bl_shouldAutoLandscape = YES;
}
}
複製代碼
當 WebView
加載完之後,咱們去查找當前的 h5 頁面中有沒有 Video
標籤,若是有,那咱們就能夠拿到結果,作對應的橫屏處理。
原本咱們的這個 UITableViewController
是不支持橫豎屏的,就像這樣,注意,這個時候那個 UISwitch
按鈕是關閉的。
接下來,咱們把這個開關打開,這個開關對應的代碼是這樣:
- (void)switchValueChanged:(UISwitch *)aswitch{
if (aswitch.isOn) {
BLAnotherWindowViewController *vc = [BLAnotherWindowViewController new];
self.anotherWindow.rootViewController = vc;
[self.anotherWindow insertSubview:vc.view atIndex:0];
}
else{
self.anotherWindow.rootViewController = nil;
}
}
複製代碼
就是打開後會爲另一個窗口添加一個根控制器,而這個根控制器的代碼是這樣的:
#import "BLAnotherWindowViewController.h"
@interface BLAnotherWindowViewController ()
@end
@implementation BLAnotherWindowViewController
- (BOOL)shouldAutorotate{
return YES;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskAll;
}
@end
複製代碼
這樣之後,咱們觀察一下控制器的表現:
看起來咱們的主界面確實仍然不支持橫豎屏,這是沒有問題的,可是好像咱們的狀態欄被藍色的這個窗口劫持了,它們倆雙宿雙飛,一塊兒幹了這麼一個橫豎屏的勾當。
咱們想象一下,如今這個藍色的窗口在最前面,咱們能敏捷的觀察到時這個藍色的窗口劫持了狀態欄。那若是這個藍色的窗口在咱們的主窗口後面呢,那咱們根本就不會察覺到這個細節,咱們能看到的就是下面這樣:
第一次碰到這個 bug,個人心裏是奔潰的。
咱們一塊兒來分析一下這個問題是怎麼形成的。再來看一下這個橫豎屏系統詢問路徑圖,當咱們有多個窗口之時,每一個窗口都是平等的,那個藍色的窗口也收到了系統的詢問。
UIViewController
的方法,此時若是窗口並無 rootViewController
的話,那系統問也白問,因此藍色窗口並不會劫持狀態欄和橫屏事件。rootViewController
的話,那麼該控制的返回值就會決定設備的方向。也就形成了這個 bug。又發現了一個新的 bug,如今補充一下,若是咱們的應用是豎屏的,只是某些界面須要橫屏,那麼若是咱們把項目的 info.plist
的橫豎屏所有都打開的話,就像下面這樣:
那麼對於 plus 手機的話,若是你是橫着手機打開 APP,那麼首個界面確定是會 UI 錯亂的,由於這個時候 APP 還沒啓動完,系統會根據咱們 info.plist
的配置進行初始化,因此會致使這個 bug。
如今解決方式是這樣的, info.plist
咱們仍然這樣寫,以保證剛啓動 APP 的時候不至於 UI 錯亂。
接下來,咱們來到 AppDelegate
裏返回支持的橫屏方向,就像下面這麼寫,功能和在 info.plist
也是同樣的。
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window{
return self.window.rootViewController.supportedInterfaceOrientations;
}
複製代碼
最後 GitHub 地址在這裏 BLLandscape。