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!
Related
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.
If I apply transformation for a view, then movement of the view on iPad Air happens with lags. It looks like implicit animations in CALayer.
I've created test project. It should be executed on iPad simulator.
This is code that I use for apply transformations:
CGAffineTransform transform = CGAffineTransformIdentity;
if (_transformSwitch.on)
{
transform = CGAffineTransformRotate(CGAffineTransformMakeScale(2.0, 2.0), M_PI / 3.0);
}
self.frameView.transform = transform;
This is code that I use for apply movement:
CGPoint transition = [_panRecognizer translationInView:self.view];
CGPoint newCenter = CGPointMake(_startCenter.x + transition.x, _startCenter.y + transition.y);
self.frameView.center = newCenter;
How I can move the view with applied transformations and avoid the animations?
UPD
I've found a solution for a moving by timer invocation, but if I'm moving frame by a finger, I've have problem with the animation.
I set a center of the view with wrapping it with [CATransaction begin] [CATransaction commit]:
[CATransaction begin];
[CATransaction setValue:#(YES) forKey:kCATransactionDisableActions];
_destinationIndicatorView.center = center;
self.frameView.center = center;
[CATransaction commit];
I've found strange solution, but I want to know why is it a worked solution?
I added to this method setNeedsDisplay, and it's solved my problem. (If I add any view over my screen with drawRect method, and call setNeedsDisplay on it view, it's also solved my problem):
[CATransaction begin];
[CATransaction setValue:#(YES) forKey:kCATransactionDisableActions];
_destinationIndicatorView.center = center;
[self.frameView setNeedsDisplay];
self.frameView.center = center;
[CATransaction commit];
I've updated my project.
UPD2
This lag happens only on iPad with retina display (for example iPad Air). I've created small video to illustrate the problem: https://yadi.sk/i/rAneCEFmjjEej
I think you could wrap the changes in this code:
[CATransaction begin];
[CATransaction disableActions];
// Your changes to the UI elements...
[CATransaction commit];
Alright, I've played around with your project and found 2 things that do not what you expected they would do:
The 1st one is drawRect: on FrameView:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
}
From Apple:
The default implementation of this method does nothing. Subclasses
that use technologies such as Core Graphics and UIKit to draw their
view’s content should override this method and implement their drawing
code there. You do not need to override this method if your view sets
its content in other ways. For example, you do not need to override
this method if your view just displays a background color or if your
view sets its content directly using the underlying layer object.
The 2nd one is the wrong (it's right according to the doc, see below) return value of the overridden - (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event on FrameView: you return nil (as described in the CALayerDelegate reference) while CoreAnimation, in fact, expects an instance of NSNull (as might be assumed when reading CALayer class reference). Your actions led to an unintended result as there's a collision in the documentation. I'm talking about this and this.
P.S. As a result, I made it working without using CATransaction. Unfortunately, I don't have a retina iPad by hand, so please check out my changes to your GitHub project (I will create a pull request containing all my changes) and tell if it resolves both the unintended animation (assuming resolved) problem and the lag of dragging (need to check for performance issues on a real iPad).
[self setTransform:CGAffineTransformIdentity];
[self setFrame:CGRectMake(o.x, o.y, width, self.frame.size.height)];
if(width != 0)
{
self.layer.anchorPoint=CGPointMake((wsCollectionView_CellSize/2)/width, 0.5);
}
[self setTransform:CGAffineTransformMakeRotation(angle)];
Im using this code to change the look of my view, however when this code is executed i can see little blinking near the anchor point, like its being adjusted, i think this is because when i set new frame, then new anchor point is set and it is being redrawn.
So im guessing what i need is to execute this at the same time or those properties must be set simultaneously. What is the way to achieve that?
or may be there is a way to set anchor in points and it will be constant?
The anchorPoint of a layer is an animatable property. This means, if you just set it, then the layer will animate to the new value by default. This animation is likely to generate the flickering you are seeing as the anchor point fights against the transforms you are making.
To prevent this, you need to make the update inside a CATransaction with actions disabled:
[CATransaction begin];
[CATransaction setDisableActions:YES];
layer.anchorPoint = ...
[CATransaction commit];
This will immediately update the anchor point of your layer.
Normally, updating the anchor point also updates the frame of the view as well, so you'd normally want to set the frame after you've set the anchor point! unless this is already taken into account in the code above.
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?
In my prototype app there are around 100 CALayers at different but fixed positions with the same small image as the content. The only thing necessary now is to toggle the hidden property repeatedly and very quickly.
This works, but it's noticably slower than with my previous approach using UIImage's drawAtPoint: method in drawRect.
I want a strobe-like look, no transitions. That's why I'm using hidden and not opacity, but still, it kind of looks like there's a fade and that tells me it's slow.
With the drawAtPoint:-approach it looked good, but it was heavy on the CPU.
Fo this reason I rewrote it using CALayer and now I'm puzzled why it is that slow.
Can you give me advice how to investigate this?
With Instruments I didn't gain any insight. It tells me it's rendering at 59-60 FPS but visibly it's much slower.
It seems like there's a delay between the (touch) events and the hiding or showing of the layers taking effect.
That's how I'm initializing the layers:
layers[i] = [CALayer layer];
layers[i].frame = frameForLayer(i);
layers[i].contents = (__bridge id)image;
[layers[i] setContentsScale:scale];
layers[i].hidden = YES;
[[self layer] addSublayer:layers[i]];
All that in the awakeFromNib in my main view.
Later, only the hidden properties are modified, the rest stays.
EDIT:
Instead of just the someLayer.hidden = NO, I'm now writing
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
someLayer.hidden = NO;
[CATransaction commit];
try doing the above code in a CATransaction block and set the animation duration like so:
[CATransaction setValue:[NSNumber numberWithInt:0] forKey:kCATransactionAnimationDuration];
you may also need to do disable transitions like so:
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
I believe CALayers have a default 'animations' for when you set their contents.