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.
Related
I'm implementing a custom pull-to-refresh component.
Create CALayer with a spinner animation, than add this layer to UICollectionView. Layer's speed is zero (self.loaderLayer.speed = 0.0f;) and layer animation is managed with timeOffset in scrollViewDidScroll:. Problem goes here, because I also want to show a loader always in the center of pulling space, so I change layer's position in scrollViewDidScroll: like this:
[self.loaderLayer setPosition:CGPointMake(CGRectGetMidX(scrollView.bounds), scrollView.contentOffset.y / 2)];
But nothing happens with layer position (calling [self.loaderLayer setNeedsDisplay] doesn't help). I understand that it's because zero speed. And currently I found the way which works (but I don't like that):
self.loaderLayer.speed = 1.0f;
[self.loaderLayer setPosition:CGPointMake(CGRectGetMidX(scrollView.bounds), scrollView.contentOffset.y / 2)];
self.loaderLayer.speed = 0.0f;
How could I change a position for a paused layer right? What am I missing?
All regards to #David for the reference. I just summarize it as an answer.
CoreAnimation works with two kinds of animations (transactions): explicit and implicit. When you see Animatable word in property documentation, it means that each time you set this property, CoreAnimation will animate this property changes implicitly with system default duration (default is 1/4 second). Under hood CALayer has actions for these properties and calling -actionForKey returns such action (implicit animation).
So in my case, when I change a layer position, CoreAnimation implicitly try animating this changes. Because layer is paused (speed is zero) and animation has default duration, we don't see this changes visually.
And answer is to disable implicit animations (disable calling layer -actionForKey). To do that we call [CATransaction setDisableActions:YES].
OR
We can mark this animation as immediate (by setting it's duration to zero) with calling [CATransaction setAnimationDuration:0.0];.
These flags/changes are per thread based and work for all transactions in specific thread until next run loop. So if we want to apply them for a concrete transaction, we wrap code with [CATransaction begin]; ... [CATransaction commit]; section.
In my case final code looks
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self.loaderLayer setPosition:CGPointMake(CGRectGetMidX(scrollView.bounds), scrollView.contentOffset.y / 2)];
self.loaderLayer.transform = CATransform3DMakeScale(scaleFactor, scaleFactor, 1);
[CATransaction commit];
And it works perfectly!
I am trying to take a UIImageView and hide it gradually from bottom to top.
What is the best and most efficient way to do it in Objective-C?
I am looking into CABasicAnimation Class and Animation Class.
If you want know CoreAnimation deeply, I think you can use this:
CABasicAnimation *animationA=[CABasicAnimation animation];
animationA.keyPath=#"position.y";
animationA.fromValue=#0;
animationA.toValue=#250;
animationA.duration=3;
animationA.timingFunction=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[yourTestView.layer addAnimation:animationA forKey:#"basic"];
yourTestView.layer.position=CGPointMake(250, 50);
or if you want to only support IOS7, you can try learn UIAttachmentBehavior and UIDynamicItemBehavior to achieve more interactive animation.
All animations like this use Core Animation and can be done really simply using the block based animations.
For this you would do something like...
[UIView animateWithDuration:3
animations:^{
imageView.frame = CGRectMake(//the end frame of the image view
}];
This will then animate the change over the duration given. (3 seconds in this case).
There are other versions that you can find in the docs of UIView that give you more options like changing the animation curve or running some code on completion etc...
NOTE
This assumes you are not using Auto Layout. If you are using auto layout then the method is exactly the same but you need to change the constraints and then run [view layoutIfNeeded]; in the animations block.
In Core Animation Programming guide, there is one paragraph about How to Animate Layer-Backed Views, it says:
If you want to use Core Animation classes to initiate animations, you must issue all of your Core Animation calls from inside a view-based animation block. The UIView class disables layer animations by default but reenables them inside animation blocks. So any changes you make outside of an animation block are not animated.
There are also an example:
[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"];
}];
In my opinion, it tells that if I don't issue Core Animation calls from inside a view-based animation block, there will no animation.
But it seems that if I add the core animation calls directly without view-based animation block, it works the same.
Have I missed something ?
tl;dr: The documentation only refers to implicit animations. Explicit animations work fine outside of animation blocks.
My paraphrasing of the documentation
The simplified version of that quote from the docs is something like (me paraphrasing it):
UIView have disabled implicit animations except for within animation blocks. If you want to do implicit layer animations you must do them inside an animation block.
What is implicit animations and how do they work?
Implicit animations is what happens when an animatable property of a standalone layer changes. For example, if you create a layer and change it's position it's going to animate to the new position. Many, many layer properties have this behaviour by default.
It happens something like this:
a transaction is started by the system (without us doing anything)
the value of a property is changed
the layer looks for the action for that property
at some point the transaction is committed (without us doing anything)
the action that was found is applied
Notice that there is no mention of animation above, instead there is the word "action". An action in this context refers to an object which implements the CAAction protocol. It's most likely going to be some CAAnimation subclass (like CABasicAnimation, CAKeyframeAnimation or CATransition) but is built to work with anything that conforms to that protocol.
How does it know what "action" to take?
Finding the action for that property happens by calling actionForKey: on the layer. The default implementation of this looks for an action in this order:
This search happens in this order (ref: actionForKey: documentation)
If the layer has a delegate and that delegate implements the Accessing the Layer’s Filters method, the layer calls that method. The delegate must do one of the following:
Return the action object for the given key.
Return nil if it does not handle the action.
Return the NSNull object if it does not handle the action and the search should be terminated.
The layer looks in the layer’s actions dictionary.
The layer looks in the style dictionary for an actions dictionary that contains the key.
The layer calls its defaultActionForKey: method to look for any class-defined actions.
The layer looks for any implicit actions defined by Core Animation.
What is UIView doing?
In the case of layers that are backing views, the view can enable or disable the actions by implementing the delegate method actionForLayer:forKey. For normal cases (outside an animation block) the view disables the implicit animations by returning [NSNull null] which means:
it does not handle the action and the search should be terminated.
However, inside the animation block, the view returns a real action. This can easily be verified by manually invoking actionForLayer:forKey: inside and outside the animation block. It could also have returned nil which would cause the layer to keep looking for an action, eventually ending up with the implicit actions (if any) if it wouldn't find anything before that.
When an action is found and the transaction is committed the action is added to the layer using the regular addAnimation:forKey: mechanism. This can easily be verified by creating a custom layer subclass and logging inside -actionForKey: and -addAnimation:forKey: and then a custom view subclass where you override +layerClass and return the custom layer class. You will see that the stand alone layer instance logs both methods for a regular property change but the backing layer does not add the animation, except when within a animation block.
Why this long explanation of implicit animations?
Now, why did I give this very long explanation of how implicit animations work? Well, it's to show that they use the same methods that you use yourself with explicit animations. Knowing how they work, we can understand what it means when the documentation say: "The UIView class disables layer animations by default but reenables them inside animation blocks".
The reason why explicit animations aren't disabled by what UIView does, is that you are doing all the work yourself: changing the property value, then calling addAnimation:forKey:.
The results in code:
Outside of animation block:
myView.backgroundColor = [UIColor redColor]; // will not animate :(
myLayer.backGroundColor = [[UIColor redColor] CGColor]; // animates :)
myView.layer.backGroundColor = [[UIColor redColor] CGColor]; // will not animate :(
[myView.layer addAnimation:myAnimation forKey:#"myKey"]; // animates :)
Inside of animation block:
myView.backgroundColor = [UIColor redColor]; // animates :)
myLayer.backGroundColor = [[UIColor redColor] CGColor]; // animates :)
myView.layer.backGroundColor = [[UIColor redColor] CGColor]; // animates :)
[myView.layer addAnimation:myAnimation forKey:#"myKey"]; // animates :)
You can see above that explicit animations and implicit animations on standalone layers animate both outside and inside of animation blocks but implicit animations of layer-backed views does only animate inside the animation block.
I will explain it with a simple demonstration (example).
Add below code in your view controller.(don't forget to import QuartzCore)
#implementation ViewController{
UIView *view;
CALayer *layer;
}
- (void)viewDidLoad
{
[super viewDidLoad];
view =[[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[self.view addSubview:view];
view.backgroundColor =[UIColor greenColor];
layer = [CALayer layer];
layer.frame =CGRectMake(0, 0, 50, 50);
layer.backgroundColor =[UIColor redColor].CGColor;
[view.layer addSublayer:layer];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
layer.frame =CGRectMake(70, 70, 50, 50);
}
See that in toucesBegan method there is no UIVIew animate block.
When you run the application and click on the screen, the opacity of the layer animates.This is because by default these are animatable.By default all the properties of a layer are animatable.
Consider now the case of layer backed views.
Change the code to below
- (void)viewDidLoad
{
[super viewDidLoad];
view =[[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[self.view addSubview:view];
view.backgroundColor =[UIColor greenColor];
// layer = [CALayer layer];
// layer.frame =CGRectMake(0, 0, 50, 50);
// layer.backgroundColor =[UIColor redColor].CGColor;
// [view.layer addSublayer:layer];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
view.layer.opacity =0.0;
// layer.frame =CGRectMake(70, 70, 50, 50);
}
You might think that this will also animate its view.But it wont happen.Because layer backed views by default are not animatable.
To make those animations happen, you have to explicitly embed the code in UIView animate block.As shown below,
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[UIView animateWithDuration:2.0 animations:^{
view.layer.opacity =0.0;
}];
// layer.frame =CGRectMake(70, 70, 50, 50);
}
That explains that,
The UIView class (which obviously was layer backed) disables layer animations by default but reenables them inside animation blocks.So any changes you make outside of an animation block are not animated.
Well, I've never used explicit Core Animation inside a view animation block. The documentation it seems to be not clear at all.
Probably the meaning is something like that, it's just a guess:
If you want to animate properties of the view that are linked to the
backed layer you should wrap them into a view animation block. In the
view's layer if you try to change the layer opacity this is not
animated, but if wrap into a view animation block it is.
In your snippet you are directly creating a basic animation thus explicitly creating an animation. Probably the doc just want to point out the differences between views and layers. In the latter animations on most properties are implicit.
You can see the difference if you write something like that:
[UIView animateWithDuration:1.0 animations:^{
// Change the opacity implicitly.
myView.layer.opacity = 0.0;
}];
This will be animated.
myView.layer.opacity = 0.0;
This will not be animated.
// 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"];
This will be animated.
I'm trying to make an exact "translation" of this UIView block-based animation code:
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
someView.frame = CGRect(0, 100, 200, 200);
}
completion:nil];
using CABasicAnimation instead.
I'm totally aware that the frame property is actually a combination of position, bounds and anchorPoint of the underlying layer, as it is described here: http://developer.apple.com/library/mac/#qa/qa1620/_index.html
... and I already made a solution like that, using two CABasicAnimations one setting the position, one for bounds and it works for that one view.
The problem is however that I have subviews inside my view. someView has a subview of type UIScrollView in which I place still another subview of type UIImageView. UIScrollView subview has autoresizingMask set to UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight. That all works perfectly if I use the UIView block-based version, however when I try using CABasicAnimations the subviews start behaving unexpectedly(i.e. get resized to incorrect widths). So it seems autoresizingMask is not working correctly when using CABasicAnimations. I noticed also that subviews don't receive a call to setFrame:, although the frame property of the parent view does change after changes to layer position and bounds are made.
That's why I would like to know what would be the correct code to replicate with CABasicAnimation that what is happening when one uses UIView's animateWithDuration method.
I'm totally aware that the frame property is actually a combination of position, bounds and anchorPoint of the underlying layer
Good, but it's important also to be aware that frame is not an animatable property for layers. If you want to animate with CABasicAnimation you must use properties that are animatable for layers. The CALayer documentation marks every such property as explicitly "animatable". The idea of using bounds and position is correct.
Thus, this code does essentially what you were doing before:
[CATransaction setDisableActions:YES];
// set final bounds and position
v.layer.bounds = CGRectMake(0,0,200,200);
v.layer.position = CGPointMake(100,200);
// cause those changes to be animated
CABasicAnimation* a1 = [CABasicAnimation animationWithKeyPath:#"bounds"];
a1.duration = 0.5;
CABasicAnimation* a2 = [CABasicAnimation animationWithKeyPath:#"position"];
a2.duration = 0.5;
[v.layer addAnimation:a1 forKey:nil];
[v.layer addAnimation:a2 forKey:nil];
However, that code has no effect on the size of any sublayers of v.layer. (A subview of v is drawn by a sublayer of v.layer.) That, unfortunately, is the problem you are trying to solve. I believe that by dropping down to the level of layers and direct explicit core animation, you have given up autoresizing, which happens at the view level. Thus you will need to animate the sublayers as well. That is what view animation was doing for you.
This is an unfortunate feature of iOS. Mac OS X has layer constraints (CAConstraint) that do at the layer level what autoresizing does at the view level (and more). But iOS is missing that feature.
I'm trying to find a way to detect if a view is being animated.
Case in point: I have applied a shadow on the layer of a view, specifying a shadowPath for performance. When the view is resized, the shadow should animate along. I can observe the frame of the view, and change the shadowPath of the layer accordingly. But while the view is resizing, the shadow jumps ahead since the change is not animated.
I know how to animate the shadowPath using a CABasicAnimation, but I need to know the properties of an ongoing animation so that I can apply them to my animation as well (mostly: duration, easing).
This is in a framework-type component, so I just cannot assume I know the duration and easing properties on beforehand.
Is there a way to detect a starting/running animation when observing the frame?
You can retrieve all animations attached to particular view's layer knowing it's key by calling
[yourView.layer animationForKey:#"key"]
to get all keys there is some animation for, call
NSArray* keys = [yourView.layer animationKeys];
I think the best practice should be....
UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.7];
[UIView setAnimationBeginsFromCurrentState:YES];
.....your code
// Set animation did stop selector before committing the animations
[UIView setAnimationDidStopSelector:#selector(animationFinished:)];
[UIView commitAnimations];