Core Animation 之 View

Jani發表於2018-04-02

最近切實的感受到了自己在介面這一塊基礎原理的匱乏,因此決定深入學習瞭解一下Core Animation, 但是在學習Core Animation的開端,不禁又懷疑自己,UIWindow是什麼?它的作用你真的明白嗎?為什麼要有UIWindow呢,如果沒有的話行不行呢?一系列問題層出不窮,因此我決定在研究學習Core Animation之前去學習學習蘋果的官方文件View Programming Guide for iOS, 那麼在開始的這一篇我只會去寫一寫我對於UIView的學習,總結,應該還會有一些問題,希望和大家一起交流交流。


可能在文中會用到不同的詞,但是他們是一個含義:
view == view物件 == 檢視物件 == UIView物件
window == window物件 == 視窗物件 == UIWindow物件

(注:UIWindow的基類是UIView)
複製程式碼

View and Window

View到底是什麼?View物件在螢幕上定義了一塊矩形的區域並且處理這個區域內部的事件。在UIKIT中,每一個UIView物件都有一個CALayer物件的layer屬性,layer物件負責檢視內容的繪製,處理檢視動畫相關的事宜。檢視由layer繪製,動畫也由layer處理,那麼UIView物件做什麼呢?其實UIView物件可以理解為是對layer的封裝,提供了處理觸控事件的具體功能以及Core Animation底層方法的介面。

什麼是Core Animation呢?

image

Core Animation 是是iOS和OSX上可用的圖形渲染和動畫基礎結構,用於為應用的檢視和其他視覺元素設定動畫。它把大量的繪圖工作交給GPU去加速渲染,所以可以實現高幀率高流暢度的動畫效果,並不會去負載CPU而使得手機卡頓。

Core Animation 並不是一個繪圖系統,而是使用硬體去合成和處理檢視內容的基礎框架,而這個基礎框架的核心就是layer物件,那麼layer又是如何對檢視內容進行處理的呢?它會將其轉化為GPU便宜處理的bitmap,然後GPU再去執行相應的操作,大多數APP中layer都是用來管理檢視內容的,但是也可以去單獨的建立獨立的layer。

那麼現在可以對Core Animation可以進行一些底層的瞭解了:

Core Animation是一套包含圖形繪製、投影、動畫的Objective-C類集合,該框架包含在QuartzCore.framework中.

image

上圖的OpenGL ES是一個C語言寫的非常底層的圖形處理框架。是個移動裝置上繪製2D和3D計算機圖形的標準的開源庫,廣泛地被用在遊戲的圖形的繪製上負責直接驅動GPU,效率很高,但是使用起來很複雜。

Core Animation的核心就是對於OpenGL ES的抽象,它並不做渲染工作,而是使用OpenGL ES的功能,讓GPU去處理渲染。從上圖的結構可以知道Core Graphic 的繪製是需要消耗CPU的,而Core Animation渲染消耗的是GPU。Core Animation最繁重的任務是去判斷出哪些layer需要被重新繪製,而OpenGL ES要做的就是將layer合併、顯示在螢幕上。

工作的流程圖

image

Core Graphic, Core Animation, Core Image是三個不同的核心庫,它們之間可能會有互動,但是不存在一個包含的關係。上圖是它們的工作流程圖

當你設定一個 layer 的內容為 CGImageRef 時,Core Animation 會建立一個 OpenGL 紋理,並確保在這個圖層中的點陣圖被上傳到對應的紋理中。以及當你重寫 -drawInContext 方法時,Core Animation 會請求分配一個紋理,同時確保 Core Graphics 會將你所做的(即你在drawInContext中繪製的東西)放入到紋理的點陣圖資料中。一個layer的屬性和 CALayer 的子類會影響到 OpenGL 的渲染結果,許多底層的 OpenGL ES 行為被簡單易懂地封裝到 CALayer 概念中。

回到主題 -> UIView

好了,走偏了我們還是繼續聊一聊UIView吧。

檢視的層次結構

每一個父檢視都會維持一個子檢視陣列,在陣列最後一個子檢視就是最上層的子檢視,如果子檢視重疊了,陣列中靠後的子檢視位於上方,父檢視和子檢視的關係會影響子檢視的顯示,在程式碼中常見的有兩個場景

//1. 要求子檢視不透明,父檢視透明
layerView.backgroundColor = UIColor.black.withAlphaComponent(0.1)

//2. scrollView自動調整子檢視size如何解決?
let scrollView = UIScrollView.init(frame: CGRect.init(x: 0, y: 0, width: 100, height: 100))
scrollView.autoresizesSubviews = false
複製程式碼
檢視的響應事件

當談及UIView和CALayer的主要區別時,最重要的區別就是UIView是可以響應事件的,那麼View是如何響應事件的呢?簡單來講分為兩步:

  1. 尋找發生觸控事件的檢視(從上往下)
  • 發生觸控事件之後,系統會將該事件加入到一個由UIApplocation管理的事件佇列中
  • UIApplication會從事件佇列中取出最靠前的事件向下分發,一般先給當前的KeyWindow
  • KeyWindow根據檢視結構依次向下分發,直到找到最合適事件處理的view
  1. 找到事件的第一響應者(從下往上)
    事件響應
  • 事件響應由最底層的View開始,如果View處理不了,就向上找父View,一直找到當前Window,如果Window處理不了,會傳給UIApplication處理,如果UIApplication處理不了就由UIApplecation的delegate處理。
檢視的繪製週期

當view第一次在螢幕上顯示的時候,系統會繪製它的內容,然後系統會擷取內容的快照,並且將快照作為檢視的可見外觀,如果你永遠不改變檢視的內容,那麼檢視的繪製程式碼永遠不會改變。如果更改了檢視的內容,不用直接重新繪製,而是使用setNeedsDisplay或者setNeedsDisplayInRect方法是檢視無效。這些方法會告知檢視內容以及改變並且需要在下一次進行重繪。如果需要立馬重繪,那麼需要使用layoutIfNeeded方法。

  • 更改了Frame,或者Bounds的寬和高
  • 分配了包含縮放係數的transform屬性
    contendMode
檢視的伸縮

有時候只需要可視檢視的一部份可以被伸縮,這個在設定按鈕背景圖片的場景是非常常見的,那麼如何讓這個常見的需求實現呢?使用resizableImageWithCapInsets:方法,可以確定可縮放的檢視的內容,如下:

var image = UIImage.init(named: "image.png")
image = image?.resizableImage(withCapInsets: UIEdgeInsetsMake(10, 10, 10, 10), resizingMode: UIImageResizingMode.stretch)
let imageview = UIImageView.init(image: image)
複製程式碼

伸縮背景的效果如下:(使四個角不變,中心進行縮放)

stretch

檢視可以實現的動畫

檢視可以實現的動畫效果的屬性如下:

  • frame
  • bounds
  • center
  • transform
  • alpha
  • background
座標形狀和座標系統

UIKit的座標系以螢幕的左上角為原點

座標
如果是OpenGL ES或者是Core Graphic的話是以螢幕的左下角為座標原點的一個笛卡爾座標系

檢視需要注意的幾點
  • 在修改檢視的transform的時候,每一種變換都是針對檢視中心而言的
  • 儘量少的使用drawRact繪製
  • 只要有可能就顯示宣告檢視為Opaque

回到主題之Window

那麼window的職責是什麼,為什麼要有Window的存在呢?沒有Window行不行呢? 下面試官網的原文

* It contains your application’s visible content.
* It plays a key role in the delivery of touch events to your views and other application objects.
* It works with your application’s view controllers to facilitate orientation changes.
複製程式碼

如果把viewController拿出來看,我們知道viewController一般對應著相應的應用內部模組,如果要和其他的應用互動呢?這個就需要靠Window了;當然還有一點是螢幕的方向,這個也是通過Window來控制的!

涉及視窗的任務

在大多數時候,我們只需要在didFinishLaunchingWithOptions:中初始化window之後就不會去涉及其他的window操作了,如果使用IB的話,連window的初始化也沒有必要了;但是在實際的程式碼過程中仍然可能會涉及兩個任務

  • 使用視窗物件將點和矩形轉換為視窗的本地座標系統或從視窗的本地座標系統轉換點和矩形
  • 通過視窗的通知來追蹤視窗的變化
    • UIWindowDidBecomeVisibleNotification
    • UIWindowDidBecomeHiddenNotification
    • UIWindowDidBecomeKeyNotification
    • UIWindowDidResignKeyNotification
關於不同的螢幕

其實吧,分屏是一個非常常見的功能,如果接觸過分屏的話就需要去仔細的研究研究分屏相關的程式碼,這一塊也是和UIWindow息息相關的。

回到主題之再談View

關於檢視的層次結構
  • 使用tag 使用viewWithTag:搜尋對應的檢視要比從父檢視的子檢視結構中搜尋要快
  • 調整檢視的層級結構(使用調整層級的方法比移除子檢視然後重新插入更快) bringSubviewToFront: sendSubviewToBack: exchangeSubviewAtIndex:withSubviewAtIndex:
自動調整尺寸自動化調整佈局變更

在佈局中有一個很討厭的事情,就是明明你的frame都已經設定好了,但是依然在執行的時候就會發生變化,其實很多這種情況的原因就是被新增的子檢視的frame會隨著父檢視的frame的變化而發生變化;其二就是有時候子檢視需要保持和父檢視一致的大小變換,或者維持原有的尺寸不變,這個時候就需要檢視的自動化調整佈局了。

父檢視的 autoresizesSubviews 屬性決定所有子檢視是否需要調整。如果屬性設定為 YES,檢視會使用每個子檢視的 autoresizingMask 屬性決定子檢視的位置和尺寸。每個子檢視的尺寸變更會對它們的子檢視觸發同樣的佈局調整。每一個子檢視的autoresizingMask屬性如何決定呢?

  • UIViewAutoresizingFlexibleHeight 當父檢視高度變更時檢視的高度也變更。如果這個常量沒被包含,檢視的高度將不會變更。
  • UIViewAutoresizingFlexibleWidth 當父檢視寬度變更時檢視的寬度也變更。如果這個常量沒被包含,檢視的寬度將不會變更。
  • UIVIewAutoresizingFlexibleLeftMargin 檢視的左邊緣和父檢視的左邊緣之間的距離根據需要增長或縮短。如果這個常量沒有設定,檢視的左邊緣與父檢視的左邊緣之間的距離保持固定。
  • UIViewAutoresizingFlexibleRightMargin 檢視的右邊緣和父檢視的右邊緣之間的距離根據需要增長或縮短。如果這個常量沒有設定,檢視的右邊緣與父檢視的右邊緣之間的距離保持固定。
  • UIViewAutoresizingFlexibleBottomMargin 檢視的底部邊緣和父檢視的底部邊緣之間的距離根據需要增長或縮短。如果這個常量沒有設定,檢視的底部邊緣與父檢視的底部邊緣之間的距離保持固定。
  • UIViewAutoresizingFlexibleTopMargin 檢視的頂部邊緣和父檢視的頂部邊緣之間的距離根據需要增長或縮短。如果這個常量沒有設定,檢視的頂部邊緣與父檢視的頂部邊緣之間的距離保持固定。
檢視初始化

在使用code時: initWithFrame: 在使用nib時: initWithCoder: -> awakeFromNib

檢視的繪製

在實際的操作中,要儘量少的使用drawRect:方法,如果要使用此方法,明確它的職責:繪製內容。使用的時候要配置環境,繪製內容,儘可能快結束繪製。 提高繪製效能的兩個小tips:

  1. 如果你清楚你的檢視繪製程式碼總是以不透明的內容覆蓋檢視的整個表面,你可以設定檢視的 opaque 屬性為 YES 提升系統效能。當你標記了檢視為不透明時,UIKit 會避開在位於檢視背後的繪製程式碼。這不是唯一縮減繪製花費的時間的方式但也最大限度減少檢視和其他內容之間的合成工作。因此,如果你的檢視的內容是完全不透明的那麼應該設定這個屬性為 YES。如果你的檢視不能保證內容總是不透明的,你應該設定這個屬性為 NO。
  2. 另一個提升繪製效能的方式,尤其在滾動過程中,是設定檢視的 clearsContextBeforeDrawing 屬性為 NO。當這個屬性設定為 YSE 時,在你的 drawRect: 方法呼叫之前 UIKit 把將要被 drawRect: 方法更新的區域自動填充為半透明的黑色。設定這個屬性為 NO 會消除填充操作的開銷但會加重你的應用程式通過 drawRect: 方法填充更新矩形的內容的負擔。
檢視的動畫

這個就非常常見了,大多使用的是UIView自帶的Block動畫,這個就不在細聊了,有一點想提一提,就是普通的動畫只能是從A -> B兩種狀態之間進行切換,如果加上互動的話,動畫就會變得不流暢了,所以Apple在iOS10新增了一個新的特性:UIViewPropertyAnimator ,這個在後面再聊吧。

總結

也不知道為什麼寫了這麼多,其實寫的時候,主要還是很多東西都是有關聯的,一旦深入其實還會有非常多的東西在裡面,以上是我的學習和一些總結。如果寫的不好,歡迎指出問題,這樣我就可以慢慢改進了,下一篇,我會用比較短的篇幅聊一聊UIViewController,然後就正式的進入我的Core Animation的學習過程中了。

相關文章