歡迎大家關注我的公眾號,我會定期分享一些我在專案中遇到問題的解決辦法和一些iOS實用的技巧,現階段主要是整理出一些基礎的知識記錄下來
文章也會同步更新到我的部落格:
ppsheep.com
關於動畫,在iOS中我們見的太多了,基本上現在每個APP或多或少都會加上一些動畫,在這個動畫系列中,我並不會實現很多很炫的動畫,很炫的動畫,現在開源的已經有很多了,成熟的也已經有很多,在這裡,我主要是講一些對於動畫的理解,對於動畫的由來以及實現原理等等。
動畫的由來
在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
未完待續。。。