Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 第一章讀書筆記--關於View

第一章讀書筆記--關於View

編輯:關於Android編程

Chapter1 Views

View管理指的就是管理view的層次.

而view的層次為其他許多功能提供支持, 如responder鏈.

View可以從nib加載, 也可以直接通過代碼編寫, 代碼或nib的使用完全取決於你的程序總體結構.

1 The Window

View hierarchy(View樹)頂部是Window, 它是UIWindow或UIWindow的子類對象, 而UIWindow也是UIView的子類.

應用中只能有一個main window(主窗口), 主窗口在程序加載時候創建, 並且永遠不會被替換或清除.

主窗口是應用中所有可見內容的容器, 即它們的superView.

由此可知, 主窗口是View樹的根對象.

NOTE: 如果程序可以顯示到額外的屏幕上, 那麼會創建另外的UIWindow對象來管理其他view. 其他情況下, 則只有一個screen, 即一個window.

當程序加載時, window會自動覆蓋全部screen的尺寸, 即window的frame等於screen的bounds.

如果使用了storyboard, 則這些工作在程序啟動時自動完成, 如果沒使用storyboard, 則在APP生命期事件方法中寫如下代碼:

 window = UIWindow()
//iOS9之後可以自動將screen的bounds賦值給window的frame.否則就要寫下面這句話.
//window?.frame = UIScreen.mainScreen().bounds

為了讓window在app生命期中都不消失, 則在AppDelegate對象內增加了一個持有window的屬性, 該屬性是強引用. 因為程序啟動時, 創建了sharedAPP對象, 創建了AppDelegate對象, 而這些對象是被UIApplicationMain過程擁有的, 所以不會被釋放, 再由AppDelegate對象持有window, 則window在程序運行時也不會被釋放.

一般不要手動在主窗口中添加視圖, 而是先實例化出主窗口的根控制器, 然後由根控制器的主視圖來顯示到主窗口上. 如果使用了storyboard, 則這些工作都自動完成了. 因為window的根控制器就是你的storyboard中初始視圖控制器.

一旦window擁有根控制器, 根控制器的主視圖就自動成為主窗口在視圖樹上的唯一子孫, 即成為主窗口的根視圖.

APP的主窗口獲取有兩種方式:

上述的代碼創建, 沒有storyboard時使用

有storyboard時, UIApplicationMain函數先詢問AppDelegate對象, 如果沒有window, 則幫它實例化出來一個UIWindow類型的主窗口.類似如下代碼理解:

lazy var window : UIWindow? = {
    return MyWindow()
}()

當程序運行時, 有多重手段可以訪問主窗口:

當主視圖顯示到界面上之後(並非加載完成之後), 可以通過它的window屬性來獲取window. 若它還沒有被嵌入到window中, 則window屬性為nil.

通過代理對象的window屬性訪問主窗口( 留意下面代碼如何訪問代理對象的).

let w = UIApplication.sharedApplication().delegate?.window
//或更加特殊的方式
let win = (UIApplication.sharedApplication().delegate as? AppDelegate)?.window!

sharedAPP對象有一個keywindow屬性:

let w = UIApplication.sharedApplication().keyWindow

2 Experimenting With Views

見代碼, 就是手動加載和使用storyboard的練習…

3 Subview and SuperView

在以前的時候, view都擁有自己的確定的矩形區域, 如果某個view不是另外一個view的subview, 那麼就不能在另外這個View的內部顯示, 原因是當view繪制的時候, 會重繪它的矩形區域, 這樣會讓它覆蓋掉之前在那個地方的任何view. 即它的subview不會在它矩形框外面顯示出來, 因為這個view只管理它自己的矩形框.

但現在, 這樣的限制得到很大放松. 現在視圖繪制是依據視圖樹的結構進行的. 即在iOS裡, 現在子視圖可以部分或全部在父視圖矩形框外顯示出來, 而且一個視圖可以覆蓋另外一個視圖, 能夠部分或全部繪制在非它的父視圖前面.

一個view擁有superview屬性和subviews屬性, 分別表示它的父視圖和子視圖數組. 並且可以用DescendantOfView:方法來查詢一個view是否是另外一個視圖的在某層級上的子視圖.

如果需要管理view, 首先想到的是通過outlet屬性.

還有一個辦法就是使用tag, 為特定view設置唯一tag. 然後向層次中任意父視圖發送viewWithTag方法來獲取特定tag的view.

在代碼中調整視圖樹很輕松, 可以將一個view調整到另外一個view的子視圖, 則該view的子視圖一起過去了就:

addSubview: removeFromSuperview:

記住當從父視圖移除子視圖時, 便釋放了子視圖. 當需要再次顯示時, 要先獲取子視圖. 這裡所說的釋放和獲取對應的是ARC內的Release和Retain.

對應的添加子視圖或移除子視圖事件方法都有:

didAddSubview:, willRemoveSubview didMoveToSuperview:, willMoveToSuperview: didMoveToWindow, willMoveToWindow

當調用addSubview方法時, 子視圖是被加入到subviews數組的最後一位, 所以被最後繪制, 故也是顯示到最前面.

還有一些方法用於將子視圖添加到視圖層次特定位置, 即對應subviews數組特定下標位置:

insertSubview:atIndex:在特定位置插入子視圖 insertSubview:belowSubview:, insertSubview:aboveSubview: 在特定子視圖前後插入子視圖 exchangeSubviewAtIndex:withSubviewAtIndex: 交換特定兩個子視圖位置 bringSubviewToFront:, sendSubviewToBack: 將特定子視圖移動到最前和最後.

但不爽的是, 沒有提供將全部子視圖一次性移除的方法.

某視圖的subviews數組屬性是一個內部subviews列表的不可變拷貝, 故可以通過下面方法移除子視圖:

view.subviews.forEach({$0.removeFromSuperview()})

代碼中使用forEach方法, 它參數是一個塊, 這個塊的第一個參數代表的就是每個要遍歷的東西, 這裡就是view, 然後調用removeFromSuperView即可.

4 可見性和透明度(Visibility and Opacity)

可以設置一個view的hidden屬性來顯示和隱藏view. 隱藏它並不會將它從視圖樹中去掉, 而是隱藏顯示, 這樣可以降低移除它的開銷. 隱藏的視圖不會接收觸摸事件, 但仍然可以在代碼中操縱它.

view可以設置背景色, 通過backgroundColor屬性. 背景色nil表示這個view有透明背景, 而透明背景的view適合作為其他view的父視圖.

可以設置alpha屬性來設置view的透明度, 1.0表示不透明, 0.0表示完全透明. 在0-1之間任意取值. view的alpha屬性會影響到它的子視圖: 如果父視圖的alpha是0.5, 則所有子視圖的alpha都不會超過0.5, 因為子視圖是以alpha值0.5為標准繼續繪制的.

顏色也具有alpha值, 即你的view的alpha值為1.0, 但背景色的alpha值為0, 則view一樣是透明的.

完全透明的view和隱藏view的行為一致, 也是不會接收觸摸事件, 且子視圖也全部透明.

view的alpha屬性會同時影響到它背景的透明度和子視圖的透明度.

view的opaque (不透明, bool類型) 屬性則是另外一回事了, 設置該屬性不會對view外觀有任何影響, 這個屬性是給繪圖系統的提示, 如果一個視圖中以完全不透明的材料填充滿它的bounds, 並且它的alpha是1.0, 那麼將它設置為true, 將上述事實通知繪圖系統, 那麼在繪制時會更高效. 否則應該設置為false. 而默認情況是true的.

5 Frame

view的frame屬性是一個CGRect結構體類型, 表示視圖在父視圖中的位置和尺寸, 屬性中的坐標系原點定為父視圖左上角.

將view的frame賦予新值會改變該view的顯示大小或位置, 如果視圖可見的話就會反應到界面中.

當然不可見視圖也可以重新設置它的frame. 這個時候frame的作用就是指定下次顯示該view時應該在何處繪制(新的superVeiw中).

NOTE: 初學者最易犯的錯誤是給出一個view並添加到父視圖, 卻忘記設置它的frame. 因為view的默認frame是CGRectZero. 如果想讓新建的視圖匹配某個尺寸, 可以使用sizeToFit屬性.

兄弟視圖之間的遮擋關系是在subviews數組越後面的view越在頂部, 而每個視圖的子視圖之間的遮擋關系要分析兄弟之間的遮擋, 然後分析subviews中的前後關系.

6 Bounds and Center

假設視圖中包含一個子視圖, 需要子視圖在父視圖中以每邊距父視圖10點距來縮放顯示, 則可以使用Bounds屬性, 利用CGRectInset方法或Swift的CGRect中的insetBy方法來設置.

實際CGRectInset方法是在該視圖原有基礎上進行縮放, 將原bounds縮放一個dx和dy.

因子視圖之前已經有一個frame, 故使用bound屬性在它自己本身的坐標系下修改尺寸.

下面是bounds的常用方式:

let v1 = UIView(frame: CGRectMake(113, 111, 132, 194))
v1.backgroundColor = UIColor.redColor()
let v2 = UIView(frame: v1.bounds.insetBy(dx: 10, dy: 10))
v2.backgroundColor = UIColor.blueColor()
view.addSubview(v1)
v1.addSubview(v2)

先創建一個view, 在以它的bounds為參照在其中放置一個縮小的view, 利用的是insetBy方法.

當改變bounds的size時, 也會改變frame的size, 改變長寬是以center為參照的, 即向兩邊擴展.

v1.bounds.origin.x += 10
v1.bounds.origin.y += 10

若改變父視圖的bounds原點, 則子視圖會相應移動. 意思是父視圖以前的原點為(0, 0), 現在的原點為(10, 10)了.

改變frame的坐標, 是在以父視圖左上角為原點的坐標系下移動.

改變父視圖的bounds的原點, 就是將以前父視圖左上角本來喊成0,0, 現在喊成改變後的值, 子視圖為了保持以前的狀態.

改變一個視圖的位置和尺寸的最佳方式是改變它的center和bounds, 而非frame.

總地來說, frame描述在父視圖坐標系中位置, 而bounds是描述本地坐標系的.

可以在同一window下的兩個視圖之間坐標轉換, 使用convertPoint:fromView:, convertPoint:toView:,convertRect:fromeView:, convertRect:toView:.

WARNING: 若通過center設置view的位置, 但此時尺寸並非整數, 則會造成錯誤對齊, 即位置有偏差.

可以通過Debug->Color Misalligned Images來查看. 解決方法是在放置後, 設置view的frame為CGRectIntergral, 或(在Swift中)frame調用makeintergralinPlace方法.

7 窗口坐標和屏幕坐標

設備屏幕即screen, 它不具有frame, 但是有bounds, main window沒有父視圖, 但是它的frame是和screenbounds關聯的. 故代碼中直接使用UIWindow()創建的窗口和screen一樣大.

大部分情況下, 窗體坐標系即屏幕坐標系. 但iOS9中有些情況不是這樣, 這裡先不說.

iOS7之前屏幕坐標系是不變的, 不管旋轉與否. iOS8之後引入了一項大的變化, 當應用隨設備旋轉的時候, screen和window也隨著一起旋轉. 這表明當豎直的時候, W比H小, 水平的時候, W比H大.

如果想獲得獨立於應用旋轉的屏幕坐標系, 可以用screen的兩個屬性來獲得, 這兩個屬性都是UICoordinateSpace協議類型:

UIScreen的coordinateSpace屬性:

這個屬性會隨著屏幕旋轉而變化, 坐標原點一直在應用的左上角.

UIScreen的fixedCoordinateSpace屬性:

這個屬性是固定不變的, 坐標原點在屏幕正常位置的左上角.

為進行坐標系之間的變換, UICoordinateSpace協議中提供了四種轉換方法:

convertPoint:fromCoordinateSpace:, convertPoint:toCoordinateSpace: convertRect:fromeCoordinateSpace:, convertRect:toCoordinateSpace:

假設有一個UIView v在界面內, 想知道v在固定設備坐標系下的坐標, 可以這樣做:

let r = v.superview?.convertRect(v.frame, toCoordinateSpace: UIScreen.mainScreen().fixedCoordinateSpace)

但實際中很少需要這樣做. 因為所有可視內容都是在根控制器的主視圖中開始的, 這樣會自動將應用的旋轉自適應到設備的旋轉, 根控制器中的主視圖才是編程時大多數時候都在關心的坐標系.

8 Transform

通過Transform屬性改變view的繪制方式.

transform是CGAffineTransform結構體類型, 其中包含變形矩陣內9個值中的6個值(其余3各是常量).

transform中的變換矩陣的賦值, 詳見蘋果Quartz 2D Programming Guide內的”The Math Behind the Matrices”章節.(就是三種變換及其相互組合, 平移translation, 縮放scale, 旋轉rotation)

因為變換方法, 都是提供的對三種基本變換的生成, 比如說生成平移, 生成旋轉, 生成縮放.

默認情況下, 視圖的變換矩陣是CGAffineTransformIdentity, 即一個單位矩陣, 不會對原始坐標造成任何影響.

任何的Transform都是以視圖的center為基准進行的.

變換的一個簡單例子:

let v1 = UIView(frame: CGRectMake(113, 111, 132, 194))
v1.backgroundColor = UIColor.redColor()
let v2 = UIView(frame: v1.bounds.insetBy(dx: 10, dy: 10))
v2.backgroundColor = UIColor.yellowColor()
view.addSubview(v1)
v1.addSubview(v2)
// 順時針旋轉45度, 這裡的參數是弧度制的, 故轉換為弧度 
v2.transform = CGAffineTransformMakeRotation(45 * CGFloat(M_PI) / 180.0)

另外還可以進行組合變換:

v2.transform = CGAffineTransformMakeTranslation(100, 0)
//這裡的變換沒有用到帶make字樣的方法, 表示是在前者基礎上進行變換的
v2.transform = CGAffineTransformRotate(v2.transform, 45 * CGFloat(M_PI) * 180.0)

上面的代碼就是先平移再旋轉.

反過來也可以先旋轉再平移:

v2.transform = CGAffineTransformMakeRotation(45 * CGFloat(M_PI) / 180.0)
v2.transform = CGAffineTransformTranslate(v2.transform, 100, 0)

這裡的旋轉平移都是基於center點的, 故先平移再旋轉和先旋轉再平移的圖形位置不一樣.

還有另外一種辦法也可以將兩個變換結合起來:

let r = CGAffineTransformMakeRotation(45 * CGFloat(M_PI) / 180.0)
let t = CGAffineTransformMakeTranslation(100, 0)
v2.transform = CGAffineTransformConcat(t, r)    //先平移再旋轉

利用Concat方法將兩個Transform連接起來(其實就是矩陣相乘).

順序相關的意思其實就是因為矩陣乘法時沒有交換律.

這裡演示如何進行形狀改變:

v2.transform = CGAffineTransformMake(1, 0, -0.2, 1, 0, 0)

這裡的6個參數分別對應官文中變換矩陣內的:a, b, c, d, tx, ty.

當進行臨時的可視化提示時, 使用Transform非常簡便.

在iOS7之前, Transform屬性是應用旋轉功能的核心, 因為窗體的frame和bounds都是固定的, 如果要讓應用隨設備旋轉, 則必須使用Transform設置旋轉.

但iOS8之後就不是這樣的了. 屏幕的坐標系會相應旋轉, 但坐標系本身沒有Transform屬性. 所以坐標系旋轉是假設的: 這裡面所有的view都沒有參與旋轉.

著眼點應放在window的二維坐標系下, 以及根控制器的主視圖上面. 這也許表示的是關注它們的絕對坐標, 但實際上經常表示的是它們的本地坐標系是處在一系列的size classes中的, size classes由視圖的traitConllection屬性引入, 並且是UITraitCollection類型的.

可以將應用的旋轉視作界面部分的旋轉: 當應用旋轉時, 根視圖的, 窗體的, 以及屏幕的bounds對應坐標系中的長軸變為短軸, 而短軸則反過來變為長軸.

9 Trait Collection and Size Classes

界面上從主窗口開始往下, 以及所有的VC, 只要有顯示到界面上的視圖, 都繼承了環境中的traitCollection屬性值. traitCollection類型實現了UITraitEnvironment協議.

traitCollection屬性是UITraitCollection類型的, 裡面包含4個成員:

displayScale: 從當前屏幕得到的縮放系數, 通常是1或2, 以及3(iPhone 6 Plus). userInterfaceIdiom: 類型為UserInterfaceIdiom, 它的值表示設備的類型, .Phone或.Pad. horizontalSizeClass 和 verticalSizeClass: 是UIUserInterfaceSizeClass類型的, 取值有兩種, .Regular或.Compact. 這就被稱為Size Class(尺寸類別), Size Class的組合有如下幾種含義:
水平和豎直方向都是 .Regular時: 運行在iPad上面. 豎直為 .Regular, 水平為 .Compact : 在iPhone上面的豎直旋轉顯示. 反過來表示在iPad上的水平分屏多任務配置上顯示. 水平和豎直都是 .Compact : 表示在iPhone(除開iPhone 6Plus)上的水平旋轉顯示 豎直為 .Compact, 水平為 .Regular: 在iPhone6 plus上的水平旋轉顯示.

一個程序在不同地方運行時的traitCollection屬性值不同. 當旋轉時, traitCollection屬性也會跟著變換.

故有一個內置事件方法traitCollectionDidChange, 在程序啟動及以後的旋轉中就會觸發(只要改變traitCollection的值時). 該消息在UITraitEnvironment層次中傳遞, 對於我們來說最主要是傳遞給控制器或視圖的時候, 此時之前的traitCollection值會作為參數傳遞過來, 而新的值可以通過self.traitCollection獲取.

可以手動創建一個traitCollection, 但不能直接手動賦值給對象的traitCollection屬性.

TIP: 不能直接給任何對象的traitCollection直接賦值, 但可以對traitCollection調用一個特殊的方法setOverrideTraitCollection進行賦值.

10 Layout

我們已經看到, 當父視圖bounds原點改變時, 子視圖會移動, 但如果父視圖bounds中size改變時, 又會怎麼樣呢?

一般情況下, 改變父視圖的bounds中size不會影響到子視圖size及center. 但實際工作中, 經常想通過改變父視圖bounds中size時子視圖可以對應改變大小和位置, 這稱為layout.

幾種動態改變父視圖size的辦法:

隨著APP旋轉到新的位置, 長邊和短邊的值發生變化. 不同手機上面運行的APP有不同的縮放系數, 視圖尺寸也隨之變化 一個universal類型的APP可能會在iPhone或iPad上運行, 視圖尺寸也隨變化 從nib加載的視圖可能會被重新調整大小以適應它的父視圖. 一個視圖可能會連同它周圍視圖一起變化 iOS9 中, iPad用戶可能會改變APP的水平長度, 是iPad多任務界面的一部分功能

在上述情況下, 都可能需要進行layout

Layout有三種主要使用方式:

Manual layout : 每當視圖尺寸變化時, 它都會調用layoutSubviews來重新布局子視圖. 當然這需要你重寫layoutSubviews方法, 然後想做什麼就做什麼, 但缺點是工作量略大… Autoresizing: iOS6之前自動進行layout的方式, 當父視圖尺寸改變時, 子視圖會根據自身autoreSizingMask中設置的規則進行自動變化. Autolayout: iOS6之後引入的新型自動Layout方式, 此方式的核心是視圖上面應用的constraints(約束). 約束(NSLayoutConstraint類型)指用於描述視圖的尺寸或位置的對象, 這種尺寸和位置是基於視圖之間的關系進行描述. 比如間距等. Autolayout是在layoutSubviews中實現的, 並且使用AutoLayout可以不用寫一行代碼就達到目的.

當然在實際工作中, 根據需要對三個方式進行靈活應用. 手動布局一直都可以用代碼實現, 而autoresizing默認打開的, 如果想關閉, 則設置父視圖的autoresizesSubviews屬性為false, 或某個view上面已經使用autolayout的時候. 使用autolayout的視圖和使用autoresizing的視圖可以共存.

10.1 Autoresizing

Autoresizing就相當於視圖的彈簧和支撐. 彈簧可以伸縮, 支撐則不能. 彈簧和支撐可以在視圖外部或內部設置, 水平或豎直方向均可. 內部可以讓子視圖尺寸重新調整. 而外部設置可以讓子視圖位置重新調整.

比如:

固定子視圖到父視圖中心, 父視圖尺寸改變時, 子視圖尺寸也跟著改變. 此時外部設置支撐, 內部設置彈簧. 固定子視圖到父視圖中心, 父視圖尺寸改變時, 子視圖尺寸不跟著改變. 則此時外部設置彈簧, 內部設置支撐. 父視圖右下角有一個按鈕固定在該處, 則此時在右上角和左下角設置支撐. 父視圖頂部固定一個文本框, 當父視圖變寬時它也同時變寬, 則設置內部豎直支撐和水平內部彈簧.

代碼中的彈簧或支撐是通過視圖的autoresizingMask屬性設置的, 該屬性值是bitMask, 所以才可以進行組合. 設置的值是UIViewAutoresizing結構體中定義的, 代表彈簧. 任何沒有被設置的值自動是支撐. 默認的是.None, 即表示所有都是支撐. 但實際中不會全部都是支撐, 因為當父視圖改變時子視圖也要跟著變.

TIP: 在調試時, 如果打印視圖到控制台, 可以看到裡面設置的autoreSizingMask, 代表的是設置的”彈簧”的列表. 父視圖邊界分別用LM, RM, TM, BM表示, 而內部的高和寬用H和W表示. 比如 打印autoresize = LM + TM,表示該視圖到父視圖的左邊界和上邊界距離是可變的.

Autoresizing還是比較有效且簡單的, 但有時正因為它太”簡單”了, 因為規則描述的唯一基礎就是子視圖和父視圖之間的關系.

10.2 AutoLayout

對每一個視圖而言, AutoLayout都是可選加入的技術. 視圖中加入AutoLayout有三種方法:

使用代碼為視圖添加約束.

從nib中加載的視圖, 這個nib勾選了”Use AutoLayout”. 那每個從這個nib加載的視圖都開啟了AutoLayout.

將界面內的視圖設置為自定義UIView類型, 然後在它的實現中重寫requiresConstraintBasedLayout類方法, 設置返回值true, 則該視圖開啟使用AutoLayout.

第三種方式在需要將本來沒有開啟AutoLayout的視圖開啟AutoLayout時使用. 通常給視圖添加約束代碼的位置是在updateConstraints方法中. 但是如果該視圖沒有開啟AutoLayout, 則updateConstraints方法不會被自動調用.

相比autoreSizing, AutoLayout可以在兄弟視圖之間使用約束關系, 並且可以針對每個視圖單獨開啟關閉AutoLayout. 但由於AutoLayout是在superview chain的基礎上實現的, 如果某視圖使用AutoLayout, 則它所有的父視圖都自動使用AutoLayout. 如果該視圖某父視圖正好是根視圖(大部分情況都是這樣), 則根視圖對應的控制器會開始接收與AutoLayout相關的事件.

TIP: 無法關閉nib中部分視圖的AutoLayout. 只能是所有視圖都使用或都不使用. 如果想分別對待, 則將它們分割到不同的nib中去.

10.2.1 Constraints

AutoLayout中的約束是NSLayoutConstraint類型的對象, 用於描述視圖的絕對尺寸, 或視圖中某尺寸值和另外視圖中某尺寸值之間的關系, 兩視圖之間的關系可以是任意, 不一定必須是父子或兄弟關系, 唯一要求的是它們必須是同一祖先的後代, 即在同一個視圖樹中.

NSLayoutConstraint類型中的主要屬性:

1 firstItem, firstAttribute, secondItem, secondAttribute:

兩個視圖及它們對應屬性(屬性類型是NSLayoutAttribute)都保存在約束中.

如果是描述某視圖絕對尺寸的約束, 則secondItem為nil, secondAttribute值為.NotAnAttribute.

其他的NSLayoutAttribute值還有:

.Top, .Bottom .Left, .Right, .Leading, .Trailing .Width, .Height .CenterX, .CenterY .FirstBaseline, .LastBaseline

.FirstBaseline主要應用在多行的label中, 它表示的是label或類似對象頂部向下的某個距離, 而LastBaseline是label底部向上某個距離.

其它的都好理解, 另外就是Leading和Trailing, 這兩個作用和left和right相同, 它們用於國際化, 即有些地區是從右往左的閱讀習慣時, 使用leading和trailing在他們的系統裡面就代表left或right, 比如一般左是leading, 右是trailing, 但有的地區相反.

而在iOS9 中, 這樣的情況是整個界面被自動鏡像到從右往左地區的習慣中, 但如果想正常工作, 則你需要設置leading和trailing來替代left和right.

2 multiplier, constant:

這兩個是數值,被應用到之前的secondAttribute上面, 以確定firstAttribute值.

firstAttribute = secondAttribute multiplier + constant*

multiplier被secondAttribute乘, 然後再加上constant即為firstAttribute.

3 relation

它是NSLayoutRelation類型, 表示上述的firstAttribute和secondAttribute如何關聯在一起. 如果是相等, 則是 firstAttribute = secondAttribute * multiplier + constant, 如果是大於, 則等號變大於號, 以此類推.

4 priority

它的值為1-1000, 某標准的行為對應有標准的優先級. 約束可以有不同優先級, 優先級決定不同約束的應用優先次序.

約束是屬於視圖的, 一個視圖可以有多個約束, 視圖中有constraints屬性, 並且有對應的對象方法:

addConstraint:, addConstraints: removeConstraint:, removeConstraints:

另外, 如何確定約束屬於哪個視圖, 方法是: 約束牽扯的兩個視圖, 如果是父子關系, 約束屬於父視圖, 如果是兄弟關系, 約束屬於它們共同的父視圖, 如果是絕對約束, 則屬於該視圖.

從iOS8 之後, 除了給一個視圖顯式添加約束, 還可以用NSLayoutConstraint的類方法activateConstraints來激活約束, 該方法接受一個約束數組, 然後將數組中約束自動加入到合適位置, 將以前需要程序員決定視圖位置的工作自動完成. 另外有一個類方法deactivateConstraints, 可以將數組中的約束從對應視圖中全部移除.

另外, 約束自己還有activate屬性, 表示激活與否.

除priority和constant外, NSLayoutConstraint對象(約束對象)的其他屬性都是只讀的. 如果想改變某約束的其他屬性值, 只有移除它在重新建一個.

WARNING: 當顯式使用約束來確定視圖的尺寸和位置後, 就不要再設置視圖的frame(以及bounds和center), 後面只能是單獨使用約束. 否則如果約束設置了又去設置frame, 當調用layoutSubviews時, 視圖會跳回到約束設置的位置. 除非你是在layoutSubviews方法中設置frame.

10.2.2 AutoReSizing Constraints

當給一個視圖設置約束時, 會自動將另外那個視圖也牽扯進來, 這時需要一個途徑將之前通過自動尺寸autoresizingmask或frame定義的另外那個視圖中的這些規則全部轉換為約束.

運行時會自動完成這樣的工作: 它將視圖的frame和bitmask設置自動轉換為約束. 結果是一個隱式約束集合, 類型為NSAutoresizingMaskLayoutConstraint.

只對被關聯進去的視圖進行轉換隱式約束, 添加約束的視圖自動就是顯式約束.

但這種轉換的前提是被轉換視圖的translateAutoresizingMaskIntoConstraints屬性必須設置為true. 這個屬性值當視圖從代碼創建或nib中未勾選使用自動約束時默認為true.

但如果不想自動轉換成隱式約束, 而是自己添加約束, 則應先將視圖的該屬性設置為false.

如果忘記關閉, 特別是代碼創建視圖時, 就可能造成隱式約束和顯式添加的約束共同作用在視圖中的情況發生.

10.2.3 代碼中創建約束

約束中屬性是在創建時設置, 除了priority可以改變, priority默認為1000.

約束屬於誰就向誰添加約束. 並且要記住兩點重要內容:

約束歸屬一定不要錯 會被轉換隱式約束的視圖一定要關閉約束轉換.

由於提供的代碼創建約束過於繁瑣, 一般不是這樣用.

10.2.4 Anchor 描述法

在iOS9中, 新增了一種在代碼中描述約束的方法, 這樣可以更容易地寫約束.

新的寫法更加簡潔, 但也更加奇特.

這種方法的核心是關注視圖的anchor屬性:

topAnchor, bottomAnchor leftAnchor, rightAnchor, leadingAnchor, trailingAnchor centerXAnchor, centerYAnchor firstBaselineAnchor, lastBaselineAnchor

anchor的值都是NSLayoutAnchor類型或其子類類型的對象. 故構造約束的方法變為了使用NSLayoutAnchor的實例方法, 有許多種可供選擇, 選擇的依據是如果約束中需要另外的Anchor, 以及包含的multiplier和constant的值.

雖然方法很多, 但如果實際用了會發現很好用, 簡潔好懂便於維護.

這種新方法需要在和activateConstraints結合使用時很方便, 我們不用關心約束將加入哪個視圖. 只是新建約束, 然後將約束激活, 系統自動決定約束歸屬.

//8句話建立8個約束
        let constraintArr1 = [
            v2.leadingAnchor.constraintEqualToAnchor(v1.leadingAnchor)!,
            v2.trailingAnchor.constraintEqualToAnchor(v1.trailingAnchor)!,
            v2.topAnchor.constraintEqualToAnchor(v1.topAnchor)!,
            v2.heightAnchor.constraintEqualToConstant(80)!,

            v3.bottomAnchor.constraintEqualToAnchor(v1.bottomAnchor)!,
            v3.leadingAnchor.constraintEqualToAnchor(v1.leadingAnchor)!,
            v3.trailingAnchor.constraintEqualToAnchor(v1.trailingAnchor)!,
            v3.heightAnchor.constraintEqualToConstant(80)!
        ]

        NSLayoutConstraint.activateConstraints(constraintArr1)
10.2.5 Visual Format Language(VFL)

還有一種更為簡潔的方式創建約束, 即使用VFL.

這種方式還有個優點是一句話可同時定義多個約束, 尤其當安排一系列的視圖在水平和豎直方向上的位置時.

比如下面這條簡單語句:

V:|[v2(10)]

其中 V:表示討論的是豎直方向的約束, 對應的是H:水平方向上的約束(默認就是H, 如果不寫)

視圖名稱用方括號括起來[v2], |代表父視圖.這裡就是指豎直方向上, v2貼著父視圖, 如果是用括號接在視圖名稱之後, 表示設置該方向上該視圖的長度. 上面即指定豎直方向v2頂部貼近父視圖, 且v2在豎直方向長度為10.

為了使用VFL, 需要使用字典來定位每個視圖的名稱, 這樣不易出錯.

好處是安排多個視圖間關系容易.

雖說語法有些奇怪, 但還是需要有所了解.

並且, 方法中的參數有一些需要知道含義:

metrics: 是一個值字典, 即在VFL中使用到的值可以在這裡定義, 然後在VFL中使用它的Key即可. options: 為一個字節掩碼. 要設置兩個相鄰視圖之間的距離, 使用 [v1]-20-[v2]這樣的語法

完整的介紹詳見蘋果Visual Format Language中的Auto Layout Guide一章.

10.2.6 Constraints as Objects

許多時候不但需要建立約束, 並且需要重用約束, 這就需要約束對象一直被保持.

比如在將來某個時候你需要對界面進行徹底的改變, 插入或刪除一些視圖, 此時如果有約束集合內保存著以前的約束, 要進行這樣的工作就十分容易了, 每個集合中的約束對應著不同的界面配置.

    //第一種配置
        let constraintArr1 = [
            v2.leadingAnchor.constraintEqualToAnchor(v1.leadingAnchor)!,
            v2.trailingAnchor.constraintEqualToAnchor(v1.trailingAnchor)!,
            v2.topAnchor.constraintEqualToAnchor(v1.topAnchor)!,
            v2.heightAnchor.constraintEqualToConstant(80)!,

            v3.bottomAnchor.constraintEqualToAnchor(v1.bottomAnchor)!,
            v3.leadingAnchor.constraintEqualToAnchor(v1.leadingAnchor)!,
            v3.trailingAnchor.constraintEqualToAnchor(v1.trailingAnchor)!,
            v3.heightAnchor.constraintEqualToConstant(80)!
        ]
        //第二種配置
        let constraintArr2 = [
            v2.leadingAnchor.constraintEqualToAnchor(v1.leadingAnchor)!,
            v2.trailingAnchor.constraintEqualToAnchor(v1.trailingAnchor)!,
            v2.bottomAnchor.constraintEqualToAnchor(v1.topAnchor)!,
            v2.heightAnchor.constraintEqualToConstant(50)!,

            v3.bottomAnchor.constraintEqualToAnchor(v1.bottomAnchor)!,
            v3.leadingAnchor.constraintEqualToAnchor(v1.leadingAnchor)!,
            v3.trailingAnchor.constraintEqualToAnchor(v1.trailingAnchor)!,
            v3.heightAnchor.constraintEqualToConstant(30)!,
        ]
        NSLayoutConstraint.activateConstraints(constraintArr1)  //激活某個配置

兩種配置的效果不一樣, 見下圖:

這裡寫圖片描述
這裡寫圖片描述

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved