I'm wondering if the following is possible with CoreAnimation.
Like the title says, I'd like to animate or seek if you will, to a position in time over an entire timeline animation.
For example, say I create a CABasicAnimation which animates a CALayer's color from blue to red over an entire timeline of 10 seconds with an easing function of EaseOut.
Rather than playing that animation from start to finish, I'd prefer finer control.
For example, I'd like to say animateTo(50%) of the entire animation. This would result in an animation starting at 0% and animating to 50% of the entire animation with an ease out function. So the animation would last 5 seconds and we'd be in the middle of our color transform.
After this I could say, animateTo(20%). This would result in an animation starting from our current position of 50% and animating in reverse to 20% of the total animation. So we'd end up with a 3 second animation and we'd be 20% into our color transform from blue to red.
In the end we're just animating to a position in time over the entire timeline. I could easily say animateTo(5 seconds) rather than animateTo(50%). Or animateTo(2 seconds) rather than animateTo(20%).
I've been reading up on CoreAnimation and it appears I am un-able to get this kind of control. Has anyone else tried this with CoreAnimation?
Thanks!
The latest SDK adds a bit to UIKit animation.
It might suit your needs...
According to the documentation:
New object-based, fully interactive and interruptible animation support that lets you retain control of your animations and link them with gesture-based interactions
The new shiny UIViewPropertyAnimator:
let animator = UIViewPropertyAnimator(duration: 3.0, curve: .easeInOut) {
self.myView.alpha = 0
}
animator.startAnimation()
animator.pauseAnimation()
animator.isReversed = true
animator.fractionComplete = 1.5 / CGFloat(animator.duration)
However there are some limitations:
The PropertyAnimator requires targeting iOS 10 (which is still in beta as at the time of writing).
Also the animations are limited to UIView Animatable Properties, which are:
frame
bounds
center
transform
alpha
backgroundColor
contentStretch
If you seek more flexibility and features take a look at some open source libraries like TRX and Advance:
import TRX
let tween = Tween(from: 0, to: 1, time:3, ease:Ease.Quad.easeOut) {
anyObject.anyProperty = $0
}
let reversed = tween.reversed()
reversed.seek(1.5)
reversed.start()
As already pointed out, there is no built-in function. But if you want to create your own solution, here are a few notes and useful functions that are already there.
The CABasicAnimation conforms to the `CAMediaTiming´ protocol and contains some neat functionality for your use case:
timeOffset - this value determines at what time into the animation you actually want to start, if you have a animation with a duration of 2 seconds and timeOffset set to 1, it will cause the animation to start in the middle.
speed - setting that value to -1 would cause the animation to reverse (e.g. for the case of from 50% to 20%)
duration - might be changed if you want to change the duration of the animation for shorter changes e.g. form 40% to 50%.
timingFunction - I was hoping it might be possible to provide a custom timing function here, which for example just completes the animation after 1 second even if the duration is set to be 5 seconds, meaning: a linear function between 0 and 1 and after that just constant. It may however be possible to achieve an approximation of that with a bezierPath as well.
Related
I am working on iOS application, there is a requirement that I have to make a wheel, with this wheel, app will get wheel image from server, then user is able to tap a button to rotate it.
Input is wheel image and specific angle, I have to stop wheel at that angle!
Beside of that, the wheel should have velocity while rotating.
In case you have any ideas or solution, please share to me.
Thanks a lot!
To create this view it is simplest to have a square UIView which clips subviews (is a property on UIView) and has a corner radius (property on a layer view.layer.cornerRadius) set to half the view width (or height). On this view add an image view with the image provided.
To rotate the view you can simply use transform like view.transform = CGAffineTransform(rotationAngle: angleInRadians). This property is animatable so you can use UIView.animate... methods to animate the rotation. For this case all you need to do is is set the transform inside the animation block to the target angle and view will rotate correctly and will animate. You may also add some options for instance the curve could be ease-in-out which will make wheel start slowly, then accelerate, then slow down and stop.
If your case is that your wheel may make a few circles before stopping at a certain angle then the UIView animations may be a bit inappropriate. In that case I would create a repeating timer with interval of 1.0/60.0 so it will trigger 60FPS. When your wheel starts you would save a start date:
startDate = Date()
and end date:
endDate = startDate.addingTimeInterval(expectedDuration)
You also need to save the start angle:
startAngle = currentAngle
and end angle like:
endAngle = CGFloat(Int(currentAngle/(CGFloat.pi*2))*(CGFloat.pi*2)) + // This will roughly return how many circles have currently pass
CGFloat.pi*(numberOfFullCircles*2) + // Add full circles
angleToStopAtInRadians // Add the destination angle
Now whenever the timer triggers you need to update the transform of the view to the target rotation. You need to find where in the animation you currently are depending on the current time:
let absoluteScale = endDate.timeIntervalSince(Date())/endDate.timeIntervalSince(startDate)
let scale = CGFloat(max(0.0, min(absoluteScale, 1.0)))
Then we need to use linear interpolation:
currentAngle = startAngle + (endAngle-startAngle)*scale // Use this in the rotation matrix
To use non linear (ease out for instance) it is best to modify the scale. For ease out you may for instance use scale = pow(scale, 1.0/1.3). You may play around with these values as you wish or find/create some more interesting algorithms.
Then the last thing that you need is to stop the timer when the animation has ended which is already written in absoluteScale: if absoluteScale >= 1.0 then the animation is complete and you should invalidate the timer and trigger whatever needs to execute after it.
Have fun with this and when you find yourself in trouble please post a new question with some details and code.
I'm creating an interactive animation on macOS (but iOS also applicable) by setting the speed to 0 and adjusting the timeOffset between 0 and 1 during user interaction.
This works fine, but I'd like the start position to be in the middle - so I have the timeOffset set initially to 0.5. The problem is that now the timeOffset range is set to 0.5 to 1.5 for some reason. Is this a bug in CoreAnimation?
Core Animation should be consistent between macOS and iOS, but I can only speak to iOS.
timeOffset is not a normalized value. That is, it's not always from 0 to 1. It's just a literal time offset from the beginTime of that animation, which is 0 by default. Depending the duration of your animation, the fill mode, and the repeat count (or repeat duration), this is expected.
The behavior I would expect, in your example, assuming the animation duration is 1.0 seconds, would be for it to start at the halfway point at timeOffset 0.5, continue until the end of the animation at timeOffset 1.0, snap back to the start of the animation, and continue up until just before the middle of the animation at timeOffset 1.5.
So yes, this is expected behavior, and it's not likely to be a bug. (As a side note, I have encountered no actual bugs in Core Animation over my 7 years of using it. It's very solid.)
I'm currently creating a small button with two different states. Every state is represented by two CAShapeLayers and the transition between the states is animated with a series of CAKeyframeAnimations/CABasicAnimations, which all change the path property of the shape layers. My question is how do I animate to a state starting from a current animation (i.e. button is pressed while animating).
Normally, I would ask the presentation layer for the current property value and use an additive animation (as perfectly described here), but since this is a multi-step animation I would have to figure out which step I am currently animating and then chain the appropriate animation to reverse to the previous state. But that is rather tricky (I wanted to have all the animation be removeOnCompletion=false and query the time offset of the animations to figure out which animation is currently active and how far).
Unfortunately, setting the layer speed to 0.0 and just animating the timeOffset back and forth doesn't work either, since I have secondary animations which behave differently in the opposite direction.
Using a custom layer property for the shape layers seems cumbersome, but I could match a progress property more easily to the current animation step.
So after some trial and error I come up with the following "solution": I use a CADisplayLink to get a time update for every frame and by subtracting the current timestamp from the timestamp when the animation started, I can figure out the elapsed animation time, thus the progress (by dividing through animationDuration). That way I can start the reverse animation with an offset. Internally, I calculate the next frame from the keyframe animation and adjust the keyTimes for the new duration.
You can see an implementation of this here
I'm currently implementing a CABasicAnimation that animates a CALayer transform property.
Now, although I'm new to Core Animation, I have been able to gather through various blogs and articles such as objc.io that it is a very bad idea to use the often (incorrectly) recommended method for getting animations to stick using the fillMode and removedOnCompletion properties of an animation. This method is considered bad practice by many because it creates a discrepancy between the model layer and the presentation layer, and so future queries to one of those layers may not match up with what the user is seeing.
Instead, the recommended way of doing animations is to update the model layer at the same time you add the animation to the layer being animated. However, I'm having trouble understanding exactly how this is working. My animation is simple, and goes like this:
CATransform3D updatedTransform = [self newTransformWithCurrentTransform];
// Location 1
CABasicAnimation *transformAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
transformAnimation.duration = 1;
transformAnimation.fromValue = [NSValue valueWithCATransform3D:self.layerBeingAnimated.transform]; // Does not work without this.
transformAnimation.toValue = [NSValue valueWithCATransform3D:updatedTransform];
// Location 2
[self.layerBeingAnimated addAnimation:transformAnimation forKey:kTransformAnimationKey];
// Location 3
I've denoted three locations where I have attempted to update the model layer using the code
self.layerBeingAnimated.transform = updatedTransform;
In location 1, the layer jumps right to newTransform and does not animate.
In location 2, the layer animates exactly as I want it to from the current transform to newTransform.
In location 3, the layer jumps right to newTransform, jumps back to the the old transform, animates correctly from the fromValue to newTransform, and then stays at newTransform.
What's the deal here? What's the correct location to update the model layer and why are these three locations producing such different results?
Thanks!
I think that it's easiest to explain what is happening for each of the three locations and then a "conclusion" at the end.
I'm also adding some illustrations, showing exactly the behaviour that you are mentioning in your question so that it will be easier to follow for someone who hasn't tried these three things themselves. I'm also extending the illustration to show both a stand alone layer and a backing layer (one that is attached to a view) and I will explain the difference where there is one.
Location 1
In the first location, the model value is updated before the animation is created. Once this is done, the transform property holds the updatedTransform. This means that when you read the transform from the layer for the fromValue, you get the updatedValue back. This in turn means that both to and from value are the same so you can't see the animation.
One thing that could have made this location work as expected is to read the oldValue before assigning the new value and then use that as the fromValue. This will look as expected.
// Location 1
CATransform3D oldValue = layer.transform; // read the old value first
layer.transform = updatedTransform; // then update to the new value
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform"];
anim.duration = 1.0;
anim.fromValue = [NSValue valueWithCATransform3D:oldValue];
anim.toValue = [NSValue valueWithCATransform3D:updatedTransform];
Location 2
In the second example, the value isn't yet updated when you read the transform for the from value, so the fromValue and toValue are different. After that, the model value is updated to it's final value. There is actually a difference between the stand-alone layer and the backing layer here, but we don't see it. The transform property on CALayer is animatable and will automatically perform an "implicit" animation when the value changes. This means that an animation will be added to the layer for the "transform" key path. The view, however, disables this behaviour when the change happens outside of an animation block, so there is not implicit animation there.
The reason why we don't see the implicit animation is that the "explicit" animation is added afterwards for the same key path. This means that the only explicit animation will be visible, in both cases, even thought there are two animation running on the stand-alone layer (more on that later). If you are feeling cautious, then you could disable the implicit action for the stand-alone layer (more on that later).
Location 3
This leaves us with the last location. In this case the animation is created just as above, with different fromValue and toValue. The only difference is the order of adding the explicit animation and changing the property which triggers an implicit animation. In this case the implicit animation is added after the explicit animation and they both run(!). Both animations actually ran for location 2, but we couldn't see it because the explicit (longer) animation was added before.
Since everything is moving so fast, I slowed down the entire layer to try and illustrate what is happening when two animations are running at once. This way it becomes much easier to see what happens when the implicit animation ends. I've overlaid the well behaving backing layer and the misbehaving stand-alone layer and made them both 50% transparent. The dashed outline is the original frame.
A short description of what is happening: the blue view get's only the explicit animation added to it (which has a 1 second duration). The orange layer first has the same explicit animation added to it and then has an implicit 0.25 second animation added to it. Neither explicit nor the implicit animations are "additive", meaning their toValue and fromValue are used as-is.
Disclaimer: I do not work at Apple and I haven't seen the source code for Core Animation so what I'm about to say is guesswork based on how things behave.
In my understanding (see disclaimer) this is what happens for every screen refresh to produce the animation: for the current time stamp, the layer goes through the animations in the order they were added and updates the presentation values. In this case, the explicit animation sets a rotation transform, then the implicit animation comes and sets another rotation transform that completely overrides the explicit transform.
If an animation is configured to be "additive", it will add to the presentation values instead of overwriting (which is super powerful). Even with additive animations, order still matters. A non-additive animation could come later and overwrite the whole thing.
Since the implicit animation is shorter than the explicit one, we see that for the first part of the total animation, the values are strictly coming from the implicit animation (which was added last). Once the implicit animation finishes, the only remaining animation is the explicit animation which has been running underneath the implicit one, all this time. So when the implicit animation finishes, the explicit animation has already progressed 0.25 seconds and we see that the orange layer jumps back to the same value as the blue view, instead of jumping back to the beginning.
Where should we update the value?
At this point, the question is, how can we prevent two animations from being added and where should we update the value? The location where the value is updated doesn't prevent there from being two animations (but it can affect how the end result looks).
To prevent two actions from being added to the stand-alone layer, we temporarily disable all "actions" (a more general term for an animation):
[CATransaction begin];
[CATransaction setDisableActions:YES]; // actions are disabled for now
layer.transform = updatedTransform;
[CATransaction commit]; // until here
When we do this, only one animation is added to the layer so either location 2 or 3 works. That is simply a matter of taste. If you read the oldValue, the you can also use location 1 (as long as the action is disabled).
If you are animating a backing layer then you don't have to disable actions (the view does that for you) but it also doesn't hurt to do so.
At this point I could keep going about other ways to configure an animation, what an additive animation is, and why you needed to specify both the toValue and fromValue in this case. But I think that I have answered the question you asked and that this answer already is a bit on the long side.
I am trying out different looks of a little game I am writing to play with animation on iOS.
My goal is this to have a grid of tiles which based on gameplay changes the display of each tile to one of a set of images. I'd like each tile (up to 24x24) to flip around when its face changes. As the game progresses, more and more tiles need to be flipped at the same time. In the first implementation where I tried to flip them simultaneously the animation got very jerky.
I changed my approach to not flip them all at once, but just a few at a time, by scheduling the animation for each tile with a slightly increasing delay per tile, so that when say the 10th tile starts animating, the first one is already done. It takes little while longer for the whole process to finish, but also leads to a nice visual ripple-effect.
However, one problem remains: At the beginning of a game move, when the player picks a new color, it takes a few fractions of a second on the device, before the animation starts. This gets worse as the game progresses and more flips need to be scheduled per move, up to the point where the animation seems to hang and then completes almost instantly without any of the frames in between being actually discernible.
This is the code (in my UIView game grid subclass) that triggers the flipping of relevant tiles. (I removed an optimization that skips tiles, because it only matters in the early stages of the game).
float delay = 0.0f;
for (NSUInteger row=0; row<numRows; row++) {
for (NSUInteger col=0; col<numCols; col++) {
delay += 0.03f;
[self updateFlippingImageAtRow:row col:col delay:delay animated:YES];
}
}
The game grid view has an NSArray of tile subviews which are addressed using the row and col variables in the loop above.
updateFlippingImageAtRow:col:delay:animated is a method in my FlippingImageView (also a subclass of UIView) boils down to this (game logic omitted):
-(void)animateToShow:(UIImage*)image
duration:(NSTimeInterval)time
delay:(float)delay
completion:(void (^)(BOOL finished))completion
{
[UIView animateWithDuration:time
delay:delay
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft
forView:self
cache:YES];
self.frontImage = image;
}
completion:completion
];
}
Works fine, however, I conclude from the Instruments measuring which tells me that my time is spent in the animation block, that as the game goes on and the number of tiles to flip goes up, that the number of animations that get scheduled at the very beginning of the operation is a problem, and that the system then tries to catch up by dropping frames.
Any suggestions how to improve the performance of this? The exact timing of the animation is not really important.
You can think about doing this with CoreAnimation and CALayers instead of UIViews. It is incredebly powerful and optimized framework.
It's not an easy thing, you'll have to recode at least some of your classes (view hierarchy and hit tests are the first things that come to my mind), but it's worth a try and it's rather painless process, because CALayer is very similar to UIView.
Next step is OpenGL. It definitely can operate several hundreds of objects in realtime, but it requires much more work to do.
You might want to try using CoreAnimation instead. One way to do the flip animation would be:
Create a CABasicAnimation that animates the first half of the flip (rotation along the y axis).
In the delegate method animationDidStop:finished: you set the new image and then create a new animation that animates the second half.
You simply apply the animation to the layer property of your view.
Note that you can use setValue:forKey: to "annotate" an animation (remember what object the animation is about). When you add an animation to a layer it gets copied, not retained, so you can't identify it by simply comparing pointer values.