How to make my UIBezierPath animated with CAShapeLayer?
Asked Answered
E

1

15

I'm trying to animate a UIBezierPath and I've installed a CAShapeLayer to try to do it. Unfortunately the animation isn't working and I'm not sure any of the layers are having any affect (as the code is doing the same thing it was doing before I had the layers).

Here is the actual code - would love any help. Draw2D is an implementation of UIView that is embedded in a UIViewController. All the drawing is happening inside the Draw2D class. The call to [_helper createDrawing... ] simply populates the _uipath variable with points.

Draw2D.h defines the following properties:

#define defaultPointCount ((int) 25)

@property Draw2DHelper *helper;
@property drawingTypes drawingType;
@property int graphPoints;
@property UIBezierPath *uipath;

@property CALayer *animationLayer;
@property CAShapeLayer *pathLayer;

- (void)refreshRect:(CGRect)rect;

below is the actual implementation :

//
//  Draw2D.m
//  Draw2D
//
//  Created by Marina on 2/19/13.
//  Copyright (c) 2013 Marina. All rights reserved.
//

#import "Draw2D.h"
#import"Draw2DHelper.h"
#include <stdlib.h>
#import "Foundation/Foundation.h"
#import <QuartzCore/QuartzCore.h>

int MAX_WIDTH;
int MAX_HEIGHT;

@implementation Draw2D

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code

        if (self.pathLayer != nil) {
            [self.pathLayer removeFromSuperlayer];
            self.pathLayer = nil;
        }

        self.animationLayer = [CALayer layer];
        self.animationLayer.frame = self.bounds;
        [self.layer addSublayer:self.animationLayer];

        CAShapeLayer *l_pathLayer = [CAShapeLayer layer];
        l_pathLayer.frame = self.frame;
        l_pathLayer.bounds = self.bounds;
        l_pathLayer.geometryFlipped = YES;
        l_pathLayer.path = _uipath.CGPath;
        l_pathLayer.strokeColor = [[UIColor grayColor] CGColor];
        l_pathLayer.fillColor = nil;
        l_pathLayer.lineWidth = 1.5f;
        l_pathLayer.lineJoin = kCALineJoinBevel;

        [self.animationLayer addSublayer:l_pathLayer];
        self.pathLayer = l_pathLayer;
        [self.layer addSublayer:l_pathLayer];
    }
    return self;
}

// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect :(int) points :(drawingTypes) type //:(Boolean) initial
{
    //CGRect bounds = [[UIScreen mainScreen] bounds];
    CGRect appframe= [[UIScreen mainScreen] applicationFrame];
    CGContextRef context = UIGraphicsGetCurrentContext();
    _helper = [[Draw2DHelper alloc ] initWithBounds :appframe.size.width  :appframe.size.height :type];

    CGPoint startPoint = [_helper generatePoint] ;

    [_uipath moveToPoint:startPoint];
    [_uipath setLineWidth: 1.5];

    CGContextSetStrokeColorWithColor(context, [UIColor lightGrayColor].CGColor);

    CGPoint center = CGPointMake(self.center.y, self.center.x) ;
    [_helper createDrawing :type :_uipath :( (points>0) ? points : defaultPointCount) :center];
    self.pathLayer.path = (__bridge CGPathRef)(_uipath);
    [_uipath stroke];

    [self startAnimation]; 
}

- (void) startAnimation {
    [self.pathLayer removeAllAnimations];

    CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    pathAnimation.duration = 3.0;
    pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
    pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
    [self.pathLayer addAnimation:pathAnimation forKey:@"strokeEnd"];

}

- (void)drawRect:(CGRect)rect {
    if (_uipath == NULL)
        _uipath = [[UIBezierPath alloc] init];
    else
        [_uipath removeAllPoints];

    [self drawRect:rect  :self.graphPoints :self.drawingType ];
}

- (void)refreshRect:(CGRect)rect {
    [self setNeedsDisplay];
}

@end

I know there's probably an obvious reason for why the path isn't animating as it's being drawing (as opposed to being shown immediately which is what happens now) but I've been staring at the thing for so long that I just don't see it.

Also, if anyone can recommend a basic primer on CAShapeLayers and animation in general I would appreciate it. Haven't come up across any that are good enough.

thanks in advance.

Emblazon answered 10/3, 2013 at 4:30 Comment(1)
On animation, I'd recommend my own explanation: apeth.com/iOSBook/ch17.html. It explains things I haven't seen explained anywhere else.Instantaneity
K
47

It looks like you're trying to animate within drawRect (indirectly, at least). That doesn't quite make sense. You don't animate within drawRect. The drawRect is used for drawing a single frame. Some animation is done with timers or CADisplayLink that repeatedly calls setNeedsDisplay (which will cause iOS to call your drawRect) during which you might draw the single frame that shows the progress of the animation at that point. But you simply don't have drawRect initiating any animation on its own.

But, since you're using Core Animation's CAShapeLayer and CABasicAnimation, you don't need a custom drawRect at all. Quartz's Core Animation just takes care of everything for you. For example, here is my code for animating the drawing of a UIBezierPath:

#import <QuartzCore/QuartzCore.h>

@interface View ()
@property (nonatomic, weak) CAShapeLayer *pathLayer;
@end

@implementation View

/*
// I'm not doing anything here, so I can comment this out
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
    }
    return self;
}
*/

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    // Drawing code
}
*/

// It doesn't matter what my path is. I could make it anything I wanted.

- (UIBezierPath *)samplePath
{
    UIBezierPath *path = [UIBezierPath bezierPath];

    // build the path here

    return path;
}

- (void)startAnimation
{
    if (self.pathLayer == nil)
    {
        CAShapeLayer *shapeLayer = [CAShapeLayer layer];

        shapeLayer.path = [[self samplePath] CGPath];
        shapeLayer.strokeColor = [[UIColor grayColor] CGColor];
        shapeLayer.fillColor = nil;
        shapeLayer.lineWidth = 1.5f;
        shapeLayer.lineJoin = kCALineJoinBevel;

        [self.layer addSublayer:shapeLayer];

        self.pathLayer = shapeLayer;
    }

    CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    pathAnimation.duration = 3.0;
    pathAnimation.fromValue = @(0.0f);
    pathAnimation.toValue = @(1.0f);
    [self.pathLayer addAnimation:pathAnimation forKey:@"strokeEnd"];
}

@end

Then, when I want to start drawing the animation, I just call my startAnimation method. I probably don't even need a UIView subclass at all for something as simple as this, since I'm not actually changing any UIView behavior. There are definitely times that you subclass UIView with a custom drawRect implementation, but it's not needed here.

You asked for some references:

Kelp answered 10/3, 2013 at 15:10 Comment(7)
Thank you. My app that takes as input the kind of drawing that will be made in scene 1 (line, bezier, fractals, etc) - and in scene 2 will generate said drawing. Scene2 view controller has 2 buttons on top - one for refresh - a new drawing of the same kind will be generated on the page - or a back button - to go back to scene 1 and enter in new parameters. It also has the UIView that contains the actual drawings on the bottom - the code was this was enclosed in the original posting. Knowing all this, would you still recommend that I go with a View as opposed to a UIView?Emblazon
@marina My View class actually is a UIView subclass, so I don't understand your "View as opposed to UIView" question. My main point was you don't need to do custom drawRect code when using Core Animation's CAShapeLayer. You can either animate using, drawRect with timers or CADisplayLink in UIView subclass (which is a little complicated), or using Core Animation. When I use CAShapeLayer, I do it in UIViewController subclass (so it's not confused with drawRect alternative), but that's a personal preference, as it works in both UIView and UIViewController subclasses.Kelp
In short, there's absolutely nothing wrong with doing CAShapeLayer code in your UIView subclass (as my example illustrates). My main point is that you don't need custom drawRect code when using CAShapeLayer.Kelp
i think I'm starting to understand... How do you recommend I clear the layer of the underlying drawing when I need to create a new one? They just keep on drawing one on top of another...Emblazon
@marina If you keep a reference to the old CAShapeLayer, you can just set its path to be the new path (and reinitiate the animation, of you want). Or you can take the old CAShapeLayer and call removeFromSuperlayer before you add the new CAShapeLayer. Both techniques work fine, though the former is slightly more efficient.Kelp
@Kelp Hi, can you please take a look at this question? I've been banging my head on this for the past couple of weeks, sorry if this is not appropriate and just ignore it in that case :) . #42272562Glennaglennie
Dropping another one for @Kelp #43931896 :)Soult

© 2022 - 2024 — McMap. All rights reserved.