Jumpy CADisplayLink Animation - ios

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;

Related

Observe UIView frame while animating [duplicate]

I want to observe changes to the x coordinate of my UIView's origin while it is being animated using animateWithDuration:delay:options:animations:completion:. I want to track changes in the x coordinate during this animation at a granular level because I want to make a change in interaction to another view that the view being animated may make contact with. I want to make that change at the exact point of contact. I want to understand the best way to do something like this at a higher level:
-- Should I use animateWithDuration:... in the completion call back at the point of contact? In other words, The first animation runs until it hits that x coordinate, and the rest of the animation takes place in the completion callback?
-- Should I use NSNotification observers and observe changes to the frame property? How accurate / granular is this? Can I track every change to x? Should I do this in a separate thread?
Any other suggestions would be welcome. I'm looking for a abest practice.
Use CADisplayLink since it is specifically built for this purpose. In the documentation, it says:
Once the display link is associated with a run loop, the selector on the target is called when the screen’s contents need to be updated.
For me I had a bar that fills up, and as it passed a certain mark, I had to change the colors of the view above that mark.
This is what I did:
let displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.frameInterval = 3
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
UIView.animateWithDuration(1.2, delay: 0.0, options: [.CurveEaseInOut], animations: {
self.viewGaugeGraph.frame.size.width = self.graphWidth
self.imageViewGraphCoin.center.x = self.graphWidth
}, completion: { (_) in
displayLink.invalidate()
})
func animationDidUpdate(displayLink: CADisplayLink) {
let presentationLayer = self.viewGaugeGraph.layer.presentationLayer() as! CALayer
let newWidth = presentationLayer.bounds.width
switch newWidth {
case 0 ..< width * 0.3:
break
case width * 0.3 ..< width * 0.6:
// Color first mark
break
case width * 0.6 ..< width * 0.9:
// Color second mark
break
case width * 0.9 ... width:
// Color third mark
break
default:
fatalError("Invalid value observed. \(newWidth) cannot be bigger than \(width).")
}
}
In the example, I set the frameInterval property to 3 since I didn't have to rigorously update. Default is 1 and it means it will fire for every frame, but it will take a toll on performance.
create a NSTimer with some delay and run particular selector after each time lapse. In that method check the frame of animating view and compare it with your colliding view.
And make sure you use presentationLayer frame because if you access view.frame while animating, it gives the destination frame which is constant through out the animation.
CGRect animationViewFrame= [[animationView.layer presentationLayer] frame];
If you don't want to create timer, write a selector which calls itself after some delay.Have delay around .01 seconds.
CLARIFICATION->
Lets say you have a view which you are animating its position from (0,0) to (100,100) with duration of 5secs. Assume you implemented KVO to the frame of this view
When you call the animateWithDuration block, then the position of the view changes directly to (100,100) which is final value even though the view moves with intermediate position values.
So, your KVO will be fired one time at the instant of start of animation.
Because, layers have layer Tree and Presentation Tree. While layer tree just stores destination values while presentation Layer stores intermediate values.
When you access view.frame it will always gives the value of frame in layer tree not the intermediate frames it takes.
So, you had to use presentation Layer frame to get intermediate frames.
Hope this helps.
UIDynamics and collision behaviours would be worth investigating here. You can set a delegate which is called when a collision occurs.
See the collision behaviour documentation for more details.

continus animation with google maps

I have this method:
private func setupCameraPositionForMovement(markerPos: CLLocationCoordinate2D, secondPos: CLLocationCoordinate2D) {
let bearing = bearingBetweenLatLngs(markerPos, end: secondPos)
let cameraPosition = GMSCameraPosition(target: markerPos, zoom: 16, bearing: bearing + 20, viewingAngle: 90)
map.animateToCameraPosition(cameraPosition)
}
I am using this method to animate movement from markerPos to secondPos, And this work fine when I call it once like:
CATransaction.begin()
CATransaction.setAnimationDuration(1)
setupCameraPositionForMovement(markers[selectedIndex - 1].position, secondPos: markers[selectedIndex].position)
CATransaction.commit()
Now, I want to call this animation multiple times to display a continues movement,
So I tried this code:
for i in 1..<markers.count {
CATransaction.begin()
CATransaction.setAnimationDuration(1)
setupCameraPositionForMovement(markers[i - 1].position, secondPos: markers[i].position)
CATransaction.commit()
}
But without any benefit.
When I searched for this problem I found something like CAAnimationGroup, But I did not know how to use it in this situation.
Can any one help me ?
CAAnimationGroup allows multiple animations to be grouped and run concurrently. The grouped animations run in the time space specified by the CAAnimationGroup instance.
The duration of the grouped animations are not scaled to the duration of their CAAnimationGroup. Instead, the animations are clipped to the duration of the animation group.
CAAnimationGroup has only one property: animations.
This property is intended to add an array of animations to a layer. Suppose, for example, you want to change the transparency of a layer while your are moving it on the screen. If you simply create the two animations and add them to a layer one by one, you loose the control of the synchronization and depending on what your app is doing, you could have some visible delay between the 2 animations.
For more information check this page.
You can also check this SO question for more information.

Ios Swift Animate a view in non linear path

I am trying to animate a UIView through non linear path(i'm not trying to draw the path itself) like this :
The initial position of the view is determinated using a trailing and bottom constraint (viewBottomConstraint.constant == 100 & viewTrailingConstraint.constant == 300)
I am using UIView.animatedWithDuration like this :
viewTrailingConstraint.constant = 20
viewBottomConstraint.constant = 450
UIView.animateWithDuration(1.5,animation:{
self.view.layoutIfNeeded()
},completition:nil)
But it animate the view in a linear path.
You can use keyFrame animation with path
let keyFrameAnimation = CAKeyframeAnimation(keyPath:"position")
let mutablePath = CGPathCreateMutable()
CGPathMoveToPoint(mutablePath, nil,50,200)
CGPathAddQuadCurveToPoint(mutablePath, nil,150,100, 250, 200)
keyFrameAnimation.path = mutablePath
keyFrameAnimation.duration = 2.0
keyFrameAnimation.fillMode = kCAFillModeForwards
keyFrameAnimation.removedOnCompletion = false
self.label.layer.addAnimation(keyFrameAnimation, forKey: "animation")
Gif
About this function
void CGContextAddQuadCurveToPoint (
CGContextRef _Nullable c,
CGFloat cpx,
CGFloat cpy,
CGFloat x,
CGFloat y
);
(cpx,cpy) is control point,and (x,y) is end point
Leo's answer of using Core Animation and CAKeyframeAnimation is good, but it operates on the view's "presentation layer" and only creates the appearance of moving the view to a new location. You'll need to add extra code to actually move the view to it's final location after the animation completes. Plus Core Animation is complex and confusing.
I'd recommend using the UIView method
animateKeyframesWithDuration:delay:options:animations:completion:. You'd probably want to use the option value UIViewKeyframeAnimationOptionCalculationModeCubic, which causes the object to move along a curved path that passes through all of your points.
You call that on your view, and then in the animation block, you make multiple calls to addKeyframeWithRelativeStartTime:relativeDuration:animations: that move your view to points along your curve.
I have a sample project on github that shows this and other techniques. It's called KeyframeViewAnimations (link)
Edit:
(Note that UIView animations like animateKeyframes(withDuration:delay:options:animations:completion:) don't actually animate your views along the path you specify. They use a presentation layer just like CALayer animations do, and while the presentation layer makes the view look like it's moving along the specified path, it actually snaps from the beginning position to the end position at the beginning of the animation. UIView animations do move the view to its destination position, where CALayer animations move the presentation layer while not moving the layer/view at all.)
Another subtle difference between Leo's path-based UIView animation and my answer using UIView animateKeyframes(withDuration:delay:options:animations:completion:)is that CGPath curves are cubic or quadratic Bezier curves, and my answer animates using a different kind of curve called a Katmull-Rom spline. Bezier paths start and end at their beginning and ending points, and are attracted to, but don't pass through their middle control points. Catmull-Rom splines generate a curve that passes through every one of their control points.

Observing change in frame of a UIView during animation

I want to observe changes to the x coordinate of my UIView's origin while it is being animated using animateWithDuration:delay:options:animations:completion:. I want to track changes in the x coordinate during this animation at a granular level because I want to make a change in interaction to another view that the view being animated may make contact with. I want to make that change at the exact point of contact. I want to understand the best way to do something like this at a higher level:
-- Should I use animateWithDuration:... in the completion call back at the point of contact? In other words, The first animation runs until it hits that x coordinate, and the rest of the animation takes place in the completion callback?
-- Should I use NSNotification observers and observe changes to the frame property? How accurate / granular is this? Can I track every change to x? Should I do this in a separate thread?
Any other suggestions would be welcome. I'm looking for a abest practice.
Use CADisplayLink since it is specifically built for this purpose. In the documentation, it says:
Once the display link is associated with a run loop, the selector on the target is called when the screen’s contents need to be updated.
For me I had a bar that fills up, and as it passed a certain mark, I had to change the colors of the view above that mark.
This is what I did:
let displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.frameInterval = 3
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
UIView.animateWithDuration(1.2, delay: 0.0, options: [.CurveEaseInOut], animations: {
self.viewGaugeGraph.frame.size.width = self.graphWidth
self.imageViewGraphCoin.center.x = self.graphWidth
}, completion: { (_) in
displayLink.invalidate()
})
func animationDidUpdate(displayLink: CADisplayLink) {
let presentationLayer = self.viewGaugeGraph.layer.presentationLayer() as! CALayer
let newWidth = presentationLayer.bounds.width
switch newWidth {
case 0 ..< width * 0.3:
break
case width * 0.3 ..< width * 0.6:
// Color first mark
break
case width * 0.6 ..< width * 0.9:
// Color second mark
break
case width * 0.9 ... width:
// Color third mark
break
default:
fatalError("Invalid value observed. \(newWidth) cannot be bigger than \(width).")
}
}
In the example, I set the frameInterval property to 3 since I didn't have to rigorously update. Default is 1 and it means it will fire for every frame, but it will take a toll on performance.
create a NSTimer with some delay and run particular selector after each time lapse. In that method check the frame of animating view and compare it with your colliding view.
And make sure you use presentationLayer frame because if you access view.frame while animating, it gives the destination frame which is constant through out the animation.
CGRect animationViewFrame= [[animationView.layer presentationLayer] frame];
If you don't want to create timer, write a selector which calls itself after some delay.Have delay around .01 seconds.
CLARIFICATION->
Lets say you have a view which you are animating its position from (0,0) to (100,100) with duration of 5secs. Assume you implemented KVO to the frame of this view
When you call the animateWithDuration block, then the position of the view changes directly to (100,100) which is final value even though the view moves with intermediate position values.
So, your KVO will be fired one time at the instant of start of animation.
Because, layers have layer Tree and Presentation Tree. While layer tree just stores destination values while presentation Layer stores intermediate values.
When you access view.frame it will always gives the value of frame in layer tree not the intermediate frames it takes.
So, you had to use presentation Layer frame to get intermediate frames.
Hope this helps.
UIDynamics and collision behaviours would be worth investigating here. You can set a delegate which is called when a collision occurs.
See the collision behaviour documentation for more details.

Rotating a Cocos2d sprite to match a joystick

I'm using a SneakyJoystick in Cocos2d, and I'm trying to get a sprite to rotate to face the same direction as the joystick is pointed (this is top down).
I can get it to rotate to face it, but it snaps into position as the rotation is updated every frame or so.
How can I make the sprite rotate smoothly towards the target angle, without jumping to it? I wasn't able to figure out how to do this with a CCRotateTo because the angle to rotate towards could change at any time.
I ended up fixing this simply by using a rotation method that I made, which rotates the node/sprite in the correct direction at the correct amount each update.
- (void)rotateNode:(CCNode*)aNode degrees:(float)targetRotation withSpeed:(float)rotationSpeed withTimeDelta:(ccTime)dt
{
rotationSpeed = rotationSpeed * dt;
// Convert the difference between the two angles to radians
float diff = (targetRotation - aNode.rotation) * (M_PI / 180);
// Find the rotation of the vector created by the sin and cos of the difference
float rotationDifference = atan2f(sinf(diff),cosf(diff));
// Rotate the clip accordingly
aNode.rotation += MAX(
MIN(
(180/M_PI) * rotationDifference,rotationSpeed), -rotationSpeed
);
}
Have you tried:
[CCRotateBy actionWithDuration:0.5f angle:CC_DEGREES_TO_RADIANS(90.0f)];
Obtain the Angle between the last Update the current Update, also if you wanted it so the character had a set time to turn around, you can scale your duration by the amount of the angle.

Resources