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.
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.
So, I am fairly new to iOS programming, and have inherited a project from a former coworker. We are building an app that contains a gauge UI. When data comes in, we want to smoothly rotate our "layer" (which is a needle image) from the current angle to a new target angle. Here is what we have, which worked well with slow data:
-(void) MoveNeedleToAngle:(float) target
{
static float old_Value = 0.0;
CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:#"transform.rotation");
[rotateCurrentPressureTick setDelegate:self];
rotateCurrentPressureTick.fromValue = [NSSNumber numberWithFloat:old_value/57.2958];
rotateCurrentPressureTick.removedOnCompletion=NO;
rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
rotateCurrentPressureTick.toValue=[NSSNumber numberWithFloat:target/57.2958];
rotateCurrentPressureTick.duration=3; // constant 3 second sweep
[imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:#"rotateTick"];
old_Value = target;
}
The problem is we have a new data scheme in which new data can come in (and the above method called) faster, before the animation is complete. What's happening I think is that the animation is restarted from the old target to the new target, which makes it very jumpy.
So I was wondering how to modify the above function to add a continuous/restartable behavior, as follows:
Check if the current animation is in progress and
If so, figure out where the current animation angle is, and then
Cancel the current and start a new animation from the current rotation to the new target rotation
Is it possible to build that behavior into the above function?
Thanks. Sorry if the question seems uninformed, I have studied and understand the above objects/methods, but am not an expert.
Yes you can do this using your existing method, if you add this bit of magic:
- (void)removeAnimationsFromView:(UIView*)view {
CALayer *layer = view.layer.presentationLayer;
CGAffineTransform transform = layer.affineTransform;
[layer removeAllAnimations];
view.transform = transform;
}
The presentation layer encapsulates the actual state of the animation. The view itself doesn't carry the animation state properties, basically when you set an animation end state, the view acquires that state as soon as you trigger the animation. It is the presentation layer that you 'see' during the animation.
This method captures the state of the presentation layer at the exact moment you cancel the animation, and then applies that state to the view.
Now you can use this method in your animation method, which will look something like this:
-(void) MoveNeedleToAngle:(float) target{
[self removeAnimationsFromView:imageView_Needle];
id rotation = [imageView_Needle valueForKeyPath:#"layer.transform.rotation.z"];
CGFloat old_value = [rotation floatValue]*57.2958;
// static float old_value = 0.0;
CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
[rotateCurrentPressureTick setDelegate:self];
rotateCurrentPressureTick.fromValue = [NSNumber numberWithFloat:old_value/57.2958];
rotateCurrentPressureTick.removedOnCompletion=NO;
rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
rotateCurrentPressureTick.toValue=[NSNumber numberWithFloat:target/57.2958];
rotateCurrentPressureTick.duration=3; // constant 3 second sweep
[imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:#"rotateTick"];
old_value = target;
}
(I have made minimal changes to your method: there are a few coding style changes i would also make, but they are not relevant to your problem)
By the way, I suggest you feed your method in radians, not degrees, that will mean you can remove those 57.2958 constants.
You can get the current rotation from presentation layer and just set the toValue angle. No need to keep old_value
-(void) MoveNeedleToAngle:(float) targetRadians{
CABasicAnimation *animation =[CABasicAnimation animationWithKeyPath:#"transform.rotation"];
animation.duration=5.0;
animation.fillMode=kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
animation.removedOnCompletion=NO;
animation.fromValue = [NSNumber numberWithFloat: [[layer.presentationLayer valueForKeyPath: #"transform.rotation"] floatValue]];
animation.toValue = [NSNumber numberWithFloat:targetRadians];
// layer.transform= CATransform3DMakeRotation(rads, 0, 0, 1);
[layer addAnimation:animation forKey:#"rotate"];
}
Another way i found (commented line above) is instead of using fromValue and toValue just set the layer transform. This will produce the same animation but the presentationLayer and the model will be in sync.
I'm trying to make an exact "translation" of this UIView block-based animation code:
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
someView.frame = CGRect(0, 100, 200, 200);
}
completion:nil];
using CABasicAnimation instead.
I'm totally aware that the frame property is actually a combination of position, bounds and anchorPoint of the underlying layer, as it is described here: http://developer.apple.com/library/mac/#qa/qa1620/_index.html
... and I already made a solution like that, using two CABasicAnimations one setting the position, one for bounds and it works for that one view.
The problem is however that I have subviews inside my view. someView has a subview of type UIScrollView in which I place still another subview of type UIImageView. UIScrollView subview has autoresizingMask set to UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight. That all works perfectly if I use the UIView block-based version, however when I try using CABasicAnimations the subviews start behaving unexpectedly(i.e. get resized to incorrect widths). So it seems autoresizingMask is not working correctly when using CABasicAnimations. I noticed also that subviews don't receive a call to setFrame:, although the frame property of the parent view does change after changes to layer position and bounds are made.
That's why I would like to know what would be the correct code to replicate with CABasicAnimation that what is happening when one uses UIView's animateWithDuration method.
I'm totally aware that the frame property is actually a combination of position, bounds and anchorPoint of the underlying layer
Good, but it's important also to be aware that frame is not an animatable property for layers. If you want to animate with CABasicAnimation you must use properties that are animatable for layers. The CALayer documentation marks every such property as explicitly "animatable". The idea of using bounds and position is correct.
Thus, this code does essentially what you were doing before:
[CATransaction setDisableActions:YES];
// set final bounds and position
v.layer.bounds = CGRectMake(0,0,200,200);
v.layer.position = CGPointMake(100,200);
// cause those changes to be animated
CABasicAnimation* a1 = [CABasicAnimation animationWithKeyPath:#"bounds"];
a1.duration = 0.5;
CABasicAnimation* a2 = [CABasicAnimation animationWithKeyPath:#"position"];
a2.duration = 0.5;
[v.layer addAnimation:a1 forKey:nil];
[v.layer addAnimation:a2 forKey:nil];
However, that code has no effect on the size of any sublayers of v.layer. (A subview of v is drawn by a sublayer of v.layer.) That, unfortunately, is the problem you are trying to solve. I believe that by dropping down to the level of layers and direct explicit core animation, you have given up autoresizing, which happens at the view level. Thus you will need to animate the sublayers as well. That is what view animation was doing for you.
This is an unfortunate feature of iOS. Mac OS X has layer constraints (CAConstraint) that do at the layer level what autoresizing does at the view level (and more). But iOS is missing that feature.
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.
I have a very simple UIView animation, which causes my view to "throb":
[UIView animateWithDuration:0.2 delay:0.0f options:UIViewAnimationOptionAllowUserInteraction+UIViewAnimationOptionRepeat+UIViewAnimationOptionAutoreverse animations:^{
CGAffineTransform transf = CGAffineTransformScale(self.view.transform, 1.05f, 1.05f);
[self.view setTransform:transf];
} completion:nil];
At some point the user hits a button to cancel the animation, and apply a new transform.
[self.view.layer removeAllAnimations];
[self.view setTransform:aNewTransform];
I'd like it to reset to it's original transform, but instead it's getting increased in size by 5%.
Edit: I tried adding a completion block that resets the transform to it's original position. This works, but causes the transform I run immediately after to be trampled... the completion block gets run AFTER I apply aNewTransform.
Edit 2: I found a solution, using CABasicAnimation, instead of UIView animations. I would still be interested if anybody found a solution using UIView animations... I like the block-based interface better. This also only works, because I happen to be keeping track of my scale value separate from the one applied to the view. Everything that changes the scale uses a method that also changes self.scale
My replacement animation:
CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
basicAnimation.toValue = [NSNumber numberWithFloat:self.scale*1.05f];
basicAnimation.fromValue = [NSNumber numberWithFloat:self.scale];
basicAnimation.autoreverses = YES;
basicAnimation.duration = 0.2;
basicAnimation.repeatCount = HUGE_VALF;
[self.view.layer addAnimation:basicAnimation forKey:#"Throb"];
Before starting a new animation on an existing view, you can reset the view if any of these attributes were previously altered:
// first, don't forget to stop ongoing animations for the view
[theView.layer removeAllAnimations];
// if the view was hidden
theView.hidden = NO;
// if you applied a transformation e.g. translate, scale, rotate..., reset to identity
theView.transform = CGAffineTransformIdentity;
// if you changed the anchor point, this will reset it to the center of the view
theView.layer.anchorPoint = CGPointMake(0.5, 0.5);
// if you changed the alpha, this will reset it to visible
theView.alpha = 1.;
Try putting the line
[self.view setTransform:aNewTransform];
in the completion block.