動效設計一直是iOS平臺的優勢,良好的動效設計可以很好地提升使用者體驗。而動畫則是動效的基礎支撐。本動畫將從易到難逐步分析,從CABasicAnimation,UIBezierPath,CAShapeLayer三個方面完整的闡述iOS動畫的實現。最終的效果如下:
例子來源與網路,不是我寫的,我只是加上了詳細的註釋,方便大家理解(我只是程式碼的搬運工…)。這個例子是CABasicAnimation,UIBezierPath,CAShapeLayer的綜合實現,如果能完全理解這個例子,相信其它的iOS動畫也難不倒你了。demo下載地址
CABasicAnimation
一、概念
這個部分你需要了解以下概念: CALayer、CAAnimation、CAAnimationGroup
1、CALayer
CALayer是個與UIView很類似的概念,同樣有backgroundColor、frame等相似的屬性,我們可以將UIView看做一種特殊的CALayer。但實際上UIView是對CALayer封裝,在CALayer的基礎上再新增互動功能。UIView的顯示必須依賴於CALayer。我們同樣可以跟新建view一樣新建一個layer,然後新增到某個已有的layer上,同樣可以對layer調整大小、位置、透明度等。一般來說,layer可以有兩種用途:一是對view相關屬性的設定,包括圓角、陰影、邊框等引數,更詳細的引數請點選這裡;二是實現對view的動畫操控。因此對一個view進行動畫,本質上是對該view的.layer進行動畫操縱。
2、CAAnimation
CAAnimation可以分為以下幾類:
CABasicAnimation基礎動畫,通過設定起始點,終點,時間,動畫會沿著你這設定點進行移動。可以看做特殊的CAKeyFrameAnimation
CAKeyframeAnimation關鍵幀動畫,可定製度比CABasicAnimation高,也是本系列的接下來的內容
CAAnimationGroup組動畫,支援多個CABasicAnimation或者CAKeyframeAnimation動畫同時執行
例項化
使用方法animationWithKeyPath:對 CABasicAnimation進行例項化,並指定Layer的屬性作為關鍵路徑進行註冊。
1 |
//圍繞y軸旋轉CABasicAnimation *transformAnima = [CABasicAnimation animationWithKeyPath:@"transform.rotation.y"]; |
設定動畫的屬性和說明屬性說明
1 2 3 4 5 6 |
transformAnima.fromValue = @(M_PI_2); transformAnima.toValue = @(M_PI); transformAnima.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; transformAnima.autoreverses = YES; transformAnima.repeatCount = HUGE_VALF; transformAnima.beginTime = CACurrentMediaTime() + 2; |
防止動畫結束後回到初始狀態只需設定removedOnCompletion、fillMode兩個屬性就可以了。
1 2 |
transformAnima.removedOnCompletion = NO; transformAnima.fillMode = kCAFillModeForwards; |
解釋:為什麼動畫結束後返回原狀態?首先我們需要搞明白一點的是,layer動畫執行的過程是怎樣的?其實在我們給一個檢視新增layer動畫時,真正移動並不是我們的檢視本身,而是 presentation layer 的一個快取。動畫開始時 presentation layer開始移動,原始layer隱藏,動畫結束時,presentation layer從螢幕上移除,原始layer顯示。這就解釋了為什麼我們的檢視在動畫結束後又回到了原來的狀態,因為它根本就沒動過。
這個同樣也可以解釋為什麼在動畫移動過程中,我們為何不能對其進行任何操作。
所以在我們完成layer動畫之後,最好將我們的layer屬性設定為我們最終狀態的屬性,然後將presentation layer 移除掉。
新增動畫
1 |
[self.imageView.layer addAnimation:transformAnima forKey:@"A"]; |
fillMode屬性的理解該屬性定義了你的動畫在開始和結束時的動作。預設值是 kCAFillModeRemoved。
kCAFillModeRemoved 這個是預設值,也就是說當動畫開始前和動畫結束後,動畫對layer都沒有影響,動畫結束後,layer會恢復到之前的狀態
kCAFillModeForwards 當動畫結束後,layer會一直保持著動畫最後的狀態
kCAFillModeBackwards 這個和kCAFillModeForwards是相對的,就是在動畫開始前,你只要將動畫加入了一個layer,layer便立即進入動畫的初始狀態。因為有可能出現fromValue不是目前layer的初始狀態的情況,如果fromValue就是layer當前的狀態,則這個引數就沒太大意義。
kCAFillModeBoth 理解了上面兩個,這個就很好理解了,這個其實就是上面兩個的合成.動畫加入後開始之前,layer便處於動畫初始狀態,動畫結束後layer保持動畫最後的狀態.
Animation Easing的使用
也即是屬性timingFunction值的設定,有種方式來獲取屬性值
(1)使用方法functionWithName:
這種方式很簡單,這裡只是簡單說明一下取值的含義:
kCAMediaTimingFunctionLinear 傳這個值,在整個動畫時間內動畫都是以一個相同的速度來改變。也就是勻速運動。
kCAMediaTimingFunctionEaseIn 使用該值,動畫開始時會較慢,之後動畫會加速。
kCAMediaTimingFunctionEaseOut 使用該值,動畫在開始時會較快,之後動畫速度減慢。
kCAMediaTimingFunctionEaseInEaseOut 使用該值,動畫在開始和結束時速度較慢,中間時間段內速度較快。
動畫的實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
CABasicAnimation *positionAnima = [CABasicAnimation animationWithKeyPath:@"position.y"]; positionAnima.fromValue = @(self.imageView.center.y); positionAnima.toValue = @(self.imageView.center.y-30); positionAnima.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn]; CABasicAnimation *transformAnima = [CABasicAnimation animationWithKeyPath:@"transform.rotation.y"]; transformAnima.fromValue = @(0); transformAnima.toValue = @(M_PI); transformAnima.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; CAAnimationGroup *animaGroup = [CAAnimationGroup animation]; animaGroup.duration = 2.0f; animaGroup.fillMode = kCAFillModeForwards; animaGroup.removedOnCompletion = NO; animaGroup.animations = @[positionAnima,transformAnima];[self.imageView.layer addAnimation:animaGroup forKey:@"Animation"]; |
動畫開始和結束時的事件為了獲取動畫的開始和結束事件,需要實現協議
1 |
positionAnima.delegate = self; |
代理方法實現
1 2 3 4 5 6 7 8 |
//動畫開始時- (void)animationDidStart:(CAAnimation *)anim{ NSLog(@"開始了"); } //動畫結束時- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{ //方法中的flag參數列明了動畫是自然結束還是被打斷,比如呼叫了removeAnimationForKey:方法 或removeAnimationForKey方法,flag為NO,如果是正常結束,flag為YES。 NSLog(@"結束了"); } |
其實比較重要的是有多個動畫的時候如何在代理方法中區分不同的動畫兩種方式
方式一:
如果我們新增動畫的檢視是全域性變數,可使用該方法。新增動畫時,我們使用了
1 |
[self.imageView.layer addAnimation:animaGroup forKey:@"Animation"]; |
所以,可根據key來區分不同的動畫
1 2 3 4 5 |
//動畫開始時- (void)animationDidStart:(CAAnimation *)anim{ if ([anim isEqual:[self.imageView.layer animationForKey:@"Animation"]]) { NSLog(@"動畫組執行了"); } } |
Note:把動畫儲存為一個屬性然後再回撥中比較,用來判定是哪個動畫是不可行的。應為委託傳入的動畫引數是原始值的一個深拷貝,不是同一個值
方式二
新增動畫的檢視是區域性變數時,可使用該方法新增動畫給動畫設定key-value對
1 2 |
[positionAnima setValue:@"PositionAnima" forKey:@"AnimationKey"]; [transformAnima setValue:@"TransformAnima" forKey:@"AnimationKey"]; |
所以,可以根據key中不同的值來進行區分不同的動畫
1 2 3 4 5 6 |
//動畫結束時- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{ if ([[anim valueForKey:@"AnimationKey"]isEqualToString:@"PositionAnima"]) { NSLog(@"位置移動動畫執行結束"); } else if ([[anim valueForKey:@"AnimationKey"]isEqualToString:@"TransformAnima"]){ NSLog(@"旋轉動畫執行結束"); }} |
一些常用的animationWithKeyPath值的總結
UIBezierPath
使用UIBezierPath可以建立基於向量的路徑,此類是Core Graphics框架關於路徑的封裝。使用此類可以定義簡單的形狀,如橢圓、矩形或者有多個直線和曲線段組成的形狀等。
UIBezierPath是CGPathRef資料型別的封裝。如果是基於向量形狀的路徑,都用直線和曲線去建立。我們使用直線段去建立矩形和多邊形,使用曲線去建立圓弧(arc)、圓或者其他複雜的曲線形狀。
1 |
+ (instancetype)bezierPath; |
這個使用比較多,因為這個工廠方法建立的物件,我們可以根據我們的需要任意定製樣式,可以畫任何我們想畫的圖形。
1 |
+ (instancetype)bezierPathWithRect:(CGRect)rect; |
這個工廠方法根據一個矩形畫貝塞爾曲線。
1 |
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect; |
這個工廠方法根據一個矩形畫內切曲線。通常用它來畫圓或者橢圓。
1 2 |
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius; + (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii; |
第一個工廠方法是畫矩形,但是這個矩形是可以畫圓角的。第一個引數是矩形,第二個引數是圓角大小。
第二個工廠方法功能是一樣的,但是可以指定某一個角畫成圓角。像這種我們就可以很容易地給UIView擴充套件新增圓角的方法了。
1 2 3 4 5 |
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise; |
這個工廠方法用於畫弧,引數說明如下:
center: 弧線中心點的座標
radius: 弧線所在圓的半徑
startAngle: 弧線開始的角度值
endAngle: 弧線結束的角度值
clockwise: 是否順時針畫弧線
1 |
- (void)closePath;//閉合弧線 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 畫三角形 - (void)drawTrianglePath { UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(20, 20)]; [path addLineToPoint:CGPointMake(self.frame.size.width - 40, 20)]; [path addLineToPoint:CGPointMake(self.frame.size.width / 2, self.frame.size.height - 20)]; // 最後的閉合線是可以通過呼叫closePath方法來自動生成的,也可以呼叫-addLineToPoint:方法來新增 // [path addLineToPoint:CGPointMake(20, 20)]; [path closePath]; // 設定線寬 path.lineWidth = 1.5; // 設定填充顏色 UIColor *fillColor = [UIColor greenColor]; [fillColor set]; [path fill]; // 設定畫筆顏色 UIColor *strokeColor = [UIColor blueColor]; [strokeColor set]; // 根據我們設定的各個點連線 [path stroke]; } |
我們設定畫筆顏色通過set方法:
1 |
UIColor *strokeColor = [UIColor blueColor];[strokeColor set]; |
如果我們需要設定填充顏色,比如這裡設定為綠色,那麼我們需要在設定畫筆顏色之前先設定填充顏色,否則畫筆顏色就被填充顏色替代了。也就是說,如果要讓填充顏色與畫筆顏色不一樣,那麼我們的順序必須是先設定填充顏色再設定畫筆顏色。如下,這兩者順序不能改變。因為我們設定填充顏色也是跟設定畫筆顏色一樣呼叫UIColor的-set方法。
1 2 3 4 |
// 設定填充顏色UIColor *fillColor = [UIColor greenColor]; [fillColor set];[path fill]; // 設定畫筆顏色 UIColor *strokeColor = [UIColor blueColor];[strokeColor set]; |
CAShapeLayer
CAShapeLayer是在其座標系統內繪製貝塞爾曲線(UIBezierPath)的。因此,使用CAShapeLayer需要與UIBezierPath一起使用。
它有一個path屬性,而UIBezierPath就是對CGPathRef型別的封裝,因此這兩者配合起來使用才可以的哦!
CAShapeLayer與UIBezierPath的關係:
CAShapeLayer中shape代表形狀的意思,所以需要形狀才能生效
貝塞爾曲線可以建立基於向量的路徑,而UIBezierPath類是對CGPathRef的封裝
貝塞爾曲線給CAShapeLayer提供路徑,CAShapeLayer在提供的路徑中進行渲染。路徑會閉環,所以繪製出了Shape
用於CAShapeLayer的貝塞爾曲線作為path,其path是一個首尾相接的閉環的曲線,即使該貝塞爾曲線不是一個閉環的曲線
CAShapeLayer與UIBezierPath畫圓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- (CAShapeLayer *)drawCircle { CAShapeLayer *circleLayer = [CAShapeLayer layer]; // 指定frame,只是為了設定寬度和高度 circleLayer.frame = CGRectMake(0, 0, 200, 200); // 設定居中顯示 circleLayer.position = self.view.center; // 設定填充顏色 circleLayer.fillColor = [UIColor clearColor].CGColor; // 設定線寬 circleLayer.lineWidth = 2.0; // 設定線的顏色 circleLayer.strokeColor = [UIColor redColor].CGColor; // 使用UIBezierPath建立路徑 CGRect frame = CGRectMake(0, 0, 200, 200); UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:frame]; // 設定CAShapeLayer與UIBezierPath關聯 circleLayer.path = circlePath.CGPath; // 將CAShaperLayer放到某個層上顯示 [self.view.layer addSublayer:circleLayer]; return circleLayer;} |
登入例子下載地址:
demo下載地址
如果你都看到這裡了,請給我點個贊吧,你的喜歡是我堅持原創的不竭動力。
參考資料:
iOS 動畫效果:Core Animation & Facebook
拍電影與CABasicAnimation
標哥的技術部落格
CABasicAnimation使用總結
蘋果文件
放肆的使用UIBezierPath和CAShapeLaye