CATransaction setting animation duration not working - ios

I am learning Core Animation and trying out sample examples.
When I use the following code, the animation duration works
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//Modifying base layer
self.view.layer.backgroundColor = [UIColor orangeColor].CGColor;
self.view.layer.cornerRadius = 20.0;
self.view.layer.frame = CGRectInset(self.view.layer.frame, 20, 20);
//Adding layer
mylayer=[CALayer layer]; //mylayer declared in .h file
mylayer.bounds=CGRectMake(0, 0, 100, 100);
mylayer.position=CGPointMake(100, 100); //In parent coordinate
mylayer.backgroundColor=[UIColor redColor].CGColor;
mylayer.contents=(id) [UIImage imageNamed:#"glasses"].CGImage;
[self.view.layer addSublayer:mylayer];
}
- (IBAction)Animate //Simple UIButton
{
[CATransaction begin];
// change the animation duration to 2 seconds
[CATransaction setValue:[NSNumber numberWithFloat:2.0f] forKey:kCATransactionAnimationDuration];
mylayer.position=CGPointMake(200.0,200.0);
mylayer.zPosition=50.0;
mylayer.opacity=0.5;
[CATransaction commit];
}
#end
On the other hand, if I lumped the Animate method code at the bottom of the ViewDidLoad button so that it happens without pressing any buttons, the animation duration is not respected. I just see the final result without any animation.
Any thoughts?
Thanks
KMB

Here's the information you're missing: there are two layer hierarchies in your app. There's the model layer hierarchy, which you normally operate on. Then there's the presentation layer hierarchy, which reflects what's on the screen. Take a look at “Layer Trees Reflect Different Aspects of the Animation State” in the Core Animation Programming Guide for a bit more information, or (highly recommended) watch the Core Animation Essentials video from WWDC 2011.
All of the code you wrote operates on the model layer (as it should).
The system adds implicit animations when it copies a changed animatable property value from a model layer to the corresponding presentation layer.
Only model layers that are in a UIWindow's view hierarchy get presentation layers. The system sends you viewDidLoad before it has added self.view to the window, so there are no presentation layers for self.view or your custom layer yet when viewDidLoad is running.
So one thing you need to do is change the property later, after the view and the layer have been added to the window and the system has created the presentation layers. The viewDidAppear: is late enough.
- (void)viewDidLoad {
[super viewDidLoad];
//Modifying base layer
self.view.layer.backgroundColor = [UIColor orangeColor].CGColor;
self.view.layer.cornerRadius = 20.0;
self.view.layer.frame = CGRectInset(self.view.layer.frame, 20, 20);
// Adding layer
mylayer = [CALayer layer]; //mylayer declared in .h file
mylayer.bounds = CGRectMake(0, 0, 100, 100);
mylayer.position = CGPointMake(100, 100); //In parent coordinate
mylayer.backgroundColor = [UIColor redColor].CGColor;
mylayer.contents = (id)[UIImage imageNamed:#"glasses"].CGImage;
[self.view.layer addSublayer:mylayer];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[CATransaction begin]; {
[CATransaction setAnimationDuration:2];
mylayer.position=CGPointMake(200.0,200.0);
mylayer.zPosition=50.0;
mylayer.opacity=0.5;
} [CATransaction commit];
}

Related

Why addAnimation:forKey not working in viewDidLoaded

I have the following method called by viewDidLoad. This method is to create a CALayer to show a image. This layer has a mask whose path is a UIBezierPath created by my private method. I want the mask to rotate infinitely, and then I add a CABasicAnimation object to the mask.
- (void) createPathLmask
{
// mask layer
self.pathLayer = [CALayer layer];
self.pathLayer.bounds = CGRectMake(0.0, 0.0, 120, 120);
CGPoint position = self.view.layer.position;
position.y += 140;
self.pathLayer.position = position;
self.pathLayer.backgroundColor = [UIColor redColor].CGColor;
UIImage *backimage = [UIImage imageNamed:#"image2"];
self.pathLayer.contents = (__bridge id)backimage.CGImage;
self.pathLayer.contentsGravity = kCAGravityResizeAspectFill;
// mask
CAShapeLayer *mask = [CAShapeLayer layer];
mask.bounds = CGRectMake(0.0, 0.0, 120, 120);
mask.position = CGPointMake(60.0f, 60.0f);
mask.contentsGravity = kCAGravityResizeAspectFill;
mask.path = [self createBezierPathInRect:mask.bounds].CGPath;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// rotate the mask repeatedly
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = #"transform.rotation";
animation.duration = 4.0f;
animation.byValue = #(M_PI * 2);
animation.repeatCount = HUGE_VALF;
[mask addAnimation:animation forKey:#"rotation_repeatedly"];
});
self.pathLayer.mask = mask;
[self.view.layer addSublayer:self.pathLayer ];
}
I find that the rotation animation can only work when I put the addAnimation:forKey into the dispatch_after block with 1 second delay. If those codes are moved out of the block, the mask will not rotate.
So there must be something not ready when animation is added to the layer in viewDidLoaded. I am wondering what is not ready yet? Is there any document or explanation about the suitable chance to add the animation?
So there must be something not ready when animation is added to the layer in viewDidLoaded
Correct. This is way too early for animation. Keep in mind that the view at this point merely exists and that's all; it isn't even part of the interface. You cannot animate a view that isn't part of the view hierarchy. There is nothing, at this point, to animate.
The view first becomes part of the interface between the first call to viewWillAppear and the first call to viewDidAppear. That is what "appear" means (as opposed to what "loaded") means.
There's great documentation on Apple's site here. Putting in simply though:
ViewWillAppear - This method is called before the view controller's view is about to be added to a view hierarchy and before any animations are configured for showing the view. You can override this method to perform custom tasks associated with displaying the view. For example, you might use this method to change the orientation or style of the status bar to coordinate with the orientation or style of the view being presented.

How do I create a growing iOS Button?

My friend gave me the following designs for iOS buttons, and I'm not sure the best way to implement this.
I need to make the reusable button shown below (in Objective-C).
I've tried:
Subclassing the button, but read that I shouldn't do that
Animating the border (while subclassed), but the border only goes inwards, so it seems like I need to animate the frame too
So how do I approach this? I'm assuming making a CustomButtonView class which has a button (composition) as well as an inner and outer circle view? How would I then animate that to grow? Would I have to animate the frame change too, or could I use insets?
What is the simplest code to make this work? Thanks!
Here Is the approach I took to create this:
Subclass UIView to create your custom button
Use UITapGestureRecognizer or touchesBegan, touchesEnded... for your interaction
Add two CALayer's for your foreground and background layers
Add your icon layer (This can be an UIImageView or any other way of displaying an image)
- (id)initWithIcon:(UIImage *)icon backgroundColor:(UIColor *)backgroundColor foregroundColor:(UIColor *)foregroundColor {
if (self = [super init]) {
// Background Layer Setup
_backgroundLayer = [CALayer new];
[_backgroundLayer setBackgroundColor:backgroundColor.CGColor];
[self.layer addSublayer:_backgroundLayer];
// Foreground Layer Setup
_foregroundLayer = [CALayer new];
[_foregroundLayer setBackgroundColor:foregroundColor.CGColor];
[self.layer addSublayer:_foregroundLayer];
// Icon Setup
_icon = [[UIImageView alloc] initWithImage:icon];
[_icon setContentMode:UIViewContentModeCenter];
[self addSubview:_icon];
UIGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(buttonTapped:)];
[self addGestureRecognizer:tapGesture];
}
return self;
}
- (void)setFrame:(CGRect)frame {
// Make sure super is called
[super setFrame:frame];
// Build the layout of backgroundLayer
[self.backgroundLayer setFrame:CGRectMake(frame.size.width*0.1, frame.size.width*0.1, frame.size.width*0.8, frame.size.width*0.8)];
[self.backgroundLayer setCornerRadius:frame.size.width*0.8/2];
// Build the layout of forgroundLayer
[self.foregroundLayer setFrame:CGRectMake(frame.size.width*0.05, frame.size.width*0.05, frame.size.width*0.9, frame.size.width*0.9)];
[self.foregroundLayer setCornerRadius:frame.size.width*0.9/2];
// Build the frame of your icon
[self.icon setFrame:CGRectMake(0, 0, frame.size.width, frame.size.width)];
}
- (void)buttonTapped:(UIGestureRecognizer*)gesture {
// Animate the foreground getting smaller
CABasicAnimation *foregroundFrameChange = [CABasicAnimation animationWithKeyPath:#"frame"];
foregroundFrameChange.fromValue = [NSValue valueWithCGRect:_foregroundLayer.frame];
foregroundFrameChange.toValue = [NSValue valueWithCGRect:CGRectMake(self.frame.size.width*0.1,self.frame.size.width*0.1, self.frame.size.width*0.8, self.frame.size.width*0.8)];
self.foregroundLayer.frame = CGRectMake(self.frame.size.width*0.1,self.frame.size.width*0.1, self.frame.size.width*0.8, self.frame.size.width*0.8);
// Animate the forground cornerRadius to stay rounded
CABasicAnimation *foregroundRadiusChange = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
foregroundRadiusChange.fromValue = [NSNumber numberWithDouble:self.foregroundLayer.cornerRadius];
foregroundRadiusChange.toValue = [NSNumber numberWithDouble:self.frame.size.width*0.8/2];
[self.foregroundLayer setCornerRadius:self.frame.size.width*0.8/2];
// Animate the background getting larger
CABasicAnimation *backgroundFrameChange = [CABasicAnimation animationWithKeyPath:#"frame"];
backgroundFrameChange.fromValue = [NSValue valueWithCGRect:self.backgroundLayer.frame];
backgroundFrameChange.toValue = [NSValue valueWithCGRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.width)];
self.backgroundLayer.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.width);
// Animate the background cornerRadius to stay rounded
CABasicAnimation *backgroundRadiusChange = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
backgroundRadiusChange.fromValue = [NSNumber numberWithDouble:self.backgroundLayer.cornerRadius];
backgroundRadiusChange.toValue = [NSNumber numberWithDouble:self.frame.size.width/2];
[self.backgroundLayer setCornerRadius:self.frame.size.width/2];
// Group all the animations to run simultaneously
CAAnimationGroup *allAnimations = [CAAnimationGroup animation];
allAnimations.duration = 2;
allAnimations.animations = #[foregroundFrameChange, foregroundRadiusChange, backgroundFrameChange, backgroundRadiusChange];
allAnimations.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.layer addAnimation:allAnimations forKey:#"animate"];
// Create your button action callback here
}
This was a quick mock up and not a complete solution but it will give you something to play with.

CALayer - remove from view

I'm adding CAShapeLayer with some CABasicAnimation. I want to be able to remove the layer and draw it again but I can't succeed with this.
- (void)drawRect:(CGRect)rect {
[self drawLayer];
}
- (void)drawLayer {
if (_alayer && _layer.superlayer) {
[_alayer removeFromSuperlayer];
_alayer = nil;
}
_alayer = [[CAShapeLayer alloc] init];
_alayer.path = [self myPath].CGPath;
_alayer.strokeColor = [UIColor redColor].CGColor;
_alayer.fillColor = [UIColor clearColor].CGColor;
_alayer.lineWidth = 2.f;
_alayer.strokeStart = 0.f;
_alayer.strokeEnd = 1.f;
[self.layer addSublayer:_alayer];
// animate
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 2.f;
animation.fromValue = #0.f;
animation.toValue = #1.f;
[_alayer addAnimation:animateStrokeEnd forKey:#"strokeEndAnimation"];
}
}
However a moment after calling [self setNeedsDisplay]; even though it stops at the breakpoint in drawLayer method, the drawing doesn't disappear and animate in again. alayer is declared as nonatomic, strong property. What am I doing wrong?
You should not call [self drawLayer]; fromm drawRect: because it is called periodically.
Use other viewDidLoad or call it from other method instead
Edited:
drawRect: should only be used for drawing, I don't know if it will even work if you add animation from there.
If you want it to disappear and appear, you should try to add delay using [self performSelector:withObject:afterDelay:].
I think changing hidden or opacity should suffice, you only need to removeAllAnimations to make sure no more animation attached to that layer.
However I think you should try other way than calling drawLayer from drawRect: because I don't it its good practice anyway.
The UIView has its own property layer by default. Please try to change a variable name to shapeLayer or another one.

Circle masked view animating too fast, and not tappable

I'm trying to make a circle-masked image view with animated mask, and played with different solutions. The example below does the job but I've got two problems with it:
1) Why is it that I cannot make the image tappable? Adding eg. a UITapGestureRecognizer does not work. My guess is that the mask prevents the touch actions being propagated to the lower level in the view hierarchy.
2) Animating the mask is running very fast and I cannot adjust the duration using UIView block animation
How can I solve these?
- (void) addCircle {
// this is the encapsulating view
//
base = [[UIView alloc] init];
//
// this is the button background
//
base_bgr = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"c1_bgr.png"]];
base_bgr.center = CGPointMake(60, 140);
[base addSubview:base_bgr];
//
// icon image
//
base_icon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"c1_ico.png"]];
base_icon.center = CGPointMake(186*0.3/2, 182*0.3/2);
base_icon.transform = CGAffineTransformMakeScale(0.3, 0.3);
[base addSubview:base_icon];
//
// the drawn circle mask layer
//
circleLayer = [CAShapeLayer layer];
// Give the layer the same bounds as your image view
[circleLayer setBounds:CGRectMake(0.0f, 0.0f, [base_icon frame].size.width,
[base_icon frame].size.height)];
// Position the circle
[circleLayer setPosition:CGPointMake(186*0.3/2-7, 182*0.3/2-10)];
// Create a circle path.
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:
CGRectMake(0.0f, 0.0f, 70.0f, 70.0f)];
// Set the path on the layer
[circleLayer setPath:[path CGPath]];
[[base layer] setMask:circleLayer];
[self.view addSubview:base];
base.center = CGPointMake(100, 100);
base.userInteractionEnabled = YES;
base_bgr.userInteractionEnabled = YES;
base_icon.userInteractionEnabled = YES;
//
// NOT working: UITapGestureRecognizer
//
UITapGestureRecognizer *tapgesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapit:)];
[base_icon addGestureRecognizer:tapgesture];
//
// BAD but WORKS :) properly positioned UIButton over the masked image
//
base_btn = [UIButton buttonWithType:UIButtonTypeCustom];
base_btn.frame = CGRectMake(base.frame.origin.x, base.frame.origin.y, base_icon.frame.size.width, base_icon.frame.size.height);
[base_btn addTarget:self action:#selector(tapit:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:base_btn];
}
This is the tap handler, and here's the mask animation. Whatever number I tried in duration, it is animating fast - approximately 0.25 second, and I cannot adjust it.
- (void) tapit:(id) sender {
//...
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^ {
[circleLayer setTransform:CATransform3DMakeScale(10.0, 10.0, 1.0)];
}
completion:^(BOOL finished) {
// it is not necessary if I manage to make the icon image tappable
base_btn.frame = [base convertRect:base_icon.frame toView:self.view];
}];
}
}
1) touches are not propagated down from base because it's initiated without a frame, so its frame will be CGRectZero. Views don't get touch events that start outside of their bounds. Simply set a valid frame on base that includes entire tap target.
2) setTransform: on a layer invokes an implicit animation which uses Core Animation's default duration of 0.25 (you guessed it right :)). The best solution would be to use CABasicAnimation instead of UIView-based animation. Something like this:
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
scaleAnimation.toValue = #(10.0f);
scaleAnimation.duration = 2;
[circleLayer addAnimation:scaleAnimation forKey:nil];
Note: by default, CABasicAnimation will remove itself from a layer when complete and the layer will snap back to old values. You can prevent it by for example setting that animation's removedOnCompletion property to NO and removing it yourself later using CALayer's removeAnimationForKey: method (just set a key instead of passing nil wheen adding the animation), but that depends on what exactly you want to accomplish with this.

No implicit animation when calling removeFromSuperlayer

I would like to take advantage of implicit animations provided by CALayer, but I can't seem to get it to work. In my view controller, I declare an instance variable CALayer *testLayer. I instantiate testLayer with this code.
- (void)viewDidLoad {
testLayer = [[CALayer alloc] init];
testLayer.bounds = CGRectMake(0, 0, 100, 100);
testLayer.position = CGPointMake(400, 400);
[testLayer setBackgroundColor:[UIColor redColor].CGColor];
testLayer.delegate = self;
[self.view.layer addSublayer:testLayer];
[testLayer release];
}
Then in touchesBegan, I remove the layer from its superlayer.
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[testLayer removeFromSuperlayer];
}
The layer disappears instantaneously, without any animation. How can I enable implicit animations for this CALayer?
First off, testLayer has been released in -viewDidLoad. When you access it again in -touchesBegan it is not valid. That layer it referenced still exists, but it is only being retained by the sublayers array. You could change the code to create an autoreleased CALayer, like this:
- (void)viewDidLoad {
testLayer = [CALayer layer];
testLayer.bounds = CGRectMake(0, 0, 100, 100);
testLayer.position = CGPointMake(400, 400);
[testLayer setBackgroundColor:[UIColor redColor].CGColor];
testLayer.delegate = self;
[self.view.layer addSublayer:testLayer];
}
Now it will actually get removed from the layer hierarchy in -touchesBegan. Keep mind that it will also be released in -touchesBegan because nothing is holding a reference to it any more. You will have to reinitialize it or you need to retain it yourself before you call -removeFromSuperlayer.

Resources