I've been searching around, but couldn't find an answer that seems to address my specific issue. In my app, I have a custom UIView that animates indefinitely. It's a piece of seaweed, and the animation is very subtle, to make it look like it's swaying in water. I do this with CAKeyframeAnimation objects on the transform.rotation.z and position keys. These are added to a CAAnimationGroup, which is added as a layer to my UIView, like so:
animGroup = [CAAnimationGroup animation];
animGroup.fillMode = kCAFillModeForwards;
[animGroup setAnimations:[NSArray arrayWithObjects:posAnim, rotateAnim, nil]];
animGroup.duration = prpAnim.duration;
animGroup.repeatCount = prpAnim.repeat;
animGroup.delegate = self;
[animGroup setValue:self forKey:[NSString stringWithFormat:#"paper.rot.pos.%d",objID]];
[self.layer addAnimation:animGroup forKey:[NSString stringWithFormat:#"rot.pos.%d",objID]];
I want to further rotate that UIView image (piece of seaweed) when tilting the iPad without disturbing the core animation. I can do this when it's not animated by keyframe, but when I try to combine the two, it doesn't work and I can't figure it out.
I've tried animating the layer using something like this:
CATransform3D rotatePiece3D = CATransform3DMakeRotation(-(tiltRadians), 0.0, 0.0, 1.0);
seaweedPiece.layer.transform = rotatePiece3D;
But that doesn't work either - only when the animation group is turned off. It's a 2D app, so I just want it rotate around the z-axis when tilting left or right. Any ideas how to do this?
You apparently can't alter the rotation separately while it's being animated, so what I was trying to do won't work. I ended up changing the animation key to path, which allowed me to rotate the view separately using the accelerometer.
Related
I am running into an issue when I create an explicit animation to change the value of a CAShapeLayer's path from an ellipse to a rect.
In my canvas controller I setup a basic CAShapeLayer and add it to the root view's layer:
CAShapeLayer *aLayer;
aLayer = [CAShapeLayer layer];
aLayer.frame = CGRectMake(100, 100, 100, 100);
aLayer.path = CGPathCreateWithEllipseInRect(aLayer.frame, nil);
aLayer.lineWidth = 10.0f;
aLayer.strokeColor = [UIColor blackColor].CGColor;
aLayer.fillColor = [UIColor clearColor].CGColor;
[self.view.layer addSublayer:aLayer];
Then, when I animate the path I get a strange glitch / flicker in the last few frames of the animation when the shape becomes a rect, and in the first few frames when it animates away from being a rect. The animation is set up as follows:
CGPathRef newPath = CGPathCreateWithRect(aLayer.frame, nil);
[CATransaction lock];
[CATransaction begin];
[CATransaction setAnimationDuration:5.0f];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"path"];
ba.autoreverses = YES;
ba.fillMode = kCAFillModeForwards;
ba.repeatCount = HUGE_VALF;
ba.fromValue = (id)aLayer.path;
ba.toValue = (__bridge id)newPath;
[aLayer addAnimation:ba forKey:#"animatePath"];
[CATransaction commit];
[CATransaction unlock];
I have tried many different things like locking / unlocking the CATransaction, playing with various fill modes, etc...
Here's an image of the glitch:
http://www.postfl.com/outgoing/renderingglitch.png
A video of what I am experiencing can be found here:
http://vimeo.com/37720876
I received this feedback from the quartz-dev list:
David Duncan wrote:
Animating the path of a shape layer is only guaranteed to work when
you are animating from like to like. A rectangle is a sequence of
lines, while an ellipse is a sequence of arcs (you can see the
sequence generated by using CGPathApply), and as such the animation
between them isn't guaranteed to look very good, or work well at all.
To do this, you basically have to create an analog of a rectangle by
using the same curves that you would use to create an ellipse, but
with parameters that would cause the rendering to look like a
rectangle. This shouldn't be too difficult (and again, you can use
what you get from CGPathApply on the path created with
CGPathAddEllipseInRect as a guide), but will likely require some
tweaking to get right.
Unfortunately this is a limitation of the otherwise awesome animatable path property of CAShapeLayers.
Basically it tries to interpolate between the two paths. It hits trouble when the destination path and start path have a different number of control points - and curves and straight edges will have this problem.
You can try to minimise the effect by drawing your ellipse as 4 curves instead of a single ellipse, but it still isn't quite right. I haven't found a way to go smoothly from curves to polygons.
You may be able to get most of the way there, then transfer to a fade animation for the last part - this won't look as nice, though.
There are many situations where you need to "shake" a UIView.
(For example, "draw child user's attention to a control", "connection is slow", "user enters bad input," and so on.)
Would it be possible to do this using UIKit Dynamics?
So you'd have to ..
take the view, say at 0,0
add a spring concept
give it a nudge, say to the "left"
it should swing back and fore on the spring, ultimately settling again to 0,0
Is this possible? I couldn't find an example in the Apple demos. Cheers
Please note that as Niels astutely explains below, a spring is not necessarily the "physics feel" you want for some of these situations: in other situations it may be perfect. As far as I know, all physics in iOS's own apps (eg Messages etc) now uses UIKit Dynamics, so for me it's worth having a handle on "UIView bouncing on a spring".
Just to be clear, of course you can do something "similar", just with an animation. Example...
But that simply doesn't have the same "physics feel" as the rest of iOS, now.
-(void)userInputErrorShake
{
[CATransaction begin];
CAKeyframeAnimation * anim =
[CAKeyframeAnimation animationWithKeyPath:#"transform"];
anim.values = #[
[NSValue valueWithCATransform3D:
CATransform3DMakeTranslation(-4.0f, 0.0f, 0.0f) ],
[NSValue valueWithCATransform3D:
CATransform3DMakeTranslation(4.0f, 0.0f, 0.0f) ]
];
anim.autoreverses = YES;
anim.repeatCount = 1.0f;
anim.duration = 0.1f;
[CATransaction setCompletionBlock:^{}];
[self.layer addAnimation:anim forKey:nil];
[CATransaction commit];
}
If you want to use UIKit Dynamics, you can:
First, define a governing animator:
#property (nonatomic, strong) UIDynamicAnimator *animator;
And instantiate it:
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
Second, add attachment behavior to that animator for view in current location. This will make it spring back when the push is done. You'll have to play around with damping and frequency values.
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:viewToShake attachedToAnchor:viewToShake.center];
attachment.damping = 0.5;
attachment.frequency = 5.0;
[self.animator addBehavior:attachment];
These values aren't quite right, but perhaps it's a starting point in your experimentation.
Apply push behavior (UIPushBehaviorModeInstantaneous) to perturb it. The attachment behavior will then result in its springing back.
UIPushBehavior *push = [[UIPushBehavior alloc] initWithItems:#[viewToShake] mode:UIPushBehaviorModeInstantaneous];
push.pushDirection = CGVectorMake(100, 0);
[self.animator addBehavior:push];
Personally, I'm not crazy about this particular animation (the damped curve doesn't feel quite right to me). I'd be inclined use block based animation to move it one direction (with UIViewAnimationOptionCurveEaseOut), upon completion initiate another to move it in the opposite direction (with UIViewAnimationOptionCurveEaseInOut), and then upon completion of that, use the usingSpringWithDamping rendition of animateWithDuration to move it back to its original spot. IMHO, that yields a curve that feels more like "shake if wrong" experience.
I have a UIView and on this UIView I have 8 buttons. I need to rotate this UIVIew every time So I am using the code below:
CABasicAnimation *fullRotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
fullRotation.fromValue = [NSNumber numberWithFloat:0];
fullRotation.toValue = [NSNumber numberWithFloat:((360*M_PI)/180)];
fullRotation.duration = 30;
fullRotation.repeatCount = MAXFLOAT;
[self.menuView.layer addAnimation:fullRotation forKey:#"360"];
It rotates my view properly, But I am unable to get touch on rotating buttons. Since the Parent view is rotating and the Buttons will also rotate, Touch isn't working. Thanks
Because you rotate layer. I think you can't do what you need, but try to use CGAffineTransform
CGAffineTransform transform //desired transform
[UIView animateWithDuration:time
animations:^{menuView.transform = transform;}];
and call it by timer.
Normal view touches do not work correctly during an animation. Animation only makes the views look like they are moving. Their actual coordinates don't move.
If you want to support tapping on objects you will need to roll your own tap handling code at the superview level using the presentation layer's hitTest method.
I have a sample project on github that shows how to do that:
iOS Core Animation demo
I'm trying to apply a perspective transform to my UIView object with the following code:
CATransform3D t = CATransform3DIdentity;
t.m34 = -1.0/1000;
t = CATransform3DRotate(t, angle, 0.0f, 1.0f, 0.0f);
myView.layer.transform = t;
and I don't see any effect at all. I tried other transforms like a simple translation and they don't work either.
However, if I do either of the following two modifications then it will work somewhat but neither satisfies my request:
Change the last line to
myView.layer.sublayerTransform = t;
This sort of works but it only transforms the subviews on myView, not myView itself.
Add an animation code to apply the change instead of directly assign the change to the layer:
CABasicAnimation *turningAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
turningAnimation.toValue = [NSValue valueWithCATransform3D:t];
turningAnimation.delegate = self;
turningAnimation.fillMode = kCAFillModeForwards;
turningAnimation.removedOnCompletion = NO;
[myView.layer addAnimation:turningAnimation forKey:#"turning"];
The thing is that I don't want the animation.
Can anybody point a direction for me?
Thanks!
You should be able to just use myView.transform = CGAffineTransformMakeRotation(angle). It won't animate the property unless you explicitly tell it to using [UIView animateWithDuration:]. Using the UIView transform property will ultimately apply your transformation to the CALayer that backs UIView, so I would make sure to only interact with the transforms on one level (UIView or CALayer).
UIView transform doc:
https://developer.apple.com/library/ios/documentation/uikit/reference/uiview_class/UIView/UIView.html#//apple_ref/occ/instp/UIView/transform
CGAffineTransform Doc:
https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CGAffineTransform/Reference/reference.html#//apple_ref/c/func/CGAffineTransformMakeRotation
I figured out the problem after test by Krishnan confirmed that layer.transform itself does work without the aid of animation or sublayerTransform. The problem I encountered was caused by an animation that had been applied to my view's layer and was not subsequently removed. Since this animation had a toValue equal to the identity transform, I didn't realize it can actually prevent subsequent non-animated transforms from working. Setting removedOnCompletion = YES (which is default) on the animation solved the mystery.
I have a problem I don't understand regarding UIViews and Core Animation. Here's what I want to do:
A small view is presented above a bigger view by putting it as one of its subviews.
When I click a button inside this view, the view should minimize and move to a specified CGRect.
Then the view is removed from its superview.
The first time I present, minimize-and-move and remove the view, everything works fine. But when I present the view again, it displays at the modified position (even though it's supposed to be set at the original position by a call to theView.frame = CGRectMake(600.0, 160.0, 339.0, 327.0);), while all the different responder elements (buttons, textviews, etc.) contained in the view act as if they were at the original position. It's like the view and the layer gets dissynchronized by the animation, and I do not know how to get them back in sync.
Having something like self.view.layer.frame = CGRectMake(600.0, 160.0, 339.0, 327.0); does not get anything right.
My minimize-and-move animation code is given below:
[CATransaction flush];
CABasicAnimation *scale, *translateX, *translateY;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.delegate = delegate;
group.duration = duration;
scale = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
translateX = [CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
translateY = [CABasicAnimation animationWithKeyPath:#"transform.translation.y"];
scale.toValue = [NSNumber numberWithFloat:0.13];
translateX.toValue = [NSNumber numberWithFloat:137.0];
translateY.toValue = [NSNumber numberWithFloat:-290.0];
group.animations = [NSArray arrayWithObjects: scale, translateX, translateY, nil];
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
[theView.layer addAnimation:group forKey:#"MyAnimation"];
How to get the layer back to the view after the animation?
What happens if you remove these two lines?
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
What you are telling core animation with those lines is that you want it to continue to display in the forward (final) state of the animation. Meanwhile, you didn't actually set the transform on the layer to have the properties you used for the animation. This would make things appear to be out of sync.
Now, the issue you're going to run into is that removing those lines will cause your transforms to revert back to the starting state when the animation has completed. What you need to do is actually set the transforms on the layer in order for them to hold their position when the animation completes.
Another option is to leave the two lines in and then actually explicitly remove the animation from the layer instead of setting the layer frame as you mentioned when you are ready to revert back to the original state. You do this with:
[theView.layer removeAnimationForKey:#"MyAnimation"];
The -removedOnCompletion property told the layer not to remove the animation when it finished. Now you can explicitly remove it and it should revert back.
HTH.