I am rotating a CATransform3D layer around y-axis by taking input from UIPanGestureRecognizer:
CGPoint movement = [recognizer translationInView:self];
CGFloat trackSpread = self.bounds.size.width;
CGFloat angle = (35 * abs(movement.x))/trackSpread;
transform = CATransform3DRotate(transform, degToRad(angle), 0, 1, 0);
The view rotates smoothly if the pan is done slowly i.e. receives continuous x values from recognizer but if the panning is at a faster pace i.e. the values receives from recognizer are not continuous I see jumps in the rotation (kind of like the layer is also translating a bit around its rotation point). Would what be the way to always keep the rotation smooth whether panning is done slow or fast?
Stand alone layers have implicit animations that happen automatically just by changing the property. This is what you are seeing. When continuously updating the property it looks like the layer is lagging and falling behind.
If you want to change the property without having the implicit animation you can change it within a transaction that has disabled all actions (a more general name for animations):
[CATransaction begin];
[CATransaction setDisableActions:YES]; // no animations
myLayer.transform = newTransform;
[CATransaction commit];
You can also configure the layer to never look for an animation for a given key path by adding NSNull to its actions dictionary for that key path:
myLayer.actions = #{
#"transform": [NSNull null] // never animate "transform"
};
Related
I want to rotate a CAShapeLayer with objective c around it center point without moving it around. The CAShapeLayer contain UIBezierPath point of rect. I'm not able to rotate the CAShapeLayer becouse i dont know how. Please show me how tp rotate around it center without moving it postion.
Here is some code that does that:
//Create a CABasicAnimation object to manage our rotation.
CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:#"transform"];
totalAnimationTime = rotation_count;
rotation.duration = totalAnimationTime;
//Start the animation at the previous value of angle
rotation.fromValue = #(angle);
//Add change (which will be a change of +/- 2pi*rotation_count
angle += change;
//Set the ending value of the rotation to the new angle.
rotation.toValue = #(angle);
//Have the rotation use linear timing.
rotation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
/*
This is the magic bit. We add a CAValueFunction that tells the CAAnimation we are modifying
the transform's rotation around the Z axis.
Without this, we would supply a transform as the fromValue and toValue, and for rotations
> a half-turn, we could not control the rotation direction.
By using a value function, we can specify arbitrary rotation amounts and directions, and even
Rotations greater than 360 degrees.
*/
rotation.valueFunction = [CAValueFunction functionWithName: kCAValueFunctionRotateZ];
/*
Set the layer's transform to it's final state before submitting the animation, so it is in it's
final state once the animation completes.
*/
imageViewToAnimate.layer.transform = CATransform3DRotate(imageViewToAnimate.layer.transform, angle, 0, 0, 1.0);
//Now actually add the animation to the layer.
[imageViewToAnimate.layer addAnimation:rotation forKey:#"transform.rotation.z"];
(That code is taken (and simplified) from my github project KeyframeViewAnimations)
In my project I'm rotating the layer of a UIImageView, but the same approach will work for any CALayer type.
I'm using CALayer for interactive book engine and want to rotate CALayer around it center but when I apply rotation transform (using angle which is based on user input) to CALayer it goes crazy: layer stretches and moves in very strange way and disappears. What I do is
[CATransaction begin];
[CATransaction setValue: (id) kCFBooleanTrue forKey: kCATransactionDisableActions];
...
layer.transform = CATransform3DMakeRotation(angle, 0, 0, 1);
...
[CATransaction commit];
Interestingly, when my angle is M_PI or M_PI/2, or it's scale transform everything is ok, but when its rotation and any other angles layer goes crazy.
Does anybody know what's happening and how to fix it?
Found the reason myself: it was because in parallel I moved layer using .frame instead of .position property: frame property itself is calculated based on transform thus setting it produced that 'crazy' behavior.
I'm developing an iPhone app where the main views are presented the user on the surface of a cube. Users switch views by rotating the cube with a pan gesture.
To achieve this I am using the GKLCubeController class from this GitHub project.
In terms of adding views to a cube and rotating, it works fine. However the angular rotation of the cube doesn't map correctly to the current x position of the finger as it pans across the screen.
The problem is that the cube rotation lags behind the finger movement by about ½ second making the cube feel ‘heavy’ as illustrated in this short screencast.
The code handling the rotation is shown below:
-(void)panHandler:(UIPanGestureRecognizer*)panner{
CGPoint translatedPoint = [panner translationInView:self.view.window];
CGFloat halfWidth = self.view.bounds.size.width / 2.0;
// save our starting points
if([panner state] == UIGestureRecognizerStateBegan) {
startingX = translatedPoint.x;
if (!transformLayer) {
transformLayer = [[CATransformLayer alloc] init];
transformLayer.frame = self.view.layer.bounds;
for (UIView *viewToTranslate in views) {
[viewToTranslate removeFromSuperview];
[transformLayer addSublayer:viewToTranslate.layer];
}
// add in this new layer
[self.view.layer addSublayer:transformLayer];
}
} else if([panner state] == UIGestureRecognizerStateEnded) {
...
} else {
// instantly adjust our transformation layer
CATransform3D transform = CATransform3DIdentity;
transform.m34 = kPerspective;
double percentageOfWidth = (translatedPoint.x - startingX) / self.view.frame.size.width;
transform = CATransform3DTranslate(transform, 0, 0, -halfWidth);
double adjustmentAngle = percentageOfWidth * M_PI_2 + startingAngle;
transform = CATransform3DRotate(transform, adjustmentAngle, 0, 1, 0);
transform = CATransform3DTranslate(transform, 0, 0, halfWidth);
transformLayer.transform = transform;
finishingAngle = adjustmentAngle;
}
}
I've a suspicion the problem is something to do with the conversion of the CGPoint.x returned by UIPanGestureRecognizer translationInView: to a rotation angle. Can anyone confirm whether this is the case, and suggest what the correct maths should be for mapping the touch position x to the rotation of a cube such that the cube edge tracks the finger motion as it pans across the screen?
There are two issues here:
The major performance issue here is the way this class is performing the transform of the sides of the cube. It's giving each side of the cube a complicated transform, and then as you're dragging the cube around, it's taken the relevant sides of the cube, added them to a CATransformLayer, and performing a complicated transform upon that layer (thus, when you look at the individual sides of the cube, you're doing a transform of a transform).
I pulled out that CATransformLayer logic, and updated the transform for the individual sides, and it was dramatically more responsive.
By the way, you may might want to still employ something like this CATransformLayer logic when you animate the letting go of the rotated cube, as that's an excellent way of synchronizing the animation of the individual sides of the cube (otherwise you get some separation in the sides of the cube during the animation). But while dragging, there's too much of a performance hit.
As you continue to refine this, there are possibly other optimizations that can be done, but my testing suggests that getting rid of a transformation on a complicated transformation made a huge impact on performance.
And, by the way, make sure to test this on a device, not the simulator, as the simulator's graphics performance is very different than that of the device.
A minor factor that might contribute a slight initial delay in responsiveness may be the inherent delay in UIPanGestureRecognizer (which looks for a certain amount of movement before recognizing the gesture as a pan, so that other gestures such as taps and the like can trigger if appropriate). It's a modest delay and a very small part of your performance problem, but for the quickest of response times, you might not want to use the UIPanGestureRecognizer. Either subclass your own, or use a UILongPressGestureRecognizer with a minimumPressDuration of 0.0, and you can get instantaneous response to the gesture.
You'll see this respond more quickly to movement (but it's also a gesture that doesn't play well with others, that if you have tap gestures or the like inside the view, they won't be triggered).
I am experimenting with Key Frame animation of the position of a UIImageView object moving along a bezier path. This pic shows the initial state before animation. The blue line is the path - initially moving straight up, the light green box is the initial bounding box or the image, and the dark green "ghost" is the image that I am moving:
When I kick off the animation with rotationMode set to nil, the image keeps the same orientation all the way through the path as expected.
But when I kick off the animation with rotationMode set to kCAAnimationRotateAuto, the image immediately rotates 90 degrees anti-clockwise and keeps this orientation all the way through the path. When it reaches the end of the path it redraws in the correct orientation (well it actually shows the UIImageView that I repositioned in the final location)
I was naively expecting that the rotationMode would orientate the image to the tangent of the path and not to the normal, especially when the Apple docs for the CAKeyframeAnimation rotationMode state
Determines whether objects animating along the path rotate to match the path tangent.
So what is the solution here? Do I have to pre-rotate the image by 90 degrees clockwise? Or is there something that I am missing?
Thanks for your help.
Edit 2nd March
I added a rotation step before the path animation using an Affine rotation like:
theImage.transform = CGAffineTransformRotate(theImage.transform,90.0*M_PI/180);
and then after the path animation, resetting the rotation with:
theImage.transform = CGAffineTransformIdentity;
This makes the image follow the path in the expected manner. However I am now running into a different problem of the image flickering. I am already looking for a solution to the flickering issue in this SO question:
iOS CAKeyFrameAnimation scaling flickers at animation end
So now I don't know if I have made things better or worse!
Edit March 12
While Caleb pointed out that yes, I did have to pre rotate my image, Rob provided an awesome
package of code that almost completely solved my problems. The only thing that Rob didn't do was compensating for my assets being drawn with a vertical rather than horizontal orientation, thus still requiring to preRotate them by 90 degrees before doing the animation. But hey, its only fair that I have to do some of the work to get things running.
So my slight changes to Rob's solution to suite my requirements are:
When I add the UIView, I pre Rotate it to counter the inherent rotation added by setting the rotationMode:
theImage.transform = CGAffineTransformMakeRotation(90*M_PI/180.0);
I need to keep that rotation at the end of the animation, so instead of just blasting the view's transform with a new scale factor after the completion block is defined, I build the scale based on the current transform:
theImage.transform = CGAffineTransformScale(theImage.transform, scaleFactor, scaleFactor);
And that's all I had to do to get my image to follow the path as I expected!
Edit March 22
I have just uploaded to GitHub a demo project that shows off the moving of an object along a bezier path. The code can be found at PathMove
I also wrote about it in my blog at Moving objects along a bezier path in iOS
The issue here is that Core Animation's autorotation keeps the horizontal axis of the view parallel to the path's tangent. That's just how it works.
If you want your view's vertical axis to follow the path's tangent instead, rotating the contents of the view as you're currently doing is the reasonable thing to do.
Here's what you need to know to eliminate the flicker:
As Caleb sort of pointed out, Core Animation rotates your layer so that its positive X axis lies along the tangent of your path. You need to make your image's “natural” orientation work with that. So, supposing that's a green spaceship in your example images, you need the spaceship to point to the right when it doesn't have rotation applied to it.
Setting a transform that includes rotation interferes with the rotation applied by `kCAAnimationRotateAuto'. You need to remove the rotation from your transform before applying the animation.
Of course that means you need to reapply the transformation when the animation completes. And of course you want to do that without seeing any flicker in the appearance of the image. That's not hard, but there some secret sauce involved, which I explain below.
You presumably want your spaceship to start out pointing along the tangent of the path, even when the spaceship is sitting still having not been animated yet. If your spaceship image is pointing to the right, but your path goes up, then you need to set the transform of the image to include a 90° rotation. But perhaps you don't want to hardcode that rotation - instead you want to look at the path and figure out its starting tangent.
I'll show some of the important code here. You can find my test project on github. You may find some use in downloading it and trying it out. Just tap on the green “spaceship” to see the animation.
So, in my test project, I have connected my UIImageView to an action named animate:. When you touch it, the image moves along half of a figure 8 and doubles in size. When you touch it again, the image moves along the other half of the figure 8 (back to the starting position), and returns to its original size. Both animations use kCAAnimationRotateAuto, so the image points along the tangent of the path.
Here's the start of animate:, where I figure out what path, scale, and destination point the image should end up at:
- (IBAction)animate:(id)sender {
UIImageView* theImage = self.imageView;
UIBezierPath *path = _isReset ? _path0 : _path1;
CGFloat newScale = 3 - _currentScale;
CGPoint destination = [path currentPoint];
So, the first thing I need to do is remove any rotation from the image's transform, since as I mentioned, it will interfere with kCAAnimationRotateAuto:
// Strip off the image's rotation, because it interferes with `kCAAnimationRotateAuto`.
theImage.transform = CGAffineTransformMakeScale(_currentScale, _currentScale);
Next, I go into a UIView animation block so that the system will apply animations to the image view:
[UIView animateWithDuration:3 animations:^{
I create the keyframe animation for the position and set a couple of its properties:
// Prepare my own keypath animation for the layer position.
// The layer position is the same as the view center.
CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
positionAnimation.path = path.CGPath;
positionAnimation.rotationMode = kCAAnimationRotateAuto;
Next is the secret sauce for preventing flicker at the end of the animation. Recall that animations do not effect the properties of the “model layer“ that you attach them to (theImage.layer in this case). Instead, they update the properties of the “presentation layer“, which reflects what's actually on the screen.
So first I set removedOnCompletion to NO for the keyframe animation. This means the animation will stay attached to the model layer when the animation is complete, which means I can access the presentation layer. I get the transform from the presentation layer, remove the animation, and apply the transform to the model layer. Since this is all happening on the main thread, these property changes all happen in one screen refresh cycle, so there's no flicker.
positionAnimation.removedOnCompletion = NO;
[CATransaction setCompletionBlock:^{
CGAffineTransform finalTransform = [theImage.layer.presentationLayer affineTransform];
[theImage.layer removeAnimationForKey:positionAnimation.keyPath];
theImage.transform = finalTransform;
}];
Now that I've set up the completion block, I can actually change the view properties. The system will automatically attach animations to the layer when I do this.
// UIView will add animations for both of these changes.
theImage.transform = CGAffineTransformMakeScale(newScale, newScale);
theImage.center = destination;
I copy some key properties from the automatically-added position animation to my keyframe animation:
// Copy properties from UIView's animation.
CAAnimation *autoAnimation = [theImage.layer animationForKey:positionAnimation.keyPath];
positionAnimation.duration = autoAnimation.duration;
positionAnimation.fillMode = autoAnimation.fillMode;
and finally I replace the automatically-added position animation with the keyframe animation:
// Replace UIView's animation with my animation.
[theImage.layer addAnimation:positionAnimation forKey:positionAnimation.keyPath];
}];
Double-finally I update my instance variables to reflect the change to the image view:
_currentScale = newScale;
_isReset = !_isReset;
}
That's it for animating the image view with no flicker.
And now, as Steve Jobs would say, One Last Thing. When I load the view, I need to set the transform of the image view so that it's rotated to point along the tangent of the first path that I will use to animate it. I do that in a method named reset:
- (void)reset {
self.imageView.center = _path1.currentPoint;
self.imageView.transform = CGAffineTransformMakeRotation(startRadiansForPath(_path0));
_currentScale = 1;
_isReset = YES;
}
Of course, the tricky bit is hidden in that startRadiansForPath function. It's really not that hard. I use the CGPathApply function to process the elements of the path, picking out the first two points that actually form a subpath, and I compute the angle of the line formed by those two points. (A curved path section is either a quadratic or cubic bezier spline, and those splines have the property that the tangent at the first point of the spline is the line from the first point to the next control point.)
I'm just going to dump the code here without explanation, for posterity:
typedef struct {
CGPoint p0;
CGPoint p1;
CGPoint firstPointOfCurrentSubpath;
CGPoint currentPoint;
BOOL p0p1AreSet : 1;
} PathState;
static inline void updateStateWithMoveElement(PathState *state, CGPathElement const *element) {
state->currentPoint = element->points[0];
state->firstPointOfCurrentSubpath = state->currentPoint;
}
static inline void updateStateWithPoints(PathState *state, CGPoint p1, CGPoint currentPoint) {
if (!state->p0p1AreSet) {
state->p0 = state->currentPoint;
state->p1 = p1;
state->p0p1AreSet = YES;
}
state->currentPoint = currentPoint;
}
static inline void updateStateWithPointsElement(PathState *state, CGPathElement const *element, int newCurrentPointIndex) {
updateStateWithPoints(state, element->points[0], element->points[newCurrentPointIndex]);
}
static void updateStateWithCloseElement(PathState *state, CGPathElement const *element) {
updateStateWithPoints(state, state->firstPointOfCurrentSubpath, state->firstPointOfCurrentSubpath);
}
static void updateState(void *info, CGPathElement const *element) {
PathState *state = info;
switch (element->type) {
case kCGPathElementMoveToPoint: return updateStateWithMoveElement(state, element);
case kCGPathElementAddLineToPoint: return updateStateWithPointsElement(state, element, 0);
case kCGPathElementAddQuadCurveToPoint: return updateStateWithPointsElement(state, element, 1);
case kCGPathElementAddCurveToPoint: return updateStateWithPointsElement(state, element, 2);
case kCGPathElementCloseSubpath: return updateStateWithCloseElement(state, element);
}
}
CGFloat startRadiansForPath(UIBezierPath *path) {
PathState state;
memset(&state, 0, sizeof state);
CGPathApply(path.CGPath, &state, updateState);
return atan2f(state.p1.y - state.p0.y, state.p1.x - state.p0.x);
}
Yow mention that you kick off the animation with "rotationMode set to YES", but the documentation states that rotationMode should be set using an NSString...
In particular:
These constants are used by the rotationMode property.
NSString * const kCAAnimationRotateAuto
NSString * const kCAAnimationRotateAutoReverse
Have you tried setting:
keyframe.animationMode = kCAAnimationRotateAuto;
The documentation states:
kCAAnimationRotateAuto: The objects travel on a tangent to the path.
I want to simultaneously scale and translate a CALayer from one CGrect (a small one, from a button) to a another (a bigger, centered one, for a view). Basically, the idea is that the user touches a button and from the button, a CALayer reveals and translates and scales up to end up centered on the screen. Then the CALayer (through another button) shrinks back to the position and size of the button.
I'm animating this through CATransform3D matrices. But the CALayer is actually the backing layer for a UIView (because I also need Responder functionality). And while applying my scale or translation transforms separately works fine. The concatenation of both (translation, followed by scaling) offsets the layer's position so that it doesn't align with the button when it shrinks.
My guess is that this is because the CALayer anchor point is in its center by default. The transform applies translation first, moving the 'big' CALayer to align with the button at the upper left corner of their frames. Then, when scaling takes place, since the CALayer anchor point is in the center, all directions scale down towards it. At this point, my layer is the button's size (what I want), but the position is offset (cause all points shrank towards the layer center).
Makes sense?
So I'm trying to figure out whether instead of concatenating translation + scale, I need to:
translate
change anchor point to upper-left.
scale.
Or, if I should be able to come up with some factor or constant to incorporate to the values of the translation matrix, so that it translates to a position offset by what the subsequent scaling will in turn offset, and then the final position would be right.
Any thoughts?
You should post your code. It is generally much easier for us to help you when we can look at your code.
Anyway, this works for me:
- (IBAction)showZoomView:(id)sender {
[UIView animateWithDuration:.5 animations:^{
self.zoomView.layer.transform = CATransform3DIdentity;
}];
}
- (IBAction)hideZoomView:(id)sender {
CGPoint buttonCenter = self.hideButton.center;
CGPoint zoomViewCenter = self.zoomView.center;
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DTranslate(transform, buttonCenter.x - zoomViewCenter.x, buttonCenter.y - zoomViewCenter.y, 0);
transform = CATransform3DScale(transform, .001, .001, 1);
[UIView animateWithDuration:.5 animations:^{
self.zoomView.layer.transform = transform;
}];
}
In my test case, self.hideButton and self.zoomView have the same superview.