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.
Related
I have two animations which are overlapping, which, because of the way I've set my method up, causes the second one not to fire. I have a check like so in the beginning of the method:
- (void)animateHidden:(BOOL)hidden duration:(CGFloat)seconds delay:(CGFloat)delay options:(UIViewAnimationOptions)options disableUserInteraction:(BOOL)disableUserInteraction {
if (self.hidden == hidden) {
return;
}
Then, further down, my animation block looks like so:
__weak UIView *weakSelf = self;
[UIView animateWithDuration:seconds delay:delay options:options animations:^{
weakSelf.alpha = hidden ? 0 : 1;
} completion:^(BOOL finished) {
// Return user interaction to previous state
if (disableUserInteraction) {
weakSelf.userInteractionEnabled = userInteractionEnabled;
}
weakSelf.hidden = hidden;
}];
Two animations are kicked off on the same view, one before a service call and one after. If the service call happens quick enough that the view is still animating, weakSelf.hidden = hidden; will never be called, and the second animation will exit out since the hidden value wasn't updated in time.
Is there anyway that I could force the completion block on the animation block to be called? I need to update my hidden property before making the check, but can't find a way to accomplish this.
Calling [self.layer removeAllAnimations] doesn't seem to work unfortunately.
You can use CABasicAnimation instead of UIView animation, that will solve the problem more accurately.
You can use it like:
CABasicAnimation* opacityZero= [CABasicAnimation animationWithKeyPath:#"opacity"];
[opacityZero setToValue:[NSNumber numberWithFloat:0.0]];
[opacityZero setDuration:duration];
[[self layer] addAnimation:opacityZero forKey:#"opacityZero"];
And when your service call ends, you can call [self.layer removeAllAnimations];
Similarly, you can make the opacity one and tweak the above method as you like.
You can find more info here.
A __block prefixed to hidden attribute declaration should help.
Something like, #property (nonatomic) __block BOOL hidden;
If you are targeting iOS 10+ take a look at UIViewPropertyAnimator
https://developer.apple.com/reference/uikit/uiviewpropertyanimator?language=objc
Combined with the UIViewAnimating and UIViewImplicitlyAnimating protocols, this allows modification / interruption / pause / resume / stop / etc of the animations.
Basic example (buttons and view set in IB):
- (IBAction)startTapped:(id)sender {
_myAnimator = [UIViewPropertyAnimator
runningPropertyAnimatorWithDuration:3.0
delay:0.0
options:UIViewAnimationOptionCurveLinear
animations:^{
_theRedBox.alpha = _theRedBox.alpha > 0 ? 0 : 1;
} completion:^(UIViewAnimatingPosition finalPosition) {
// do stuff
}];
}
- (IBAction)stopTapped:(id)sender {
[_myAnimator stopAnimation:NO];
[_myAnimator finishAnimationAtPosition:UIViewAnimatingPositionEnd];
}
I have a CAShapeLayer (which is the layer of a UIView subclass) whose path should update whenever the view's bounds size changes. For this, I have overridden the layer's setBounds: method to reset the path:
#interface CustomShapeLayer : CAShapeLayer
#end
#implementation CustomShapeLayer
- (void)setBounds:(CGRect)bounds
{
[super setBounds:bounds];
self.path = [[self shapeForBounds:bounds] CGPath];
}
This works fine until I animate the bounds change. I would like to have the path animate alongside any animated bounds change (in a UIView animation block) so that the shape layer always adopts to its size.
Since the path property does not animate by default, I came up with this:
Override the layer's addAnimation:forKey: method. In it, figure out if a bounds animation is being added to the layer. If so, create a second explicit animation for animating the path alongside the bounds by copying all the properties from the bounds animation that gets passed to the method. In code:
- (void)addAnimation:(CAAnimation *)animation forKey:(NSString *)key
{
[super addAnimation:animation forKey:key];
if ([animation isKindOfClass:[CABasicAnimation class]]) {
CABasicAnimation *basicAnimation = (CABasicAnimation *)animation;
if ([basicAnimation.keyPath isEqualToString:#"bounds.size"]) {
CABasicAnimation *pathAnimation = [basicAnimation copy];
pathAnimation.keyPath = #"path";
// The path property has not been updated to the new value yet
pathAnimation.fromValue = (id)self.path;
// Compute the new value for path
pathAnimation.toValue = (id)[[self shapeForBounds:self.bounds] CGPath];
[self addAnimation:pathAnimation forKey:#"path"];
}
}
}
I got this idea from reading David Rönnqvist's View-Layer Synergy article. (This code is for iOS 8. On iOS 7, it seems that you have to check the animation's keyPath against #"bounds" and not `#"bounds.size".)
The calling code that triggers the view's animated bounds change would look like this:
[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:0.3 initialSpringVelocity:0.0 options:0 animations:^{
self.customShapeView.bounds = newBounds;
} completion:nil];
Questions
This mostly works, but I am having two problems with it:
Occasionally, I get a crash on (EXC_BAD_ACCESS) in CGPathApply() when triggering this animation while a previous animation is still in progress. I am not sure whether this has anything to do with my particular implementation. Edit: never mind. I forgot to convert UIBezierPath to CGPathRef. Thanks #antonioviero!
When using a standard UIView spring animation, the animation of the view's bounds and the layer's path is slightly out of sync. That is, the path animation also performs in a springy way, but it does not follow the view's bounds exactly.
More generally, is this the best approach? It seems that having a shape layer whose path is dependent on its bounds size and which should animate in sync with any bounds changes is something that should just work™ but I'm having a hard time. I feel there must be a better way.
Other things I have tried
Override the layer's actionForKey: or the view's actionForLayer:forKey: in order to return a custom animation object for the path property. I think this would be the preferred way but I did not find a way to get at the transaction properties that should be set by the enclosing animation block. Even if called from inside an animation block, [CATransaction animationDuration] etc. always return the default values.
Is there a way to (a) determine that you are currently inside an animation block, and (b) to get the animation properties (duration, animation curve etc.) that have been set in that block?
Project
Here's the animation: The orange triangle is the path of the shape layer. The black outline is the frame of the view hosting the shape layer.
Have a look at the sample project on GitHub. (This project is for iOS 8 and requires Xcode 6 to run, sorry.)
Update
Jose Luis Piedrahita pointed me to this article by Nick Lockwood, which suggests the following approach:
Override the view's actionForLayer:forKey: or the layer's actionForKey: method and check if the key passed to this method is the one you want to animate (#"path").
If so, calling super on one of the layer's "normal" animatable properties (such as #"bounds") will implicitly tell you if you are inside an animation block. If the view (the layer's delegate) returns a valid object (and not nil or NSNull), we are.
Set the parameters (duration, timing function, etc.) for the path animation from the animation returned from [super actionForKey:] and return the path animation.
This does indeed work great under iOS 7.x. However, under iOS 8, the object returned from actionForLayer:forKey: is not a standard (and documented) CA...Animation but an instance of the private _UIViewAdditiveAnimationAction class (an NSObject subclass). Since the properties of this class are not documented, I can't use them easily to create the path animation.
_Edit: Or it might just work after all. As Nick mentioned in his article, some properties like backgroundColor still return a standard CA...Animation on iOS 8. I'll have to try it out.`
I know this is an old question but I can provide you a solution which appears similar to Mr Lockwoods approach. Sadly the source code here is swift so you will need to convert it to ObjC.
As mentioned before if the layer is a backing layer for a view you can intercept the CAAction's in the view itself. This however isn't convenient for example if the backing layer is used in more then one view.
The good news is actionForLayer:forKey: actually calls actionForKey: in the backing layer.
It's in the actionForKey: in the backing layer where we can intercept these calls and provide an animation for when the path is changed.
An example layer written in swift is as follows:
class AnimatedBackingLayer: CAShapeLayer
{
override var bounds: CGRect
{
didSet
{
if !CGRectIsEmpty(bounds)
{
path = UIBezierPath(roundedRect: CGRectInset(bounds, 10, 10), cornerRadius: 5).CGPath
}
}
}
override func actionForKey(event: String) -> CAAction?
{
if event == "path"
{
if let action = super.actionForKey("backgroundColor") as? CABasicAnimation
{
let animation = CABasicAnimation(keyPath: event)
animation.fromValue = path
// Copy values from existing action
animation.autoreverses = action.autoreverses
animation.beginTime = action.beginTime
animation.delegate = action.delegate
animation.duration = action.duration
animation.fillMode = action.fillMode
animation.repeatCount = action.repeatCount
animation.repeatDuration = action.repeatDuration
animation.speed = action.speed
animation.timingFunction = action.timingFunction
animation.timeOffset = action.timeOffset
return animation
}
}
return super.actionForKey(event)
}
}
I think you have problems because you play with the frame of a layer and it's path at the same time.
I would just go with CustomView that has custom drawRect: that draws what you need, and then just do
[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:0.3 initialSpringVelocity:0.0 options:0 animations:^{
self.customView.bounds = newBounds;
} completion:nil];
Sor the is no need to use pathes at all
Here is what i've got using this approach
https://dl.dropboxusercontent.com/u/73912254/triangle.mov
Found solution for animating path of CAShapeLayer while bounds is animated:
typedef UIBezierPath *(^PathGeneratorBlock)();
#interface AnimatedPathShapeLayer : CAShapeLayer
#property (copy, nonatomic) PathGeneratorBlock pathGenerator;
#end
#implementation AnimatedPathShapeLayer
- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key {
if ([key rangeOfString:#"bounds.size"].location == 0) {
CAShapeLayer *presentationLayer = self.presentationLayer;
CABasicAnimation *pathAnim = [anim copy];
pathAnim.keyPath = #"path";
pathAnim.fromValue = (id)[presentationLayer path];
pathAnim.toValue = (id)self.pathGenerator().CGPath;
self.path = [presentationLayer path];
[super addAnimation:pathAnim forKey:#"path"];
}
[super addAnimation:anim forKey:key];
}
- (void)removeAnimationForKey:(NSString *)key {
if ([key rangeOfString:#"bounds.size"].location == 0) {
[super removeAnimationForKey:#"path"];
}
[super removeAnimationForKey:key];
}
#end
//
#interface ShapeLayeredView : UIView
#property (strong, nonatomic) AnimatedPathShapeLayer *layer;
#end
#implementation ShapeLayeredView
#dynamic layer;
+ (Class)layerClass {
return [AnimatedPathShapeLayer class];
}
- (instancetype)initWithGenerator:(PathGeneratorBlock)pathGenerator {
self = [super init];
if (self) {
self.layer.pathGenerator = pathGenerator;
}
return self;
}
#end
I think it is out of sync between bounds and path animation is because different timing function between UIVIew spring and CABasicAnimation.
Maybe you can try animate transform instead, it should also transform the path (untested), after it finished animating, you can then set the bound.
One more possible way is take snapshot the path, set it as content of the layer, then animate the bound, the content should follow the animation then.
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.
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 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!