歡迎大家關注我的公眾號,我會定期分享一些我在專案中遇到問題的解決辦法和一些iOS實用的技巧,現階段主要是整理出一些基礎的知識記錄下來
文章也會同步更新到我的部落格:
ppsheep.com
動畫的由來
在iOS中所有的檢視都是從UIView的基類派生而來,UIView可以處理觸控事件,可以支援基於Core Graphics繪圖,也能夠做一些簡單的動畫,比如旋轉、縮放或者其他一些滑動漸變的動畫。但是實際上,這是蘋果為我們封裝了一層,真正實現動畫的其實是一個叫做圖層的玩意兒(CALayer),UIView所做的一切動畫,都是蘋果從CALayer封裝而來。
CALayer
CALayer類在概念上和UIView類似,也可以像View一樣新增一些子layer(圖片,文字等等),也能夠像View一樣,管理子圖層的位置大小等等,並且,CALayer有一些非常重要的屬性和方法,iOS中的動畫就是通過這些來做動畫和變換,CALayer和UIView最大的不同就是CALayer不處理使用者的互動。
UIView和CALayer的關係
每一個UIView都有一個CALayer例項屬性,UIView的職責就是建立並建立這個圖層,以確保在子檢視被建立時,子圖層也能夠被建立,子檢視被新增和移除的時候,子圖層也能夠做相對的新增移除操作。 他們的關係是一一對應的。
實際上,我們在螢幕上看到的檢視或者動畫,其實都是圖層。UIView只是蘋果為我們封裝的高階API
這個就有個歷史原因了,主要呢 是要在Mac上也使用CALayer,但是iOS裝置的觸控和Mac的滑鼠點選又不一樣,在Mac上,高階API就叫做NSView了,更多了,就不在這裡講了。
哪裡能用到CALayer
一般的,我們在處理一些簡單的動畫時,都用不到CALayer,既然蘋果為我們封裝好了,幹嘛不用呢?但是如果需要再處理一些高階的動畫,那UIView可能就不能滿足我們的需求了。
沒有暴露出來的CALayer功能:
- 陰影,圓角,帶顏色的邊框
- 3D變換
- 非矩形範圍
- 透明遮罩
- 多級非線性動畫
使用圖層
我們先來感受一下圖層
新建一個工程
在view中新增一個view
UIView *layerView = [[UIView alloc] initWithFrame:CGRectMake(10, 10, 200, 200)];
layerView.backgroundColor = [UIColor redColor];
layerView.center = self.view.center;
[self.view addSubview:layerView];複製程式碼
然後我們想要在這個小紅方框的中間新增上一個藍色的小方框。當然,我們肯定知道這很簡單,直接加上一個子view就行了,但是這樣做的,就失去了我們學習CALayer的意義。
我的想法是這樣,既然layer像view一樣,那我們是否可以在layerView的layer上加上一個藍色方框樣式的layer 我們的做法是這樣
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50, 50, 100, 100);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;
[layerView.layer addSublayer:blueLayer];複製程式碼
效果就出來了 我們來看看3D圖
我們可以看到,這樣的效果,我們只能看到一個圖層,一個view 並沒有向layerView上新增子view
當然這裡我們只是做一個layer的介紹,並不是說你之後新增檢視這樣新增,這樣肯定是錯誤的,我們之前講過,layer不能處理使用者的互動,這個很重要。
但是在什麼情況下,我們需要這樣來使用CALayer呢?
- 開發同時可以在macOS上執行的跨平臺應用
- 使用多種CALayer,並不想建立額外的UIView去封裝他們(這個後面會講到)
- 做一些對效能要求較高的工作,但是遇到這種情況,我們很多時候都直接使用OpenGL繪圖了
總的來說呢,處理檢視肯定比處理圖層簡單多了
我們這裡建立這一個例子,只是為了來介紹,圖層的樹狀結構,和檢視的一一關聯關係。
寄宿圖
寄宿圖是什麼意思呢?其實呢,就是圖層中包含圖
contents屬性
在CALayer中有一個屬性,叫做contents,這個屬性的型別被定義為id,看上去好像這個屬效能夠接收任意型別的值,如果給contents賦予了任意一個型別的值,你的APP也能夠編譯成功,但是得到的圖層確實一個空白的圖層,事實上,這個contents在iOS下,是需要一個CGImage的值。
那為什麼這個又要寫作一個id型別呢,這個又是一個歷史原因了 ,明顯的這個是因為macOS的原因,因為在macOS下,這個是接收NSImage型別。
在UIImage中 有一個CGImage屬性,他返回的是一個CGImageRef(指向CGImage的指標),如果你直接把這個賦給contents,那是會編譯出錯的。CGImageRef是一個Core Foundation物件,並不是一個cocoa物件,但是我們可以通過bridged來進行轉換,我們來向剛剛建立的layerView的圖層賦予一張圖片
UIImage *image = [UIImage imageNamed:@"plane"];
layerView.layer.contents = (__bridge id _Nullable)(image.CGImage);複製程式碼
這樣我們就避開了UIImageView,直接向UIView的圖層設定一張圖片
contentsGravity屬性
但是我們看到中間的圖片明顯被拉伸了,我們想要展示他原有的效果,怎麼做呢?
在我們使用UIImageView時,我們處理這種拉伸,一般是使用UIimageView的一個屬性
imageView.contentModel = UIViewContentModeScaleAspectFit;複製程式碼
在CALayer中有一個和contentMode相似的屬性,叫做contentsGravity,不同的是他是一個NSString型別。
contentsGravity值的型別包含:
- kCAGravityCenter
- kCAGravityTop
- kCAGravityBottom
- kCAGravityLeft
- kCAGravityRight
- kCAGravityTopLeft
- kCAGravityTopRight
- kCAGravityBottomLeft
- kCAGravityBottomRight
- kCAGravityResize
- kCAGravityResizeAspect
- kCAGravityResizeAspectFill
這其中的型別與contentMode都有一一對應關係的,其中kCAGravityResizeAspect相當於檢視中contentMode型別的UIViewContentModeScaleAspectFit
當我們將layerView的contentsGravity設定成kCAGravityResizeAspect
layerView.layer.contentsGravity = kCAGravityResizeAspect;複製程式碼
效果就不一樣了
contentsScale屬性(主要針對Mac)
contentsScale定義了寄宿圖的畫素尺寸和檢視大小的比例,預設情況下是一個值為1.0的浮點數。那麼我們一般怎麼使用這個屬性呢?
這個屬性其實是為了支援高解析度螢幕機制而出現的,他用來判斷在繪製圖層的時候,應該為寄宿圖建立建立的空間大小和需要顯示圖片的拉伸度。
簡單來說,如果我們將contentsScale設定為1.0 那麼寄宿圖建立出來的圖片將會以每個點一個畫素來繪製圖片,如果設定為2,那麼將會以每個點2個畫素來繪製圖片。這個就是我們熟知的retina螢幕。
在我們之前使用contentsGravity = kCAGravityResizeAspect這個屬性時,預設是將圖片等比例拉伸至適應圖層大小,但是,如果我們將contentsGravity設定成kCAGravityCenter,我們看一下,效果會是什麼樣?
整個圖片直接放大,將原圖層蓋住,這是因為kCAGravityCenter屬性值,預設是不會對圖片進行拉伸,所以將圖片的原始大小展示了出來,這時候,我們的contentsScale就起到了作用
layerView.layer.contentsScale = image.scale;複製程式碼
這時,我們看到,現在使用了正確的圖片來進行繪製
注意,當我們使用程式碼來設定寄宿圖時,我們一定要手動設定contentsScale
layerView.layer.contentsScale = [UIScreen mainScreen].scale;複製程式碼
這樣,我們的圖片在retina裝置上,才會顯示正常。
maskToBounds屬性
不知大家有沒有注意到,在上面的圖中,我們的圖片已經超過了圖層的邊界,預設情況下,在UIView中,也會繪製超過邊界的內容或者子檢視。
在UIView中,控制是否超出邊界的屬性是clipsToBounds,在CALayer中,控制的屬性是masksToBounds,將他設定成yes
contentsRect屬性
contentRect屬性,允許我們在圖層中顯示寄宿圖的部分割槽域,這涉及到圖片的顯示和圖片是如何拉伸的,所以比contentsGravity靈活得多,這裡也會多講一下。
雖然這個屬性有帶一個rect的樣式,這樣很容易讓我們想到bounds和frame,但是這個屬性和他們的使用方法確是不一樣的。他使用的是單位座標,單位座標指定在0到1之間,是一個相對值,是相對於寄宿圖的位置和大小。
預設的contentsRect是{0,0,1,1} 這樣就表示寄宿圖的全部區域。如果我們指定一個區域,那麼寄宿圖就會被顯示一部分割槽域
可以看到現在寄宿圖是全部顯示的,這時候我們來設定一下layer的contentsRect屬性
layerView.layer.contentsRect = CGRectMake(0.5, 0.5, 0.5, 0.5);複製程式碼
這時候,圖層就會這樣顯示
明顯我們可以看到 這是顯示的圖片的右下角區域 這樣顯示也是我們給定的{0.5,0.5,0.5,0.5}決定的 從圖片的中點位置開始,顯示半寬半高
那麼我們在做APP時,什麼地方經常使用到這個屬性呢?
我們經常在圖片拼合的時候用到這個屬性,這個圖片拼合概念在遊戲開發中經常碰到。
說簡單點,就是將多張圖片打包成一張圖片,然後一次性載入這一帳圖片,這樣帶來的好處就是能夠在記憶體使用上,螢幕渲染上節省很多效能。
例子:這種使用方法,我們常見的APP中,很多美顏的相機使用了layer新增一些效果圖片到當前的顯示檢視上,比如新增一個什麼笑臉啊,相框啊之類的。
contentsCenter屬性
咋一看,我們會以為這個和寄宿圖的位置相關,其實不是的,這個屬性主要是用來控制圖片的拉伸,其實他是一個CGRect,他定義了一個固定的邊框和一個在圖層上可以拉伸的區域,改變contentsCenter的值,並不會影響寄宿圖的顯示,除非這個圖層的大小改變了,我們才能看得到結果
contentsCenter預設的大小是{0,0,1,1},這就意味著,如果圖層的大小改變了,那麼整個寄宿圖都會被均勻拉伸,如果我們改變contentsCenter這個屬性,定義一個拉伸的區域,那麼我們就能看到效果了
注意
這裡我畫了一張圖,整個一個方框表示的是一個圖層,如果我們將圖層的contentsCenter設定為{0.25,0.25,0.25,0.25}那麼,其實這個rect形成的一個方框就是中間的I區域,相當於整個圖層的正中心,寬高各一半的位置 如果我們現在改變了圖層的大小的話,我們定義了這樣一個拉伸區域{0.25,0.25,0.25,0.25},表示的就是在橫向拉伸中H區域和D區域會被拉伸,在縱向拉伸中,B區域和F區域會被拉伸,而中間的I區域則是橫向縱向均會被拉伸,而其中的A、C、E、G則不會被拉伸,可能這裡需要著重理解一下
到這裡,基本上就將CALayer中我們可能會經常使用到的一些重要屬性講解了一下