你可能非常了解用不同的方式去適配不同尺寸的iPhone屏幕,在適配iPhone屏幕時你需要考慮的只是屏幕大小變化帶來的UI元素間隔的變化,但是在iPad上主要針對的是橫豎屏下完全不同的UI元素的布局,在這種情況下要考慮的就不僅僅是元素之間間隔這種問題了,除了要確保UI元素在這兩種模式下的正確顯示還要兼顧屏幕旋轉的過渡動畫.下圖是QQZone For iPad 在橫豎屏下的布局,可以看到橫豎屏下菜單欄的布局方式差別很大.
QQZone for iPad GitHub地址
QQZone for iPad 豎屏
QQZone for iPad 橫屏
屏幕適配的N種方法
無論iPad還是iPhone適配不同屏幕(尺寸,方向)的方式都跑不出以下幾種,以下會一一對不同方式做一下簡單的回顧.
Autoresizing
Autoresizing可以說是Autolayout始祖,Autoresizing的是一項比較有歷史的技術了,其在iOS2的時代就推出了.當設置UIView實例對象的autoresizesSubviews屬性為true(默認值為true),那麼其子view會根據自已的autoresizingMask屬性值自動調整與superview的位置和大小關系.autoresizingMask有六種可組合的使用的值,默認值是.None.這六種有效枚舉值的意思如下:
FlexibleLeftMargin 按比例跟隨父控件變化的左間距
FlexibleWidth 按比例跟隨交控件變化的寬度
FlexibleRightMargin 按比例跟隨父控件變化的右間距
FlexibleTopMargin 按比例跟隨父控件變化的頂部間距
FlexibleHeight 按比例跟隨控件變化的高度
FlexibleBottomMargin 按比例跟隨父控件變化的底部間距
另外在xib,storyboard取消Autolayout時(Autoresizing與Autolayout相互沖突)可以在Size inspector可以更加直觀地按需求進行組和使用.Autoresizing技術在一定應用場景下可以勉強使用但應對更為精細的布局就無能為力了,你可以在使用Autoresizing同時重寫layoutSubviews方法去做更為精細的布局,盡管如此但還是不推薦這麼做,因為同時得寫layoutSubviews和使用Autoresizing去布局會讓你的布局邏輯變得不清晰,這將給後期的維護帶來麻煩.
Autoresizing in storyboard
Autolayout
Autolayout是iOS6時代引入的技術,專門用來處理不同屏幕尺寸下的UI布局.從Xcode6開始Autolayout配合xib,storyboard極大的提高屏幕的適配工作效率.在一定程度上甚至可以完全擺脫設置frame布局的方式.由於storyboard,xib在多人合作開發沖突不斷的尴尬境地,在實際的開發中多使用第三方框架用代碼進行Autolayout布局.這樣既避免了解決沖突麻煩又享受到了Autolayout帶來的宏利.比較受歡迎的Autolayout每三方框架有Masonry還有GSD_iOS大神的SDAutoLayout
盡管Autolayout有很多好處但還是很多代碼黨不願使用,究其原因還是約束.約束的問題大至可以分約束沖突和約束不滿足兩大類,當在storyboard中對一個復雜的界面進行Autolayout約束,一但出現問題將很難排查,用代碼行約束往往程序運行起來才能確認約束是否滿足條件,同樣排查起來也不是那麼方便.關於Autolayout這不再占用過多的篇幅,網上有相當多的資料可供參考.
SizeClass
SizeClass是要配合Autolayout使用的,SizeClass實際上是對屏幕尺寸的抽象,把屏幕寬高分成Compact:緊湊、Regular:寬松、Any:任意三種類型這樣就可以組合出九種不同的屏幕類型.在storyboard,xib編輯界面下最下方可以選擇某一約束在只在某一類屏幕下生效.這樣可以在不同屏幕下得到不同的UI布局效果.關於SizeClass的使用可以參考raywenderlich系例文章.
SizeClass
代碼計算坐標
在所有的布局方法中這種可能是最費體力的一項,因為所有的UI元素都需要一個一個明確的計算或者指定出來.盡管如此正因為每個元素的frame是手動計算因此靈活性也非常大你可以隨心所欲的計算每個控件的frame,出現問題時也非常好排查.如果需要動態的改變view的frame就需要重寫父控件的layoutSubviews方法,在重寫的layoutSubviews明確計算出frame. 如果view是固定的則只需要在添加到父控件時指定view的frame.一般常見的代碼布局形式如下:
override func layoutSubviews() { super.layoutSubviews() let x: CGFloat = 0 let y: CGFloat = frame.height * 0.7 // 根據父控件高度按比例確定y座標 let w: CGFloat = frame.width let h: CGFloat = frame.height - y // 根據父控件高度,子控件按比例調整高度 subView.frame = CGRectMake(x, y, w, h) }
上面是常見的根據父控件動態調整子控件frame的形式,真實開發中可能還需要考慮橫豎屏下動態的布局(下面將提到的),以上基本形式可以根據需求進行擴展.當然你也可以不用重寫layoutSubviews方法而在需要改變frame的時機顯式直接改變frame,但在這種方式並不符合蘋果的邏輯.在view的層次結構中某一view肯定是有superview的,而子view是否變化,以及什麼時候變化應是由superview來決定的,在一個多層次結構的view視圖中如果顯示的設定子view的frame那你不得不根據view的層次結構一級一級的設置子view的frame.superview和subView之前會出現較強的關聯性.理想情況下一個superview應該只關注自身的subView的布局,無論這個superview的frame或層次結構怎麼變化其subviews並不需要知道.因此我覺得比較好的做法是有所關於subviews的frame的設定都應該重寫layoutSubviews,在layoutSubviews中去做.這樣做的另一個好處是屏幕旋轉時你並不需要顯示的去寫UIView動畫.
layoutsubviews的調用時機
用代碼在layoutSubviews中布局你必需要知道系統會在哪些時機去調用layoutsubviews函數.關於這個問題的結論可以參看stackoverflow的討論.關於這個問題我個人比較贊同第二個回答者對第一個回答的糾正.基本上layoutsubviews會在以下幾種情況下調用:
當view的bounds發生改變時
當view的直接subView的bounds發生改變時
當subView添加或移除時
調用setNeedsLayout方法會在下一個顯示周期主動調用layoutsubviews
如何獲取當前屏幕方向
關於獲取當前屏幕方向我所知道的方法僅包括以下幾種:
通過控制器的interfaceOrientation只讀屬性獲取,iOS8後過期
能過狀態欄的方向間接獲取,UIApplication的只讀屬性statusBarOrientation,iOS9後過期
能過UIDevice只讀屬性orientation獲取.需主動調用beginGeneratingDeviceOrientationNotifications開啟通知
通過根控制器的view寬高推導獲取,當高>寬為豎屏否則為橫屏
Demo預覽
下面是這個Demo的最終效果,我將通個下面的例子記錄我認為合理的代碼適配橫豎屏的方式.
Demo預覽慢速
Demo預覽正常
Demo分析
上面一些基礎的知識將有助於理解Demo的做法,所以盡管有一點廢話連篇的感覺好在也並不是一無是處.在講解Demo的實現思路之前你可以在GitHub下載這個Demo,以便更方便的查看我講到的代碼.
Demo中最復雜的,橫豎屏布局變化最大的部分就是左側的菜單欄可以稱它為Dock欄,通過旋轉屏幕可以看到原生QQZone HD的Dock欄的變化.可以根據變化的特征將整個Dock欄分為三部分.一是頂部的頭像 二是中間的類TabBar,我稱它為TabBar 三是 底部的快捷導航菜單.因此DockView的subview包含iconButton,tabBarView,menuBar三個,而這三個subview又可以分另包含各自的子控件.
view層次結構
實際上實現QQZone for iPad屏幕的橫豎屏的布局並不復雜.一個view要知道怎樣在layoutSubviews中去布局其子view只需知道當前其superview的狀態(橫豎屏).在這裡我聲明了一個協議,這個協議只包含一個獲取當前view是否是豎屏的方法.讓每一個需要根據橫豎屏動態變化的view都實現這個協議的方法,這樣在layoutSubviews方法就可以詢問當前應該怎麼樣布局子控件而當前狀態是由父控件狀態決定的.由此形成了屏幕狀態的傳遞鏈,使得每個veiw只關心自身直接subview的布局.
UIViewisPortrait協議 protocol UIViewisPortrait: NSObjectProtocol { func isPortrait() -> Bool }
根控制器view的任意subview可都可以通過如下代碼獲取當前是否是豎屏
subview 如何獲取superview狀態 func isPortrait() -> Bool { guard let superview = superview else { // 如果不存大superview默認返回豎屏 return true } return ((superview as? UIViewisPortrait)?.isPortrait())! }
而根控制器view則直接通過寬高獲取屏幕狀態
func isPortrait() -> Bool { return frame.width < frame.height }
當前view知道是橫豎屏後就可以直接在layoutsubviews布局子控件了,以menuBar為例
override func layoutSubviews() { super.layoutSubviews() guard subviews.count > 0 else { return } var x, y, w, h:CGFloat for (index, view) in subviews.enumerate() { if isPortrait() == true { w = frame.width h = kDockItemHeight x = 0 y = CGFloat(index) * h view.frame = CGRect(x: x, y: y, width: w, height: h) } else { w = frame.width / CGFloat(subviews.count) h = kDockItemHeight x = CGFloat(index) * w y = frame.height - h view.frame = CGRect(x: x, y: y, width: w, height: h) } } }
其它類型的所有子控件都可以用類似的方法進行布局,如此你只需要定義一個view並始之成為某個View的subview,在你定義的view中你可以隨意的獲取屏幕狀態布局子控件了.
為了在根控制器View的layoutSubviewsr的方法中布局DockView,需要重寫控制器的loadView方法,讓控制器加載自定義的View. 如果你注意到原生QQZone for iPad的內容顯示區域在橫豎屏下的變化會發現在橫堅屏下內容顯示區域寬都是一樣的,所以還需要在根控制器View中添加一個容器View以顯示內容.
class HomePageView: UIView, UIViewisPortrait { private lazy var dockView:DockView = DockView() lazy var contentView: UIView = { let contentView = UIView() contentView.backgroundColor = UIColor.whiteColor() // 作為容器View,子控制器的view將添加到容器view上 self.addSubview(contentView) return contentView }() func isPortrait() -> Bool { return frame.width < frame.height } override init(frame: CGRect) { super.init(frame: frame) addSubview(dockView) dockView.backgroundColor = globalBackgroudColor } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() dockView.frame.size.height = frame.height dockView.frame.size.width = isPortrait() ? kDockProtraitWidth : kDockLandscapeWidth let x: CGFloat = dockView.frame.width let y: CGFloat = 20 // 無論橫豎屏,內容視圖的寬都一樣 let w: CGFloat = min(frame.width, frame.height) - kDockProtraitWidth let h: CGFloat = frame.height - y contentView.frame = CGRectMake(x, y, w, h) } }
最後由於內容區域不用區分橫豎屏,因此內容區域的子視圖可以只考慮豎屏的情況,以上可以說得不是很清楚,如果感覺有興趣可下載原碼參閱.
總結
以上重點僅僅是用代碼進行iPad橫豎屏適配方法的探討,這裡只是記錄了我認為較為合理的方法,當然這種方法可能並不適用所有的布局,畢竟每個App都有自己獨特的UI部分.如果覺得這種方法不好歡迎指出,我將虛心請教.如果這個方法對你的業務提供了一點點的靈感希望點個贊,以上完.