Advanced iOS Core Animation Techniques-5

9. Layer Time

Layer Time

The biggest difference between time and space is that time can't be reused--Forster Merrick

In the previous two chapters, we explored a variety of layer animations that can be implemented with CAAnimation and its subclasses.Animation takes place over a period of time, so timing is critical to the whole concept.In this chapter, let's take a look at CAMediaTiming and see how Core Animation tracks time.

9.1 CAMediaTiming Protocol

CAMediaTiming`Protocol

The CAMediaTiming protocol defines a set of properties used to control elapsed time within an animation. Both CALayer and CAAAnimation implement this protocol, so time can be controlled by any class based on a layer or an animation.

Continuity and repetition

In Chapter 8, Explicit Animation, we briefly mentioned duration, one of the properties of CAMediaTiming, which is a type of CFTimeInterval (a double-precision floating-point type similar to NSTimeInterval) that specifies the time for an iteration of the animation to be performed.

What does an iteration mean here?Another property of CAMediaTiming is called repeatCount, which represents the number of iterations the animation repeats.If duration is 2 and repeatCount is set to 3.5 (three and a half iterations), the full animation will take 7 seconds.

A developer, it is especially important to have a learning atmosphere and a communication circle. This is my iOS communication group: 1012951431, share BAT, Ali interview questions, interview experience, discussion techniques, everyone can exchange learning and growth together!Want to help developers avoid detours.

duration and repeatCount are both 0 by default.This does not mean that the animation takes 0 seconds or 0 times, where 0 simply represents the "default", that is, 0.25 seconds and once. You can try to assign multiple values to these two attributes with a simple test, as shown in Listing 9.1 and Figure 9.1.

Listing 9.1 Testing duration and repeatCount

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UITextField *durationField;
@property (nonatomic, weak) IBOutlet UITextField *repeatField;
@property (nonatomic, weak) IBOutlet UIButton *startButton;
@property (nonatomic, strong) CALayer *shipLayer;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    //add the ship
    self.shipLayer = [CALayer layer];
    self.shipLayer.frame = CGRectMake(0, 0, 128, 128);
    self.shipLayer.position = CGPointMake(150, 150);
    self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:self.shipLayer];
}

- (void)setControlsEnabled:(BOOL)enabled
{
    for (UIControl *control in @[self.durationField, self.repeatField, self.startButton]) {
        control.enabled = enabled;
        control.alpha = enabled? 1.0f: 0.25f;
    }
}

- (IBAction)hideKeyboard
{
    [self.durationField resignFirstResponder];
    [self.repeatField resignFirstResponder];
}

- (IBAction)start
{
    CFTimeInterval duration = [self.durationField.text doubleValue];
    float repeatCount = [self.repeatField.text floatValue];
    //animate the ship rotation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation";
    animation.duration = duration;
    animation.repeatCount = repeatCount;
    animation.byValue = @(M_PI * 2);
    animation.delegate = self;
    [self.shipLayer addAnimation:animation forKey:@"rotateAnimation"];
    //disable controls
    [self setControlsEnabled:NO];
}

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    //reenable controls
    [self setControlsEnabled:YES];
}

@end

 

Figure 9.2 Animation of swing door

The code for swinging the door is shown in List 9.2.We use autoreverses to make the door close automatically when it opens. Here we set the repeatDuration to INFINITY, so the animation loops indefinitely and the repeatCount to INFINITY has the same effect.Note that repeatCount and repeatDuration may conflict with each other, so you only need to specify a non-zero value for one of them.The behavior of setting non-zero values for both attributes is not defined.

Listing 9.2 uses the autoreverses property to swing the door

@interface ViewController ()

@property (nonatomic, weak) UIView *containerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    //add the door
    CALayer *doorLayer = [CALayer layer];
    doorLayer.frame = CGRectMake(0, 0, 128, 256);
    doorLayer.position = CGPointMake(150 - 64, 150);
    doorLayer.anchorPoint = CGPointMake(0, 0.5);
    doorLayer.contents = (__bridge id)[UIImage imageNamed: @"Door.png"].CGImage;
    [self.containerView.layer addSublayer:doorLayer];
    //apply perspective transform
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0 / 500.0;
    self.containerView.layer.sublayerTransform = perspective;
    //apply swinging animation
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation.y";
    animation.toValue = @(-M_PI_2);
    animation.duration = 2.0;
    animation.repeatDuration = INFINITY;
    animation.autoreverses = YES;
    [doorLayer addAnimation:animation forKey:nil];
}

@end

 

Relative Time

Every time Core Animation is discussed, time is relative, and each animation has its own time description, which can be independently accelerated, delayed, or offset.

beginTime specifies the delay time before the animation starts.The delay here is measured from the moment the animation is added to the visible layer and defaults to 0 (that is, the animation is executed immediately).

Speed is a multiple of time, defaulting to 1.0, which slows down the time of the layer/animation and speeds up it.With a speed of 2.0, an animation with a duration of 1 is actually completed in 0.5 seconds.

TimOffset is similar to beginTime, but unlike delayed animations caused by increasing beginTime, increasing timeOffset only allows the animation to move forward to a point. For example, for an animation that lasts one second, setting timeOffset to 0.5 means the animation will start in half.

Unlike beginTime, timeOffset is not affected by speed.So if you set speed to 2.0 and timeOffset to 0.5, your animation will start where the animation ends, because the one-second animation is actually shortened to 0.5 seconds.However, even if you use timeOffset to let the animation start where it ends, it still plays for a full length of time, and the animation simply loops around and starts from the beginning.

You can verify with the test program in Listing 9.3, set the speed and timeOffset sliders to arbitrary values, then click Play to see the effect (see Figure 9.3)

Listing 9.3 Tests the timeOffset and speed attributes

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UILabel *speedLabel;
@property (nonatomic, weak) IBOutlet UILabel *timeOffsetLabel;
@property (nonatomic, weak) IBOutlet UISlider *speedSlider;
@property (nonatomic, weak) IBOutlet UISlider *timeOffsetSlider;
@property (nonatomic, strong) UIBezierPath *bezierPath;
@property (nonatomic, strong) CALayer *shipLayer;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a path
    self.bezierPath = [[UIBezierPath alloc] init];
    [self.bezierPath moveToPoint:CGPointMake(0, 150)];
    [self.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 = self.bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.containerView.layer addSublayer:pathLayer];
    //add the ship
    self.shipLayer = [CALayer layer];
    self.shipLayer.frame = CGRectMake(0, 0, 64, 64);
    self.shipLayer.position = CGPointMake(0, 150);
    self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
    [self.containerView.layer addSublayer:self.shipLayer];
    //set initial values
    [self updateSliders];
}

- (IBAction)updateSliders
{
    CFTimeInterval timeOffset = self.timeOffsetSlider.value;
    self.timeOffsetLabel.text = [NSString stringWithFormat:@"%0.2f", timeOffset];
    float speed = self.speedSlider.value;
    self.speedLabel.text = [NSString stringWithFormat:@"%0.2f", speed];
}

- (IBAction)play
{
    //create the keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.timeOffset = self.timeOffsetSlider.value;
    animation.speed = self.speedSlider.value;
    animation.duration = 1.0;
    animation.path = self.bezierPath.CGPath;
    animation.rotationMode = kCAAnimationRotateAuto;
    animation.removedOnCompletion = NO;
    [self.shipLayer addAnimation:animation forKey:@"slide"];
}

@end

 

9.2 Hierarchical Relationship Time

Hierarchical Relationship Time

9.3 Manual Animation

Manual Animation

A useful feature of timeOffset is that it allows you to control the animation process manually. By setting speed to 0, you can disable the automatic playback of the animation, and then use timeOffset to display the animation sequence back and forth.This makes it easy to use gestures to control the animation manually.

For a simple example: Or the previous closed animation, modify the code to use gestures to control the animation.We add a UIPanGestureRecognizer to the view and shake it around with timeOffset.

Since the animation can't be modified after it's added to the layer, we'll do the same by adjusting the timeOffset of the layer (Listing 9.4).

Listing 9.4 Manually control animation with touch gestures

@interface ViewController ()

@property (nonatomic, weak) UIView *containerView;
@property (nonatomic, strong) CALayer *doorLayer;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    //add the door
    self.doorLayer = [CALayer layer];
    self.doorLayer.frame = CGRectMake(0, 0, 128, 256);
    self.doorLayer.position = CGPointMake(150 - 64, 150);
    self.doorLayer.anchorPoint = CGPointMake(0, 0.5);
    self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Door.png"].CGImage;
    [self.containerView.layer addSublayer:self.doorLayer];
    //apply perspective transform
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0 / 500.0;
    self.containerView.layer.sublayerTransform = perspective;
    //add pan gesture recognizer to handle swipes
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init];
    [pan addTarget:self action:@selector(pan:)];
    [self.view addGestureRecognizer:pan];
    //pause all layer animations
    self.doorLayer.speed = 0.0;
    //apply swinging animation (which won't play because layer is paused)
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation.y";
    animation.toValue = @(-M_PI_2);
    animation.duration = 1.0;
    [self.doorLayer addAnimation:animation forKey:nil];
}

- (void)pan:(UIPanGestureRecognizer *)pan
{
    //get horizontal component of pan gesture
    CGFloat x = [pan translationInView:self.view].x;
    //convert from points to animation duration //using a reasonable scale factor
    x /= 200.0f;
    //update timeOffset and clamp result
    CFTimeInterval timeOffset = self.doorLayer.timeOffset;
    timeOffset = MIN(0.999, MAX(0.0, timeOffset - x));
    self.doorLayer.timeOffset = timeOffset;
    //reset pan gesture
    [pan setTranslation:CGPointZero inView:self.view];
}

@end

 

This is a trick, and it may be easier to set the door transform directly with a moving gesture than setting up an animation and then displaying one frame at a time.

This is true in this example, but it's much more convenient for more complex cases like the key, or animation groups with multiple layers than for calculating the attributes of each layer in real time.

9.4 Summary

summary

In this chapter, we learn about the CAMediaTiming protocol and the mechanisms Core Animation uses to manipulate time-controlled animations.In the next chapter, we'll touch buffering, another technique for making animations more realistic with time.

10. Buffering

buffer

Life, like art, is always a curve.--Edward Bulward-Lighton

In Chapter 9, Layer Time, we discuss animation time and the CAMediaTiming protocol.Now let's look at another time-related mechanism called buffering.Core Animation uses buffering to make animations move smoother and more natural, rather than the kind of machinery and artifacts that look like, and in this chapter we'll look at how to control your animations and customize the buffering curves.

10.1 Animation Speed

Animation speed

Animations are actually changes over a period of time, which implies that changes must occur at a certain rate.The rate is calculated from the following formula:

velocity = change / time

 

Change here can refer to the distance an object moves, or the duration of the animation. This movement can be used to describe more visually (such as the position and bounds attributes animation), but it can actually be applied to any attribute that can be animated (such as color and opacity).

The equation above assumes that speed is constant throughout the animation process (as in Chapter VIII, Explicit Animation). For animations of this constant speed, we call them Linear Step, and technically this is the easiest way to animate, but it is also a completely unreal effect.

Consider a scenario where a car is traveling within a certain distance and does not start at 60 mph, then suddenly turn to 0 mph at the end.One is that it requires an infinite amount of acceleration (even the best car won't run from zero to 60 in 0 seconds), or it will kill all the passengers.In reality, it slowly accelerates to full speed, then slows down until it stops at the end.

So what about an object that falls to the ground?It first stops in the air, then speeds up until it falls to the ground, then stops abruptly (and then with a loud bang as the accumulated kinetic energy shifts!).

Any object in real life accelerates or decelerates in motion.So how do we achieve this acceleration in the animation?One method is to use a physical engine to model the friction and momentum of a moving object, which can make calculations too complex.We call this type of equation a buffer function, and fortunately Core Animation has a series of standard functions built into it for us to use.

CAMediaTimingFunction

So how do you use the buffer equation?First you need to set the timingFunction property of CAAnimation, which is an object of the CAMediaTimingFunction class.If you want to change the timer function of implicit animation, you can also use the + setAnimationTimingFunction: method of CATransaction.

Here are some ways to create CAMediaTimingFunction, the easiest way is to call the construction method of + timingFunctionWithName:.Here you pass in one of the following constants:

kCAMediaTimingFunctionLinear 
kCAMediaTimingFunctionEaseIn 
kCAMediaTimingFunctionEaseOut 
kCAMediaTimingFunctionEaseInEaseOut
kCAMediaTimingFunctionDefault

 

The kCAMediaTimingFunctionLinear option creates a linear timing function, which is also the default function when the timingFunction property of CAAnimation is empty.Linear step makes sense for scenes where you accelerate immediately and reach the end point at a constant speed (for example, a bullet that fires a gun), but by default it looks strange because it's really rarely used for most animations.

The kCAMediaTimingFunctionEaseIn constant creates a method that slowly accelerates and then suddenly stops.This is appropriate for the example of a free fall mentioned earlier, or for example, for a missile launched against a target.

kCAMediaTimingFunctionEaseOut, on the other hand, starts at a full speed and slows down to stop.It has a weakening effect, and scenarios such as a door closing slowly instead of banging.

kCAMediaTimingFunctionEaseInEaseOut creates a process that slowly accelerates and then slows down.This is how most objects move in the real world and is the best choice for most animations.If only one buffer function can be used, it must be it.Then you'll wonder why this is not the default choice. In fact, when you use the UIView's animation method, it is the default, but when you create a CAAnimation, you need to set it manually.

Finally, there is a kCAMediaTimingFunctionDefault, which is similar to kCAMediaTimingFunctionEaseInEaseOut, but the acceleration and deceleration processes are slightly slower.The difference between this and kCAMediaTimingFunctionEaseInEaseOut is hard to see, probably because Apple found it more suitable for implicit animation (and then changed its mind on UIKit, using kCAMediaTimingFunctionEaseInEaseOut as the default), although its name is the default, remember that when creating explicit CAAnimation it is not the default option (in other words)In other words, the default layer behavior animation uses kCAMediaTimingFunctionDefault as their timing method.

You can experiment with a simple test project (Listing 10.1), change the code of the buffer function before running, and click anywhere to see how the layer moves through the specified buffer.

Listing 10.1 Simple test of buffer function

@interface ViewController ()

@property (nonatomic, strong) CALayer *colorLayer;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a red layer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(0, 0, 100, 100);
    self.colorLayer.position = CGPointMake(self.view.bounds.size.width/2.0, self.view.bounds.size.height/2.0);
    self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
    [self.view.layer addSublayer:self.colorLayer];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //configure the transaction
    [CATransaction begin];
    [CATransaction setAnimationDuration:1.0];
    [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
    //set the position
    self.colorLayer.position = [[touches anyObject] locationInView:self.view];
    //commit transaction
    [CATransaction commit];
}

@end

 

Animation buffer for UIView

The UIKit's animations also support the use of these buffering methods, although the syntax and constants are somewhat different. To change the buffering options for UIView animations, add one of the following constants to the options parameter:

UIViewAnimationOptionCurveEaseInOut 
UIViewAnimationOptionCurveEaseIn 
UIViewAnimationOptionCurveEaseOut 
UIViewAnimationOptionCurveLinear

 

They are closely related to the CAMediaTimingFunction, where UIViewAnimationOptionCurveEaseInOut is the default value (there is no corresponding value for kCAMediaTimingFunctionDefault).

See Listing 10.2 for details (note that additional layers added by the UIView are no longer used here because they are not supported by UIKit animations).

Listing 10.2 Buffer test project using UIKit animation

@interface ViewController ()

@property (nonatomic, strong) UIView *colorView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a red layer
    self.colorView = [[UIView alloc] init];
    self.colorView.bounds = CGRectMake(0, 0, 100, 100);
    self.colorView.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
    self.colorView.backgroundColor = [UIColor redColor];
    [self.view addSubview:self.colorView];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //perform the animation
    [UIView animateWithDuration:1.0 delay:0.0
                        options:UIViewAnimationOptionCurveEaseOut
                     animations:^{
                            //set the position
                            self.colorView.center = [[touches anyObject] locationInView:self.view];
                        }
                     completion:NULL];

}

@end

 

Buffer and keyframe animation

You may recall that the color-switching keyframe animation in Chapter 8 looks strange due to linear transformations (see Listing 8.5), which make color transformations very unnatural.To correct this, let's use a more appropriate buffer method, such as kCAMediaTimingFunctionEaseIn, to add a pulse effect to the color change of the layer to make it more like a color bulb in reality.

We don't want to apply this effect to the entire animation process. We want to repeat this buffer for each animation process, so every color change will have a pulse effect.

CAKeyframe Animation has a timingFunctions property of NSArray type that we can use to specify different timing functions for each step of the animation.However, the number of specified functions must be equal to the number of elements in the keyframes array minus one, because it is a function that describes the speed of animation between frames.

In this example, we want to use the same buffer function from start to finish, but we also need an array of functions to tell the animation to repeat each step continuously instead of buffering only once throughout the animation sequence. We can simply use an array containing multiple copies of the same function (see Listing 10.3).

Run the updated code and you'll see that the animation looks more natural.

Listing 10.3 uses CAMediaTimingFunction for CAKeyframe Animation

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView;
@property (nonatomic, weak) IBOutlet CALayer *colorLayer;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    //create sublayer
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
    self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
    //add it to our view
    [self.layerView.layer addSublayer:self.colorLayer];
}

- (IBAction)changeColor
{
    //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 ];
    //add timing function
    CAMediaTimingFunction *fn = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn];
    animation.timingFunctions = @[fn, fn, fn];
    //apply animation to layer
    [self.colorLayer addAnimation:animation forKey:nil];
}

@end

 

10.2 Custom Buffer Function

Custom Buffer Function

In Chapter 8, we add animations to the clock item.It looks great, but it would be better if you had the right buffer function.In the display world, when the clock pointer turns, it usually starts very slowly, then quickly clicks and finally buffers to the end point.But the standard buffer function fits every one here, so how do you create a new one?

In addition to +functionWithName:, CAMediaTimingFunction also has another constructor, a +functionWithControlPoints::: (Note that the strange syntax here does not contain the names of specific parameters, which is legal in objective-C, but violates Apple's guidelines for naming methods, and it looks strangeDesign).

Using this method, we can create a custom buffer function to match our clock animation. To understand how to use this method, we need to understand how some CAMediaTimingFunction s work.

Cubic Bezier Curve

The main principle of the CAMediaTimingFunction function is that it converts the input time into a proportional change between the start and end points.We can use a simple icon to explain that the horizontal axis represents time and the vertical axis represents the amount of change, so the linear buffer is a simple diagonal line from the start (Fig. 10.1).

Figure 10.2 Cubic Bezier Buffer Function

In fact, it is a very strange function, which accelerates first, then decelerates, and then accelerates as soon as the end point is reached. How can a standard buffer function be represented as an image?

CAMediaTimingFunction has a method called -getControlPointAtIndex:values: that can be used to retrieve points of curves. This method is designed to be a bit odd (perhaps only Apple can answer why not simply return a CGPoint), but with it we can find the point of the standard buffer function and draw it with UIBezierPath and CAShapeLayer.

The start and end points of the curve are always {0,0} and {1,1}, so we only need to retrieve the second and third points (control points) of the curve.See Listing 10.4 for the code.An image of all the standard buffer functions is shown in Figure 10.3.

Listing 10.4 Draws CAMediaTimingFunction using UIBezierPath

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    //create timing function
    CAMediaTimingFunction *function = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut];
    //get control points
    CGPoint controlPoint1, controlPoint2;
    [function getControlPointAtIndex:1 values:(float *)&controlPoint1];
    [function getControlPointAtIndex:2 values:(float *)&controlPoint2];
    //create curve
    UIBezierPath *path = [[UIBezierPath alloc] init];
    [path moveToPoint:CGPointZero];
    [path addCurveToPoint:CGPointMake(1, 1)
            controlPoint1:controlPoint1 controlPoint2:controlPoint2];
    //scale the path up to a reasonable size for display
    [path applyTransform:CGAffineTransformMakeScale(200, 200)];
    //create shape layer
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.strokeColor = [UIColor redColor].CGColor;
    shapeLayer.fillColor = [UIColor clearColor].CGColor;
    shapeLayer.lineWidth = 4.0f;
    shapeLayer.path = path.CGPath;
    [self.layerView.layer addSublayer:shapeLayer];
    //flip geometry so that 0,0 is in the bottom-left
    self.layerView.layer.geometryFlipped = YES;
}

@end

 

Figure 10.4 Customize a clock-appropriate buffer function

Listing 10.5 adds a clock program with a custom buffer function

- (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated
{
    //generate transform
    CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1);
    if (animated) {
        //create transform animation
        CABasicAnimation *animation = [CABasicAnimation animation];
        animation.keyPath = @"transform";
        animation.fromValue = [handView.layer.presentationLayer valueForKey:@"transform"];
        animation.toValue = [NSValue valueWithCATransform3D:transform];
        animation.duration = 0.5;
        animation.delegate = self;
        animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];
        //apply animation
        handView.layer.transform = transform;
        [handView.layer addAnimation:animation forKey:nil];
    } else {
        //set transform directly
        handView.layer.transform = transform;
    }
}

 

More complex animated curves

Consider a scenario where a rubber ball falls onto a hard ground. When it starts to fall, it continues to accelerate until it falls on the ground, then bounces several times before stopping.If illustrated with a diagram, it will be shown in Figure 10.5.

This works, but it doesn't work very well. So far all we've done is replicate the behavior of CABasicAnimation using linear buffering in a very complex way.The advantage of this approach is that we can control buffering more accurately, which also means that we can apply a fully customized buffer function.So what should I do?

The math behind buffering is not simple, but fortunately we don't need to implement it at all.Robert Burner has a web page about buffer functions (http://www.robertpenner.com/easing), which contains links to the implementation of many programming languages for most common buffer functions, including C.Here is an example of a buffer entry and exit function (there are actually many different ways to implement it).

float quadraticEaseInOut(float t) 
{
    return (t < 0.5)? (2 * t * t): (-2 * t * t) + (4 * t) - 1; 
}

 

For our elastic sphere, we can use the bounceEaseOut function:

float bounceEaseOut(float t)
{
    if (t < 4/11.0) {
        return (121 * t * t)/16.0;
    } else if (t < 8/11.0) {
        return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0;
    } else if (t < 9/10.0) {
        return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
    }
    return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
}

 

If you modify the code in Listing 10.7 to introduce the bounceEaseOut method, our task is to simply swap the buffer functions, and you can now choose any buffer type to create the animation (see Listing 10.8).

Listing 10.8 Implementing custom buffer functions with keyframes

- (void)animate
{
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //set up animation parameters
    NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    CFTimeInterval duration = 1.0;
    //generate keyframes
    NSInteger numFrames = duration * 60;
    NSMutableArray *frames = [NSMutableArray array];
    for (int i = 0; i < numFrames; i++) {
        float time = 1/(float)numFrames * i;
        //apply easing
        time = bounceEaseOut(time);
        //add keyframe
        [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]];
    }
    //create keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 1.0;
    animation.delegate = self;
    animation.values = frames;
    //apply animation
    [self.ballView.layer addAnimation:animation forKey:nil];
}

 

10.3 Summary

In this chapter, we learned about buffering and the CAMediaTimingFunction class, which allows us to create custom buffer functions to improve our animation, and how to use CAKeyframe Animation to avoid the limitations of CAMediaTimingFunction and create fully customized buffer functions.

In the next chapter, we'll look at timer-based animation--another option that gives us more control over animation and enables real-time manipulation of it.

Keywords: iOS Attribute Programming

Added by TGWSE_GY on Thu, 28 Nov 2019 08:46:23 +0200