IOS屏幕旋轉適配

原文地址:http://blog.anbig.com/2019/08/24/ios屏幕旋轉適配/

一、最讓人糾結的三種枚舉
二、兩種屏幕旋轉的觸發方式
三、屏幕旋轉控制的優先級
四、開啓屏幕旋轉的全局權限
五、開啓屏幕旋轉的局部權限 (視圖控制器)
六、實現需求:項目主要界面豎屏,部分界面橫屏
七、默認橫屏無效的問題
八、關於旋轉後的適配問題
九、APP 啓動即全屏

剛開始接觸屏幕旋轉這塊知識的時候,最讓人抓狂的也許就是三種相關的枚舉類型了,它們就是 UIDeviceOrientation、UIInterfaceOrientation、UIInterfaceOrientationMask。下面我們針對三種屬性進行解析:

1. 設備方向:UIDeviceOrientation

UIDeviceOrientation 是硬件設備 (iPhone、iPad 等) 本身的當前旋轉方向,設備方向有 7 種(包括一種未知的情況),判斷設備的方向是以 home 鍵的位置作爲參照的,源碼中的定義:

typedef NS_ENUM(NSInteger, UIDeviceOrientation) {

     UIDeviceOrientationUnknown,

     UIDeviceOrientationPortrait,           

     UIDeviceOrientationPortraitUpsideDown, 

     UIDeviceOrientationLandscapeLeft,      

     UIDeviceOrientationLandscapeRight,     

     UIDeviceOrientationFaceUp,             

     UIDeviceOrientationFaceDown            

   } __TVOS_PROHIBITED;

設備方向只能取值, 不能設置,
獲取設備當前旋轉方向使用方法:[UIDevice currentDevice].orientation

監測設備方向的變化,我們可以在 Appdelegate 文件中使用通知如下:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onDeviceOrientationDidChange)
                     name:UIDeviceOrientationDidChangeNotification
                                               object:nil];

[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];

 - (BOOL)onDeviceOrientationDidChange{

    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 程序界面的當前旋轉方向 (可以設置),源碼中的定義如下:


    typedef NS_ENUM(NSInteger, UIInterfaceOrientation) {

        UIInterfaceOrientationUnknown               = UIDeviceOrientationUnknown,

        UIInterfaceOrientationPortrait              = UIDeviceOrientationPortrait,

        UIInterfaceOrientationPortraitUpsideDown    = UIDeviceOrientationPortraitUpsideDown,

        UIInterfaceOrientationLandscapeLeft         = UIDeviceOrientationLandscapeRight,

        UIInterfaceOrientationLandscapeRight        = UIDeviceOrientationLandscapeLeft

    } __TVOS_PROHIBITED;

區別與 UIDeviceOrientation,表示我們開發的程序界面的方向使用 UIInterfaceOrientation。

UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight, 
UIInterfaceOrientationLandscapeRight = UIDeviceOrientationLandscapeLeft

我們可以發現兩者的枚舉值大多是可以對應上的。只有左右旋轉的時候是 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 之後,控制單個界面的旋轉我們通常是下面三個方法來控制:

- (BOOL)shouldAutorotate NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;

- (UIInterfaceOrientationMask)supportedInterfaceOrientations NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;

方法 2 的作用是設置當前界面支持的所有方向,所以返回值是 UIInterfaceOrientationMask,更加方便的表達支持多方向旋轉的情況。

方法 3 作用是設置進入界面默認支持的方向,使用了返回值類型 UIInterfaceOrientation,默認進入界面的方向是個確定的方向,所以使用 UIInterfaceOrientation 更適合。

我們開發的 App 的,大多情況都是大多界面支持豎屏,幾個特別的界面支持旋轉橫屏,兩種界面相互切換,觸發其旋轉有兩種情況:

情況 1:系統沒有關閉自動旋轉屏幕功能,

這種情況,支持旋轉的界面跟隨用戶手持設備旋轉方向自動旋轉。我們需要在當前視圖控制器中添加如下方法:

- (BOOL)shouldAutorotate {
      return YES;
}

- (UIInterfaceOrientationMask)supportedInterfaceOrientations{
     return UIInterfaceOrientationMaskAll;
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
     return UIInterfaceOrientationPortrait;
}

情況 2:單個界面強制旋轉

在程序界面通過點擊等方式切換到橫屏 (尤其是視頻播放的情況),有以下兩種方法:

- (void)setInterfaceOrientation:(UIDeviceOrientation)orientation {
      if ([[UIDevice currentDevice]   respondsToSelector:@selector(setOrientation:)]) {
          [[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:orientation]     
                                       forKey:@"orientation"];
        }
    }

- (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 如下:

-(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

- (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 方法,具體代碼使用示例如下:

- (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 啓動就默認是橫屏,這裏有個很方便的方法,就是我們在 Device Orientation 屬性配置裏設置如下:

 

但是隻這樣處理的話,會讓項目只支持橫屏,所以我們可以在 Appdelegate 裏再次調整我們所支持的方向,方法已經說過,這裏就不累述了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章