Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to synchronously animate a UIView and a CALayer

Tags:

To illustrate this question, I gisted a very small Xcode project on Github (two classes, 11kb download). Have a look at the gist here or use git clone [email protected]:93982af3b65d2151672e.git.


Please consider the following scenario. A custom view of mine, called 'container view', contains two little squares. Illustrated in this screenshot:

Screenshot of the initial position of the two squares

The blue square is a 22x22 pt UIView instance and the red square is a 22x22 pt CALayer instance. For purposes of this question I want the two squares to 'stick' to the bottom right corner of the outer view, while I animate the frame of that outer view.

I change the container view's frame within a UIView's class method animateWithDuration:delay:options:animations:completion:, with a non-default easing parameter of type UIViewAnimationCurveEaseOut.

[UIView animateWithDuration:1.0 delay:0                      options:UIViewAnimationCurveEaseOut                   animations:^{ _containerView.frame = randomFrame; }                   completion:nil]; 

Note: Both the blue UIView's and the red CALayer's frame are set in the overridden setFrame: method of the container view, but results would have been the same if I had set the blue UIView's autoresizingMask property to UIView flexible left and top margins.

In the resulting animation, the blue square 'sticks' to the corner just the way I intended it, but the red square completely disregards both timing and easing of the animation. I assume this is because of the implicit animation feature of Core Animation, a feature that has helped me in many occasions before.

Here are a few screenshots in the animation sequence that illustrate the asynchronicity of the two squares:

Three screenshots mid-animation where the red square is not in the 'right' place

On to the question: Is there a way to synchronize the two frame changes so that the red CALayer and the blue UIView both move with the same animation duration and easing curve, sticking to each other as if they were one view?

P.S. Of course the required visual result of the two squares sticking together could be achieved in any number of ways, for example by having both layers become either CALayers or UIViews, but the project that the real issue is in has a very legit cause for the one to be a CALayer and the other to be a UIView.

like image 272
epologee Avatar asked May 29 '12 14:05

epologee


People also ask

Does UIView animate need weak self?

You don't need to use [weak self] in static function UIView. animate() You need to use weak when retain cycle is possible and animations block is not retained by self.

What is the difference between UIView and CALayer?

Working with UIViews happens on the main thread, it means it is using CPU power. CALayer: Layers on other hand have simpler hierarchy. That means they are faster to resolve and quicker to draw on the screen. There is no responder chain overhead unlike with views.

Does UIView animate run on the main thread?

The contents of your block are performed on the main thread regardless of where you call [UIView animateWithDuration:animations:] . It's best to let the OS run your animations; the animation thread does not block the main thread -- only the animation block itself.


2 Answers

I'm assuming that you got the end position correct and that after the animation that the view and the layer are aligned. That has nothing to do with the animation, just geometry.


When you change the property of a CALayer that isn't the layer of a view (like in your case) it will implicitly animate to its new value. To customize this animation you could use an explicit animation, like a basic animation. Changing the frame in a basic animation would look something like this.

CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:@"frame"]; [myAnimation setToValue:[NSValue valueWithCGRect:myNewFrame]]; [myAnimation setFromValue:[NSValue valueWithCGRect:[myLayer frame]]]; [myAnimation setDuration:1.0]; [myAnimation setTimingFunction:[CAMediaTimingFuntion functionWithName: kCAMediaTimingFunctionEaseOut]];  [myLayer setFrame:myNewFrame]; [myLayer addAnimation:myAnimation forKey:@"someKeyForMyAnimation"]; 

If the timing function and the duration of both animations are the same then they should stay aligned.

Also note that explicit animations doesn't change the value and hat you have to both add the animation and set the new value (like in the sample above)


Yes, there are a number of ways to achieve the same effect. One example is having the view and the layer be subviews of the same subview (that in turn is a subview of the outer frame).

Edit

You can't easily group the UIView-animation with an explicit animation. Neither can you use an animation group (since you are applying them to different layers) but yo can use a CATransaction:

[CATransaction begin]; [CATransaction setAnimationDuration:1.0]; [CATransaction setAnimationTimingFunction:[CAMediaTimingFuntion functionWithName: kCAMediaTimingFunctionEaseOut]];  // Layer animation CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:@"frame"]; [myAnimation setToValue:[NSValue valueWithCGRect:myNewFrame]]; [myAnimation setFromValue:[NSValue valueWithCGRect:[myLayer frame]]];  [myLayer setFrame:myNewFrame]; [myLayer addAnimation:myAnimation forKey:@"someKeyForMyAnimation"];  // Outer animation CABasicAnimation *outerAnimation = [CABasicAnimation animationWithKeyPath:@"frame"]; [outerAnimation setToValue:[NSValue valueWithCGRect:myNewOuterFrame]]; [outerAnimation setFromValue:[NSValue valueWithCGRect:[outerView frame]]];  [[outerView layer] setFrame:myNewOuterFrame]; [[outerView layer] addAnimation:outerAnimation forKey:@"someKeyForMyOuterAnimation"];  [CATransaction commit]; 
like image 164
David Rönnqvist Avatar answered Sep 22 '22 02:09

David Rönnqvist


If you want David Rönnqvist's snippet in Swift 3.0, here you have it:

    CATransaction.begin()     CATransaction.setAnimationDuration(1.0)     CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut))      // Layer animation     let myAnimation = CABasicAnimation(keyPath: "frame");     myAnimation.toValue = NSValue(cgRect: myNewFrame)     myAnimation.fromValue = NSValue(cgRect: myLayer.frame)      myLayer.frame = myNewFrame     myLayer.add(myAnimation, forKey: "someKeyForMyAnimation")      // Outer animation     let outerAnimation = CABasicAnimation(keyPath: "frame")     outerAnimation.toValue = NSValue(cgRect: myNewOuterFrame)     outerAnimation.fromValue = NSValue(cgRect: outerView.frame)      outerView.layer.frame = myNewOuterFrame     outerView.layer.add(outerAnimation, forKey: "someKeyForMyOuterAnimation")      CATransaction.commit() 
like image 28
Juan Fran Jimenez Avatar answered Sep 25 '22 02:09

Juan Fran Jimenez