目錄
一、最讓人糾結的三種枚舉
二、兩種屏幕旋轉的觸發方式
三、屏幕旋轉控制的優先級
四、開啟屏幕旋轉的全局權限
五、開啟屏幕旋轉的局部權限(視圖控制器)
六、實現需求:項目主要界面豎屏,部分界面橫屏
七、默認橫屏無效的問題
八、關于旋轉后的適配問題
九、APP啟動即全屏
一、最讓人糾結的三種枚舉
剛開始接觸屏幕旋轉這塊知識的時候,最讓人抓狂的也許就是三種相關的枚舉類型了,它們就是UIDeviceOrientation、UIInterfaceOrientation、UIInterfaceOrientationMask。下面我們針對三種屬性進行解析:
1. 設備方向:UIDeviceOrientation
UIDeviceOrientation是硬件設備(iPhone、iPad等)本身的當前旋轉方向,設備方向有7種(包括一種未知的情況),判斷設備的方向是以home鍵的位置作為參照的,我們來看一下它們在源碼中的定義如下:
//Portrait 表示縱向,Landscape 表示橫向。
typedef NS_ENUM(NSInteger, UIDeviceOrientation) {
? ? UIDeviceOrientationUnknown,
? ? // Device oriented vertically, home button on the top
? ? UIDeviceOrientationPortraitUpsideDown,
? ? // Device oriented horizontally, home button on the right
? ? UIDeviceOrientationLandscapeLeft,? ? ?
? ? // Device oriented horizontally, home button on the left
? ? UIDeviceOrientationLandscapeRight,? ?
? ? // Device oriented flat, face up
? ? UIDeviceOrientationFaceUp,? ? ? ? ? ?
? ? // Device oriented flat, face down
? ? UIDeviceOrientationFaceDown? ? ? ? ? ?
} __TVOS_PROHIBITED;
設備方向只能取值,不能設置,
獲取設備當前旋轉方向使用方法:[UIDevice currentDevice].orientation
監測設備方向的變化,我們可以在Appdelegate文件中使用通知如下:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onDeviceOrientationDidChange)
? ? ? ? ? ? ? ? ? ? name:UIDeviceOrientationDidChangeNotification
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? object:nil];
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
- (BOOL)onDeviceOrientationDidChange{
? ? //獲取當前設備Device
? ? UIDevice *device = [UIDevice currentDevice] ;
? ? //識別當前設備的旋轉方向
? ? switch (device.orientation) {
? ? ? ? case UIDeviceOrientationFaceUp:
? ? ? ? ? ? NSLog(@"屏幕幕朝上平躺");
? ? ? ? ? ? break;
? ? ? ? case UIDeviceOrientationFaceDown:
? ? ? ? ? ? NSLog(@"屏幕朝下平躺");
? ? ? ? ? ? break;
? ? ? ? case UIDeviceOrientationUnknown:
? ? ? ? ? ? //系統當前無法識別設備朝向,可能是傾斜
? ? ? ? ? ? NSLog(@"未知方向");
? ? ? ? ? ? break;
? ? ? ? case UIDeviceOrientationLandscapeLeft:
? ? ? ? ? ? NSLog(@"屏幕向左橫置");
? ? ? ? ? ? break;
? ? ? ? case UIDeviceOrientationLandscapeRight:
? ? ? ? ? ? NSLog(@"屏幕向右橫置");
? ? ? ? ? ? break;
? ? ? ? case UIDeviceOrientationPortrait:
? ? ? ? ? ? NSLog(@"屏幕直立");
? ? ? ? ? ? break;
? ? ? ? case UIDeviceOrientationPortraitUpsideDown:
? ? ? ? ? ? NSLog(@"屏幕直立,上下顛倒");
? ? ? ? ? ? break;
? ? ? ? default:
? ? ? ? ? ? NSLog(@"無法識別");
? ? ? ? ? ? break;
? ? }
? ? return YES;
}
2.頁面方向:UIInterfaceOrientation
UIInterfaceOrientation程序界面的當前旋轉方向(可以設置),其源碼的定義如下:
// Note that UIInterfaceOrientationLandscapeLeft is equal to UIDeviceOrientationLandscapeRight (and vice versa).
// This is because rotating the device to the left requires rotating the content to the right.
typedef NS_ENUM(NSInteger, UIInterfaceOrientation) {
? ? UIInterfaceOrientationUnknown? ? ? ? ? ? ? = UIDeviceOrientationUnknown,
? ? UIInterfaceOrientationPortrait? ? ? ? ? ? ? = UIDeviceOrientationPortrait,
? ? UIInterfaceOrientationPortraitUpsideDown? ? = UIDeviceOrientationPortraitUpsideDown,
? ? UIInterfaceOrientationLandscapeLeft? ? ? ? = UIDeviceOrientationLandscapeRight,
? ? UIInterfaceOrientationLandscapeRight? ? ? ? = UIDeviceOrientationLandscapeLeft
? ? } __TVOS_PROHIBITED;
區別與UIDeviceOrientation,表示我們開發的程序界面的方向使用UIInterfaceOrientation。
值得注意的是:
UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,
UIInterfaceOrientationLandscapeRight = UIDeviceOrientationLandscapeLeft
1
2
我們可以發現兩者的枚舉值大多是可以對應上的。只有左右旋轉的時候是UIInterfaceOrientationLandscapeLeft 與UIDeviceOrientationLandscapeRight相等,反之亦然,這是因為向左旋轉設備需要旋轉程序界面右邊的內容。
3.頁面方向:UIInterfaceOrientationMask
UIInterfaceOrientationMask是iOS6之后增加的一種枚舉,其源碼如下:
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),
} __TVOS_PROHIBITED;
我們知道UIDeviceOrientation與UIInterfaceOrientation的區別在于:前者是真實的設備方向,后者是頁面方向。
而UIInterfaceOrientation和UIInterfaceOrientationMask的區別是什么呢?其實觀察源碼,我們就會發現這是一種為了支持多種UIInterfaceOrientation而定義的類型。下面的示例將很好的說明這點:
在iOS6之后,控制單個界面的旋轉我們通常是下面三個方法來控制:
//方法1
- (BOOL)shouldAutorotate NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
//方法2
- (UIInterfaceOrientationMask)supportedInterfaceOrientations NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
// Returns interface orientation masks.
//方法3
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
方法2的作用是設置當前界面支持的所有方向,所以返回值是UIInterfaceOrientationMask,更加方便的表達支持多方向旋轉的情況。
方法3的作用是設置進入界面默認支持的方向,使用了返回值類型UIInterfaceOrientation,默認進入界面的方向是個確定的方向,所以使用UIInterfaceOrientation更適合。
二、兩種屏幕旋轉的觸發方式
我們開發的App的,大多情況都是大多界面支持豎屏,幾個特別的界面支持旋轉橫屏,兩種界面相互切換,觸發其旋轉有兩種情況:
情況1:系統沒有關閉自動旋轉屏幕功能,
這種情況,支持旋轉的界面跟隨用戶手持設備旋轉方向自動旋轉。我們需要在當前視圖控制器中添加如下方法:
//1.決定當前界面是否開啟自動轉屏,如果返回NO,后面兩個方法也不會被調用,只是會支持默認的方向
- (BOOL)shouldAutorotate {
? ? ? return YES;
}
//2.返回支持的旋轉方向
//iPad設備上,默認返回值UIInterfaceOrientationMaskAllButUpSideDwon
//iPad設備上,默認返回值是UIInterfaceOrientationMaskAll
- (UIInterfaceOrientationMask)supportedInterfaceOrientations{
? ? return UIInterfaceOrientationMaskAll;
}
//3.返回進入界面默認顯示方向
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
? ? return UIInterfaceOrientationPortrait;
}
情況2:單個界面強制旋轉
在程序界面通過點擊等方式切換到橫屏(尤其是視頻播放的情況),有以下兩種方法:
// 方法1:
- (void)setInterfaceOrientation:(UIDeviceOrientation)orientation {
? ? ? if ([[UIDevice currentDevice]? respondsToSelector:@selector(setOrientation:)]) {
? ? ? ? ? [[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:orientation]? ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? forKey:@"orientation"];
? ? ? ? }
? ? }
//方法2:
- (void)setInterfaceOrientation:(UIInterfaceOrientation)orientation {
? if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
? ? ? ? ? ? SEL selector = NSSelectorFromString(@"setOrientation:");
? ? ? ? ? ? NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice? ?
? ? ? ? instanceMethodSignatureForSelector:selector]];
? ? ? ? ? ? [invocation setSelector:selector];
? ? ? ? ? ? [invocation setTarget:[UIDevice currentDevice]];
? ? ? ? ? ? int val = orientation;
? ? ? ? ? ? [invocation setArgument:&val atIndex:2];
? ? ? ? ? ? [invocation invoke];
? ? ? ? }
? ? }
注意:使用這兩個方法的時候,也要確保shouldAutorotate方法返回YES,這樣這兩個方法才會生效。還要注意兩者使用的參數類型不同。
三、屏幕旋轉控制的優先級
事實上,如果我們只用上面的方法來控制旋轉的開啟與關閉,并不能符合我們的需求,而且方法無效。這是因為我們忽略了旋轉權限優先級的問題。關于屏幕旋轉的設置有很多,有Xcode的General設置,也有info.plist設置,更還有代碼設置等,這么多的設置很是繁雜。但是這些其實都是在不同級別上實現旋轉的設置,我們會遇到設置關閉旋轉無效的情況,這就很可能是被上一級別控制的原因。
我們首先有個大致的了解,控制屏幕旋轉優先級為:工程Target屬性配置(全局權限) = Appdelegate&&Window > 根視圖控制器> 普通視圖控制器
四、開啟屏幕旋轉的全局權限
這里我使用全局權限來描述這個問題可能不太準備,其實是設置我們的設備能夠支持的方向有哪些,這也是實現旋轉的前提。
開啟屏幕旋轉的全局權限有三種方法,包括通過Xcode直接配置的兩種方法和代碼控制的一種方法。這三種方法作用相同,但是由于代碼的控制在程序啟動之后,所以也是最有效的。下面分別對三種方法的用法介紹:
1.Device Orientation屬性配置
我們創建了新工程,Xcode就默認替我們選擇了支持旋轉的幾個方向,這就是Device Orientation屬性的默認配置。在Xcode中依次打開:【General】—>【Deployment Info】—>【Device Orientation】,我們可以看到默認支持的設備方向如下:
可以發現,UpsideDown沒有被默認支持,因為對于iPhone即使勾選也沒有UpSideDown的旋轉效果。我們可以在這里勾選或者取消以修改支持的旋轉方向。如果是iPad設備勾選之后會同時支持四個方向。
值得注意的是,對于iPhone,如果四個屬性我們都選或者都不選,效果和默認的情況一樣。
2.Info.Plist設置
其實我們設置了Device Orientation之后,再到info.plist中查看Supported interface orientation,我們會看到:
沒錯,此時Supported interface orientation里的設置和UIDevice Orientation的值一致的,并且我們在這里增加或者刪除其中的值,UIDevice Orientation的值也會隨之變化,兩者屬于同一種設置。
3.Appdelegate&&Window中設置
正常情況下,我們的App從Appdelegate中啟動,而Appdelegate所持有唯一的Window對象是全局的,所以在Appdelegate文件中設置屏幕旋轉也是全局有效的。下面的代碼設置了只支持豎屏和右旋轉:
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
? ? return? UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft;
}
值得注意的是:如果我們實現了Appdelegate的這一方法,那么我們的App的全局旋轉設置將以這里的為準,即使前兩種方法的設置與這里的不同。
五、開啟屏幕旋轉的局部權限(視圖控制器)
在設置了全局所支持的旋轉方向后,接著就開始設置具體的控制器界面了。我們在上面已經說明了關于旋轉的優先級了。而這里主要涉及了三種視圖控制器(UITabbarViewController,UINavigationBarController ,UIViewController)
自全局權限開啟之后,接下來具有最高權限的就是Window的根視圖控制器rootViewController了。如果我們要具體控制單個界面UIViewController的旋轉就必須先看一下根視圖控制器的配置情況了。
當然,在一般情況下,我們的項目都是用UITabbarViewController作為Window的根視圖控制器,然后管理著若干個導航控制器UINavigationBarController,再由導航欄控制器去管理普通的視圖控制器UIViewController。若以此為例的話,關于旋轉的優先級從高到低就是UITabbarViewController>UINavigationBarController >UIViewController了。如果具有高優先級的控制器關閉了旋轉設置,那么低優先級的控制器是無法做到旋轉的。
比如說我們設置要單個視圖控制器可以自動旋轉,這需要在視圖控制器中增加shouldAutorotate方法返回YES或者NO來控制。但如果存在上層根視圖控制器,而我們只在這個視圖控制器中實現方法,會發現這個方法是不走的,因為這個方法被上層根視圖控制器攔截了。理解這個原理后,我們有兩種方法實現自動可控的旋轉設置。
方法1:逐級設置各視圖控制器,高優先級的視圖控制器影響低優先級控制器,
解決上述的問題我們需要設置UITabbarViewController如下:
//是否自動旋轉
-(BOOL)shouldAutorotate{
? ? return self.selectedViewController.shouldAutorotate;
}
//支持哪些屏幕方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
? ? return [self.selectedViewController supportedInterfaceOrientations];
}
//默認方向
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation{
? ? return [self.selectedViewController preferredInterfaceOrientationForPresentation];
}
設置導航控制器UINavigationController如下:
//是否自動旋轉
//返回導航控制器的頂層視圖控制器的自動旋轉屬性,因為導航控制器是以棧的原因疊加VC的
//topViewController是其最頂層的視圖控制器,
-(BOOL)shouldAutorotate{
? ? return self.topViewController.shouldAutorotate;
}
//支持哪些屏幕方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
? ? return [self.topViewController supportedInterfaceOrientations];
}
//默認方向
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation{
? ? return [self.topViewController preferredInterfaceOrientationForPresentation];
}
到這里,我們就應該明白了,其實就是高優先級的視圖控制器要跟隨低優先級控制器的旋轉配置。這樣就能夠達到目的。
方法2: 另辟蹊徑,使用模態視圖
使用模態視圖可以不受這種根視圖控制器優先級的限制。這個也很容易理解,模態彈出的視圖控制器是隔離出來的,不受根視圖控制的影響。具體的設置和普通視圖器代碼相同,這里就不累述了。
六、實現需求:項目主要界面豎屏,部分界面橫屏
這其實也是一個我們做屏幕旋轉最常見的需求,在根據上面的講述之后,我們實現這個需求會很容易,但是具體的實現卻有著不同的思路,我在這里總結了兩種方法:
方法1:使用基類控制器逐級控制
步驟:
1.開啟全局權限設置項目支持的旋轉方向
2.根據第五節中的方法1,自定義標簽控制器和導航控制器來設置屏幕的自動旋轉。
3.自定義基類控制器設置不支持自動轉屏,并默認只支持豎屏
4.對項目中需要轉屏幕的控制器開啟自動轉屏、設置支持的旋轉方向并設置默認方向
demo1鏈接: https://github.com/DreamcoffeeZS/Demo_TestRotatesOne.git
方法2:Appdelegate增設旋轉屬性
步驟:
1.在Applegate文件中增加一個用于記錄當前屏幕是否橫屏的屬性
2.需要橫屏的界面,進入界面后強制橫屏,離開界面時恢復豎屏
demo2鏈接: https://github.com/DreamcoffeeZS/Demo_TestRotatesTwo.git
七、默認橫屏無效的問題
在上面的項目中,我們可能會遇到一個關于默認橫屏的問題,把它拿出來細說一下。
我們項目中有支持豎屏的界面A,也有支持橫豎屏的界面B,而且界面B需要進入時就顯示橫屏。從界面A到界面B中,如果我們使用第五節中的方法1會遇到無法顯示默認橫屏的情況,因為沒有旋轉設備,shouldAutorotate就沒被調用,也就沒法顯示我們需要的橫屏。這里有兩個解決方法:
方法1:在自定義導航控制器中增加以下方法
#pragma mark -UINavigationControllerDelegate
//不要忘記設置delegate
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
? ? [self presentViewController:[UIViewController new] animated:NO completion:^{
? ? ? ? [self dismissViewControllerAnimated:NO completion:nil];
? ? }];
}
這個方法的缺點是,原理上利用彈出模態視圖來調用轉屏,造成切換界面的時候有閃爍效果,體驗不佳。所以這里也只是提供一種思路,不推薦使用。
方法2:在需要默認橫屏的界面里設置,進入時強制橫屏,離開時強制豎屏
關于這種使用,這個具體可以參考第五節中的demo2
注:兩種方法不可同時使用
八、關于旋轉后的適配問題
屏幕旋轉的實現會帶來相應的UI適配問題,我們需要針對不同方向下的界面重新調整視圖布局。首先我們要能夠監測到屏幕旋轉事件,這里分為兩種情況:
1.視圖控制器UIViewController里的監測
當發生轉屏事件的時候,下面的UIViewControoller方法會監測到視圖View的大小變化,從而幫助我們適配
/*
This method is called when the view controller's view's size is
changed by its parent (i.e. for the root view controller when its window rotates or is resized).
If you override this method, you should either call super to
propagate the change to children or manually forward the
change to children.
*/
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator NS_AVAILABLE_IOS(8_0);
從注釋里可以看出此方法在屏幕旋轉的時候被調用,我們使用時候也應該首先調用super方法,具體代碼使用示例如下:
//屏幕旋轉之后,屏幕的寬高互換,我們借此判斷重新布局
//橫屏:size.width > size.height
//豎屏: size.width < size.height
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator{
? ? [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
? ? if (size.width > size.height) {
? ? ? ? //橫屏設置,為防止遮擋鍵盤,調整輸入視圖的高度
? ? ? ? self.textView_height.constant = 50;
? ? }else{
? ? ? ? //豎屏設置
? ? ? ? self.textView_height.constant = 200;
? ? }
}
2.子視圖橫豎屏監測
如果是類似于表視圖的單元格,要監測到屏幕變化實現適配,我們需要用到layoutSubviews方法,因為屏幕切換橫豎屏時會觸發此方法,然后我們根據狀態欄的位置就可以判斷橫豎屏了,代碼示例如下:
- (void)layoutSubviews {
? ? [super layoutSubviews];
? ? //通過狀態欄電池圖標判斷橫豎屏
? ? if ([UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationMaskPortrait) {
? ? ? ? //豎屏布局
? ? } else {
? ? ? ? //橫屏布局
? ? }
}
九、APP啟動即全屏
有時項目需要從App啟動就默認是橫屏,這里有個很方便的方法,就是我們在Device Orientation屬性配置里設置如下:
但是只這樣處理的話,會讓項目只支持橫屏,所以我們可以在Appdelegate里再次調整我們所支持的方向,方法已經說過,這里就不累述了。
---------------------