基礎概念
UIDeviceOrientation
UIDeviceOrientation,表示設備朝向,可以通過[UIDevice currentDevice] orientation]
獲取,取值有:
typedef NS_ENUM(NSInteger, UIDeviceOrientation) {
UIDeviceOrientationUnknown, // 未知,啟動時會出現
UIDeviceOrientationPortrait, // 豎屏,home鍵在底部
UIDeviceOrientationPortraitUpsideDown, // 倒立,home鍵在頂部
UIDeviceOrientationLandscapeLeft, // 左橫屏,home鍵在右邊
UIDeviceOrientationLandscapeRight, // 右橫屏,home鍵在左邊
UIDeviceOrientationFaceUp, // 屏幕朝上
UIDeviceOrientationFaceDown // 屏幕朝下
}
UIInterfaceOrientation
UIInterfaceOrientation,表示頁面內容朝向,注意UIInterfaceOrientation和UIDeviceOrientation的關系,其中UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,這是因為:
This is because rotating the device to the left requires rotating the content to the right.
不用特別細究兩者之間關系,我們只需要根據需要設置好UIInterfaceOrientation即可,通過
[UIApplication shareApplication] statusBarOrientation]
可以獲取當前狀態欄朝向。
typedef NS_ENUM(NSInteger, UIInterfaceOrientation) {
UIInterfaceOrientationUnknown = UIDeviceOrientationUnknown,
UIInterfaceOrientationPortrait = UIDeviceOrientationPortrait,
UIInterfaceOrientationPortraitUpsideDown = UIDeviceOrientationPortraitUpsideDown,
UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,
UIInterfaceOrientationLandscapeRight = UIDeviceOrientationLandscapeLeft
};
UIInterfaceOrientationMask
UIInterfaceOrientationMask,是由頁面內容朝向的二進制偏移組成,用來更方便描述某個界面支持的朝向。比如說下面的UIInterfaceOrientationMaskLandscape,其實就是由MaskLandscapeLeft和MaskLandscapeRight組成,這樣可以方便描述設備支持兩個橫屏方向。
typedef NS_OPTIONS(NSUInteger, UIInterfaceOrientationMask) {
UIInterfaceOrientationMaskPortrait = (1 << UIInterfaceOrientationPortrait),
UIInterfaceOrientationMaskLandscapeLeft = (1 << UIInterfaceOrientationLandscapeLeft),
UIInterfaceOrientationMaskLandscapeRight = (1 << UIInterfaceOrientationLandscapeRight),
UIInterfaceOrientationMaskPortraitUpsideDown = (1 << UIInterfaceOrientationPortraitUpsideDown),
UIInterfaceOrientationMaskLandscape = (UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
UIInterfaceOrientationMaskAll = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortraitUpsideDown),
UIInterfaceOrientationMaskAllButUpsideDown = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
};
UIViewController相關
UIViewController關于橫豎屏的三個方法:
- shouldAutorotate,頁面是否允許自動旋轉,被棄用api:
-shouldAutorotateToInterfaceOrientation
的取代者;默認值為YES,表示當前界面允許跟隨設備旋轉而自動旋轉; - supportedInterfaceOrientations,該界面支持的界面朝向,可以返回四個朝向的任意組合,iPad默認值是四個朝向都支持,iPhone默認值是除了UpsideDown的三個朝向。這個方法回調的前提是shouldAutorotate=YES。
- preferredInterfaceOrientationForPresentation,該界面被present出來的界面朝向,可以返回四個朝向的任意組合。如果沒有返回,則present時和原來界面保持一致。
AppDelegate相關
AppDelegate的supportedInterfaceOrientationsForWindow方法,根據需要返回當前window是否支持橫屏。
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window;
工程配置相關
在xcode的工程設置的General可以配置iPhone和iPad的頁面朝向支持。
橫豎屏切換實例
豎屏界面如何present橫屏界面
豎屏present橫屏是很普遍的場景,比如說視頻播放場景的全屏切換,就可以在當前豎屏的界面present一個橫屏播放界面的方式,實現橫豎屏切換。具體的操作步驟只需要兩步:
1,設置modalPresentationStyle為UIModalPresentationFullScreen;
2、preferredInterfaceOrientationForPresentation方法,返回UIInterfaceOrientationLandscapeRight;
比如說下面的LandscapeViewController界面:
// 點擊時設置
- (void)onClick {
LandscapeViewController *landVC = [[LandscapeViewController alloc] init];
landVC.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:landVC animated:YES completion:nil];
}
// LandscapeViewController內部代碼
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationLandscapeRight;
}
思考??:
1、如果是橫屏轉豎屏呢?
2、如果想要自定義旋轉效果實現呢?(UIViewControllerAnimatedTransitioning協議)
豎屏界面如何push橫屏界面
比如說這樣的場景:App的rootVC是navigationVC,導航棧內先有一個豎屏界面,現在想要push一個橫屏界面LandscapeViewController。
一個簡單的方式如下:
// appdelegate實現
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
if ([self.navigationVC.topViewController isKindOfClass:LandscapeViewController.class]) {
return UIInterfaceOrientationMaskLandscapeRight;
}
else {
return UIInterfaceOrientationMaskPortrait;
}
}
// LandscapeViewController內部實現
- (void)viewDidLoad {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:@selector(setOrientation:)]];
invocation.selector = NSSelectorFromString(@"setOrientation:");
invocation.target = [UIDevice currentDevice];
int initOrientation = UIDeviceOrientationLandscapeRight;
[invocation setArgument:&initOrientation atIndex:2];
[invocation invoke];
}
思考??:
1、這里為什么沒有用到UIViewController的三個方法?
2、在viewDidLoad調用的旋轉方法是什么意思?
橫屏豎切換機制分析
前面的實例介紹了如何支持切換,但是也產生一些疑問:
工程配置文件也沒有設置橫屏,為什么后面就能支持橫屏?
工程配置、AppDelegate、UIViewController這三者,在橫豎屏切換過程的關系是什么?
自動旋轉和手動旋轉有什么區別?
....
僅僅知道切換適配代碼,是無法形成橫豎屏切換理解,也就很難回答上述的問題。
由于沒有找到解釋橫豎屏切換機制的官方文檔,以下根據自己的經驗對這個切換的機制進行分析。
系統如何知道App對界面朝向的支持
這里分兩種情況,App啟動前和App運行時。
App啟動前
在App啟動前進程還未加載,代碼無法運行,系統肯定無法通過AppDelegate或者UIViewController這種代碼的方式獲取橫豎屏的配置。所以在這種情況下,工程配置中的plist描述App對屏幕的適配,就可以很好幫助系統識別應該以什么樣的朝向啟動App。
所以在plist中增加橫屏的支持,好處是開屏能夠支持橫屏,這樣界面展示更加順滑;壞處也是開屏支持了橫屏,導致開屏為橫屏啟動的時候,UIScreen的mainScreen是橫屏的大小,但很多業務邏輯代碼都會以[UIScreen mainScreen]去取屏幕寬度和高度,所以很容易取到錯誤的值。
App運行時
當App進程加載完成,此時系統可以通過運行時詢問的方式,來動態獲取不同時機的界面朝向。
此時AppDelegate控制的是UIWindow層面的朝向,UIViewController控制的是VC層面的朝向。需要注意的是,當我們返回UIViewController的朝向時,還要考慮父容器的朝向。通常一個App的界面層級是UIWindow=>RootViewController(容器vc)=>UIViewController(界面vc)。假如只在UIWindow返回界面朝向也是允許的,就如同上面的實例分析中的push橫屏。
不同界面的朝向控制
還是假設UIWindow=>RootViewController(容器vc)=>UIViewController(界面vc)的層級,且當前ViewController是豎屏vc,現在需要push一個橫屏界面LandscapeViewController。
在每次界面切換的時候,系統都會回調確認新的界面朝向,最終結果為UIWindow朝向、容器vc朝向、界面vc朝向三者的“與”值。那么假如這個值沖突了呢?
假如supportedInterfaceOrientationsForWindow一直返回的豎屏,那么后面VC設置橫屏不會生效;
類似,假如UIWindow設置的是橫屏,那么后面VC設置豎屏也不會生效;
如果在界面切換的過程中發現返回的朝向值未確定,系統更傾向于保持當前朝向不變,并且可能會遇到以下的crash。
Terminating app due to uncaught exception 'UIApplicationInvalidInterfaceOrientation', reason: 'Supported orientations has no common orientation with the application, and [LandscapeViewController shouldAutorotate] is returning YES'
這個原則同樣適用于當返回多個結果,比如說當前界面是豎屏,然后UIWindow和ViewController的界面朝向都支持橫屏和豎屏都支持,那么此時會保持豎屏。
一種比較常用的設計:
// AppDelegate
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
return self.navigationVC.topViewController.supportedInterfaceOrientations;
}
// NavigationController
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return self.topViewController.supportedInterfaceOrientations;
}
// LandscapeViewController
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationLandscapeRight;
}
自動旋轉和手動旋轉
自動旋轉指的是我們旋轉物理設備時,系統會觸發界面的旋轉。當我們從一個豎屏界面push一個橫屏界面時,即使橫屏界面設置了shouldAutorotate=YES,這個界面也不會變成橫屏,但是拿起來設備左右翻轉的時候,會發現隨著設備旋轉,界面也從橫屏變成了豎屏。這就是自動旋轉。
手動旋轉指的是手動觸發旋轉,根據經驗發現有兩個api,UIViewController的+attemptRotationToDeviceOrientation
,還有UIDevice的setOrientation:
可以調整界面朝向。前者是將界面朝向對齊設備朝向,是標準api;后者是調整設備朝向,是私有api。
假如我們在很多個豎屏界面中,需要強制橫屏某一個界面,如果是子界面可以使用present的方式,如果是push那么就必須要用到這個私有api。
注意事項
其他橫豎屏適配方式
1、視圖適配:通過transform修改layer從而在視圖上實現橫屏,但是此時屏幕寬度、狀態欄、安全距離等都保留豎屏狀態,這種方式僅僅適用于橫屏彈窗等部分場景;
2、新建Window:由于App的適配是UIWindow為單位,那么理論上是可以新建一個UIWindow來橫屏的界面;
橫豎屏切換通知
NSNotification通知
[[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceOrientationDidChangeNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
NSLog(@"NSNotification:%@, orientation:%d", note.userInfo, [(UIDevice *)note.object orientation]);
}];
UIViewController回調
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator API_AVAILABLE(ios(8.0));