iOS Core Animation (Phần 2)

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+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:

  1. 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ả.
  2. 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.
  3. 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.
  4. 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 basickeyframe.

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. CATransitiontypesubtype đượ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

demo.png

Các bạn có thể download source code tại đây

All Rights Reserved