I have an image that I want to rotate 360° (clockwise) and then repeat and I'm having trouble hitting on the right way to do this. I can do this:
UIView.Animate(1, 0, UIViewAnimationOptions.Repeat,
() =>
{
LoadingImage.Transform = MonoTouch.CoreGraphics.CGAffineTransform.MakeRotation(-(float)(Math.PI));
},
() =>
{
});
Which rotates my image 180° and then snaps back to the start and repeats. So, I thought if I do something like 1.99 * PI, it would rotate almost all the way around and probably look ok. Unfortunately, the animation system is smart than that and will just rotate in the opposite direction instead!
So what's the right way to get an image to spin 360° continuously?
Update
With the help or Eric's comment, I've got to this:
CABasicAnimation rotationAnimation = new CABasicAnimation();
rotationAnimation.KeyPath = "transform.rotation.z";
rotationAnimation.To = new NSNumber(Math.PI * 2);
rotationAnimation.Duration = 1;
rotationAnimation.Cumulative = true;
rotationAnimation.RepeatCount = 10;
LoadingImage.Layer.AddAnimation(rotationAnimation, "rotationAnimation");
Which does spin 360°, 10 times in my example (see RepeatCount), but I don't see a way to tell it to repeat until I stop it. CABasicAnimation has a RepeatCount and a RepeatDuration, but doesn't seem to have a property to tell it to just keep repeating. I can set RepeatCount to some suitably high value that it'll keep spinning until the user has probably lost interest, but that doesn't seem like a great solution.
Looks like the solution adapted from the link Eric provided looks like the best choice:
CABasicAnimation rotationAnimation = new CABasicAnimation();
rotationAnimation.KeyPath = "transform.rotation.z";
rotationAnimation.To = new NSNumber(Math.PI * 2);
rotationAnimation.Duration = 1;
rotationAnimation.Cumulative = true;
rotationAnimation.RepeatCount = float.MaxValue;
LoadingImage.Layer.AddAnimation(rotationAnimation, "rotationAnimation");
Set RepeatCount to float.MaxValue to effectively keep it repeating forever (why a count is a float rather than an int remains a mystery).
Try animation.repeatCount = HUGE_VALF;
I think the reason repeat count is a float is to allow for partial cycles of an animation. Maybe you want a circle to animate by pulsing, but stop at the opposite end of where you started.
Related
I'm trying animate a UIView along a portion of a bezier path. I found a way to move the the view to any part of the path using this code:
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = trackPath.cgPath
animation.rotationMode = kCAAnimationRotateAuto
animation.speed = 0
animation.timeOffset = offset
animation.duration = 1
animation.calculationMode = kCAAnimationPaced
square.layer.add(animation, forKey: "animate position along path")
However, this just moves the view to the desired point and doesn't animate it. How do you animate a view along over a portion of a bezier path?
Thanks
You can accomplish this by modifying the timing of the "complete" animation and a animation group that wraps it.
To illustrate how the timing of such an animation works, imagine – instead of animating a position along a path – that a color is being animated. Then, the complete animation from one color to another can be illustrated as this, where a value further to the right is at a later time — so that the very left is the start value and the very right is the end value:
Note that the "timing" of this animation is linear. This is intentional since the "timing" of the end result will be configured later.
In this animation, imagine that we're looking to animate only the middle third, this part of the animation:
There are a few steps to animating only this part of the animation.
First, configure the "complete" animation to have linear timing (or in the case of an animation along a path; to have a paced calculation mode) and to have a the "complete" duration.
For example: if you're looking to animate a third of the animation an have that take 1 second, configure the complete animation to take 3 seconds.
let relativeStart = 1.0/3.0
let relativeEnd = 2.0/3.0
let duration = 1.0
let innerDuration = duration / (relativeEnd - relativeStart) // 3 seconds
// configure the "complete" animation
animation.duration = innerDuration
This means that the animation currently is illustrated like this (the full animation):
Next, so that the animation "starts" a third of the way into the full animation, we "offset" its time by a third of the duration:
animation.timeOffset = relativeStart * innerDuration
Now the animation is illustrated like this, offset and wrapping from its end value to its start value:
Next, so that we only display part of this animation, we create an animation group with the wanted duration and add only the "complete" animation to it.
let group = CAAnimationGroup()
group.animations = [animation]
group.duration = duration
Even though this group contains an animation that is 3 seconds long it will end after just 1 second, meaning that the 2 seconds of the offset "complete" animation will never be shown.
Now the group animation is illustrated like this, ending after a third of the "complete" animation:
If you look closely you'll see that this (the part that isn't faded out) is the middle third of the "complete" animation.
Now that this group animates animates between the wanted values, it (the group) can be configured further and then added to a layer. For example, if you wanted this "partial" animation to reverse, repeat, and have a timing curve you would configure these things on the group:
group.repeatCount = HUGE
group.autoreverses = true
group.timingFunction = CAMediaTimingFunction(name: "easeInEaseOut")
With this additional configuration, the animation group would be illustrated like this:
As a more concrete example, this is an animation that I created using this technique (in fact all the code is from that example) that moves a layer back and forth like a pendulum. In this case the "complete" animation was a "position" animation along a path that was a full circle
I've done a similar animation just a few days ago:
// I want the animation to go along the circular path of the oval shape
let flightAnimation = CAKeyframeAnimation(keyPath: "position")
flightAnimation.path = ovalShapeLayer.path
// I set this one to make the animation go smoothly along the path
flightAnimation.calculationMode = kCAAnimationPaced
flightAnimation.duration = 1.5
flightAnimation.rotationMode = kCAAnimationRotateAuto
airplaneLayer.add(flightAnimation, forKey: nil)
I see that you set speed to zero and a time offset. Why do you need them?
I would suggest to try the animation using just the parameters in the above code and then try to tune it from them.
I want to skip the first part of an animation and jump right to let's say 3/4 of the duration. For this purpose I build the following script in Dart.
Element target = getTarget(layer.element);
CoreAnimation animation = new CoreAnimation();
animation.target = target;
animation.duration = 2000;
animation.iterationStart = 0.75; // 1500ms
animation.keyframes = [
{'transform': 'translate(0px, 0px)'},
{'transform': 'translate(20px, 50px)'}
];
// I also tried to add the following attributes
animation.iterations = 1; // doesnt work
animation.iterationCount = 1; // doesnt work
animation.play();
The animation starts at the correct position, but once it has reached its last keyframe {'transform': 'translate(20px, 50px)'} it jumps back to the first keyframe and play the complete animation again. What for gods sake is happening here? Am I missing something?
Note: I already tried to set iteration count to 1.
Try to set animation.iterations to 1.
I have a very odd issue, trying to animate a triangle shape along a circular arc, from -π/2 to 2π-π/2 (to start from vertical position). Like this:
// Construct the invisible animation path
var path = UIBezierPath.FromArc(new PointF(rect.GetMidX(), rect.GetMidY()), (rect.Height - 20) / 2, (float)(- Math.PI / 2), (float)(2*Math.PI - Math.PI / 2), true);
var pathLayer = new CAShapeLayer();
pathLayer.Frame = rect;
pathLayer.Path = path.CGPath;
pathLayer.FillColor = UIColor.Clear.CGColor;
pathLayer.StrokeColor = UIColor.Clear.CGColor;
view.AddSublayer(pathLayer); // It appears that I need to create and add a layer to be able to animate along the path?
// Create the shape to animate
var triangleLayer = new CAShapeLayer();
var triangle = CreateTriangle();
triangleLayer.Frame = triangle.Bounds;
triangleLayer.Path = triangle.CGPath;
triangleLayer.StrokeColor = UIColor.Orange.CGColor;
triangleLayer.FillColor = UIColor.Red.CGColor;
var triangleAnim = CAKeyFrameAnimation.GetFromKeyPath("position");
triangleAnim.Path = path.CGPath;
triangleAnim.Duration = 60;
triangleAnim.RotationMode = CAKeyFrameAnimation.RotateModeAuto;
triangleAnim.RemovedOnCompletion = false;
triangleLayer.AddAnimation(triangleAnim, "progress");
view.AddSublayer(triangleLayer);
The full round should take 60 seconds, but it finishes in about 45 seconds, then does nothing for 15 seconds, and then completes the animation. So the animation does seem to take 60 seconds, but the visual animation is only 45 seconds. The odd thing is that if I change the arc from 0 to 2π, the timing is correct. What could cause this issue?
The code is C# with Xamarin.iOS, but Objective C/Swift code would be similar.
Edit:
I found out that it works if I create a UIBezierPath arc from 0 - 2π and then rotate it with -π/2. So I suspect that there is something fishy going on with Xamarin.iOS and UIBezierPath with negative angles.
I made a quick prototype app to test this. For simplicity, I used an image instead of a shape layer. Here is the outcome:
Animating from -M_PI_2 to 3 * M_PI_2 works as expected
There is no need to add the invisible layer (just use the path you created)
This leads me to believe that perhaps your problem is buried in Xamarin.iOS. There have been some quirks with the CGFloat type, so maybe if you could cast the arguments explicitly somehow this might help.
I want to access get the value of the transform scale at a point in time. Here is the animation creation :
CABasicAnimation *grow = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
grow.toValue = #1.5f;
grow.duration = 1;
grow.autoreverses = YES;
grow.repeatCount = HUGE_VALF;
[view.layer addAnimation:grow forKey:#"growAnimation"];
I'd like to get, for example when a user presses a button, the current size of the view.
Logging the frame or the bounds always returns constant values. Any help would be much appreciated !
In Core Animation if an animation is "in flight" on a layer, the layer has a second layer property known as presentationLayer. That layer contains the values of the in-flight animation.
Edited
(With a nod to Mohammed, who took the time to provide an alternate answer for Swift)
Use code like this:
Objective-C
CGFloat currentScale = [[layer.presentationLayer valueForKeyPath: #"transform.scale"] floatValue];
Swift:
let currentScale = layer.presentation()?.value(forKeyPath: "transform.scale") ?? 0.0
Note that the Swift code above will return 0 if the layer does not have a presentation layer. That's a fail case that you should program for. It might be better to leave it an optional and check for a nil return.
Edit #2:
Note that as Kentzo pointed out in their comment, reading a value from an in-flight animation will give you a snapshot at an instant in time. The new value of the parameter you read (transform.scale in this example) will start to deviate from the reading you get. You should either pause the animation, or use the value you read quickly and get another value each time you need it.
The CALayer documentation describes presentationLayer quite clearly:
The layer object returned by this method provides a close approximation of the layer that is currently being displayed onscreen. While an animation is in progress, you can retrieve this object and use it to get the current values for those animations.
swift version of #Duncan C answer will be:
let currentValue = someView.layer.presentation()?.value(forKeyPath: "transform.scale")
Swift 5: The nicest answer, in a function from Hacking With Swift
func scale(from transform: CGAffineTransform) -> Double {
return sqrt(Double(transform.a * transform.a + transform.c * transform.c))
}
As detailed in a previous post (Here), I'm creating an animation that starts at an initial angle and moves to an ending angle. I've decided to use CADisplayLink for this animation because the animation needs to run as fast as possible during user input, so a CALayer with a CAKeyframeAnimation seemed like it would be too slow to achieve this.
After implementing the CADisplayLink, which calls setNeedsDisplay, I do have the animation working, but it looks really bad because it chunks up the difference between endAngle and initialAngle into heavily visible blocks of angles instead of creating a continuous flow from one angle to the next. Here's the current code I have:
CGFloat newAngleToAnimate = animationProgress + ((endAngle-initialAngle)/kDrawDuration)*elapsedTime;
// Use newAngleToAnimate to draw in drawInContext
animationProgress = newAngleToAnimate; // Update the progress for the next frame.
Also, kDrawDuration is defined as 3.0f, so I want the animation from initialAngle to endAngle to take 3.0 seconds. I break up the full circle (2*M_PI radians) into equal segments by calculating 2*M_PI / kNumAnglesInAnimation, and preferably I want to animate one of those angles every frame, but somehow I still have to take kDrawDuration and elapsedTime into account, which I'm just not seeing how to achieve.
Thanks for any help with fixing this!
CGFloat newAngleToAnimate = animationProgress + ((endAngle-initialAngle)/kDrawDuration)*elapsedTime;
don't track "animationProgress". Your elapsedTime is all you need in order to get your animation correct. So just remove it and use:
CGFloat newAngleToAnimate = ((endAngle-initialAngle)/kDrawDuration)*elapsedTime;