How to inherit animation properties while animating CALayer with implicit animation - ios

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.

Related

Core Animation short hand causes odd behaviour [duplicate]

Here's some relevant code inside a UIView subclass:
- (void) doMyCoolAnimation {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.duration = 4;
[self.layer setValue:#200 forKeyPath:anim.keyPath];
[self.layer addAnimation:anim forKey:nil];
}
- (CGFloat) currentX {
CALayer* presLayer = self.layer.presentationLayer;
return presLayer.position.x;
}
When I use [self currentX] while the animation is running, I get 200 (the end value) rather than a value between 0 (the start value) and 200. And yes, the animation is visible to the user, so I'm really confused here.
Here's the code where I call doMyCoolAnimation:, as well as currentX after 1 second.
[self doMyCoolAnimation];
CGFloat delay = 1; // 1 second delay
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
NSLog(#"%f", [self currentX]);
});
Any ideas?
I don't know where the idea for using KVC setters in animation code came from, but that's what the animation itself is for. You're basically telling the layer tree to immediately update to the new position with this line:
[self.layer setValue:#200 forKeyPath:anim.keyPath];
Then wondering why the layer tree won't animate to that position with an animation that has no starting or ending values. There's nothing to animate! Set the animation's toValue and fromValue as appropriate and ditch the setter. Or, if you wish to use an implicit animation, keep the setter, but ditch the animation and set its duration by altering the layer's speed.
My UIView's layer's presentationLayer was not giving me the current values. It was instead giving me the end values of my animation.
To fix this, all I had to do was add...
anim.fromValue = [self.layer valueForKeyPath:#"position.x"];
...to my doMyCoolAnimation method BEFORE I set the end value with:
[self.layer setValue:#200 forKeyPath:#"position.x"];
So in the end, doMyCoolAnimation looks like this:
- (void) doMyCoolAnimation {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.duration = 4;
anim.fromValue = [self.layer valueForKeyPath:anim.keyPath];
[self.layer setValue:#200 forKeyPath:anim.keyPath];
[self.layer addAnimation:anim forKey:nil];
}
The way you are creating your animation is wrong, as CodaFi says.
Either use an explicit animation, using a CABasicAnimation, or use implicit animation by changing the layer's properties directly and NOT using a CAAnimation object. Don't mix the two.
When you create a CABasicAnimation object, you use setFromValue and/or setToValue on the animation. Then the animation object takes care of animating the property in the presentation layer.

UIView's Backing CALayer Does Not Reenable Implicit Animations In Block Anymore?

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?

How do I find details of the current UIView animation?

I have a setter method for a property on a custom UIView class. If it's set within a UIView animation block, I'd like it to add a CABasicAnimation to the view's layer with the same duration and easing as the UIView animation. How do I find out whether I'm inside a UIView animation block, and how do I get its duration and easing curve?
Very simple..
You can get all animation keys applied to your view using
[self.YourView.layer animationKeys];
Based on this question I made this extension to UIView block animations: UIView+AnimatedProperty.
It allows you to run CAAnimations when the setter is called from animation block. Example with cornerRadius is included.
You can get the current animation easily enough. For instance, setting up a CATransaction:
CAAnimation *animation = [self.layer animationForKey:self.layer.animationKeys.firstObject];
[CATransaction begin];
[CATransaction setAnimationDuration:animation.duration];
[CATransaction setAnimationTimingFunction:animation.timingFunction];
// CALayer animation here
[CATransaction commit];
The documentation for [UIView animateWithDuration] states that
This method performs the specified animations immediately using the UIViewAnimationOptionCurveEaseInOut and UIViewAnimationOptionTransitionNone animation options.
As for the duration, you set the duration yourself, so you already now that.

CAGradientLayer properties not animating within UIView animation block

I have a feeling I'm overlooking something elementary, but what better way to find it than to be wrong on the internet?
I have a fairly basic UI. The view for my UIViewController is a subclass whose +layerClass is CAGradientLayer. Depending on the user's actions, I need to move some UI elements around, and change the values of the background's gradient. The code looks something like this:
[UIView animateWithDuration:0.3 animations:^{
self.subview1.frame = CGRectMake(...);
self.subview2.frame = CGRectMake(...);
self.subview2.alpha = 0;
NSArray* newColors = [NSArray arrayWithObjects:
(id)firstColor.CGColor,
(id)secondColor.CGColor,
nil];
[(CAGradientLayer *)self.layer setColors:newColors];
}];
The issue is that the changes I make in this block to the subviews animate just fine (stuff moves and fades), but the change to the gradient's colors does not. It just swaps.
Now, the documentation does say that Core Animation code within an animation block won't inherit the block's properties (duration, easing, etc.). But is it the case that that doesn't define an animation transaction at all? (The implication of the docs seems to be that you'll get a default animation, where I get none.)
Do I have to use explicit CAAnimation to make this work? (And if so, why?)
There seem to be two things going on here. The first (as Travis correctly points out, and the documentation states) is that UIKit animations don't seem to hold any sway over the implicit animation applied to CALayer property changes. I think this is weird (UIKit must be using Core Animation), but it is what it is.
Here's a (possibly very dumb?) workaround for that problem:
NSTimeInterval duration = 2.0; // slow things down for ease of debugging
[UIView animateWithDuration:duration animations:^{
[CATransaction begin];
[CATransaction setAnimationDuration:duration];
// ... do stuff to things here ...
[CATransaction commit];
}];
The other key is that this gradient layer is my view's layer. That means that my view is the layer's delegate (where, if the gradient layer was just a sublayer, it wouldn't have a delegate). And the UIView implementation of -actionForLayer:forKey: returns NSNull for the "colors" event. (Probably every event that isn't on a specific list of UIView animations.)
Adding the following code to my view will cause the color change to be animated as expected:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
id<CAAction> action = [super actionForLayer:layer forKey:event];
if( [#"colors" isEqualToString:event]
&& (nil == action || (id)[NSNull null] == action) ) {
action = [CABasicAnimation animationWithKeyPath:event];
}
return action;
}
You have to use explicit CAAnimations, because you're changing the value of a CALayer.
UIViewAnimations work on UIView properties, but not directly on their CALayer's properties...
Actually, you should use a CABasicAnimation so that you can access its fromValue and toValue properties.
The following code should work for you:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[UIView animateWithDuration:2.0f
delay:0.0f
options:UIViewAnimationCurveEaseInOut
animations:^{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"colors"];
animation.duration = 2.0f;
animation.delegate = self;
animation.fromValue = ((CAGradientLayer *)self.layer).colors;
animation.toValue = [NSArray arrayWithObjects:(id)[UIColor blackColor].CGColor,(id)[UIColor whiteColor].CGColor,nil];
[self.layer addAnimation:animation forKey:#"animateColors"];
}
completion:nil];
}
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
NSString *keyPath = ((CAPropertyAnimation *)anim).keyPath;
if ([keyPath isEqualToString:#"colors"]) {
((CAGradientLayer *)self.layer).colors = ((CABasicAnimation *)anim).toValue;
}
}
There is a trick with CAAnimations in that you HAVE to explicitly set the value of the property AFTER you complete the animation.
You do this by setting the delegate, in this case I set it to the object which calls the animation, and then override its animationDidStop:finished: method to include the setting of the CAGradientLayer's colors to their final value.
You'll also have to do a bit of casting in the animationDidStop: method, to access the properties of the animation.

Animating UILabel with CoreAnimation / QuartzCore in iOS App

I actually stuck on a problem with animating a UILabel in my iOS Application.
After 2 days of searching the web for code snippets, still no result.
Every sample I found was about how to animate UIImage, adding it as a subview to UIView by layer. Is there any good example about animating a UILabel?
I found a nice solution for a blinking animation by setting the alpha property, like this:
My function:
- (void)blinkAnimation:(NSString *)animationID finished:(BOOL)finished target:(UIView *)target
{
NSString *selectedSpeed = [[NSUserDefaults standardUserDefaults] stringForKey:#"EffectSpeed"];
float speedFloat = (1.00 - [selectedSpeed floatValue]);
[UIView beginAnimations:animationID context:target];
[UIView setAnimationDuration:speedFloat];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(blinkAnimation:finished:target:)];
if([target alpha] == 1.0f)
[target setAlpha:0.0f];
else
[target setAlpha:1.0f];
[UIView commitAnimations];
}
Call my function on the UILabel:
[self blinkAnimation:#"blinkAnimation" finished:YES target:labelView];
But how about a Pulse, or scaling animation?
Unfortunately font size is not an animatable property of NSView. In order to scale a UILabel, you'll need to use more advanced Core Animation techniques, using CAKeyframeAnimation:
Import the QuartzCore.framework into your project, and #import <QuartzCore/QuartzCore.h> in your code.
Create a new CAKeyframeAnimation object that you can add your key frames to.
Create a CATransform3D value defining the scaling operation (don't get confused by the 3D part--you use this object to do any transformations on a layer).
Make the transformation one of the keyframes in the animation by adding it to the CAKeyframeAnimation object using its setValues method.
Set a duration for the animation by calling its setDuration method
Finally, add the animation to the label's layer using [[yourLabelObject layer] addAnimation:yourCAKeyframeAnimationObject forKey:#"anyArbitraryString"]
The final code could look something like this:
// Create the keyframe animation object
CAKeyframeAnimation *scaleAnimation =
[CAKeyframeAnimation animationWithKeyPath:#"transform"];
// Set the animation's delegate to self so that we can add callbacks if we want
scaleAnimation.delegate = self;
// Create the transform; we'll scale x and y by 1.5, leaving z alone
// since this is a 2D animation.
CATransform3D transform = CATransform3DMakeScale(1.5, 1.5, 1); // Scale in x and y
// Add the keyframes. Note we have to start and end with CATransformIdentity,
// so that the label starts from and returns to its non-transformed state.
[scaleAnimation setValues:[NSArray arrayWithObjects:
[NSValue valueWithCATransform3D:CATransform3DIdentity],
[NSValue valueWithCATransform3D:transform],
[NSValue valueWithCATransform3D:CATransform3DIdentity],
nil]];
// set the duration of the animation
[scaleAnimation setDuration: .5];
// animate your label layer = rock and roll!
[[self.label layer] addAnimation:scaleAnimation forKey:#"scaleText"];
I'll leave the repeating "pulse" animation as an exercise for you: hint, it involves the animationDidStop method!
One other note--the full list of CALayer animatable properties (of which "transform" is one) can be found here. Happy tweening!

Resources