user1272965
user1272965

Reputation: 3064

Animate path of shape layer

I'm stumped by what I thought would be a simple problem.

I'd like to draw views connected by lines, animate the position of the views and have the connecting line animate too. I create the views, and create a line between them like this:

- (UIBezierPath *)pathFrom:(CGPoint)pointA to:(CGPoint)pointB {
    CGFloat halfY = pointA.y + 0.5*(pointB.y - pointA.y);
    UIBezierPath *linePath=[UIBezierPath bezierPath];
    [linePath moveToPoint: pointA];
    [linePath addLineToPoint:CGPointMake(pointA.x, halfY)];
    [linePath addLineToPoint:CGPointMake(pointB.x, halfY)];
    [linePath addLineToPoint:pointB];
    return linePath;
}

-(void)makeTheLine {
    CGPoint pointA = self.viewA.center;
    CGPoint pointB = self.viewB.center;

    CAShapeLayer *lineShape = [CAShapeLayer layer];
    UIBezierPath *linePath=[self pathFrom:pointA to:pointB];
    lineShape.path=linePath.CGPath;

    lineShape.fillColor = nil;
    lineShape.opacity = 1.0;
    lineShape.strokeColor = [UIColor blackColor].CGColor;
    [self.view.layer addSublayer:lineShape];
    self.lineShape = lineShape;
}

It draws just how I want it to. My understanding from the docs is that I am allowed to animate a shape's path by altering it in an animation block, like this:

- (void)moveViewATo:(CGPoint)dest {
    UIBezierPath *destPath=[self pathFrom:dest to:self.viewB.center];
    [UIView animateWithDuration:1 animations:^{
        self.viewA.center = dest;
        self.lineShape.path = destPath.CGPath;
    }];
}

But no dice. The view position animates as expected, but the line connecting to the other view "jumps" right away to the target path.

This answer implies that what I'm doing should work. And this answer suggests a CABasic animation, which seems worse to me since (a) I'd then need to coordinate with the much cooler block animation done to the view, and (b) when I tried it this way, the line didn't change at all....

// worse way
- (void)moveViewATo:(CGPoint)dest {
    UIBezierPath *linePath=[self pathFrom:dest to:self.viewB.center];
    [UIView animateWithDuration:1 animations:^{
        self.viewA.center = dest;
        //self.lineShape.path = linePath.CGPath;
    }];

    CABasicAnimation *morph = [CABasicAnimation animationWithKeyPath:@"path"];
    morph.duration = 1;
    morph.toValue = (id)linePath.CGPath;
    [self.view.layer addAnimation:morph forKey:nil];
}

Thanks in advance.

Upvotes: 2

Views: 3567

Answers (2)

user1272965
user1272965

Reputation: 3064

Thanks all for the help. What I discovered subsequent to asking this is that I was animating the wrong property. It turns out, you can replace the layer's shape in an animation, like this:

CABasicAnimation *morph = [CABasicAnimation animationWithKeyPath:@"path"];
morph.duration  = 1;
morph.fromValue = (__bridge id)oldPath.path;
morph.toValue   = (__bridge id)newPath.CGPath;
[line addAnimation:morph forKey:@"change line path"];
line.path=linePath.CGPath;

Upvotes: 2

Jinghan Wang
Jinghan Wang

Reputation: 1137

I guess this is all you need:



    #import "ViewController.h"

    @interface ViewController ()

    //the view to animate, nothing but a simple empty UIView here.
    @property (nonatomic, strong) IBOutlet UIView *targetView;
    @property (nonatomic, strong) CAShapeLayer *shapeLayer;
    @property NSTimeInterval animationDuration;
    @end

    @implementation ViewController

    - (void)viewDidLoad {
        [super viewDidLoad];

        //the shape layer appearance
        self.shapeLayer = [[CAShapeLayer alloc]init];
        self.shapeLayer.strokeColor = [UIColor blackColor].CGColor;
        self.shapeLayer.fillColor = [UIColor clearColor].CGColor;
        self.shapeLayer.opacity = 1.0;
        self.shapeLayer.lineWidth = 2.0;
        [self.view.layer insertSublayer:self.shapeLayer below:self.targetView.layer];

        //animation config
        self.animationDuration = 2;
    }

    - (UIBezierPath *)pathFrom:(CGPoint)pointA to:(CGPoint)pointB {
        CGFloat halfY = pointA.y + 0.5*(pointB.y - pointA.y);
        UIBezierPath *linePath=[UIBezierPath bezierPath];
        [linePath moveToPoint: pointA];
        [linePath addLineToPoint:CGPointMake(pointA.x, halfY)];
        [linePath addLineToPoint:CGPointMake(pointB.x, halfY)];
        [linePath addLineToPoint:pointB];
        return linePath;
    }

    - (void) moveViewTo: (CGPoint) point {
        UIBezierPath *linePath= [self pathFrom:self.targetView.center to:point];

        self.shapeLayer.path = linePath.CGPath;

        //Use CAKeyframeAnimation to animate the view along the path
        //animate the position of targetView.layer instead of the center of targetView
        CAKeyframeAnimation *viewMovingAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
        viewMovingAnimation.duration = self.animationDuration;
        viewMovingAnimation.path = linePath.CGPath;
        //set the calculationMode to kCAAnimationPaced to make the movement in a constant speed
        viewMovingAnimation.calculationMode =kCAAnimationPaced;
        [self.targetView.layer addAnimation:viewMovingAnimation forKey:viewMovingAnimation.keyPath];

        //draw the path, animate the keyPath "strokeEnd"
        CABasicAnimation *lineDrawingAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
        lineDrawingAnimation.duration = self.animationDuration;
        lineDrawingAnimation.fromValue = [NSNumber numberWithDouble: 0];
        lineDrawingAnimation.toValue = [NSNumber numberWithDouble: 1];
        [self.shapeLayer addAnimation:lineDrawingAnimation forKey:lineDrawingAnimation.keyPath];


        //This part is crucial, update the values, otherwise it will back to its original state
        self.shapeLayer.strokeEnd = 1.0;
        self.targetView.center = point;
    }

    //the IBAction for a UITapGestureRecognizer
    - (IBAction) viewDidTapped:(id)sender {
        //move the view to the tapped location
        [self moveViewTo:[sender locationInView: self.view]];
    }

    @end

Some explanation:

  • For UIViewAnimation, the property value is changed when the animation is completed. For CALayerAnimation, the property value is never change, it is just an animation and when the animation is completed, the layer will go to its original state (in this case, the path).

  • Putting self.lineShape.path = linePath.CGPath doesn't work is because self.linePath is a CALayer instead of a UIView, you have to use CALayerAnimation to animate a CALayer

  • To draw a path, it's better to animate the path drawing with keyPath strokeEnd instead of path. I'm not sure why path worked in the original post, but it seems weird to me.

  • CAKeyframeAnimation (instead of CABasicAnimation or UIViewAnimation) is used to animate the view along the path. (I guess you would prefer this to the linear animation directly from start point to end point). Setting calculationMode to kCAAnimationPaced will give a constant speed to the animation, otherwise the view moving will not sync with the line drawing.

Upvotes: 1

Related Questions