0
votes

Suppose I want to move a layer from point A to B then to C:

CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position.x"];
animation.values = @[pointA, pointB, pointC];
animation.duration = 1;

After adding the animation to the layer, I set its speed to 0:

animation.speed = 0;

So I can use a slider to adjust the layer's position:

layer.timeOffset = slider.value;

But if my layer's current position is at pointB, after adding the animation it moves back to pointA even I set the layer's timeOffset to 0.5.

Is there anything I missed?

Thanks!

Update: To better illustrate the problem, here's some test code:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    self.testLayer = [CALayer layer];
    self.testLayer.frame = CGRectMake(100, 100, 30, 30);
    self.testLayer.backgroundColor = [UIColor redColor].CGColor;

    [self.view.layer addSublayer:self.testLayer];
}

- (IBAction)action:(id)sender
{
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position.y"];

    animation.values = @[@50, @100, @150];
    animation.duration = 3.0f;

    [self.testLayer addAnimation:animation forKey:@"animation"];
    self.testLayer.speed = 0;
}

The testLayer's Y position is 100, while the animation added has key values 50, 100 and 150, when the animation's added, testLayer would be moved to the first key value position which is 50, but how to prevent this? I tried to set self.testLayer.timeOffset = 0.5 but it doesn't help.

3
give my answer a try. Just fixed some typos so it should be good to goagibson007

3 Answers

0
votes

There's some mystical functionality behind the veil, after some experiment myself, seems like the immediate call of the timeOffset setter after the animation is acting differently to those calls made after some time: (This is in Swift, but the concept is the same)

layer.add(animation, forKey: "...")
layer.timeOffset = CFTimeInterval(0.5)

This somehow set a "Global" offset to all of the animations under the layer, if you call the timeOffset setter this way, all animations start to work after 0.5s instead.

/* The animation won't do anything when slider value is 0 - 0.5,
 * even if the function call is exactly the same. It would perform
 * the animation at time offset 0 - 0.5 when the slider value is 
 * 0.6 - 1.0.
 */
layer.timeOffset = CFTimeInterval(slider.value)

To achieve what you want, you could do the task this way:

// Note: do not set timeOffset on the layer.
animation.timeOffset = CFTimeInterval(0.5)
layer.add(animation, forKey: "...")

However, since your point A, B and C did not necessarily form a close path, after the slider value (which is therefore timeOffset) exceed 0.5, your layer would instantly move back to A, and the layer would simply disappear if the slider value is less than 0, so you have to do:

// set the initial slider value at 0.5
slider.value = 0.5

// match the slider value to actual animation time offset
layer.timeOffset = CFTimeInterval(slider.value > 0.5 ? slider.value - 0.5 : slider.value + 0.5)

This makes the animation performed correctly as your need, but there got to be some better way and I'm also curious about why the timeOffset setter acted differently, hope there's somebody excel at CAAnimation will be able to explain.

0
votes

Honestly this was one of the toughest questions. Admittedly, the timing of CALayers and CAAnimation in general is pretty complex. Usually it is good to update model values when adding the animation but you want it to be done by a percentage/slider so this was a challenge I completely understood. Cytus's answers is really pretty far along in that there seemed to be a bug when setting layer.timeOffset when adding a new animation. See When is layer.timeOffset Available. Setting the timeOffset on the animation itself worked except the values were removed when the slider was at the max value. It also works perfectly if you add the animation in viewDidAppear and only update the timeOffset in the slider action method. I started logging the layers time values and noticed some crazy stuff with beginTime. The short answer is when you add the new animation you have to set the beginTime(Especially in the case) to the CACurrentMediaTime() and it all works perfectly. Strange, as usually I use this for delays but something about replacing the current animation over and over creates a need for this property that I always thought was optional. I would say that after you are finished you should remove the animation to keep the presentation layer from being polluted but that is up to you. Here is a working example that holds the animations in place without the glitching and reverting to model values.

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (nonatomic,strong) CALayer *testLayer;
@property (nonatomic,strong) UILabel *testLabel;

@end

And the ViewController.m

 #import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    CGRect frame = CGRectMake(0.0, 0.0, 200.0, 20.0);
    UISlider *slider = [[UISlider alloc] initWithFrame:frame];
    [slider addTarget:self action:@selector(sliderAction:) forControlEvents:UIControlEventValueChanged];
    [slider setBackgroundColor:[UIColor clearColor]];
    slider.minimumValue = 0.0;
    slider.maximumValue =  1;
    slider.continuous = YES;
    slider.value = 0.0;
    slider.center = CGPointMake(self.view.center.x, self.view.bounds.size.height - 30);
    [self.view addSubview:slider];

    self.testLabel = [[UILabel alloc]initWithFrame:CGRectMake(40, 30, self.view.bounds.size.width - 80, 40)];
    self.testLabel.text = @"0";
    [self.testLabel setTextColor:[UIColor blackColor]];
    [self.view addSubview:self.testLabel];
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    self.testLayer = [CALayer layer];
    self.testLayer.frame = CGRectMake(100, 100, 30, 30);
    self.testLayer.backgroundColor = [UIColor redColor].CGColor;

    [self.view.layer addSublayer:self.testLayer];
}

-(void)sliderAction:(id)sender
{
    self.testLayer.speed = 0;
    UISlider *slider = (UISlider*)sender;
    float value = slider.value * 3.0; //3 is duration so we need to normalize slider
    NSLog(@"The value is %f",value);
    self.testLabel.text = [NSString stringWithFormat:@"%f",value];

    [_testLayer removeAllAnimations];
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position.y"];
    //   //get the real position and add it to the keyframe
    CGFloat positionY = self.testLayer.position.y;
    //I did make A the current position to make it smooth
    animation.values = @[@(positionY), @100, @250];
    animation.duration = 3.0;
    animation.fillMode = kCAFillModeForwards;
    //this line fixes readding the animation over and over with a timeOffset
    animation.beginTime = CACurrentMediaTime();
    [animation setRemovedOnCompletion:NO];


    [self.testLayer addAnimation:animation forKey:@"animation"];

    //updating the offset on the layer and not the animation
    self.testLayer.timeOffset = value + CACurrentMediaTime();


}

@end

Cheers

-1
votes

Try setting layer's position to destination after addAnimation

[layer addAnimation:animation forKey:@"basic"];

layer.position = CGPointMake(x, y);