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!
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];
}
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.
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