Aug 27, 2011

Core Animation Part 2: View based animation in UIKit

Animating UIView Properties

Several view properties are animatable. They are the view's frame (the view's dimensions in its parent's coordinate space), its bounds(its dimensions in its own space), the center of the view, the transform (2D transforms only), the alpha, or transparency, the backgroundColor and the contentStretch.

Most of these properties are straightforward. The really interesting one is the
transform. The transform property allows you to apply an affine transform to
the view. This is really powerful. Generally, affine transforms allow you to scale a view (change its size), translate a view (change its location), or rotate a view, or any combination of the three. For UIView animations you use the CGAffineTransform structure. If you have a view and you want it to appear 50 percent smaller on the screen, you would simply:

view.transform = CGAffineTransformMakeScale(0.5, 0.5);

This will shrink the view 50 percent in each direction. It does this without changing the actual size of the view as far as the view is concerned. If you want to scale the view down by 50% and also rotate it 75 degrees you simply:

CGAffineTransform scale = CGAffineTransformMakeScale(0.5,0.5);
CGAffineTransform scaleAndRotate =
    CGAffineTransformRotate(scale, 75.0 * M_PI / 180.0);
view.transform = scaleAndRotate;

Thus you can combine multiple transforms together into a single transform. You also need to be careful with animating the frame if you have set the transform to anything except for the identity transform. Since I'm using transforms extensively in this app, I'm not going to touch the frame.

In CADemo the views are sized to fill most of the screen, with a bit of space around them. The views are positioned in the center. As far as these views are concerned, their size and rotation never change. View transforms like this are only 2D - all rotations are in the plane of the screen. If you want 3D rotation of your views, you'll have to step down into direct Core Animation.

Another interesting point is that the views can still interact with the user in their transformed state. You don't have to write a single line of code. The system knows how to deal with the transforms of views and makes sure that each view receives events in the coordinate space that it expects.

View Layout in CADemo Using View Animations

You can use these simple view animations to do some fun and surprisingly sophisticated things. For example, in CADemo, there are two different layouts (grid and circle) for the main screen. These views have some simple code to determine their layout. This code is defined in CardLayoutView.m in the demo project. The -updateCircle method computes the layout of the views in a circle. The -updateGrid method does the same thing for the grid.

The grid layout is a flexible grid which dynamically computes the size of each subview, based on the number of rows and columns desired, the spacing between the views, and the inset of the grid from the edge of the screen. We do some simple math:

- (void)updateGrid {
  CGRect bounds = self.bounds;

  subviewSize_.width = (bounds.size.width - (2 * inset_.width) -
                       ((columns_ - 1) * spacing_.width)) / columns_;
  subviewSize_.height = (bounds.size.height - (2 * inset_.height) -
                        ((rows_ - 1) * spacing_.height)) / rows_;

We'll get back to this in a moment in more detail, but here we start our animation transaction, set the animation curve (ease in/out starts and ends slowly), and the duration of the animation.

[UIView beginAnimations:@"gridView" context:NULL];
  [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
  [UIView setAnimationDuration:animationDuration_];

  NSUInteger index = 0;
  NSArray *newViews = [self viewsFromDataSource];
  for (DemoCardView *view in newViews) {
    if (!view.isZoomedIn) {
      NSUInteger row = index / columns_;
      NSUInteger col = index % columns_;
      row = row % rows_;

For each view index, I compute the row and column for that view. for this demo,
all the views fit on a single page, but it's also easy to compute an offset for
paging, which would allow you to have multiple pages of the grid.

// Compute the new rect
      CGRect newFrame = CGRectMake(
          inset_.width + col * (subviewSize_.width + spacing_.width),
          inset_.height + row * (subviewSize_.height + spacing_.height),
          subviewSize_.width, subviewSize_.height);
 
      // Use the transform to resize the view. Move it by setting the center.
      CGFloat scale = [GraphicsUtils scaleForSize:self.bounds.size
                                           inRect:newFrame];
      view.center = [GraphicsUtils centerOfRect:newFrame];
      view.transform = CGAffineTransformMakeScale(scale, scale);

Here we are moving the view into the correct location and scaling it to fit in the grid.

if (![self.subviews containsObject:view]) {
          [self addSubview:view];
      }
    }
    index++;
  }

Any view properties set between the +beginAnimations:context: are now animated at the same time.

[UIView commitAnimations];
}

For the circle layout, we put all the demo views into a circle around the screen. I've taken a few shortcuts here for the purposes of the demo, which you'd want to fix if you ever used this code for a real application

- (void)updateCircle {
  CGRect bounds = self.bounds;
  NSUInteger index = 0;
This is a repeat of the animation code in -updateGrid.
[UIView beginAnimations:@"circleView" context:NULL];
  [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
  [UIView setAnimationDuration:animationDuration_];

  // This is pure laziness. I should figure out the correct size based on
  // number of views and the the screen size.
  subviewSize_ = CGSizeMake(175, 175);

  CGPoint center = [GraphicsUtils centerOfRect:bounds];
  CGSize size = bounds.size;
  CGFloat offset = 0.5 * MIN(
      size.width - subviewSize_.width - inset_.width,
      size.height - subviewSize_.height - inset_.height);
The offset computes how far from the center of the screen we want the center of each view.
NSArray *newViews = [self viewsFromDataSource];
  CGFloat angle = 2 * M_PI / [newViews count];
We compute the change in angle by dividing a circle up by the number of views.
for (DemoCardView *view in newViews) {
    if (!view.isZoomedIn) {
     CGFloat xOffset = offset * cosf(angle * index);
      CGFloat yOffset = offset * sinf(angle * index);
      view.center = CGPointMake(center.x + xOffset, center.y + yOffset);
This little bit of trigonometry computes the location of the center of each view, rotated around the center of the screen.
CGRect newBounds = CGRectMake(0, 0, 
          subviewSize_.width, subviewSize_.height);
      CGFloat scale = [GraphicsUtils scaleForSize:view.bounds.size
                                           inRect:newBounds];
      CGAffineTransform scaleAndRotate = CGAffineTransformRotate(
          CGAffineTransformMakeScale(scale, scale),
          angle * index + M_PI_2);
      view.transform = scaleAndRotate;
Then we compute the scale and rotation of the view so that it is rotated with the top of the view towards the outside of the circle.
if (![self.subviews containsObject:view]) {
            [self addSubview:view];
        }
      }
      index++;
    }
And commit the animations.
[UIView commitAnimations];
}
I didn't talk about the animation code in here yet. The simple answer is that adding the 4 lines of code from +beginAnimations:context: to +commitAnimations is all you need to do to make these animate. When you toggle the UISegmentedControl in the app's toolbar, the views will smoothly animate between the two layouts. Really, that's all you have to do. View 0 is at the upper left hand corner in the grid view and rotated 90 degrees and all the way on the right side in the circle.

I don't have to tell that view how to get from the corner to the side. The animation system figures it out for me. If you toggle the grid/circle mode quickly, you'll see that the views stop in the middle of the animation and head off to their new location seamlessly. It's awesome. I recommend setting the animationDuration_ variable to something long, like 10 seconds so you can really play around with it.

One thing I haven't mentioned is that in iOS 4 and above, you should use blocks instead of the transaction style I'm using here. Blocks are recommended since it's easier to see everything together, and it's also easier to chain a series of animations together. With blocks, animations can be nested as well. I'll show that a little later, this post is getting long. Next time, I finish up with view animations, including animation delegates and blocks.

No comments: