【轉】從viewController講到強制橫屏,附IOS5強制橫屏的有效辦法


文字羅嗦,篇幅較長,只需營養可直接看紅字部分。

一個viewController的初始化大概涉及到如下幾個方法的調用: 

initWithNibName:bundle:

viewDidLoad

viewWillAppear:animated:

viewDidAppear:animated:

viewWillLayoutSubviews

viewDidLayoutSubviews

通常情況幾個方法是依次被調用的,我們會在init方法中初始化一些成員變量,做一些與view無關的事情。而后在viewDidLoad中進行view布局相關的屬性調整,比如改變一下背景顏色,增加一些subview之類的。不知道大家有沒有想過,這樣不在init中寫view相關代碼是為了什么?難道僅僅是為了代碼結構清晰?如果我非要在init做一些與view相關的初始化工作,能不能實現?有什么問題?

@implementation  testViewController

- ( void) printFrame:( CGRect) frame  name:( NSString  *) name
{
    NSLog( @"%@ :(%f, %f, %f, %f)" ,  name ,  frame . origin . x ,  frame . origin . y , frame . size . width ,  frame . size . height);
}

- ( id) initWithNibName:( NSString  *) nibNameOrNil  bundle:( NSBundle  *) nibBundleOrNil
{
    self  =  [ super  initWithNibName: nibNameOrNil  bundle: nibBundleOrNil ];
    if ( self{
        // Custom initialization
        self . view . backgroundColor  =  [ UIColor  yellowColor ];      
        [ self  printFrame: self . view . frame  name: @"initFrame" ];
    }
    return  self;
}

- ( void) viewDidLoad
{
    [ super  viewDidLoad ];
    // Do any additional setup after loading the view.
    [ self  printFrame: self . view . frame  name: @"didloadFrame" ];
}

- ( void) viewWillLayoutSubviews
{
    [ super  viewWillLayoutSubviews ];
    [ self  printFrame: self . view . frame  name: @"willLayoutFrame" ];   
}

-( void) viewDidLayoutSubviews
{
    [ super  viewDidLayoutSubviews ];
    [ self  printFrame: self . view . frame  name: @"didLayoutFrame" ]; 
}

- ( void) viewWillAppear:( BOOL) animated
{
    [ super  viewWillAppear: animated ];
    [ self  printFrame: self . view . frame  name: @"willAppearFrame" ];
}

-( void) viewDidAppear:( BOOL) animated
{
    [ super  viewDidAppear: animated ];
    [ self  printFrame: self . view . frame  name: @"didappearFrame" ];
}

這段代碼在init方法中設置了一下view的backgroundColor。運行結果很正常,view的背景色被成功地設定為黃色,但是看控制台的log輸出,出現了一個不符合預期的現象:

didloadFrame  :( 0.000000 ,  20.000000 ,  768.000000 ,  1004.000000)
initFrame  :( 0.000000 ,  20.000000 ,  768.000000 ,  1004.000000)
willAppearFrame  :( 0.000000 ,  0.000000 ,  768.000000 ,  960.000000)
didappearFrame  :( 0.000000 ,  0.000000 ,  768.000000 ,  960.000000)
willLayoutFrame  :( 0.000000 ,  0.000000 ,  768.000000 ,  960.000000)
didLayoutFrame  :( 0.000000 ,  0.000000 ,  768.000000 ,  960.000000)

viewDidLoad竟然先於init給出了輸出,經過跟蹤發現,原來當程序第一次調用self.view的時候,viewDidLoad方法就會被執行,而不一定非要等到init之后willAppear之前。這給我們敲響了警鍾,這樣的代碼就隱藏了問題:

- ( id) initWithNibName:( NSString  *) nibNameOrNil  bundle:( NSBundle  *) nibBundleOrNil
{
    self  =  [ super  initWithNibName: nibNameOrNil  bundle: nibBundleOrNil ];
    if ( self{
        // Custom initialization
        self . view . backgroundColor  =  [ UIColor  yellowColor ];
        aInstanceVariable_ =  0// Custom initialization of an instance variable
    }
    return  self;
}

- ( void) viewDidLoad
{
    [ super  viewDidLoad ];
    // Do any additional setup after loading the view.
    aInstanceVariable_  =  10086;
}

這段代碼執行完后的aInstanceVariable_是0而不是10086,可能會為一些bug深深地埋下一顆種子。

搞清楚了代碼執行順序,下面我們來關注一下frame和bounds的問題。frame和bounds的定義和區別在這篇blog里講的很清楚,總結起來要點就是,frame是相對於父view參照系(是父view而不是父viewController)的,bounds是本地參照系,改frame的時候center和bounds聯動,但改bounds的時候center不動。

把上面的程序稍微修改一下,來看一組值得研究一下的結果(此viewController由帶導航條的navigationController推送),實際上不用navigationController而直接加載這個vc,結果又不一樣,viewDidAppear會在最后viewDidLayoutSubviews之后才調用,其他順序不變,亂吧……

didLoadFrame :( 0.00000020.000000768.0000001004.000000) direction:( 11)
didLoadBounds :( 0.0000000.000000768.0000001004.000000) direction:( 11)
initFrame :( 0.00000020.000000768.0000001004.000000) direction:( 11)
initBounds :( 0.0000000.000000768.0000001004.000000) direction:( 11)
willAppearFrame :( 0.0000000.000000768.000000960.000000) direction:( 11)
willAppearBounds :( 0.0000000.000000768.000000960.000000) direction:( 11)
didAppearFrame :( 0.0000000.000000768.000000960.000000) direction:( 11)
didAppearBounds :( 0.0000000.000000768.000000960.000000) direction:( 11)
willLayoutFrame :( 0.0000000.000000768.000000960.000000) direction:( 11)
willLayoutBounds :( 0.0000000.000000768.000000960.000000) direction:( 11)
didLayoutFrame :( 0.0000000.000000768.000000960.000000) direction:( 11)
didLayoutBounds :( 0.0000000.000000768.000000960.000000) direction:( 11)

剛才這個是豎屏的,再來個橫屏的:

didLoadFrame  :( 0.000000 ,  0.000000 ,  748.000000 ,  1024.000000direction:( 3 ,  3)
didLoadBounds  :( 0.000000 ,  0.000000 ,  748.000000 ,  1024.000000direction:( 3 ,  3)
initFrame  :( 0.000000 ,  0.000000 ,  748.000000 ,  1024.000000direction:( 3 ,  3)
initBounds  :( 0.000000 ,  0.000000 ,  748.000000 ,  1024.000000direction:( 3 ,  3)
willAppearFrame  :( 0.000000 ,  0.000000 ,  1024.000000 ,  704.000000direction:( 3 ,  3)
willAppearBounds  :( 0.000000 ,  0.000000 ,  1024.000000 ,  704.000000direction:( 3 ,  3)
didAppearFrame  :( 0.000000 ,  0.000000 ,  1024.000000 ,  704.000000direction:( 3 ,  3)
didAppearBounds  :( 0.000000 ,  0.000000 ,  1024.000000 ,  704.000000direction:( 3 ,  3)
willLayoutFrame  :( 0.000000 ,  0.000000 ,  1024.000000 ,  704.000000direction:( 3 ,  3)
willLayoutBounds  :( 0.000000 ,  0.000000 ,  1024.000000 ,  704.000000direction:( 3 ,  3)
didLayoutFrame  :( 0.000000 ,  0.000000 ,  1024.000000 ,  704.000000direction:( 3 ,  3)
didLayoutBounds  :( 0.000000 ,  0.000000 ,  1024.000000 ,  704.000000direction:( 3 ,  3)

總結一下不難發現其特征:1. 在viewWillAppear之前,無論橫屏還是豎屏,view的frame和bounds都是按豎屏方式計算的;2. 在viewWillAppear之前,navigationController(而非父view,實際上這個vc的superview是navigationController的view的一個subview)的導航條並沒有計算在frame和bounds中,但電池條的寬度是一直計算了的;3. 在轉屏時,觸發的是viewWillLayoutSubview及viewDidLayoutSubview(data not shown)。

由此結論,我們繼續往下想,如果我們要改變self.view的frame值,我們應當在哪個方法中修改呢?很容易想到的是,init和viewDidLoad中是不行的,實踐證明,在viewWillAppear中也是不行的,要在viewDidAppear/viewWillLayoutSubviews/viewDidLayoutSubviews方法中修改才能產生效果。

看起來越來越復雜了……對了,以上的結論對iOS5和6是通用的。下面開始研究轉屏,轉屏對iOS5和6來說,差別就大了。

先看iOS5

iOS5的時候,轉屏函數主要是這幾個:(補:其實還有一個willAnimationRotationToInterfaceOrientation:duration:,調用時機在viewDidLayoutSubviews之后,didRotation之前)

-( BOOL) shouldAutorotateToInterfaceOrientation:( UIInterfaceOrientation) toInterfaceOrientation
{
    NSLog( @"shouldRotate");
    return  YES;
} //以下簡稱shouldRotate

-( void) willRotateToInterfaceOrientation:( UIInterfaceOrientation) toInterfaceOrientation duration:( NSTimeInterval) duration
{
    NSLog( @"willRotate");
}

-( void) didRotateFromInterfaceOrientation:( UIInterfaceOrientation) fromInterfaceOrientation
{
    NSLog( @"didRotate");
}

初始化一個正常viewController時轉屏函數的調用過程如下:

2012 - 11 - 18  16 : 40 : 58.090  testRotation [ 1874 : c07 ]  init
2012 - 11 - 18  16 : 40 : 58.091  testRotation [ 1874 : c07 ]  shouldRotate
2012 - 11 - 18  16 : 40 : 58.092  testRotation [ 1874 : c07 ]  didLoad
2012 - 11 - 18  16 : 40 : 58.092  testRotation [ 1874 : c07 ]  shouldRotate
2012 - 11 - 18  16 : 40 : 58.093  testRotation [ 1874 : c07 ]  willappear
2012 - 11 - 18  16 : 40 : 58.093  testRotation [ 1874 : c07 ]  shouldRotate
2012 - 11 - 18  16 : 40 : 58.094  testRotation [ 1874 : c07 ]  willlayout
2012 - 11 - 18  16 : 40 : 58.095  testRotation [ 1874 : c07 ]  didlayout
2012 - 11 - 18  16 : 40 : 58.096  testRotation [ 1874 : c07 ]  didappear

我的媽呀,初始化一個vc怎么調用了三次shouldRotate方法……(別着急,三次算什么,這種情況下調用幾次都有可能……)

如果初始化vc是在一個navigationController下,看起來還比較正常:

2012 - 11 - 19  20 : 42 : 42.037  testRotation [ 462 : c07 ]  init
2012 - 11 - 19  20 : 42 : 42.039  testRotation [ 462 : c07 ]  didload
2012 - 11 - 19  20 : 42 : 42.040  testRotation [ 462 : c07 ]  willappear
2012 - 11 - 19  20 : 42 : 42.041  testRotation [ 462 : c07 ]  shouldRotate
2012 - 11 - 19  20 : 42 : 42.042  testRotation [ 462 : c07 ]  didappear
2012 - 11 - 19  20 : 42 : 42.042  testRotation [ 462 : c07 ]  willlayout
2012 - 11 - 19  20 : 42 : 42.043  testRotation [ 462 : c07 ]  didlayout

shouldRotate在willAppear之后調用一次。

無論有navigationController與否,再轉一下屏后,方法調用過程是一樣的:

2012 - 11 - 19  20 : 51 : 00.729  testRotation [ 527 : c07 ]  shouldRotate
2012 - 11 - 19  20 : 51 : 00.730  testRotation [ 527 : c07 ]  willRotate
2012 - 11 - 19  20 : 51 : 00.731  testRotation [ 527 : c07 ]  willlayout
2012 - 11 - 19  20 : 51 : 00.731  testRotation [ 527 : c07 ]  didlayout
2012 - 11 - 19  20 : 51 : 00.732  testRotation [ 527 : c07 ]  shouldRotate
2012 - 11 - 19  20 : 51 : 01.133  testRotation [ 527 : c07 ]  didRotate

注意,shouldRotate方法依然被調用了兩次。

為了把shouldRotate方法的調用次數以及這幾次調用的返回值有什么用搞明白,我做了個實驗,詳細過程不贅述,只說結論。結論是一個壞消息和一個好消息:壞消息是,shouldRotate方法可能調用很多次(只出現在非navigationController方式直接將vc作為rootViewController的情況),我最多遇到過連續調用6次的,弄的我一頭霧水,具體原因尚不詳;好消息是,無論在哪個階段調用多少次,起決定作用的只有willAppear調用后,willLayoutSubviews調用前shouldRotate的最后一次調用,其余階段返回yes還是no都不重要。

再看iOS6

iOS6對轉屏邏輯做了修改,去掉了原來的shouldRotate方法,代之以新的幾個方法,具體可看這篇blog,介紹很詳細,不再贅述,做一些補充:

-( BOOL) shouldAutorotate
{
    return  YES;
}
-( NSUInteger) supportedInterfaceOrientations
{
    return  UIInterfaceOrientationMaskAll;
}
-( UIInterfaceOrientation) preferredInterfaceOrientationForPresentation
{
    return  UIInterfaceOrientationLandscapeRight;
}

這3個方法代替了原來的shouldRotate方法,但並不是換湯不換葯。

iOS6把轉屏的邏輯判斷放到了rootViewController里,也就是說,無論當前顯示的是那個子vc,都是rootViewController響應轉屏事件(或者present出來的modalViewController也可以,總之是最根部的vc才行),而且不向下傳遞。直接在一個childViewController里寫這幾個方法,是根本不會被調用到的。這就帶來一個問題,根vc的轉屏邏輯直接決定了子vc的轉屏邏輯,如果老子說這個方向支持,那兒子只好支持,而且所有的兒子還都得支持。解決這個專制問題,可以在根vc里這樣寫:

-( BOOL) shouldAutorotate
{
    return  [ self . topViewController  shouldAutorotate ];
}
-( NSUInteger) supportedInterfaceOrientations
{
    return  [ self . topViewController  supportedInterfaceOrientations ];
}

讓老子每次轉屏被問到的時候,都親自問下他現在正在活躍的子孫。

轉屏時調用順序跟iOS5一樣,不過shouldRotate被順序拆分為shouldAutoRotate和supported。並且如果shouldAutoRotate返回了NO,則轉屏過程中斷,不再繼續執行supported。

最后說到強制橫屏。

iOS5和6都有這個問題,如果我們采用presentViewController的方式展示一個vc,那么我們是可以在進入vc的時候控制present的方向的。但是如果我們采用的是pushViewController的方式,問題就出現了,無論我們用何種方式設置這個vc支持的屏幕方向,都只能在轉屏的時候進行調整,而無法在第一次進入這個vc的時候調整。也就是說,豎屏push進入一個只支持橫屏的vc,顯示依然是豎屏,但當轉橫屏之后,就轉不回豎屏了。

這顯然不對,解決這個問題,要么用私有API setOrientation: 這個顯然是風險太大的。比較好的解決方式就是檢測屏幕方向,然后用view.transform去人工轉view,setStatusBarOrientation。這里面要注意幾個要點:

1. view.transform的makeRotation方式轉view是中心點center不動,view旋轉。

2. 旋轉過后view的frame會改變,所以要人工調整,這里計算frame的新位置和尺寸是重點。由於是人工轉屏,改變電池條的方向並不會改變view的坐標系,所以一切要在原坐標系里算。

3. view轉屏退出后要記得用identity恢復之前view轉過的狀態。

4. 最坑爹的一點是,用setStatusBarOrientation:animated:方法來設置電池條方向時,在iOS5下沒有問題,但在iOS6下,這個方法會調用rootvc的shouldAutoRotate(相當於一次轉屏判斷),如果shouldAutoRotate返回YES(無論supported返回什么),電池條方向都不會被設定!非常坑,所以邏輯要想好,比如可以通過一個bool值判斷是在改變電池條方向還是系統轉屏,如果是前者,返回個NO騙騙它……

5. 在哪個方法里處理轉屏,設置電池條方向,以及在哪個方法里調整view的frame,都是很重要的,要視你的view是怎么push進來的(有rootvc還是本身就是),要具體情況具體分析。中心思想是:比如強制要求橫屏,則在橫屏進入的時候,直接用系統轉屏邏輯限制方向即可;而在豎屏進入時,禁用系統轉屏邏輯,人工將view旋轉至需要的方向,而后再轉為橫屏時,可采用兩種方式,一是恢復原本view方向后重新開啟系統轉屏邏輯,二是繼續根據方向人工轉屏。設計這個過程代碼時,明確之前研究的frame尺寸應該什么時候重設以及各個view方法的執行順序,是必須的。

5. iOS5和6要區分處理。總之,強制橫屏絕對不是網上隨處可見的transform一下然后重設一下bound就ok了的事情。

附上一種強制橫屏實現的代碼:

// 強制橫屏的一種實現
// 使用方法:
// 在vc的init方法中調用initLogic
// 在vc關閉之前調用cleanRotateTrace方法

-( void) initLogic
{
    isPortraitIn_  =  NO;
    isSettingStatusBar_  =  NO;
}
-( BOOL) shouldAutorotate
{
    if ( isSettingStatusBar_)
    {
        return  NO;
    }
    return  YES;
}

-( NSUInteger) supportedInterfaceOrientations
{
    return  UIInterfaceOrientationMaskLandscape;
}

-( BOOL) shouldAutorotateToInterfaceOrientation:( UIInterfaceOrientation) toInterfaceOrientation
{
    return (( toInterfaceOrientation  ==  UIInterfaceOrientationLandscapeLeft)||( toInterfaceOrientation  ==  UIInterfaceOrientationLandscapeRight));
}

- ( void) willRotateToInterfaceOrientation:( UIInterfaceOrientation) toInterfaceOrientation duration:( NSTimeInterval) duration
{
    if ( isPortraitIn_)
    {
        self . view . transform  =  CGAffineTransformIdentity;
        isPortraitIn_  =  NO;
    }
}

- ( void) cleanRotationTrace
{
    if ( isPortraitIn_)
    {
        self . view . transform  =  CGAffineTransformIdentity;
        isPortraitIn_  =  NO;
        UIInterfaceOrientation  orientation  =  [ UIApplication sharedApplication ]. statusBarOrientation;
        if ( orientation  ==  UIInterfaceOrientationLandscapeRight)
        {
            isSettingStatusBar_  =  YES;
            [[ UIApplication  sharedApplication ] setStatusBarOrientation: UIInterfaceOrientationPortrait  animated: NO ];
            isSettingStatusBar_  =  NO;
        }
        else
        {
            isSettingStatusBar_  =  YES;
            [[ UIApplication  sharedApplication ] setStatusBarOrientation: UIInterfaceOrientationPortraitUpsideDown  animated: NO ];
            isSettingStatusBar_  =  NO;
        }
        [ self . view  setFrame: CGRectMake( 0 ,  0 ,  self . view . frame . size . height  +  20 , self . view . frame . size . width  -  20 )];
    }
}

-( void) viewDidAppear:( BOOL) animated
{
    [ super  viewDidAppear: animated ];
    UIInterfaceOrientation  orientation  =  [ UIApplication sharedApplication ]. statusBarOrientation;
    if ( UIInterfaceOrientationIsPortrait( orientation))
    {
        isPortraitIn_  =  YES;
        self . view . transform  =  CGAffineTransformMakeRotation( M_PI_2);
        if ( orientation  ==  UIInterfaceOrientationPortrait)
        {
            isSettingStatusBar_  =  YES;
            [[ UIApplication  sharedApplication ] setStatusBarOrientation: UIInterfaceOrientationLandscapeRight  animated: NO ];
            isSettingStatusBar_  =  NO;
        }
        else
        {
            isSettingStatusBar_  =  YES;
            [[ UIApplication  sharedApplication ] setStatusBarOrientation: UIInterfaceOrientationLandscapeLeft  animated: NO ];
            isSettingStatusBar_  =  NO;
        }
        [ self . view  setFrame: CGRectMake( 0 ,  - 20 ,  self . view . frame . size . height  -  20 , self . view . frame . size . width  +  20 )];
    }
}

雖然強制橫屏的中心思想都差不多,但具體實現方式可以有很多種,我自己寫過兩種,效果都差不多,代碼簡潔程度不同。這些實現目前我都沒有解決的問題是轉屏的動畫,用系統邏輯的部分沒有問題,但如果是豎屏進入強制橫屏的,在第一次轉到真正橫屏的時候,電池條的轉動與view的轉動是不同步的,動畫很難看,之后再轉就又是系統轉屏沒有問題了。

這個動畫問題我至今能夠想到的唯一解決方法是完全不用系統轉屏,而是所有的轉屏都自己寫。求更好解決方案。

到此為止。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM