Advertise here




Advertise here

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

CAAnimation tutorial

Duncan CDuncan C Posts: 9,114 @ @ @ @ @ @ @
edited March 2014 in iPhone SDK Tutorials
I just wrapped up development on a Core Animation tutorial.


This project demonstrates a number of techniques for using Core Animation:
  • Using different animation timing functions like kCAMediaTimingFunctionLinear, kCAMediaTimingFunctionEaseIn, and kCAMediaTimingFunctionEaseInEaseOut to get different effects
  • Using CAKeyframeAnimation and a CGPath to animate a layer along a curved path (a figure 8).
  • Creating a custom subclass of UIView that has a CAShapeLayer as it's backing layer so you can draw shapes in a view "for free."
  • Adding a CGPath to a shape layer to draw shapes on the screen.
  • Using CAAnimationGroup to create a linked series of animations that run in sequence
  • Creating a very clean "per animation" completion block scheme using the fact that CAAnimation objects support the setValue:forKey: method. I add a code block to an animation object and set up the animation delegate's animationDidStop:finished method to check for a special key/value pair with the key kAnimationCompletionBlock.
  • Using the cumulative property on animations to create a single repeating animation that continuously rotates a layer by any desired amount.
  • Using a CATapGestureRecognizer to detect taps on a view.
  • Detecting taps on a view while it animates "live" by using the hitTest method of the view's presentation layer
  • Pausing and resuming animation on a layer.


I posted the project on GitHub.

You can download it here:

https://github.com/DuncanMC/iOS-CAAnimation-group-demo

The easiest thing to do, if you're not planning on pushing changes to the project, is to click the "ZIP" button, which downloads a zip version of the project without setting you up with GIT source control.

Here is a GIF of one of the animations it does:

VJVju.gif
Post edited by Duncan C on
Regards,
Duncan C
WareTo

widehead.gif
Animated GIF created with Face Dancer, available for free in the app store.

I'm available for one-on-one help at CodeMentor

Replies

  • rocotilosrocotilos Posts: 3,295 @ @ @ @ @
    edited March 2012
    Hi Duncan.

    Thanks for this tutorial. Lots of cool stuffs! I'm sure it will become handy to me, as an amateur coder. :)
  • Duncan CDuncan C Posts: 9,114 @ @ @ @ @ @ @
    edited March 2012
    There are several ways to use Core Animation.

    The simplest is to use UIView Animation.

    There are 2 different families of UIView animation methods. The "old style", for iOS before 4.0, uses the methods beginAnimations:context: and commitAnimations to enclose code that makes changes to "animatable" properties of the view. The system then animates those changes instead of doing them suddenly.

    For iOS 4.0 and later, there is a similar family of methods, most beginning with animateWithDuration. Those methods take a code block, and those methods apply the changes inside the code block as an animation. There are variations of those methods that take option parameters, a delay parameter, and a completion block, which is code that you want to run once the animation is complete.

    Then there are animations that deal with Core Animation Layers.

    Layers are the way the UIKit represents an area that's drawn to the screen in the iOS tiled graphics engine. Every UIView object has at least one layer attached to it that contains the actual content that is drawn to the screen.

    There are 2 different classes of animation you can do with layers: implicit and explicit animation.

    Implicit animation is really easy to use. All you do is change a property (an "animatable property") of a layer, and the system generates an animation to make that change. it's called implicit animation because changing a layer's property implies that you want an animation for that change, without your having to specify the animation explicitly.

    Layers have a "position" value, which is equivalent to the "center" property in a UIView. If you change the position value, the layer moves from it's old position to it's new position over a short time (usually around 1/4 second.) There are ways to change the duration of the animation.

    Explicit animations also operate on layers, but you create special Core Animation objects and attach them to the layer. Core Animation objects descend from the base class CAAnimation. An odd thing about CAAnimation objects is that they are temporary. Once the animation is complete, it is removed, and the layer reverts to it's previous state. There are properties you can set on CAAnimations that keep them from being removed. When you do that, the effects of the animation persist after the animation is complete. However, the underlying property that you animated does not get changed - it just looks that way. For example, if you create an animation to move a layer's position, and set the animation to not be deleted on completion, the layer will be drawn at the new position, but the underlying "position" property won't be changed. This makes using CAAnimations a little strange.

    CAAnimation objects are the most complex API for doing Core Animation, but also the most powerful.

    The app linked at the beginning of this tutorial focuses on animations using CAAnimation objects. Specifically, it uses something called animation groups (CAAnimationGroup objects.) A CAAnimationGroup enables you to create a set of linked animations that act on a layer in concert. You create an animation group with a specific start time and duration, and then you add other animation objects to it that have their own duration values, and start times relative to the beginning of their enclosing group.

    The sample app creates an animation group that moves an image view in a straight line, then moves it in a figure 8, then rotates it 2.5 full rotations, moves it back down, rotates it a final 1/2 turn back to it's initial position, and then fades the image away.
    Regards,
    Duncan C
    WareTo

    widehead.gif
    Animated GIF created with Face Dancer, available for free in the app store.

    I'm available for one-on-one help at CodeMentor
  • Duncan CDuncan C Posts: 9,114 @ @ @ @ @ @ @
    edited August 2013
    It's actually a little tricky to create an animation that rotates an object more than 180 degrees.

    The problem is that you specify rotation using a transform. A transform lets you shift, rotate, stretch, or even skew the object it transforms.

    If you set the rotation of an object to a value greater than 180 degrees, the system doesn't know if you want it to rotate clockwise or counter-clockwise.

    What I've done until recently to handle this is to create a series of explicit animations that animate the object by 1/4 or 1/3 of a rotation, and then each one triggers another animation once it is complete.

    I Figured out how to do it more simply. It turns out that most animations have a boolean property "cumulative", as well as a repeatCount property. When you set cumulative to TRUE, each repeat of the animation is added to the value you're changing. So if the animation rotates the object by 1/4 turn, and you set cumulative to TRUE and repeatCount to 8, your object rotates 8 quarter turns, or 2 full turns.

    The code do to that looks like this:

    CABasicAnimation* rotate =  [CABasicAnimation animationWithKeyPath: @transform.rotation.z];
      rotate.removedOnCompletion = FALSE;
      rotate.fillMode = kCAFillModeForwards;
      
      //Do a series of 5 quarter turns for a total of a 1.25 turns
      //(2PI is a full turn, so pi/2 is a quarter turn)
      [rotate setToValue: [NSNumber numberWithFloat: -M_PI / 2]];
      rotate.repeatCount = 11;
    
      rotate.duration = duration/2;
      rotate.beginTime = start;
      rotate.cumulative = TRUE;
      rotate.timingFunction = [CAMediaTimingFunction 
        functionWithName:kCAMediaTimingFunctionLinear];
    
    Post edited by Duncan C on
    Regards,
    Duncan C
    WareTo

    widehead.gif
    Animated GIF created with Face Dancer, available for free in the app store.

    I'm available for one-on-one help at CodeMentor
  • Duncan CDuncan C Posts: 9,114 @ @ @ @ @ @ @
    edited August 2013
    Another form of CAAnimation is called CAKeyframeAnimation.

    A CAKeyframeAnimation lets you do a couple of different things.

    You can specify a series of values, and animates a layer so a property shifts smoothly from one value to the next value,

    Or you can specify a CGPath, which lets you describe a curve, and animate your layer so it moves along that curve.

    Keyframe animations that specify a series of values can animate more than just position. You can use keyframe animation to animate changes in color, transform, position, opacity, and lots of other things.

    Keyframe animations that use a path are pretty much limited to animating a position.

    This demo app animates a UIImageView's layer to follow a figure 8 shaped path.

    It does that by creating a CGPath object that describes a bezier path. The code to animate a layer along a curved path looks like this:

    CAKeyframeAnimation* figure8 = nil;
      figure8=  [CAKeyframeAnimation animationWithKeyPath: @position];
      figure8.removedOnCompletion = FALSE;
      figure8.fillMode = kCAFillModeForwards;
      figure8.duration = 2;
      figure8.beginTime = start;
      figure8.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    
      
      //Create a CGPath to hold the figure 8
      CGMutablePathRef figure8Path;
      figure8Path = CGPathCreateMutable();
      CGFloat left, right, middleX, bottom, top, middleY;
      CGFloat boxHalfWidth = 175;
      
      //Calculate the points use to create the figure 8 path.
      middleX = newOrigin.x;
      left = middleX - boxHalfWidth;
      right = middleX + boxHalfWidth;
      
      bottom = newOrigin.y;
      middleY = bottom - boxHalfWidth * 2;
      top = middleY - boxHalfWidth * 2;
      
      
      CGPathMoveToPoint(figure8Path, NULL, middleX, bottom);
      
      //Make the first S of the figure 8
      CGPathAddCurveToPoint(figure8Path, NULL, right, bottom, right, middleY, middleX, middleY);
      CGPathAddCurveToPoint(figure8Path, NULL, left, middleY, left, top, middleX, top);
      
      //Loop down in a reverse S for the second half of a figure 8
      CGPathAddCurveToPoint(figure8Path, NULL, right, top, right, middleY, middleX, middleY);
      CGPathAddCurveToPoint(figure8Path, NULL, left, middleY, left, bottom, middleX, bottom);
    
      //Make the figure 8 do 2 full cycles.
      figure8.repeatCount = 2;
      
      
      start = start + figure8.duration * figure8.repeatCount + pause;
      figure8.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
      
      //install the path in the CAKeyframeAnimation
      [figure8 setPath: figure8Path];
    

    The code above starts from a point newOrigin as the bottom center for the figure 8. It uses a constant boxHalfWidth to describe the height and width of the figure 8, and generates a CGPath that describes that figure 8. It then installs the figure 8 path as the path for the keyframe animation.

    It doesn't release the CGPath because the next step in the animation draws the figure 8 path to a separate "shape layer" so you can see the path that the image is following in the animation.
    Post edited by Duncan C on
    Regards,
    Duncan C
    WareTo

    widehead.gif
    Animated GIF created with Face Dancer, available for free in the app store.

    I'm available for one-on-one help at CodeMentor
  • Duncan CDuncan C Posts: 9,114 @ @ @ @ @ @ @
    edited August 2013
    There is a special type of Core Animation Layer called a CAShapeLayer. You can use a CAShapeLayer to draw to the screen, or you can use it to act as a mask to clip drawing in other layers.

    This sample app uses a CAShapeLayer to draw a dotted outline of the figure 8 path along which it is going to move an image.

    The code installs the same CGPath object it used for the previous key frame animation into a shape layer:

    //Also install the path into our view's layer so you can see the figure 8 shape.
      //The shape layer is set up as a CAShapeLayer
      CAShapeLayer *shapeLayer = (CAShapeLayer *)[myContainerView layer];
      shapeLayer.path = figure8Path;
      shapeLayer.lineWidth = 1.0;
      shapeLayer.strokeColor = [[UIColor grayColor] CGColor];
      shapeLayer.fillColor = [[UIColor clearColor] CGColor];
      shapeLayer.lineDashPattern = [NSArray arrayWithObjects: [NSNumber numberWithInt: 5], [NSNumber numberWithInt: 9], nil];
      
      //Release our figure 8 path now that we are done with it.
      CFRelease(figure8Path);
    

    Shape layers have a property lineDashPattern that let you draw shapes as a dotted line (or some other pattern.) You specify a table of numbers that are used as alternating "draw" and "don't draw" line widths. The code above specifies a dashed line that draws for 5 points, then doesn't draw for 9 points (where a point is 1/72 of an inch on the display device.)
    Post edited by Duncan C on
    Regards,
    Duncan C
    WareTo

    widehead.gif
    Animated GIF created with Face Dancer, available for free in the app store.

    I'm available for one-on-one help at CodeMentor
  • Duncan CDuncan C Posts: 9,114 @ @ @ @ @ @ @
    edited August 2013
    It seems that gesture recognizers don't work on views who's position is being animated. The gesture recognizer does not fire.

    In order to detect taps on a view as it moves, you have to do a trick.

    What you do is to put a gesture recognizer on the parent view, and then ask that parent view's presentation layer to hit test the point on which the user tapped. That returns the layer on which the user tapped, and you can easily use that to figure out which view was tapped.

    Here is the code that's called by a tap gesture recognizer attached to the parent view where the Core Animation takes place:

    /*
     This method gets called from a tap gesture recognizer installed on the view myContainerView.
     We get the coordinates of the tap from the gesture recognizer and use it to hit-test 
     myContainerView.layer.presentationLayer to see if the user tapped on the moving image view's 
     (presentation) layer. The presentation layer's properties are updated as the animation runs, so hit-testing
     the presentation layer lets you do tap and/or collision tests on the "in flight" animation.
     */
    
    - (IBAction)testViewTapped:(id)sender 
    {
      CALayer *tappedLayer;
      id layerDelegate;
      UITapGestureRecognizer *theTapper = (UITapGestureRecognizer *)sender;
      CGPoint touchPoint = [theTapper locationInView: myContainerView];
      if (animationInFlight)
      {
        tappedLayer = [myContainerView.layer.presentationLayer hitTest: touchPoint];
        //The layer delegate of a view is usually the view it's associated with.
        layerDelegate = [tappedLayer delegate];
        
        //imageOne is the image view (or view layer) we are animating
        if (layerDelegate == imageOne)
        {
          //We set the container view's layer's speed to zero to pause the animation.
          if (myContainerView.layer.speed == 0)
          {
            //if the animation is paused, resume it.
            [self resumeLayer: myContainerView.layer];
          }
          else
          {
            //else the animation is running, so pause it.
            [self pauseLayer: myContainerView.layer];
                    
            //Also kill all the pending label changes that we set up using 
            //performSelector:withObject:afterDelay
            [NSObject cancelPreviousPerformRequestsWithTarget: animationStepLabel];
          }
        }
      }
    }
    


    The code above works to detect taps on views that are animated both with UIView animations and with layer-based implicit or explicit animations.
    Post edited by Duncan C on
    Regards,
    Duncan C
    WareTo

    widehead.gif
    Animated GIF created with Face Dancer, available for free in the app store.

    I'm available for one-on-one help at CodeMentor
  • Duncan CDuncan C Posts: 9,114 @ @ @ @ @ @ @
    edited August 2012
    Another thing this tutorial shows how to do is to animate transitions between views using a "wipe." This demo shows how to do a "Clock wipe".

    Here is an image of a clock wipe in action (taken from an article on transitions which is linked below)

    image

    The secret to this type of animation is a special type of Core Animation Layer called a CAShapeLayer. A CAShapeLayer has a path property that determines the shape that is drawn in that layer. You can create a CAShapeLayer, install a path in it, and then make that shape layer a mask for another layer. Any place the mask layer is filled (opaque), the content of the masked layer shows through. Any place the mask is transparent, the masked layer is hidden.

    Shape layers also have properties strokeStart and strokeEnd. Those properties let you control what parts of the path are drawn. If you imagine the path as a curved line drawn from beginning to end, the strokeStart property tells where along that line to start drawing, and the strokeEnd property says where to stop drawing. Both values range from 0 to 1. By default, strokeStart is 0 (start drawing at the beginning of the path) and strokeEnd is 1.0 (stop drawing at the end.) If you set strokeStart to .25, the first quarter of the path won't be drawn. Likewise, if you set strokeEnd to .75, the last quarter of the path won't be drawn.

    The strokeStart and strokeEnd properties can be animated. By making the path an arc that describes a full circle, and making the thickness of the stroke be 1/2 the radius of the circle, we can create a fully filled circle. By changing strokeEnd to a value less than 1, we can draw a circle with a slice cut out of it, or even only a slice of a circle. By animating the strokeEnd value from 1 to zero, we can make the circle disappear. By making the mask layer a rectangular shape, and making the arc big enough to reach all the way into the corners of the rectangle, we can create the "clock wipe" effect.

    Tap the "Mask Animation" button in the demo project to see the Clock wipe animation in action.

    It's also possible to create a path that is just about any shape, and have it grow from a point to reveal a layer. These animations are known as "Iris wipes". You can use a circle shape, a diamond shape, a star, a keyhole shape, and many others. Implementing iris wipes is left as an exercise of the reader.

    If you are interested in different types of transition animations, check out this link:


    Transition animations


    It includes lots of small GIF animations of different types of transitions, like the clock wipe animation above.
    Regards,
    Duncan C
    WareTo

    widehead.gif
    Animated GIF created with Face Dancer, available for free in the app store.

    I'm available for one-on-one help at CodeMentor
  • trongdthtrongdth Posts: 2
    Hello Duncan,

    Thanks for this tutorial, it's really helpful for me.

    I just have a small question about CoreAnimation. I am doing a animation filling color from left to right of text (like karaoke). So far here is my code
    animationCompletionBlock theBlock;
    CAGradientLayer *gradientLayer = (CAGradientLayer *)self.layer;
    if([gradientLayer animationForKey:kAnimationKey] == nil)
    {
    CABasicAnimation *startPointAnimation = [CABasicAnimation animationWithKeyPath:kGradientStartPointKey];
    startPointAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(1.0, 0.0)];
    startPointAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

    CABasicAnimation *endPointAnimation = [CABasicAnimation animationWithKeyPath:kGradientEndPointKey];
    endPointAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(1.0, 0)];
    endPointAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

    CAAnimationGroup *group = [CAAnimationGroup animation];
    group.animations = @[startPointAnimation, endPointAnimation];
    group.duration = self.animationDuration;
    group.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    group.repeatCount = 0;
    group.delegate = self;
    [group setValue:theBlock forKey:kAnimationCompletionBlock];

    [gradientLayer addAnimation:group forKey:kAnimationKey];
    }
    theBlock = ^void(void)
    {
    [self stopAnimating];
    };
    I know it just a fake animation. Can you teach me how to do that?
  • Duncan CDuncan C Posts: 9,114 @ @ @ @ @ @ @
    I missed your post until now. I don't understand what you're trying to do. You're using constants for your animation's key paths, so I'm not sure what you are trying to animate. I guess you're animating a gradient layer and animating the start and end points?
    And what are you doing with that gradient layer?
    trongdth wrote: »
    Hello Duncan,<br />
    <br />
    Thanks for this tutorial, it's really helpful for me. <br />
    <br />
    I just have a small question about CoreAnimation. I am doing a animation filling color from left to right of text (like karaoke). So far here is my code<br />
    <br />
    animationCompletionBlock theBlock;<br />
    CAGradientLayer *gradientLayer = (CAGradientLayer *)self.layer;<br />
    if([gradientLayer animationForKey:kAnimationKey] == nil)<br />
    {<br />
    CABasicAnimation *startPointAnimation = [CABasicAnimation animationWithKeyPath:kGradientStartPointKey];<br />
    startPointAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(1.0, 0.0)];<br />
    startPointAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];<br />
    <br />
    CABasicAnimation *endPointAnimation = [CABasicAnimation animationWithKeyPath:kGradientEndPointKey];<br />
    endPointAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(1.0, 0)];<br />
    endPointAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];<br />
    <br />
    CAAnimationGroup *group = [CAAnimationGroup animation];<br />
    group.animations = @[startPointAnimation, endPointAnimation];<br />
    group.duration = self.animationDuration;<br />
    group.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];<br />
    group.repeatCount = 0;<br />
    group.delegate = self;<br />
    [group setValue:theBlock forKey:kAnimationCompletionBlock];<br />
    <br />
    [gradientLayer addAnimation:group forKey:kAnimationKey];<br />
    }<br />
    theBlock = ^void(void)<br />
    {<br />
    [self stopAnimating];<br />
    };<br />
    <br />
    I know it just a fake animation. Can you teach me how to do that?

    Regards,
    Duncan C
    WareTo

    widehead.gif
    Animated GIF created with Face Dancer, available for free in the app store.

    I'm available for one-on-one help at CodeMentor
Sign In or Register to comment.