1. 程式人生 > >【iOS 開發】從 setNeedsLayout 說起

【iOS 開發】從 setNeedsLayout 說起

本文從 setNeedsLayout 這個方法說起,分享與其相關的 UIKit 檢視互動、使用場景等內容。

UIKit 為 UIView 提供了這些方法來進行檢視的更新與重繪:

1234567publicfunc setNeedsLayout()publicfunc layoutSubviews()publicfunc layoutIfNeeded()publicfunc setNeedsDisplay()publicfunc setNeedsDisplayInRect(rect:CGRect)publicfunc drawRect(rect:CGRect)

執行時檢視互動模型

無論是使用者互動觸發還是程式碼自動觸發,下圖展示的事件序列都同樣適用,這裡用到了 setNeedsLayout

 方法:

UIKit interactions with your view objects

上圖對應的事件序列如下:

  1. 使用者觸控式螢幕幕
  2. 硬體報告觸控事件給 UIKit 框架
  3. UIKit 框架將觸控事件打包成 UIEvent 物件,然後分發給合適的檢視
  4. 事件處理程式碼會對相應事件作出響應,程式碼可以是這樣的:
    -更改 frameboundsalpha 等屬性
    -呼叫 setNeedsLayout 方法以標記該檢視(或者它的子檢視)為需要進行佈局更新
    -呼叫 setNeedsDisplay 或者 setNeedsDisplayInRect: 方法以標記該檢視(或者它的子檢視)需要進行重畫
    -通知 Controller 有資料變化
  5. 如果一個檢視的幾何結構改變了,UIKit 會更新它的子檢視
  6. 如果任何檢視的任何部分被標記為需要重畫,UIKit 會要求檢視重畫自身
  7. 任何已經更新的檢視會與應用餘下的可視內容組合在一起,同時被髮送到圖形硬體去顯示
  8. 圖形硬體將已解釋內容轉化到螢幕上

方法呼叫邏輯

在上面的過程中,我們可以接觸到文章開頭提到的方法,他們的呼叫邏輯是這樣的:

  1. setNeedsLayout 會給當前 UIView 立一個 flag,以表示後續應該呼叫 layoutSubviews 方法,以調整當前檢視及其子檢視的佈局。
  2. setNeedsDisplayInRect: 會給當前 UIView 立一個 flag,以表示後續應該呼叫 drawRect:
     方法,以進行檢視重繪。

View Drawing Cycle

Apple 官方文件已經明確說明,開發者不應該直接呼叫 layoutSubviews 與 drawRect:,而應該在你認為系統預設的佈局和重繪不能帶給你想要的效果時,在子類中重寫這些方法,然後分別通過 setNeedsLayout 和 setNeedsDisplayInRect: 來進行呼叫。

當然你可以給多個 UIView 設定 setNeedsLayout,然後當下一個 View Drawing Cycle 到來時,多個 UIView 的檢視會一同更改佈局。

那麼這個 View Drawing Cycle 到底是什麼呢,官方是這樣解釋的:

The system waits until the end of the current run loop before initiating any drawing operations. This delay gives you a chance to invalidate multiple views, add or remove views from your hierarchy, hide views, resize views, and reposition views all at once. All of the changes you make are then reflected at the same time.

顯然這樣用 RunLoop 把多次修改聚集在一個 Cycle 一併進行渲染是更加高效的行為。

(我個人對 View Drawing Cycle 的理解是這樣的:UIKit 需要處理非常多的事件,這些事件組合起來變成了一個非常複雜的事件序列,在這個序列中有些特定的點是 UIKit 專門提供給 UIView 來進行檢視更改的。如上所述,在當前 run loop 結束之前,我們有機會做各種檢視更改,並且這些更改會在下一個 run loop 體現出來,所以 View Drawing Cycle 就是一次次 run loop 中我們通過 UIKit 得到的 UIView 重佈局、重繪機會所組成的迴圈。有理解不對的地方,歡迎評論指正。)

如何善用 View Drawing Cycle

一個很常見的例子是,一個 iPad App,橫屏和豎屏時介面佈局不一樣,那麼你可以監聽裝置旋轉,在裝置旋轉時執行 setNeedsLayout 方法,然後在 layoutSubviews 裡面通過判斷接下來是橫屏還是豎屏來進行不一樣的佈局設定。基本上你不可能只在這個方法裡只進行了單個 UIView 的佈局修改,而是多項修改,那麼 App 會在下一個 View Drawing Cycle 到來時,把這些修改一起執行,這是最正常的情況。

那麼假如我不按 Apple 規定的來,直接呼叫 layoutSubviews 呢?我們可以猜想一下:因為這個方法裡面提供了我們需要的佈局方式,所以 UIView 會按我們想要的方式來佈局,但是因為各種檢視修改的請求時機是零碎的,所以這樣效率會低一些。所以重要的其實是瞭解何時會觸發 layoutSubviews

  • init 初始化不會觸發 layoutSubviews
  • addSubview 會觸發 layoutSubviews
  • 設定 view 的 frame 會觸發 layoutSubviews,當然前提是 frame 的值設定前後發生了變化
  • 滾動一個 UIScrollView 會觸發 layoutSubviews
  • 旋轉 Screen 會觸發父 UIView 上的 layoutSubviews 事件
  • 改變一個 UIView 大小的時候也會觸發父 UIView 上的 layoutSubviews 事件

然後按 Apple 要求的方式來做就好了(分別通過 setNeedsLayout 和 setNeedsDisplayInRect: 來呼叫 layoutSubviews 和 drawRect:

但有些情況比較特殊:你開啟 iOS 的時鐘應用,去看裡面的秒錶頁面,這個頁面裡面的兩個按鈕是沒有 UIButton 預設的動畫的,點選之後,按鈕會瞬間改變自身的狀態(顏色、內部 Label 的內容),這種情況我們需要跳出 View Drawing Cycle,來實現一個瞬間改變的效果。實現方法如下:

1 2 3 4 5 6 7 8 extensionUIButton{ func quickButtonAction(){ UIView.performWithoutAnimation({ // do something self.layoutIfNeeded() }) } }

可以看出 layoutIfNeeded 作為一個輔助選項給了 setNeedsLayout 一個可以瞬時執行的特點。當然預設這個“選項”是關閉的。

setNeedsDisplay 補充

setNeedsLayout 的使用場景之前已經提過了(iPad App),下面舉個栗子說一下 setNeedsDisplayInRect:的使用場景。

假如我需要在兩點之間繪製一條直線,有兩個 dotView,需要繪製一個 lineView。我在 drawRect: 方法裡實現了 lineView 的具體繪製方法(根據兩個點來繪製)。那麼如果我想要這個直線一直根據兩個點同步變化的話,就需要在 dotView 的位置發生改變時,執行:

1 lineView.setNeedsDisplay()// 重繪 lineView

至於 drawRect: 方法什麼時候會被觸發:

From StackOverFlow