iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (75 page)

Read iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) Online

Authors: Aaron Hillegass,Joe Conway

Tags: #COM051370, #Big Nerd Ranch Guides, #iPhone / iPad Programming

BOOK: iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides)
12.29Mb size Format: txt, pdf, ePub
Implicitly Animatable Properties

Several of the properties of
CALayer
are
implicitly animatable
. This means that changes to these properties are automatically animated when their setters are called. The property
position
is an implicitly animatable property. Sending the message
setPosition:
to a
CALayer
doesn’t just move the layer to a new position; it animates the change from the old position to the new one.

 

In this section, you will have the application respond to user taps: the
boxLayer
will move to wherever the user starts a touch. This change in position will be animated because
position
is an implicitly animatable property.

 

In
HypnosisView.m
, implement
touchesBegan:withEvent:
to change the layer’s position.

 
- (void)touchesBegan:(NSSet *)touches
           withEvent:(UIEvent *)event
{
    UITouch *t = [touches anyObject];
    CGPoint p = [t locationInView:self];
    [boxLayer setPosition:p];
}

Build and run the application. The layer will move smoothly from its current position to where you tap. All you had to do to get this animation was send
setPosition:
. Pretty cool, huh?

 

If the user drags rather than taps, let’s have the layer follow the user’s finger. In
HypnosisView.m
, implement
touchesMoved:withEvent:
to send
setPosition:
to the layer.

 
- (void)touchesMoved:(NSSet *)touches
           withEvent:(UIEvent *)event
{
    UITouch *t = [touches anyObject];
    CGPoint p = [t locationInView:self];
    [boxLayer setPosition:p];
}

Build and run the application. This is not so cool. Notice how the animation makes the layer lag behind your dragging finger, making the application seem sluggish.

 

Why the poor response? An implicitly animatable property changes to its destination value over a constant time interval. However, changing a property while it is being animated starts another implicit animation. Therefore, if a layer is in the middle of traveling from point A to point B, and you tell it to go to point C, it will never reach B; and that little change of direction coupled with the timer restarting makes the animation seem choppy. (
Figure 22.7
)

 

Figure 22.7  Animation missing waypoints

 

To disable an implicit animation, you can use an
animation transaction
. Animation transactions allow you to batch animations and set their parameters, like the duration and animation curve. To begin a transaction, you send the message
begin
to the class
CATransaction
. To end a transaction, you send
commit
to
CATransaction
. Within the
begin
and
commit
block, you can set properties of a layer and also set values for the transaction as a whole.

 

Animation transactions can be used for lots of things, but here we’ll use it to disable the animation of the layer’s change in position. In
HypnosisView.m
, edit
touchesMoved:withEvent:
.

 
- (void)touchesMoved:(NSSet *)touches
            withEvent:(UIEvent *)event
{
    UITouch *t = [touches anyObject];
    CGPoint p = [t locationInView:self];
    
[CATransaction begin];
    [CATransaction setDisableActions:YES];
    [boxLayer setPosition:p];
    
[CATransaction commit];
}

Build and run the application. Now the dragging should feel much more responsive.

 
Bronze Challenge: Another Layer

Add another layer as a sublayer of the
boxLayer
– with any image you choose. Make it so the sublayer sits in the center of the
boxLayer
at all times and is half the size of the
boxLayer
.

 
Silver Challenge: Corner Radius

Have the
CALayer
draw with rounded corners.

 
Gold Challenge: Shadowing

Have the
CALayer
draw with a shadow. Make sure that, even though the image is translucent, the shadow only appears at the edges of the layer.

 
For the More Curious: Programmatically Generating Content

In this chapter, you set a layer’s contents with an image file. Now let’s look at how to set a layer’s contents programmatically. There are two ways to draw to a layer using Core Graphics: subclassing and delegation. In practice, subclassing is the last thing you want to do. The only reason to subclass
CALayer
to provide custom content is if you need to draw differently depending on some state of the layer. If this is the approach you wish to take, you must override the method
drawInContext:
.

 
@implementation LayerSubclass
- (void)drawInContext:(CGContextRef)ctx
{
    UIImage *layerImage = nil;
    if (hypnotizing)
        layerImage = [UIImage imageNamed:@"Hypno.png"];
    else
        layerImage = [UIImage imageNamed:@"Plain.png"];
    CGRect boundingBox = CGContextGetClipBoundingBox(ctx);
    CGContextDrawImage(ctx, boundingBox, [layerImage CGImage]);
}
@end
 

Delegation is the more common way to programmatically draw to a layer. This is how implicit layers work, but you can also give an explicit layer a delegate. However, it is not a good idea to assign a
UIView
as the delegate of an explicit layer. Use a controller object instead. (A
UIView
is already the delegate of another layer, and bad things happen when a view is the delegate of two layers.)

 

A layer sends the message
drawLayer:inContext:
to its delegate object when it is being displayed. The delegate can then perform Core Graphics calls on this context.

 
@implementation Controller
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
    if (layer == hypnoLayer)
    {
        UIImage *layerImage = [UIImage imageNamed:@"Hypno.png"];
        CGRect boundingBox = CGContextGetClipBoundingBox(ctx);
        CGContextDrawImage(ctx, boundingBox, [layerImage CGImage]);
    }
}
@end
 

For both subclassing and delegation, you must send an explicit
setNeedsDisplay
to the layer in order for these methods to be invoked. Otherwise, the layer thinks it doesn’t have any content and won’t draw.

 
For the More Curious: Layers, Bitmaps, and Contexts

A layer is simply a bitmap – a chunk of memory that holds the red, green, blue, and alpha values of each pixel. When you send the message
setNeedsDisplay
to a
UIView
instance, that method is forwarded to the view’s layer. After the run loop is done processing an event, every layer marked for re-display prepares a
CGContextRef
. Drawing routines called on this context generate pixels that end up in the layer’s bitmap.

 

How do drawing routines get called on the layer’s context? After a layer prepares its context, it sends the message
drawLayer:inContext:
to its delegate. The delegate of an implicit layer is its view, so in the implementation for
drawLayer:inContext:
, the view sends
drawRect:
to itself. Therefore, when you see this line at the top of your
drawRect:
implementations...

 
- (void)drawRect:(CGRect)r
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();
}

you are getting a pointer to the layer’s context. All of the drawing in
drawRect:
is filling the layer’s bitmap, which is then copied to the screen.

 

Need to see this for yourself? Set an
Xcode
breakpoint in
HypnosisView
’s
drawRect:
and check out the stack trace in the debug navigator, as shown in
Figure 22.8
.

 

Figure 22.8  Stack trace in drawRect:

 

A few paragraphs up, we said that the pixels generated by drawing routines

end up in the layer’s bitmap.

What does that mean? When you want to create a bitmap context in Cocoa Touch (as you did when you created the thumbnails for the possessions), you typically do something like this:

 
    // Create context
    UIGraphicsBeginImageContextWithOptions(size, NO, [[UIScreen mainScreen] scale]);
        ... Do drawing here ...
    // Get image result
    UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
    // Clean up image context
    UIGraphicsEndImageContext();

A bitmap context is created and drawn to, and the resulting pixels are stored in a
UIImage
instance.

 

The UIGraphics suite of functions provides a convenient way of creating a bitmap
CGContextRef
and writing that data to a
UIImage
object:

 
    // Create a color space to use for the context
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    // Create a context of appropriate width and height
    // with 4 bytes per pixel - RGBA
    CGContextRef ctx =
        CGBitmapContextCreate(NULL, width, height, 8, width * 4,
                    colorSpace, kCGImageAlphaPremultipliedLast);
    // Make this context the current one
    UIGraphicsPushContext(ctx);
    ... Do drawing here ...
    // Get image result
    CGImageRef image = CGBitmapContextCreateImage(ctx);
    UIImage *result = [[[UIImage alloc] initWithCGImage:image] autorelease];
    // Clean up image context - make previous context current if one exists
    UIGraphicsPopContext();
    CGImageRelease(image);
    CGContextRelease(ctx);
    CGColorSpaceRelease(colorSpace);
 

A layer creates the same kind of context when it needs to redraw its contents. However, a layer does it a little differently. See the
NULL
as the first parameter to
CGBitmapContextCreate
? That is where you pass a data buffer to hold the pixels generated by drawing routines in this context. By passing
NULL
, we say,

Core Graphics, figure out how much memory is needed for this buffer, create it, and then dispose of it when the context is destroyed.

A
CALayer
already has a buffer (its contents), so it would call the function as follows:

 
    CGContextRef ctx =
        CGBitmapContextCreate(myBitmapPixels, width, height, 8, width * 4,
                    colorSpace, kCGImageAlphaPremultipliedLast);
 

Then, when this context is drawn to, all of the resulting pixels are immediately written to the bitmap that is the layer.

 

Other books

Hindsight by Peter Dickinson
Rebel Queen by Michelle Moran
CyberStorm by Matthew Mather
The Abandoned Bride by Edith Layton
No Story to Tell by K. J. Steele
This Side of Home by Renée Watson
The Last Man by Vince Flynn