iOS Core Animation (Phần 2)
Tiếp theo Phần 1 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 ...
Tiếp theo Phần 1
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 trong CALayerDelegate 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 dictionary actions 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 dictionary style.
- 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.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
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.awidth, frameSize.height) bgLayer.contentsGravity = kCAGravityResizeAspectFill bgLayer.opacity = backgroundOpacity let contentLayer = CALayer() contentLayer.contents = image.CGImage contentLayer.frame = CGRectMake(0, 0, frameSize.awidth, 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