how to reset/restart an animation and have it appear continuous?

1.7k views Asked by At

So, I am fairly new to iOS programming, and have inherited a project from a former coworker. We are building an app that contains a gauge UI. When data comes in, we want to smoothly rotate our "layer" (which is a needle image) from the current angle to a new target angle. Here is what we have, which worked well with slow data:

-(void) MoveNeedleToAngle:(float) target
{
   static float old_Value = 0.0;
   CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:@"transform.rotation");
   [rotateCurrentPressureTick setDelegate:self];
   rotateCurrentPressureTick.fromValue = [NSSNumber numberWithFloat:old_value/57.2958];
   rotateCurrentPressureTick.removedOnCompletion=NO;
   rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
   rotateCurrentPressureTick.toValue=[NSSNumber numberWithFloat:target/57.2958];
   rotateCurrentPressureTick.duration=3; // constant 3 second sweep

   [imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:@"rotateTick"];
   old_Value = target;
}

The problem is we have a new data scheme in which new data can come in (and the above method called) faster, before the animation is complete. What's happening I think is that the animation is restarted from the old target to the new target, which makes it very jumpy.

So I was wondering how to modify the above function to add a continuous/restartable behavior, as follows:

  1. Check if the current animation is in progress and
  2. If so, figure out where the current animation angle is, and then
  3. Cancel the current and start a new animation from the current rotation to the new target rotation

Is it possible to build that behavior into the above function?

Thanks. Sorry if the question seems uninformed, I have studied and understand the above objects/methods, but am not an expert.

2

There are 2 answers

1
foundry On

Yes you can do this using your existing method, if you add this bit of magic:

    - (void)removeAnimationsFromView:(UIView*)view {
        CALayer *layer = view.layer.presentationLayer;
        CGAffineTransform transform = layer.affineTransform;
        [layer removeAllAnimations];
        view.transform = transform;
    }

The presentation layer encapsulates the actual state of the animation. The view itself doesn't carry the animation state properties, basically when you set an animation end state, the view acquires that state as soon as you trigger the animation. It is the presentation layer that you 'see' during the animation.

This method captures the state of the presentation layer at the exact moment you cancel the animation, and then applies that state to the view.

Now you can use this method in your animation method, which will look something like this:

-(void) MoveNeedleToAngle:(float) target{
    [self removeAnimationsFromView:imageView_Needle];
    id rotation = [imageView_Needle valueForKeyPath:@"layer.transform.rotation.z"];
    CGFloat old_value = [rotation floatValue]*57.2958;

   // static float old_value = 0.0;
    CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    [rotateCurrentPressureTick setDelegate:self];
    rotateCurrentPressureTick.fromValue = [NSNumber numberWithFloat:old_value/57.2958];
    rotateCurrentPressureTick.removedOnCompletion=NO;
    rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
    rotateCurrentPressureTick.toValue=[NSNumber numberWithFloat:target/57.2958];
    rotateCurrentPressureTick.duration=3; // constant 3 second sweep

    [imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:@"rotateTick"];

    old_value = target;

}

(I have made minimal changes to your method: there are a few coding style changes i would also make, but they are not relevant to your problem)

By the way, I suggest you feed your method in radians, not degrees, that will mean you can remove those 57.2958 constants.

0
stefos On

You can get the current rotation from presentation layer and just set the toValue angle. No need to keep old_value

-(void) MoveNeedleToAngle:(float) targetRadians{ 

    CABasicAnimation *animation =[CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    animation.duration=5.0;
    animation.fillMode=kCAFillModeForwards;
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    animation.removedOnCompletion=NO;

    animation.fromValue = [NSNumber numberWithFloat: [[layer.presentationLayer valueForKeyPath: @"transform.rotation"] floatValue]];
    animation.toValue = [NSNumber numberWithFloat:targetRadians];
   // layer.transform= CATransform3DMakeRotation(rads, 0, 0, 1);
   [layer addAnimation:animation forKey:@"rotate"];
}

Another way i found (commented line above) is instead of using fromValue and toValue just set the layer transform. This will produce the same animation but the presentationLayer and the model will be in sync.