Core Animation rotating a layer around arbitrary point - iphone

How can I rotate a layer using Core Animation around an arbitrary point? ( In my case a point that is not inside the layer I want to rotate )
I prefer doing this without changing the anchor point, because unless I get something wrong every time I change the anchor point it also changes the position of the layer.
I tried something like that but it didn't work:
[UIImageView beginAnimations:nil context:nil];
CATransform3D rotationTransform = CATransform3DIdentity;
rotationTransform = CATransform3DTranslate(rotationTransform, 0.0, -100.0, 0.0);
rotationTransform = CATransform3DRotate(rotationTransform, DegreesToRadians(180),
0.0, 0.0, 1.0);
rotationTransform = CATransform3DTranslate(rotationTransform, 0.0, 100.0, 0.0)
shape1.layer.transform = rotationTransform;
[UIImageView commitAnimations];
It looks like the rotation axis is moving during the rotate.

Your solution is fine but the 'expected way' is to use the anchor point.
It is moving around when you set the anchor point because the position is attached to the anchor point, i.e. setting the position sets where the anchorPoint is in the superlayer's coord system.
Probably not worth changing since you have something working, but just something to add to the grey matter for next time.
Good luck!

You can do this by appending multiple transformations:
translate the layer by (-rotCenterX, rotCenterY)
rotate the layer
translate the layer by (rotCenterX, rotCenterY)

Your initial solution is not fine as the translation is animated just like the rotation is. So the "custom-center" of your rotation is moving during the rotation. What you actually want is a translation without animation and the rotation with the animation based on that non-animated translation.
Thing is, I currently have the exact same problem and couldnt come up with a solution yet. I am looking at CATransaction at the moment, hoping that could be the key...

I finaly did it by creating a new bigger layer with it's center at my rotation axis and setting the layer i want to rotate as it's sub layer.
Then I rotate the bigger layer instead of the sub layer

Related

Most efficient way to translate view back to (0,0) origin after scaling or rotating?

I'm scaling an object as below:
CGAffineTransform newTransform =
CGAffineTransformScale(self.graphicView.transform,
(graphicViewSize.width + resizeValue) / graphicViewSize.width,
(graphicViewSize.height + resizeValue) / graphicViewSize.height);
This scales correctly, however it moves the origin position of the view. What is the best way to move it back? Best to do a translate? What is a nice way to do this, is it possible translate and scale at the same time so this happens?
Change the layer's anchor point property. By default, it is set to the center so the view will scale in all directions, but you can change it to the corners to maintain one of them in a fixed location.
Use this:
CGAffineTransform newtransform = CGAffineTransformIdentity;
This should work if your intial view was at (0,0) before scaled or rotated.

iPhone animation based on input values (touches) not time

For an animation effect perfectly suited to an animation group approach as shown in Brad Larson's answer here, I need the animation to proceed according to inputs. Specifically touch and position of detected touches. It is easy to handle touchesMoved: and to set the position of the elements for every touch but it just isn't smooth like the core animation approach.
Imagine a marble in a grooved track. I want to push the marble along to any position at any speed in one direction or the other. The animation has to do something like that, moving a visual element along a path in response to touches. CAKeyframeAnimation has the path bit exactly but seems to always want to base the transition from frame to frame on time elapsed, not on any other factor, and in one direction.
31 January update - Thanks all for the responses so far however none is really solving the problem. I have a circular menu that is dragged to select an option. All of it needs to move together and I have worked around it by using an view that has a rotational transform applied and the inverse rotational transform applied to its subviews so the icons all rotate with appropriate ferris wheel orientation. It really looks better when the icons are animated along a slightly ovoid path though... the marble description is an attempt to make clear what I'm trying to do. Better perhaps to imagine magnets oriented to repel all travelling in a groove - move one and its neighbours move too but not necessarily in the direction that the dragged magnet moves as the path curves.
Right now the problem is one of following a simple path created with one circle but I'd really like to know how to animate objects along an arbitrary path, position controlled purely by touch with no calculations involving velocity or direction.
You may be able to use the hierarchical nature of timelines in layer trees to achieve what you’re looking for. Objects implementing CAMediaTiming, which include both CAAnimation and CALayer, inherit a timespace from their parent, which they can modify (via scaling, shifting and repeating) and propagate to their children. By setting the speed property of a layer to 0.0 and adjusting the timeOffset property manually, you can decouple that layer (and all of its sublayers) from the usual notion of time.
What you’d do in your case is define the animations for all your menu items to animate their position along your desired CGPath from time t0 to t1, with the appropriate timeOffset on each animation to keep your items appropriately spaced. Note that a beginTime of 0.0 on an animation is usually interpreted as starting from the time the animation was added to its layer, so if you want t0 to be 0.0, you'll probably have to set it to a tiny epsilon > 0.0.
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animation.beginTime = 1e-100;
animation.duration = 1.0;
animation.fillMode = kCAFillModeBoth;
animation.removedOnCompletion = NO;
animation.path = path;
animation.calculationMode = kCAAnimationPaced;
animation.timeOffset = timeOffset;
You’d then set the speed property to 0.0 on a parent layer (containing only these menu items) and update its timeOffset to values between t0 and t1 in response to your touch events.
This approach has two potential caveats. Because you’ve taken over the nature of time on this layer subtree, you probably won’t be able to animate other properties at the same time. Additionally, if you want coasting behavior for a fast flick, you’ll probably need to animate time forward on your own.
If you are targeting iOS 4.0 or greater then you can use the new block based class methods to start animatable changes to view objects. So from your touch event you can initiate an animatable change to properties on the view:
[UIView animateWithDuration:1.0 animations:^{
yourView.alpha = 0.0; // fade out yourView over 1 second
}];
N.B. This could just as easily be a change to another property on the view, like its location. You can animate the following properties on the view this way:
#property frame
#property bounds
#property center
#property transform
#property alpha
#property backgroundColor
#property contentStretch
If you are targetting earlier versions of iOS you will need to use UIView beginAnimations and commitAnimations methods to create an animation block:
[UIView beginAnimations:nil context:context];
[UIView setAnimationDuration:1.0];
yourView.alpha = 0.0;
[UIView commitAnimations];
This stuff works really well and once you start using it, you will have to be careful you don't get addicted. ;)
Update for comment:
You can bind the position of the marble to the location of the touch event. Once you get the touchesEnded event you can then animate the location of the marble using an animation block.
velocity = distance/time. You can try by giving delay according to touches moved and time. You can calculate time between touchesBegan and touchesEnded methods.
I'm not entirely certain I understand exactly what you want to do but... Why not just draw the marble wherever the user's finger is? No animation required.
If you want to keep the marble moving after the user lets go, you need to maintain a concept of momentum and use that to animate the marble slowing down. You could use either CoreAnimation for that or a timer.
I would make a velocity calculation in touches moved, and add it to a local variable that a block can mutate via a timer.
in other words, in touches moved make a velocity calculation bearing in mind the direction of a previous velocity. In touches ended, fire a block that translates the 'marble' decaying the velocity as you disire. If the user moves the marble again, modify the local variable, this will in turn speed up the animation of the marble or slow it down/change direction depending on on the direction of the touch.

iOS - Animating text size change in UILabel or UITextView?

In an application showing chunks of text, I'm having the font size increase when the device is turned to a landscape orientation. I don't like how it does the whole animation and then suddenly jumps to the new size, so I'd like to animate the size change over the course of the rotation.
I read somewhere that throwing this change in a UIView animation block doesn't work because the font property is not animatable, so what are my options for doing this?
For my specific implementation I'm not simply scaling the UILabel/UITextView as-is; The bounds of the box is increasing more (proportionally) than the font-size, so there will be re-flow in the text. That's fine by me.
Edit: I would be fine with simply scaling the UITextView.
Also, I was considering "animating" it manually: I have a method that lays out my views and adjusts for font size. If I knew when the rotation was about to start, and the duration of the animation, I could time it so it renders an intermediate font size or two in the middle of the animation. Any help with getting those would be appreciated.
One option is to fade the old text out, change the font size, and fade it back in. The font property may not be animatable, but alpha is. Since alpha is a property of UIView, you can treat all your text-bearing views the same way: UILabel, UITextView, etc. It looks good, too.
If I knew when the rotation was about
to start, and the duration of the
animation...
Funny you should mention that. Just before the animation starts, your view controller will receive a willAnimateRotationToInterfaceOrientation:duration: message that gives you the exact information you need.
A way to proceed could be to:
Create a CAKeyframeAnimation
Define the scaling and rotation you want to achieve during that animation using a set of CATransform3D objects
Add these transforms to your keyframed animation
Send the addAnimation message to your label layer object: [[label layer] addAnimation];
Here would be a code sample assuming yourLabel is the UILabel you want to scale and rotate:
CAKeyframeAnimation *scale = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
CATransform3D scaleUp = CATransform3DMakeScale(1.5, 1.5, 1); // Scale in x and y
CATransform3D rotationScaled = CATransform3DRotate (scaleUp, 90, 0, 0, 1); // Rotate the scaled font
[scale setValues:[NSArray arrayWithObjects:
[NSValue valueWithCATransform3D:CATransform3DIdentity],
[NSValue valueWithCATransform3D:rotationScaled],
nil]];
// set the duration
[scale setDuration: 1.0];
// animate your label layer
[[yourLabel layer] addAnimation:scale forKey:#"scaleText"];
This is typically how a text bouncing around would be animated for instance.
You could start this when the device starts rotating and retrieve the animation when it's done rotating so you can update your label with the proper scale/position.
You will need tuning to find the proper timing and rotation.
Change the font size when didAnimateFirstHalfOfRotationToInterfaceOrientation: is called.
That way the user won't see the change once the rotation completed. It would be pretty hard to see the font size change then, as the rotation is happening!

iPhone landscape-only app view axis confusion

Using the information here: iPhone Landscape-Only Utility-Template Application I am able to launch, use and maintain my view as landscape only. However, I am confused about the axis.
The absolute axis behaves as expected, meaning (0,0) is the top left, (240,0) is the top center, (0,320) is the bottom left corner, etc. However, when I attempt to do calculations related to the view I am drawing on I find x,y to be oriented as if the portrait top left were the origin. So to draw something at the center-point in my view controller I need to do:
CGPoint center = CGPointMake(self.view.center.y, self.view.center.x);
I assume this due to the fact that the UIView referenced by my controllers self.view is giving it's values relative to it's enclosing frame, meaning the window which has it's axis origin on the top left in portrait mode.
Is there some simple way to account for this that I am missing?
Documentation suggests that the transform property is exactly what I am looking for, however, I experiencing further confusion here. There are 3 essential properties involved:
frame
bounds
center
If I do this in viewDidLoad:
// calculate new center point
CGFloat x = self.view.bounds.size.width / 2.0;
CGFloat y = self.view.bounds.size.height / 2.0;
CGPoint center = CGPointMake(y, x);
// set the new center point
self.view.center = center;
// Rotate the view 90 degrees counter clockwise around the new center point.
CGAffineTransform transform = self.view.transform;
transform = CGAffineTransformRotate(transform, -(M_PI / 2.0));
self.view.transform = transform;
Now according to the reference docs, if transform is not set to the identity transform self.view.frame is undefined. So I should work with bounds and center.
self.view.center is correct now, because I set it to what I wanted it to be.
self.view.bounds appears unchanged.
self.view.frame appears to be exactly what I want it to be, but as noted above, the reference claims it is invalid.
So while I can get what I believe to be the right numbers, I fear I am overlooking something critical that will become troublesome later.
Thanks.
With Quartz 2D, the coordinate system has its origin (0,0) at the lower-left corner of the graphic, not at the upper-left corner. Maybe that's what you are missing.
See: http://developer.apple.com/documentation/graphicsimaging/Conceptual/drawingwithquartz2d/dq_overview/dq_overview.html#//apple_ref/doc/uid/TP30001066-CH202-CJBBAEEC

Convert a CGPoint from a UIView coordinate system to a CALayer coordinate system

There are methods to transfer a CGPoint from one UIView to another and from one CALayer to another. I cannot find a way to change a CGPoint from a UIView's coordinate system to a CALayer's coordinate system.
Does a layer and it's host view have the same coordinate system? Do I need to transform points between them?
Thanks,
Corey
[EDIT]
Thanks for the answer! It is hard to find information on the differences/similarities between CA on the iPhone and Mac. I am surprised I couldn't find this issue addressed directly in any Apple Documentation.
I was pursuing this answer to help with a bug I am troubleshooting, and this was so far my best guess, but I suppose I am barking up the wrong tree. If the coordinate systems are the same, then I have another issue...
The actual issue I am having can be found here on Stack Overflow:
layer hit test only returning layer when bottom half of layer is touched
The documentation for hitTest says:
/* Returns the farthest descendant of the layer containing point 'p'.
* Siblings are searched in top-to-bottom order. 'p' is in the
* coordinate system of the receiver's superlayer. */
So you need to do (something like this):
CGPoint thePoint = [touch locationInView:self];
thePoint = [self.layer convertPoint:thePoint toLayer:self.layer.superlayer];
CALayer *theLayer = [self.layer hitTest:thePoint];
In terms of Apple documentation, I found an "iPhone OS Note" in the Layer Coordinate System section of the Core Animation Programming Guide (2008-11-13).
The default root layer of a UIView instance uses a flipped coordinate system that matches the default coordinate system of a UIView instance–the origin is in the top-left and values increase down and to the right. Layers created by instantiating CALayer directly use the standard Core Animation coordinate system.
You can set the transform property of CALayers to the appropriate transform if you want to flip their coordinate system, but note that this will probably flip their drawing of the contents as well (I have not tested that, but it makes sense that this would be true). My assertion that the CALayer associated with a UIView shares the same coordinate system could in fact be entirely erroneous. It could also be that CALayers use the same coordinate system as UIViews (i.e. they're never flipped vertically), but I thought they were since CoreGraphics uses a flipped coordinate system relative to UIKit.
A simple way to test would be to add a screen-sized CALayer as the sublayer of a view's layer, then add another small CALayer as a sublayer of that. You could set it to show up at (0, 0, 320, 100) and see if it shows up on the top or the bottom of the iPhone's screen. This will tell you in which direction the Y axis goes for CoreAnimation.
- (void)viewDidLoad {
[super viewDidLoad];
CALayer *rootLayer = [CALayer layer];
rootLayer.frame = self.view.layer.bounds;
CALayer *smallLayer = [CALayer layer];
smallLayer.frame = CGRectMake(0, 0, rootLayer.bounds.size.width, 50);
smallLayer.backgroundColor = [UIColor blueColor].CGColor;
[rootLayer addSublayer:smallLayer];
[self.view.layer addSublayer:rootLayer];
}
I just performed this test myself, and it appears CALayers actually use the same coordinate system as UIViews, so my assertion that CALayer's flip the Y axis is definitely wrong. However, if you do drawing with CoreGraphics directly, be aware that CoreGraphics does use a flipped Y axis (though when drawing in a UIView subclass or, I assume, a CALayer delegate, the CoreGraphics context has already been flipped to match the UIView's (or CALayer's) coordinate system).
So the short answer, if you made it this far, is the coordinate system for CALayer should match the coordinate system for its corresponding UIView.
From my tests I've found out that sublayers share the same coordinate system as it's parent layer and therefore if you are adding sublayer to a UIView layer then they will share the same coordinate system.
In case you still want to flip the coordinate system so that it's origin is in the upper left corner you should use this transformation that flips the y axis.
(x,y,1) = (x', y', 1) * [1 0 0],[0 -1 0],[0 heigth 1]
which translated to code is:
[your_layer setAffineTransform:CGAffineTransformMake(1,0,0,-1,0,heigth)];
I believe the coordinate system of a UIView and its associated layer should be the same. However, if you create layers yourself, the coordinate system is flipped vertically.
Methods that fixing frames and points
- (CGRect) fixRect:(CGRect)rect inRect:(CGRect)container
{
CGRect frame = rect;
frame.origin.y = container.size.height - frame.origin.y - frame.size.height;
return frame;
}
- (CGPoint) fixPoint:(CGPoint)point fromPoint:(CGSize)containerSize
{
CGPoint frame = point;
frame.y = size.height - frame.y;
return frame;
}