iOS Core Animation (Phần 2)
Bài đăng này đã không được cập nhật trong 3 năm
Tiếp theo Phần 1
7. Animation ngầm định
7.1. Transactions
Core Animation được xây dựng dựa trên giả định rằng tất cả mọi thứ bạn làm trên màn hình đều là hình động (trừ khi bạn tắt tính năng này).
Khi bạn thay đổi một thuộc tính có khả năng animation (animatable) của CALayer
, thay đổi không được phản ánh ngay lập tức trên màn hình mà được chuyển đổi từ trạng thái cũ sang trạng thái mới một cách mượt mà. Bạn không cần phải thiết lập hay cấu hình gì cả để đạt được việc này, đó là hành vi mặc định.
Ở ví dụ dưới đây màu của layer sẽ chuyển từ màu xanh sang 1 màu ngẫu nhiên 1 cách từ từ:
//create sublayer
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
//randomize the layer background color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX; self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
Loại animation trên gọi là animation ngầm định. Nó là ngầm định vì chúng ta không xác định rõ kiểu của animation, chúng ta chỉ thay đổi thuộc tính và Core Animation quyết định sẽ xử lý khi nào và như thế nào.
Core Animation sử dụng 1 cơ chế đóng gói các loại animation theo thuộc tính gọi là transaction. Các transaction được quản lý trong class CATransaction
. CATransaction
không thể khởi tạo instance, thay vào đó chúng ta dùng hàm +begin
và +commit
để push transaction mới vào stack hay pop transaction hiện tại khỏi stack.
//begin a new transaction
[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0];
//randomize the layer background color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX; self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
//commit the transaction
[CATransaction commit];
7.2. Completion Blocks
Animation dựa trên block của UIView
cho phép gọi 1 block khi animation hoàn thành.
//begin a new transaction
[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0]; //add the spin animation on completion
[CATransaction setCompletionBlock:^{
//rotate the layer 90 degrees
CGAffineTransform transform = self.colorLayer.affineTransform; transform = CGAffineTransformRotate(transform, M_PI_2); self.colorLayer.affineTransform = transform;
}];
//randomize the layer background color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX; self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
//commit the transaction
[CATransaction commit];
7.3. Layer Actions
Các animation mà CALayer
tự động áp dụng khi thuộc tính thay đổi gọi là action. Khi 1 thuộc tính của CALayer
bị thay đổi, nó gọi hàm -actionForKey:
, truyền vào tham số là tên của thuộc tính. Các việc có thể xảy ra tiếp theo:
- Layer sẽ kiểm tra xem nó có delegate hay không, nếu có thì sẽ kiểm tra tiếp delegate có thi hành hàm
-actionForLayer:forKey
được xác định trongCALayerDelegate
hay không, nếu có thì nó sẽ gọi hàm và trả về kết quả. - Nếu không có delegate hay delegate không thi hành hàm
-actionForLayer:forKey
thì layer sẽ kiểm tra trong dictionaryactions
của nó, thư viện này chứa liên kết giữa tên thuộc tính và action. - Nếu dictionary
actions
không chứa action liên quan tới thuộc tính, layer sẽ tiếp tục tìm kiếm trong dictionarystyle
. - Cuối cùng nếu tất cả các việc trên đều thất bại thì nó sẽ gọi hàm
-defaultActionForKey:
, hàm định nghĩa các action mặc định cho các thuộc tính.
Mọi UIView
đóng vai trò delegate cho layer bên dưới và cung cấp implement của hàm -actionForLayer:forKey
. Khi không ở bên trong một animation block, UIView
trả về nil
cho mọi action của các layer và trả về kết quả khác nil
khi trong block. Như vậy, UIView
ngầm định sẽ bỏ kích hoạt animation khi thuộc tính thay đổi bên ngoài animation block bằng cách trả về nil
cho action của thuộc tính. Action được trả về khi animation được kích hoạt thông thường thuộc kiểu CABasicAnimation
, chúng ta sẽ tìm hiểu ở phần tiếp theo.
8. Animation minh bạch
8.1. Property Animation
Property Animation bao gồm 2 loại basic và keyframe.
8.1.1. Basic Animation
CABasicAnimation
là class con của CAPropertyAnimation
, con của CAAnimation
. CAPropertyAnimation
hoạt động trên 1 thuộc tính, được xác định thông qua giá trị keyPath
. Do 1 CAAnimation
luôn áp dụng cho 1 CALayer
cụ thể nên keyPath
có quan hệ với layer đó.
CABasicAnimation
mở rộng CAPropertyAnimation
với 3 thuộc tính bổ sung:
id fromValue
id toValue
id byValue
Trong đó id là kiểu object của các kiểu CGFloat
, CGPoint
, CGSize
, CGRect
, CATransform3D
, CGImageRef
, CGColorRef
:
id obj = @(float);
id obj = [NSValue valueWithCGPoint:point);
id obj = [NSValue valueWithCGSize:size);
id obj = [NSValue valueWithCGRect:rect);
id obj = [NSValue valueWithCATransform3D:transform);
id obj = (__bridge id)imageRef;
id obj = (__bridge id)colorRef;
Ví dụ dưới đây sẽ đổi màu nền của layer 1 cách ngẫu nhiên
//create a new random color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
//create a basic animation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"backgroundColor";
animation.toValue = (__bridge id)color.CGColor;
//apply animation to layer
[self.colorLayer addAnimation:animation forKey:nil];
8.1.2. Keyframe Animation
CAKeyframeAnimation
cũng là 1 class con của CAPropertyAnimation
. Không giống như CABasicAnimation
bị giới hạn bởi giá trị đầu và cuối, mà là 1 tập hợp các giá trị.
Ở ví dụ dưới đây, layer sẽ đổi màu lần lượt qua nhiều giá trị:
//create a keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"backgroundColor";
animation.duration = 2.0;
animation.values = @[
(__bridge id)[UIColor blueColor].CGColor,
(__bridge id)[UIColor redColor].CGColor,
(__bridge id)[UIColor greenColor].CGColor,
(__bridge id)[UIColor blueColor].CGColor ];
//apply animation to layer
[layer addAnimation:animation forKey:nil];
8.2. Nhóm Animation
CAAnimationGroup
, 1 nhóm con khác của CAAnimation
, được sử dụng để nhóm các animation khác nhau. Việc áp dụng nhóm animation vào layer không khác việc áp dụng lẻ animation vào layer, tuy nhiên nó đem lại cho chúng ta 1 vài tiện ích như có thể đặt thời gian của animation theo nhóm hay xóa nhóm animation chỉ bằng 1 lệnh, và đặc biệt hữu dụng đó là khi ta có thể đặt thời gian theo cấu trúc nhánh (hierarchical timing)
//create a path
UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
[bezierPath moveToPoint:CGPointMake(0, 150)];
[bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
//draw the path using a CAShapeLayer
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.path = bezierPath.CGPath;
pathLayer.fillColor = [UIColor clearColor].CGColor;
pathLayer.strokeColor = [UIColor redColor].CGColor;
pathLayer.lineWidth = 3.0f;
[self.containerView.layer addSublayer:pathLayer];
//add a colored layer
CALayer *colorLayer = [CALayer layer];
colorLayer.frame = CGRectMake(0, 0, 64, 64);
colorLayer.position = CGPointMake(0, 150);
colorLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.containerView.layer addSublayer:colorLayer];
//create the position animation
CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animation];
animation1.keyPath = @"position";
animation1.path = bezierPath.CGPath;
animation1.rotationMode = kCAAnimationRotateAuto;
//create the color animation
CABasicAnimation *animation2 = [CABasicAnimation animation];
animation2.keyPath = @"backgroundColor";
animation2.toValue = (__bridge id)[UIColor redColor].CGColor;
//create group animation
CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
groupAnimation.animations = @[animation1, animation2];
groupAnimation.duration = 4.0;
//add the animation to the color layer
[colorLayer addAnimation:groupAnimation forKey:nil];
8.3. Transition
Transaction không chỉ ảnh hưởng tới 1 loại thuộc tính mà nó gây ảnh hưởng tới toàn bộ layer. Transaction sẽ tạo một bản sao chụp của layer sau đó sẽ tạo animation tới trạng thái mới của layer.
Để tạo transaction, chúng ta dùng CATransition
, cũng là class con của CAAnimation
. CATransition
có type
và subtype
được sử dụng để xác định các hiệu ứng chuyển động. type
có kiểu là NSString
và có thể được đặt thông qua các hằng số như dưới đây:
kCATransitionFade
kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal
kCATransitionMoveIn
, kCATransitionPush
, kCATransitionReveal
có thể tùy chỉnh hướng của chuyển động thông qua subtype
với các giá trị:
kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom
Ví dụ dưới sẽ tạo hiệu ứng slide ảnh:
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *imageView;
@property (nonatomic, copy) NSArray *images;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//set up images
self.images = @[
[UIImage imageNamed:@"Anchor.png"],
[UIImage imageNamed:@"Cone.png"],
[UIImage imageNamed:@"Igloo.png"],
[UIImage imageNamed:@"Spaceship.png"]];
- (IBAction)switchImage {
//set up crossfade transition
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
//apply transition to imageview backing layer
[self.imageView.layer addAnimation:transition forKey:nil];
//cycle to next image
UIImage *currentImage = self.imageView.image;
NSUInteger index = [self.images indexOfObject:currentImage];
index = (index + 1) % [self.images count];
self.imageView.image = self.images[index];
}
@end
9. Ứng dụng demo
Bằng việc áp dụng các kiến thức trên về Core Animation, chúng ta có thể xây dựng 1 chương trình tạo slide ảnh động đơn giản. Slide ảnh có thể preview và sau đó xuất ra file video.
9.1. Animation Service
AnimationService
sử dụng CABasicAnimation
để tạo hiệu ứng fade in, fade out và hiệu ứng di chuyển
class AnimationService: NSObject {
func animationLayerFromImages(images: [UIImage], texts: [String], frameSize: CGSize, startTime: Double = CACurrentMediaTime()) -> CALayer {
let parentLayer = CALayer()
let animationTime: Double = 3
let fadeTime: Double = 1
let backgroundOpacity: Float = 0.5
for (index, image) in images.enumerate() {
let bgLayer = CALayer()
bgLayer.contents = image.CGImage
bgLayer.frame = CGRectMake(0, 0, frameSize.width, frameSize.height)
bgLayer.contentsGravity = kCAGravityResizeAspectFill
bgLayer.opacity = backgroundOpacity
let contentLayer = CALayer()
contentLayer.contents = image.CGImage
contentLayer.frame = CGRectMake(0, 0, frameSize.width, frameSize.height)
contentLayer.contentsGravity = kCAGravityTop
let animation = animationWithDuration(animationTime, autoReverse: false, fromValue: nil, toValue: contentLayer.position.x + 150, beginTime: (Double(index) * animationTime) + startTime, keyPath: "position.x", repeatCount: 0, fillMode: nil)
if index > 0 {
bgLayer.opacity = 0
contentLayer.opacity = 0
let fadeInAnimation = animationWithDuration(fadeTime, autoReverse: false, fromValue: 0, toValue: 1, beginTime: (Double(index) * animationTime) + startTime - fadeTime, keyPath: "opacity", repeatCount: 0, fillMode: kCAFillModeForwards)
let bgFadeInAnimation = animationWithDuration(fadeTime, autoReverse: false, fromValue: 0, toValue: backgroundOpacity, beginTime: (Double(index) * animationTime) + startTime - fadeTime, keyPath: "opacity", repeatCount: 0, fillMode: kCAFillModeForwards)
bgLayer.addAnimation(bgFadeInAnimation, forKey: "bgFadeIn")
contentLayer.addAnimation(fadeInAnimation, forKey: "fadeIn")
}
let fadeOutAnimation = animationWithDuration(fadeTime, autoReverse: false, fromValue: 1, toValue: 0, beginTime: animationTime + (Double(index) * animationTime) + startTime - fadeTime, keyPath: "opacity", repeatCount: 0, fillMode: kCAFillModeForwards)
let bgFadeOutAnimation = animationWithDuration(fadeTime, autoReverse: false, fromValue: backgroundOpacity, toValue: 0, beginTime: animationTime + (Double(index) * animationTime) + startTime - fadeTime, keyPath: "opacity", repeatCount: 0, fillMode: kCAFillModeForwards)
contentLayer.addAnimation(fadeOutAnimation, forKey: "fadeOut")
contentLayer.addAnimation(animation, forKey: "animate")
bgLayer.addAnimation(bgFadeOutAnimation, forKey: "bgFadeOut")
parentLayer.addSublayer(bgLayer)
parentLayer.addSublayer(contentLayer)
}
return parentLayer
}
private func animationWithDuration(duration: Double, autoReverse: Bool, fromValue: AnyObject?, toValue: AnyObject?, beginTime: Double, keyPath: String, repeatCount: Float, fillMode: String?) -> CAAnimation {
let animation = CABasicAnimation()
animation.keyPath = keyPath
if let fromValue = fromValue {
animation.fromValue = fromValue
}
animation.toValue = toValue
if let fillMode = fillMode{
animation.fillMode = fillMode
}
animation.beginTime = beginTime
animation.autoreverses = autoReverse
animation.duration = duration
animation.repeatCount = repeatCount
animation.removedOnCompletion = false
return animation
}
}
9.2. Chạy demo
Các bạn có thể download source code tại đây
All rights reserved