I'm new to Objective-C/iOS programming and I'm trying to understand how UIView animation works under the hood.
Say I have a code like this:
[UIView animateWithDuration:2.0 animations:^{
self.label.alpha = 1.0;
}];
The thing that gets passed as an animations argument is an Objective-C block (something like lambdas/anonymous functions in other languages) that can be executed and then it changes the alpha property of label from current value to 1.0.
However, the block does not accept an animation progress argument (say going from 0.0 to 1.0 or from 0 to 1000). My question is how the animation framework uses this block to know about intermediate frames, as the block only specifies the final state.
EDIT:
My questions is rather about under the hood operation of animateWithDuration method rather than the ways to use it.
My hypothesis of how animateWithDuration code works is as follows:
animateWithDuration activates some kind of special state for all view objects in which changes are not actually performed but only registered.
it executes the block and the changes are registered.
it queries the views objects for changed state and gets back the initial and target values and hence knows what properties to change and in what range to perform the changes.
it calculates the intermediate frames, based on the duration and initial/target values, and fires the animation.
Can somebody shed some light on whether animateWithDuration really works in such way?
Of course I don't know what exactly happens under the hood because UIKit isn't open-source and I don't work at Apple, but here are some ideas:
Before the block-based UIView animation methods were introduced, animating views looked like this, and those methods are actually still available:
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:duration];
myView.center = CGPointMake(300, 300);
[UIView commitAnimations];
Knowing this, we could implement our own block-based animation method like this:
+ (void)my_animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations
{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:duration];
animations();
[UIView commitAnimations];
}
...which would do exactly the same as the existing animateWithDuration:animations: method.
Taking the block out of the equation, it becomes clear that there has to be some sort of global animation state that UIView then uses to animate changes to its (animatable) properties when they're done within an animation block. This has to be some sort of stack, because you can have nested animation blocks.
The actual animation is performed by Core Animation, which works at the layer level – each UIView has a backing CALayer instance that is responsible for animations and compositing, while the view mostly just handles touch events and coordinate system conversions.
I won't go into detail here on how Core Animation works, you might want to read the Core Animation Programming Guide for that. Essentially, it's a system to animate changes in a layer tree, without explicitly calculating every keyframe (and it's actually fairly difficult to get intermediate values out of Core Animation, you usually just specify from and to values, durations, etc. and let the system take care of the details).
Because UIView is based on a CALayer, many of its properties are actually implemented in the underlying layer. For example, when you set or get view.center, that is the same as view.layer.location and changing either of these will also change the other.
Layers can be explicitly animated with CAAnimation (which is an abstract class that has a number of concrete implementations, like CABasicAnimation for simple things and CAKeyframeAnimation for more complex stuff).
So what might a UIView property setter do to accomplish "magically" animating changes within an animation block? Let's see if we can re-implement one of them, for simplicity's sake, let's use setCenter:.
First, here's a modified version of the my_animateWithDuration:animations: method from above that uses the global CATransaction, so that we can find out in our setCenter: method how long the animation is supposed to take:
- (void)my_animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations
{
[CATransaction begin];
[CATransaction setAnimationDuration:duration];
animations();
[CATransaction commit];
}
Note that we don't use beginAnimations:... and commitAnimations anymore, so without doing anything else, nothing will be animated.
Now, let's override setCenter: in a UIView subclass:
#interface MyView : UIView
#end
#implementation MyView
- (void)setCenter:(CGPoint)position
{
if ([CATransaction animationDuration] > 0) {
CALayer *layer = self.layer;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [layer valueForKey:#"position"];
animation.toValue = [NSValue valueWithCGPoint:position];
layer.position = position;
[layer addAnimation:animation forKey:#"position"];
}
}
#end
Here, we set up an explicit animation using Core Animation that animates the underlying layer's location property. The animation's duration will automatically be taken from the CATransaction. Let's try it out:
MyView *myView = [[MyView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
myView.backgroundColor = [UIColor redColor];
[self.view addSubview:myView];
[self my_animateWithDuration:4.0 animations:^{
NSLog(#"center before: %#", NSStringFromCGPoint(myView.center));
myView.center = CGPointMake(300, 300);
NSLog(#"center after : %#", NSStringFromCGPoint(myView.center));
}];
I'm not saying that this is exactly how the UIView animation system works, it's just to show how it could work in principle.
The values intermediate frames for are not specified; the animation of the values (alpha in this case, but also colours, position, etc) is generated automatically between the previously set value and the destination value set inside the animation block. You can affect the curve by specifying the options using animateWithDuration:delay:options:animations:completion: (the default is UIViewAnimationOptionCurveEaseInOut, i.e., the speed of the value change will accelerate and decelerate).
Note that any previously set animated changes of values will finish first, i.e., each animation block specifies a new animation from the previous end value to the new. You can specify UIViewAnimationOptionBeginFromCurrentState to start the new animation from the state of the already in-progress animation.
This will change the property specified inside the block from the current value to whatever value you provide, and will do it linearly over the duration.
So if the original alpha value was 0, this would fade in the label over 2 seconds. If the original values was already 1.0, you wouldn't see any effect at all.
Under the hood, UIView takes care of figuring out over how many animation frames the change needs to take place.
You can also change the rate at which the change takes place by specifying an easing curve as a UIViewAnimationOption. Again, UIView handles the tweening for you.
According to official documentation,
The UIView class disables layer animations by default but reenables them inside animation blocks.
backing layer's implicit animations should be reenables within the UIView's animation blocks. In fact, this official snippets
[UIView animateWithDuration:1.0 animations:^{
// Change the opacity implicitly.
myView.layer.opacity = 0.0;
// Change the position explicitly.
CABasicAnimation* theAnim = [CABasicAnimation animationWithKeyPath:#"position"];
theAnim.fromValue = [NSValue valueWithCGPoint:myView.layer.position];
theAnim.toValue = [NSValue valueWithCGPoint:myNewPosition];
theAnim.duration = 3.0;
[myView.layer addAnimation:theAnim forKey:#"AnimateFrame"];
}];
attempts to trigger implicit animations on the backing layer. Also, according the the answer from this question, this is the expected behavior. This blog's post also confirmed that
the view returns the NSNull object outside of the block and returns a CABasicAnimation inside of the block
However, this is currently not true. From my simple test, UIView's animation block does not reenable the implicit animations of back CALayer. In addition, actionForLayer:forKey: now return NSNull both inside and outside of the animation block. Does this means the official is out-dated and this is no longer the expected behavior?
I am trying to animate a custom property on a CALayer with an implicit animation:
[UIView animateWithDuration:2.0f animations:^{
self.imageView.myLayer.myProperty = 1;
}];
In -actionForKey: method I need to return the animation taking care of interpolating the values. Of course I have to tell somehow the animation how to retrieve the other parameters for the animation (i.e. the duration and the timing function).
- (id<CAAction>)actionForKey:(NSString *)event
{
if ([event isEqualToString:#"myProperty"])
{
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"myProperty"];
[anim setFromValue:#(self.myProperty)];
[anim setKeyPath:#"myProperty"];
return anim;
}
return [super actionForKey:event];
}
}
Any idea on how to achieve that? I tried looking for the animation in the layer properties but could not find anything interesting. I also have a problem with the layer animating since actionForKey: is called outside of animations.
I reckon that you have a custom layer with you custom property "myProperty" that you added to the backing layer of UIView - according to the Documentation UIView animation blocks does not support the animation of custom layer properties and states the need to use CoreAnimation:
Changing a view-owned layer is the same as changing the view itself,
and any animations you apply to the layer’s properties respect the
animation parameters of the current view-based animation block. The
same is not true for layers that you create yourself. Custom layer
objects ignore view-based animation block parameters and use the
default Core Animation parameters instead.
If you want to customize the animation parameters for layers you
create, you must use Core Animation directly.
Further the documentation sates that UIView supports just a limited set of animatable properties
which are:
frame
bounds
center
transform
alpha
backgroundColor
contentStretch
Views support a basic set of animations that cover many common tasks.
For example, you can animate changes to properties of views or use
transition animations to replace one set of views with another.
Table 4-1 lists the animatable properties—the properties that have
built-in animation support—of the UIView class.
https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/AnimatingViews/AnimatingViews.html#//apple_ref/doc/uid/TP40009503-CH6-SW12
You have to create a CABasicAnimation for that.
You can have sort of a workaround with CATransactions if you return a CABasicAnimation in actionForKey: like that
[UIView animateWithDuration:duration animations:^{
[CATransaction begin];
[CATransaction setAnimationDuration:duration];
customLayer.myProperty = 1000; //whatever your property takes
[CATransaction commit];
}];
Just change your actionForKey: method to something like that
- (id<CAAction>)actionForKey:(NSString *)event
{
if ([event isEqualToString:#"myProperty"])
{
return [CABasicAnimation animationWithKeyPath:event];
}
return [super actionForKey:event];
}
There is something in Github in case you wan't to have a look: https://github.com/iMartinKiss/UIView-AnimatedProperty
I don't think you can access the duration if you use :
[UIView animateWithDuration:duration animations:^{
}];
Other issue with your code is, the implementation of actionForKey: of UIView only returns a CAAnimation object if the code is called inside an animation block. Otherwise it returns null to turn off animation. In your implementation, you always return a CAAnimation, hence changing to that property will always be animated.
You should use this :
[CATransaction begin];
[CATransaction setAnimationDuration:duration];
[CATransaction setAnimationTimingFunction:timingFunction];
customLayer.myProperty = 1000; //whatever your property takes
[CATransaction commit];
Then in your actionForKey: method, use [CATransaction animationDuration] and [CATransaction animationTimingFunction] to retrieve the current duration and timing function.
The easiest way could be onother property in the your custom layer to set before myProperty. like:
self.imageView.myLayer.myTimingFunction = kCAMediaTimingFunctionEaseInEaseOut;
self.imageView.myLayer.myProperty = 1;
And
-(id<CAAction>)actionForKey:(NSString *)event
{
if ([event isEqualToString:#"myProperty"])
{
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"myProperty"];
[anim setFromValue:#(self.myProperty)];
[anim setKeyPath:#"myProperty"];
[anim setTimingFunction:myTimingFunction];
return anim;
}
return [super actionForKey:event];
}
}
If you want to get parameters, like duration, set in
[UIView animateWithDuration:2.0f ...
So in this case duration = 2.0f
you can use CATransaction and valueForKey. CATransaction should return the specific value of the context.
As apple document said: 'transform
Specifies the transform applied to the receiver, relative to the center of its bounds.
#property(nonatomic) CGAffineTransform transform
Discussion The origin
of the transform is the value of the center property, or the layer’s
anchorPoint property if it was changed. (Use the layer property to get
the underlying Core Animation layer object.) The default value is
CGAffineTransformIdentity.
Changes to this property can be animated. Use the
beginAnimations:context: class method to begin and the
commitAnimations class method to end an animation block. The default
is whatever the center value is (or anchor point if changed)'
I don't need the animation ,how to disable the animation when changing the transform property of UIView?
You can disable the implicit animations this way:
[CATransaction begin];
[CATransaction setDisableActions:YES];
// or if you prefer: [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
// Your code here for which to disable the implicit animations.
[CATransaction commit];
https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CATransaction_class/Introduction/Introduction.html
It (should) only animate when you change the transform property inside e.g. UIView animateWithDuration: block.
I.e. disabling animation can be achieved by simply not changing the transform property inside an animation part of your code.
Can you post some code where you get animations that you didn't expect?
I want to change the parent layer's sublayers dynamically when using AVVideoCompositionCoreAnimationTool. I noticed that sublayers is an animatable property accordingto 《Core Animation Programming Guide》, but still can't figure it out how to achieve that. Any idea? Thanks
I don't know about AVVideoCompositionCoreAnimationTool, but in general it works like following Code. It will show an animation when removing and adding at the new parent layer. The action identifiers if you'd like to change them are kCAOnOrderIn and kCAOnOrderOut.
CALayer *layerToMove = ....;
CALayer *newParent = ...;
[CATransaction begin];
[layerToMove removeFromSuperLayer];
[newParent addSublayer:layerToMove];
[CATransaction commit];