Change duration (speed) on a running animation

2020-02-06 04:53发布

问题:

I am doing an indefinite rotation animation, which works perfectly well when I first start it. What I wanted to achieve was to be able to change the rate of rotation at the runtime. I have this function in animationView:

-(void)startBlobAnimation:(float)deltaT
{
    [UIView beginAnimations:@"Spinning" context:nil];
    [UIView setAnimationCurve:UIViewAnimationCurveLinear];
    [UIView setAnimationDuration:deltaT];
    [UIView setAnimationBeginsFromCurrentState:YES];
    [UIView setAnimationRepeatCount:FLT_MAX];

    CGAffineTransform rotation = CGAffineTransformMakeRotation(-symmetryAngle);
    blobView.transform = rotation;

    // Commit the changes and perform the animation.
    [UIView commitAnimations];
}

Calling it with different values of deltaT after the animation was first started doesn't have any effect. If I add [wheelView.layer removeAllAnimations]; at the start of the function then it successfully stops the animation but doesn't restart it. I also tried using the block command to start animation, with the same result. I am totally baffled at this point. Could somebody explain what the problem is? Thanks!

回答1:

After a long struggle I came up with a solution that seems to work perfectly and give a smooth transition between animations. Basically I just figure out the current angle of rotation and use it to restart the animation at a different rate. One critical point here is in the last line: you MUST have that anim.keyPath there - it can't be Nil (as learned from experience). That way the new animation replaces the old animation, I guess. Oh, and to make it more clear: symmetryAngle is a rotation that makes the object look the same, like 72 degrees for 5-fold symmetry.

-(void)startWheelsAnimation:(float)deltaT
{
    float startingAngle = 0.0;

    if(isAnimating) {
        // If animation is in progress then calculate startingAngle to
        // reflect the current angle of rotation
        CALayer *presLayer = (CALayer*)[blobView.layer presentationLayer];
        CATransform3D transform = [presLayer transform];
        startingAngle = atan2(transform.m12, transform.m11);
    }

    isAnimating = YES;

    // Restart the animation with different duration, and so that it starts
    // from the current angle of rotation
    CABasicAnimation * anim = [ CABasicAnimation animationWithKeyPath:@"transform.rotation.z" ] ;
    anim.duration = deltaT;
    anim.repeatCount = CGFLOAT_MAX;
    anim.fromValue = @(startingAngle);
    anim.toValue = @(startingAngle - symmetryAngle) ;
    [blobView.layer addAnimation:anim forKey:anim.keyPath];
}


回答2:

2017

Change the speed of a CA animation as it is running.

Here's exactly how I do the "Alex method" ...

I have a shape layer ...

@IBDesignable
class SpinningThing: UIView {

    public func runAnimation() { // call this from the view controller, say
        speedy = 1.0
        startFastThenSlowDown()
    }


    override func layoutSubviews() {

        super.layoutSubviews()
        setup()
    }

    func setup() {

        setup .. other stuff (draw things)
        setupThingThatSpins()
    }

    var thingThatSpins = CAShapeLayer()

    var speedy = 0.5        // start fast
    var finalSpeed = 13.0   // final slow speed

    func setupThingThatSpins() {

        let p = UIBezierPath( .. etc etc
        thingThatSpins.strokeColor .. etc etc
        // so actually draw the thingThatSpins
    }

Here's what happens in startFastThenSlowDown ...

func startFastThenSlowDown() {

    speedy *= 1.175
    animeSpinRestartable(spr: speedy)
    if speedy < finalSpeed {
        delay(0.1) { self._slowDown() }
    }
}

Now, here is a restartable animation thanks to Alex...

func animeSpinRestartable(spr: CFTimeInterval) {

    let _key = "animeSpinRestartable"

    thingThatSpins.removeAnimation(forKey: _key)
    // is safe the first time through

    // get the current angle...
    let t3 = arcs.presentation()?.transform
    let cur = (t3 == nil) ? 0 : atan2(t3!.m12, t3!.m11)

    let r = _typicalAnim(keyPath: "transform.rotation.z")
    r.fromValue = cur
    r.toValue = cur + 2 * CGFloat.pi
    r.duration = spr

    ///r.isCumulative = true ?  seems unnecessary, so don't do it

    thingThatSpins.add(r, forKey: _key)
}

In my case, the thingThatSpins was a mask (for goodness sake) which was on top of something else (a color gradient, which, in itself animated!)

Good one, OP.



回答3:

A couple issues--

  • I think for endless animations you will have to use the CoreAnimation API directly. (This is incorrect, you can use UIViewAnimationOptionRepeat)

  • Animations are copied to the layer they are applied to, so you can't change them after that (you can replace them however, but you might see a discontinuity)

  • You can set layer.speed to change the effective animation rate

Here's a working example. Create a new "empty" iOS project in Xcode and replace your AppDelegate like this:

@interface AppDelegate ()
@property ( nonatomic, strong ) UIView * animationView ;
@end

@implementation AppDelegate

-(void)buttonClicked
{
    self.animationView.layer.speed = 3.0 - self.animationView.layer.speed ;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];

    CGRect bounds = self.window.bounds ;
    {
        UIView * subview = [ [ UIView alloc ] initWithFrame:(CGRect){ .size = { 100, 100 } } ] ;
        [ self.window addSubview:subview ] ;

        subview.backgroundColor = [ UIColor orangeColor ] ;
        subview.center = (CGPoint){ CGRectGetMidX( bounds ), CGRectGetMidY( bounds ) } ;

        CABasicAnimation * anim = [ CABasicAnimation animationWithKeyPath:@"transform.rotation.z" ] ;
        anim.duration = 1.0 ;
        anim.repeatCount = CGFLOAT_MAX ;
        anim.fromValue = @0.0 ;
        anim.toValue = @(2.0*M_PI) ;

        [ subview.layer addAnimation:anim forKey:nil ] ;

        self.animationView = subview ;
    }

    UIControl * button = [[ UIControl alloc ] initWithFrame:bounds ] ;
    [ button addTarget:self action:@selector( buttonClicked ) forControlEvents:UIControlEventTouchUpInside ] ;

    [ self.window addSubview:button ] ;


    return YES;
}

@end