iOS 屏幕旋轉(zhuǎn)/橫豎屏切換適配

前言

現(xiàn)在大部分的智能移動設備通過自動旋轉(zhuǎn),能夠自動切換去呈現(xiàn)最適合當前屏幕顯示的內(nèi)容,無疑大大提升了使用者的用戶體驗。不過作為開發(fā)者,想要達到完美的適配效果,還是要下一番功夫鉆研嘗試才能做得的。筆者就根據(jù)自己適配屏幕自動旋轉(zhuǎn)的工作經(jīng)驗,在此做一點總結(jié)。

硬件原理

為了檢測設備(最關鍵的就是面子——屏幕)當前在三維空間中的朝向,現(xiàn)在的智能設備都內(nèi)置了加速計。這一部分完全參照來源【1】:

通過感知特定方向的慣性力總量,加速計可以測量出加速度和重力,ios設備內(nèi)的加速計是一個三軸加速計,這意味著它能夠檢測出三維空間中的運動或重力引力。因此加速計不但可以指示握持電話的方式(如自動旋轉(zhuǎn)功能),而且如果電話放在桌子上的話還可以指示電話的正面朝上還是朝下。

加速計可以測量g引力(g代表重力),因此加速計返回值為1.0時,表示在特定的方向上感知到1g。

  • 如果是靜止握持iphone而沒有任何運動,那么地球引力對其施加的力大約為1g
  • 如果是縱向豎直握持,那么設備會檢測并報告在其y軸上施加的力大約為1g
  • 如果是以一定的角度握持,那么1g的力會分布到不同的軸上,這取決于握持的方式,在以45度握持時,1g的力會均勻的分解到兩個軸上。如果檢測到加速計值遠大于1g,那么可以判斷是突然運動,,正常使用時加速計在任何一個軸上都不會檢測到遠大于1g的值,如果搖動、墜落或投擲設備,那么加速計便會在一個或多個軸上檢測到很大的力。

下圖所示加速計所使用的三軸結(jié)構(gòu)


當然,如今的智能手機里往往不光內(nèi)置了加速計,往往還有陀螺儀。這一方面的知識就由大家自行去挖掘吧,很多游戲都是利用它去實現(xiàn)很自然的操作感。

軟件適配

朝向定義

既然硬件能獲取到當前屏幕的朝向,蘋果的SDK也一定會為開發(fā)者提供接口指定有哪些朝向可選,以及如何獲取到當前朝向。在 UIDevice.h 以及 UIApplication.h 中可見其定義如下:

7種設備朝向:

typedef NS_ENUM(NSInteger, UIDeviceOrientation) {
    UIDeviceOrientationUnknown,
    UIDeviceOrientationPortrait,            // Device oriented vertically, home button on the bottom
    UIDeviceOrientationPortraitUpsideDown,  // Device oriented vertically, home button on the top
    UIDeviceOrientationLandscapeLeft,       // Device oriented horizontally, home button on the right
    UIDeviceOrientationLandscapeRight,      // Device oriented horizontally, home button on the left
    UIDeviceOrientationFaceUp,              // Device oriented flat, face up
    UIDeviceOrientationFaceDown             // Device oriented flat, face down
} __TVOS_PROHIBITED;

5種界面朝向:

// 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;

可見二者的枚舉值相互之間對應得上。

另外還有可組合使用的OrientationMask定義,通常在頁面聲明支持的朝向時用到,后面再展開討論。

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;

朝向獲取和設置

有了朝向的定義,該如何獲取當前的朝向取值呢?
如果是要獲取設備朝向,可以直接通過 UIDevice 實例的屬性

// return current device orientation.  this will return UIDeviceOrientationUnknown unless device orientation notifications are being generated.
@property(nonatomic,readonly) UIDeviceOrientation orientation __TVOS_PROHIBITED;       

需要注意注釋的內(nèi)容,也就是必須首先在 UIDevice 朝向通知生成之后才可以正常獲取朝向數(shù)據(jù)。

也就是要監(jiān)聽UIDevice拋出的系統(tǒng)通知 UIDeviceOrientationDidChangeNotification

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

不過這里其實有一點小坑,那就是還有一對關鍵的接口蘋果沒有直接告訴你,那就是

- (void)beginGeneratingDeviceOrientationNotifications __TVOS_PROHIBITED;      // nestable
- (void)endGeneratingDeviceOrientationNotifications __TVOS_PROHIBITED;

必須要在調(diào)用前者之后,才會在每次設備朝向變化時觸發(fā) UIDeviceOrientationDidChangeNotification 通知。
不過沒有必要的話,也要及時調(diào)用后者去結(jié)束對加速計數(shù)據(jù)的獲取,默默的為用戶電池續(xù)航助力。


類似的,也同樣可以通過監(jiān)聽下面兩個通知去獲取UIInterfaceOrientation的變化:

UIKIT_EXTERN NSString *const UIApplicationWillChangeStatusBarOrientationNotification __TVOS_PROHIBITED; // userInfo contains NSNumber with new orientation
UIKIT_EXTERN NSString *const UIApplicationDidChangeStatusBarOrientationNotification __TVOS_PROHIBITED;  // userInfo contains NSNumber with old orientation

二者的差異關鍵是在notification的userInfo中攜帶的值,一個是新的朝向值,一個是舊的朝向值,可不要搞反了哦。

再有是通過UIApplication的下面這個屬性也可以獲取界面朝向。

// Explicit setting of the status bar orientation is more limited in iOS 6.0 and later.
@property(readwrite, nonatomic) UIInterfaceOrientation statusBarOrientation NS_DEPRECATED_IOS(2_0, 9_0) __TVOS_PROHIBITED;

有的同學可能會有疑問,DeviceOrientation 和 StatusBarOrientation是否可以等同使用?關于這個問題,有句話說的好:

紙上得來終覺淺,絕知此事要躬行

動手試一試就會明白,二者實則有著本質(zhì)不同。

真相在此:前者是指示設備朝向,而后者則是指示當前界面中狀態(tài)欄的朝向;在[UIDevice beginGeneratingDeviceOrientationNotifications]之后,每次設備旋轉(zhuǎn),都會有UIDeviceOrientationDidChangeNotification的通知生成,而 UIApplicationWillChangeStatusBarOrientationNotification 則是當前顯示controller支持對應的InterfaceOrientation時才會觸發(fā)。

所以可能會出現(xiàn)這種情況,DeviceOrientation 值 為UIDeviceOrientationLandscapeLeft,但InterfaceOrientation 值卻是 UIInterfaceOrientationPortrait,下圖就是典型的例子:


另外,某些應用場景下,還需要去手動設置屏幕旋轉(zhuǎn),比如播放器往往都既支持自動旋轉(zhuǎn)屏幕去切換全屏播放,同時也允許用戶去手動切換全屏或小屏播放。但翻看了半天API描述和文檔,要么就是不提供接口,要么就是警告設置受限,那要怎么做呢?其實很簡單,只要兩行代碼搞定:

NSNumber *value = @(UIInterfaceOrientationPortrait);//或者別的想要的值
[[UIDevice currentDevice] setValue:value forKey:@"orientation"];

App及頁面適配

  • App全局配置

App中全局配置支持朝向的地方,最方便的就是在工程的Target中了,如圖所示:

理所當然全局配置其優(yōu)先級當然是最高的,即使某個頁面聲明支持某Orientation,但全局配置中并沒有選中對應的Device Orientation,是不會起效的。

  • 單個頁面配置
    具體到某個頁面(controller)層級的配置,UIViewController提供了如下的回調(diào)方法

      // New Autorotation support.
      - (BOOL)shouldAutorotate NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
      - (UIInterfaceOrientationMask)supportedInterfaceOrientations NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
      // Returns interface orientation masks.
      - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
    

第一個方法在首次進入controller以及屏幕方向未鎖定且觸發(fā)旋轉(zhuǎn)時會被系統(tǒng)調(diào)用(且不重寫的話,默認返回值為YES),如果返回NO,那么表明該頁面不支持對屏幕旋轉(zhuǎn)做適配;若返回YES,則表明支持旋轉(zhuǎn),但具體適配了哪幾個朝向,則依賴于supportedInterfaceOrientations 方法的返回值,也就是UIInterfaceOrientationMask類型的Option組合。

看起來并不復雜對不對?在設定了App的全局配置,并在相應的controller中實現(xiàn)了這些回調(diào)之后發(fā)現(xiàn),有同學可能會失望地發(fā)現(xiàn),設備旋轉(zhuǎn)時這些方法卻并沒有期望地那樣被調(diào)到,這是為什么呢?

通過反復驗證,發(fā)現(xiàn)其實系統(tǒng)確實會調(diào)用這個方法,但默認執(zhí)行粒度是到系統(tǒng)級的 Container View Controller(UINavigationController/UITabBarController)為止(其實直接掛在UIWindow上作為其rootViewController的UIViewController對象的 shouldAutoRotate 方法也會得到調(diào)用,但畢竟大多數(shù)情況下,我們不會用這么簡單的組合結(jié)構(gòu)的)。所以我們額外需要實現(xiàn)的一步,就是轉(zhuǎn)發(fā)這個調(diào)用消息到我們真正想要處理的那個controller上。當然,可以通過hook系統(tǒng)類的對應方法去做實現(xiàn),但筆者采用的是在自行定義的UINavigationController繼承類中重寫這些方法:

#pragma mark Orientation

- (BOOL)shouldAutorotate
{
    BOOL shouldAutorotate = NO;
    UIViewController *viewController;
    if (IOS_VERSION_FLOAT_VALUE >= 8.0)
    {
        viewController = [self visibleViewController];
    }
    else
    {
        viewController = [self topViewController];
    }
    
    if ([viewController isKindOfClass:K12RootViewController.class] && ((K12RootViewController *)viewController).visibleNav) {
        viewController = ((K12RootViewController *)viewController).visibleNav;
    }
    
    if (viewController.ht_currentChildViewController) {
        viewController = viewController.ht_currentChildViewController;
    }
    
    
    if ([viewController isKindOfClass:[UIViewController class]])
    {
        shouldAutorotate = [(UIViewController *)viewController shouldAutorotate];
    }
    
    //彈框也要支持旋轉(zhuǎn)
    if ([viewController isKindOfClass:K12PlayerController.class] || ((IOS_VERSION_FLOAT_VALUE >= 8.0) ? [viewController isKindOfClass:UIAlertController.class] : NO)) {
        return YES;
    }
    else {
        return NO;
    }
    return shouldAutorotate;;
}

- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{    
    NSUInteger supportedInterfaceOrientations = UIInterfaceOrientationMaskPortrait;
    UIViewController *viewController;
    if (IOS_VERSION_FLOAT_VALUE >= 8.0)
    {
        viewController = [self visibleViewController];
    }
    else
    {
        viewController = [self topViewController];
    }
    
    if ([viewController isKindOfClass:K12RootViewController.class] && ((K12RootViewController *)viewController).visibleNav) {
        viewController = ((K12RootViewController *)viewController).visibleNav;
    }
    
    if (viewController.ht_currentChildViewController) {
        viewController = viewController.ht_currentChildViewController;
    }

    //向UIAlertController發(fā)送supportedInterfaceOrientations消息會crash……
    if ([viewController isKindOfClass:UIAlertController.class]) {
        return UIInterfaceOrientationMaskAllButUpsideDown;
    }
    
    if ([viewController isKindOfClass:[UIViewController class]])
    {
        supportedInterfaceOrientations = [(UIViewController *)viewController supportedInterfaceOrientations];
    }
    
    return supportedInterfaceOrientations;
}

可以看到其中有各種各樣case的處理,原因就是除了播放頁面支持豎屏、左橫屏以及右橫屏(UIInterfaceOrientationMaskAllButUpsideDown)之外,我們產(chǎn)品中的其他頁面都是只支持橫屏顯示的(UIInterfaceOrientationMaskPortrait),同時在當前頁面上有UIAlertController(iOS8 之后)彈出時,也要設置其支持跟隨屏幕旋轉(zhuǎn)。

類似的,如果 UIWindow 對象的 rootViewController 是 UITabBarController 的話,則需要轉(zhuǎn)發(fā)消息給其 selectedViewController 屬性對象,具體實現(xiàn)就不再贅言啦。

  • 踩過的坑

說起來,項目開發(fā)中不踩點坑簡直對不起程序猿這個title啊 —— 前面提到過

...掛在UIWindow上作為其rootViewController的UIViewController對象的 shouldAutoRotate 方法也會得到調(diào)用

這里往往會隱藏一個問題,默認在AppDelegate.m中,我們會這樣做:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    ...
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    
    self.k12RootController = [[K12RootViewController alloc] init];
    K12NavigationController *navController = [[K12NavigationController alloc] initWithRootViewController: self.k12RootController];
    
    self.window.rootViewController = navController;
    
    [self.window makeKeyAndVisible];
    ...
}

當然,這看起來沒有問題。但是假如App中還存在別的 UIWindow 對象呢?旋轉(zhuǎn)時,它的rootViewController 的 shouldAutoRotate 方法也將被調(diào)用,若沒有重寫過,則其默認返回YES;如果與其他 UIWindow對象(特別是keyWindow) 所呈現(xiàn)的最頂部頁面的返回值不一致,就會出現(xiàn)一些神奇的表現(xiàn),如下圖所示:

切換到橫屏下時,狀態(tài)欄居然消失了!!該情況的出現(xiàn),就是因為在該答題頁面的上一個頁面(播放頁面)中使用了一個第三方組件去繪制Menu,而其設計存在瑕疵,在生成Menu對象而非顯示時就已經(jīng)生成了一個UIWindow對象并持有了它。然后在進入答題頁面時,雖然對應的controller的 shouldAutoRotate 方法返回了 NO,但Menu對應的UIWindow對象其rootViewController默認返回YES,導致出現(xiàn)頁面保持豎屏顯示,但狀態(tài)欄響應了旋轉(zhuǎn)的奇怪現(xiàn)象。

這個問題最終還是通過hook掉 UIViewController 的shouldAutoRotate 方法,去追蹤究竟是哪個controller對象返回了默認值 YES 才最終大白天下。這也提醒我們,對開源庫的品質(zhì)也是謹慎對待的,往往太復雜業(yè)務場景,還是需要自己去定制功能才能滿足。

總結(jié)

這篇文章也算是在參與某產(chǎn)品開發(fā)過程中,屏幕旋轉(zhuǎn)適配過程中,踩了不少坑之后經(jīng)驗教訓的一個總結(jié)。當然,想要實現(xiàn)頁面的橫豎屏切換效果,并不是只有這一條路徑,還可以通過UIView的transform屬性去實現(xiàn),不過那就是另一個話題啦 。

@property(nonatomic) CGAffineTransform transform;   // default is CGAffineTransformIdentity. animatable

ヾ( ̄▽ ̄)ByeBye

參考資料

【1】ios 關于屏幕旋轉(zhuǎn)和屏幕晃動

【2】iOS指定頁面屏幕旋轉(zhuǎn),手動旋轉(zhuǎn)(某app實現(xiàn)功能全過程)

【3】iOS: Using UIDeviceOrientation to Determine Orientation

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • [這是第11篇] 導語: iOS App中大多數(shù)頁面是只展示豎屏下的效果,但是少部分頁面需要支持橫豎屏。本文分別介...
    南華coder閱讀 14,614評論 18 93
  • iOS屏幕旋轉(zhuǎn)學習筆記iOS開發(fā)中使用屏幕旋轉(zhuǎn)功能的相關方法 1、基本知識點解讀 了解屏幕旋轉(zhuǎn)首先需要區(qū)分兩種 o...
    Laughingg閱讀 13,770評論 13 39
  • 一: 序 二: 基本知識點了解 三: 屏幕旋轉(zhuǎn)流程 四: 屏幕旋轉(zhuǎn)設置 四: 結(jié)言 序: 這幾天項目要適...
    Luyc_Han閱讀 594評論 0 1
  • 時光匆匆流去一別竟是半生回憶想起您循循善誘的教誨想起您如沐春風的話語想起您在燈下批閱作業(yè)的身影想起您在三尺講臺上揮...
    哲語細細閱讀 369評論 26 32
  • “標題真的好難取??!” 幾乎每個人都遇到過這樣的困惑。 不可否認的是,一篇文章的標題是否足夠吸引人眼球,很大程度決...
    元気少女的后花園閱讀 346評論 3 1

友情鏈接更多精彩內(nèi)容