Smooth aspect change during orientation change - iphone

When the screen of the iPhone orientation changes, I adjust my projection matrix of my 3D rendering to the new aspect value. However, doing this in either willRotateToInterfaceOrientation or didRotateFromInterfaceOrientation would cause the aspect ratio being wrong during the transition, because at the beginning of the animation, the screen size is still the same as before and is changed to the new bounds gradually while being rotated. Therefore I want the aspect value used for my 3D projection matrix to change gradually as well. To achieve this, I retrieve start time and duration for the rotation animation in willAnimateRotationToInterfaceOrientation:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
_aspect.isChanging = YES;
_aspect.startedChanging = [[NSDate date] retain];
_aspect.changeDuration = duration;
_aspect.oldValue = self.renderer.aspect;
_aspect.currentValue = fabsf(self.view.bounds.size.width / self.view.bounds.size.height);
}
Note that the view bound size is already set to the new value that will be valid after the animation.
Then, in update, I do the following:
- (void)update
{
if (_aspect.isChanging) {
float f = MIN(1, [[NSDate date] timeIntervalSinceDate:_aspect.startedChanging] / _aspect.changeDuration);
self.renderer.aspect = _aspect.oldValue * (1-f) + _aspect.currentValue * f;
} else {
self.renderer.aspect = _aspect.currentValue;
}
[self.renderer update];
}
This already works quite well, but the render aspect change does not match the actual aspect change, which is because I'm interpolating it linearly. Therefore I tried to match the easing of the actual aspect change by throwing math functions at the problem: The best result I could get was by adding the following line:
f = 0.5f - 0.5f*cosf(f*M_PI);
This results in almost no visible stretching of the image during the rotation, however if you look closely, it still seems to be a bit unmatched somewhere in between. I guess, the end user won't notice it, but I'm asking here if there might be a better solution, so these are my questions:
What is the actual easing function used for the change in aspect ratio during the rotation change animation?
Is there a way to get the actual width and height of the view as it is displayed during the orientation change animation? This would allow me to retrieve the in-between aspect directly.

On the first bullet point, you can use CAMediaTimingFunction (probably with kCAMediaTimingFunctionEaseInEaseOut) to get the control points for the curve that defines the transition. Then just use a cubic bezier curve formula.
On the second bullet point, you can use [[view layer] presentationLayer] to get a version of that view's CALayer with all current animations applied as per their current state. So if you check the dimensions of that you should get the then current values — I guess if you act upon a CADisplayLink callback then you'll be at most one frame behind.

Related

Curving/warping views with CoreAnimation or OpenGL for carousel effect

Right now I'm populating a UIScrollView with a series of views. The views need to be warped to make the UIScrollView appear like a carousel. In other words when the user scrolls it needs to be like a circle. I've never done anything quite like this before, but I'm assuming CoreAnimation is out of the question and OpenGL needs to be used. If this is possible with CoreAnimation or Quartz then I really just need a sample on how to warp the views and I can figure the rest out myself but I'm not familiar with OpenGL.
If you want to warp the views, you'll either need OpenGL or you could use Core Animation's CAShapLayer which allows you to specify a bezier path which can have this curve in it. But keep in mind that this curving you're seeing is likely just an optical illusion (though in your image above it looks like an actual curve). If you get enough rectangles with the correct y axis rotation in a row, I think you can come up with the effect you're looking for with straight Core Animation. I'm pretty sure that's how things are implemented in the Core Animation demos Apple provided a couple years ago. Here's a screenshot from the video from that presentation:
I messed around with the transform of a view's layer a little bit and came up with this:
- (IBAction)sliderDidChange:(id)sender
{
CGFloat value = [(UISlider*)sender value];
CGFloat xOff = value - 0.5;
CATransform3D trans = CATransform3DIdentity;
trans.m34 = 1.0f / -1000.0f;
trans = CATransform3DRotate(trans, degreesToRadians(xOff * -25.0f), 0.0f, 1.0f, 0.0f);
trans = CATransform3DTranslate(trans, 0.0f, 0.0f, 900.0f * fabs(xOff));
[[frameView layer] setTransform:trans];
CGPoint center= [frameView center];
[frameView setCenter:CGPointMake(1024.0 * value, center.y)];
}
I threw together a demo project that shows how the rotation works in response to a slider. It doesn't use a scroll view so you would have to adapt it, but I think you can track the current scroll offset and apply the transform accordingly. Not sure if it will help but there it is.
In my experience, it is a bit tricky to get the values right. The way to give a view perspective is by manipulating it's layer transform. I have used the following method to achieve the transfor for a similar effect:
-(CATransform3D)makeTransformForAngle:(CGFloat)angle from:(CATransform3D)start{
CATransform3D transform = start;
// the following two lines are the key to achieve the perspective effect
CATransform3D persp = CATransform3DIdentity;
persp.m34 = 1.0 / -1000;
transform = CATransform3DConcat(transform, persp);
transform = CATransform3DRotate(transform,angle, 0.0, 1.0, 0.0);
return transform;
}
This was done to create a "flip page" animation, so you may need to adapt. To use it, do the following:
flip_page.layer.transform = [self makeTransformForAngle:angle from:CATransform3DIdentity];
where flip_page is a UIView. Cheers!

Position on UIImageView

Me again. I have a simple question. I have an UIImageView like the one shown below.
alt text http://img683.imageshack.us/img683/1999/volumen.png
That UIimageView is supposed to be the knob to control the volume of my iphone project. My question is, how to know the positions of bar on the UIImageView when it is rotated? Because the volume needs to be 0.5 when the little bar on the cercle is vertical.
I got a piece of code which is (in the touchMoved method):
float dx = locationT.x - imgVVolume.center.x;
float dy = locationT.y - imgVVolume.center.y;
CGFloat angleDif = 0.0f;
movedRotationAngle = atan2(dy,dx);
if (beganRotationAngle == 0.0) {
beganRotationAngle = movedRotationAngle;
initialTransform = imgVVolume.transform;
}
else {
angleDif = beganRotationAngle - movedRotationAngle;
CGAffineTransform newTrans = CGAffineTransformRotate(initialTransform, -angleDif);
imgVVolume.transform = newTrans;
}
Help please.
It depends on what input mechanism you want to use to control the rotation.
If the knob is to rotate based on a single finger touch dragging from side to side then you can create a UIPanGestureRecognizer and attach it to the knob UIImageView. The translationInView: method returns a CGPoint which is the amount of X and Y movement from the touch-down point. You can feed that into a formula like the one you post to get an angle of rotation. You'll want to keep track of delta from last position and also check for stop limits (like 0..360) to prevent over-rotation.
OTOH, if you're going to use two finger rotation then you'll want to use a UIRotationGestureRecognizer and look for the rotation value. Just feed that into a CGAffineTransformRotate and set it to the UIImageView transform. That takes care of all of the above for you. Again, you'll want to check for stop limits.

How to set the initial orientation of an animated UIImageView?

I am programming a gauge display for an iphone application: a centered needle UIImageView against a fixed background UIImageView. I am new to iPhone programming, and don't know much about CoreAnimation, but with looking through examples and fiddling with my anchorPoint setting and the position of the needle image against the background in InterfaceBuilder, my needle is coming up with my desired rotation point properly centered against the background:
self.needleIV.layer.anchorPoint = CGPointMake( 0.5, 0.68 );
My test animations work perfectly (the needle remains properly centered in the gauge, and rotates around my desired rotation point, the anchor point), using this code:
CABasicAnimation *rotateAnimation = [CABasicAnimation animation];
rotateAnimation.keyPath = #"transform.rotation.z";
rotateAnimation.fromValue = [NSNumber numberWithFloat:DegreesToRadians( (rand() % 270 - 135) )];
rotateAnimation.toValue = [NSNumber numberWithFloat:DegreesToRadians( (rand() % 270 - 135) )];
rotateAnimation.duration = 4.0;
rotateAnimation.removedOnCompletion = NO;
// leaves presentation layer in final state; preventing snap-back to original state
rotateAnimation.fillMode = kCAFillModeBoth;
rotateAnimation.repeatCount = 0;
rotateAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
// Add the animation to the selection layer. This causes it to begin animating.
[self.needleIV.layer addAnimation:rotateAnimation forKey:#"rotateAnimation"];
The problem is that at application launch the needle comes up initially pointing straight up (which is how the needle looks in the image file), and I can't figure out how to give it an initial rotation value which doesn't mess up the animation code. (By "mess up" I mean the needle doesn't rotate on the correct axis, and becomes uncentered in the gauge).
I've tried setting layer.transform to a 3d rotation matrix rotated around the z axis, but this moves the needle off center. I've tried this code as well:
[self.needleIV.layer setValue:[NSNumber numberWithFloat:DegreesToRadians(-135.0)] forKeyPath:#"transform.rotation.z"];
which didn't work either (needle moved off anchorPoint center), and I had high hopes since it's apparently using the same transform to the working animation code.
I also tried just putting the working animation snippet in my view controller's viewDidLoad function to "animate" the needle to it's start location, but it doesn't do anything at all -- apparently the animation doesn't run until the image is actually up and on the screen, and I've confirmed in the debugger that the screen is still black in viewDidLoad.
At this point I'm about to try just hacking in a timer to run an initial animation "long enough" after the app starts to set an animation that rotates the needle properly, but I'd really like to understand what's going wrong here, and why the animation is apparently rotating around a different axis than is my other attempts at pre-rotation/pre-animation.
The anchorPoint is relative to the bounds of the parent. Is it possible the bounds of the parent layer/view change after viewDidLoad?
You should be able to do all this using the UIView center and transform properties, which are 2D so implicitly rotate about the z axis.

Changing X & Y of Button upon Rotation

Edited Mar 30 - Note: Monotouch used. When I rotate to LandscapeRight/Left I want to change the location of some of my buttons. Here's what I'm trying:
Have my ShouldAutorotateToInterfaceOrientation returning True.
In my WillRotate (I tried putting this in DidRotate as well) it doesn't show any difference, here's my code:
public override void WillRotate (UIInterfaceOrientation toInterfaceOrientation, double duration)
{
// As a test move one of the buttons somewhere different than where it should be
System.Drawing.RectangleF rect = new System.Drawing.RectangleF(40, 1,
btn2.Bounds.Width, btn2.Bounds.Height);
btn2.Bounds = rect;
}
I figure that changing the bounds may not be the right way, but given that the Bounds.X and Bounds.Y are immutable, this was the only way I could find to change the position.
This is what you want...
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
It can be found here.
The idea is to adjust the X and Y of any objects you want to move around after you determine the direction of the move.

Is there a way to get a position (or center) state from a unfinished UIView animation?

I have a UIView doing a simple animation. It adjusts its y position from 100 to 130 and then reverses. I want it to keep repeating so I have the repeat counter set to 999. Upon user input, I want to take the same UIView and adjust the x position. This is done by means of another UIView animation. The problem is that when the 2nd animation starts, the 1st animation (that goes from 100 to 130 in the y direction) just ends abruptly (as I read that it should). Is there any way to get the final position of the y coordinate of that UIView before it was ended? I would ideally like to have the UIView stay in the same y position that it was in while I translate the x coordinates.
Summary: UIView moves in the y direction from 100-130, reverses and repeats until user input is received. Once received, animation is cut short and UIView jumps to y=130. I would like a way to find out what the final y value was before the animation was cut short, so when new animation with x translation is used, the UIView will not jump to 130, but remain the same position it was in when the 1st animation ended.
I can't seem to see anything that would let you do that. It appears to me that once you set the animation in motion with UIView, then it (and all current state changes) are out of your hands and will only be "returned" to your control and availability once the animation is done and at the designated end point. Is this correct? Any insight would be appreciated. Thank you for your time.
You're looking for the "presentation layer".
Each UIView is rendered using a Core Animation layer, which is accessible from UIView's layer property.
CALayer has a presentationLayer method, which returns a CALayer that represents "a close approximation to the version of the layer that is currently being displayed".
So, to get the current position of your view:
CGRect currentViewFrame = [[myView.layer presentationLayer] frame];
I'm not sure if this is exactly what you want, but UIView's setAnimationBeginsFromCurrentState: used inside a beginAnimations block will cause the x animation to start wherever the y is in progress.
Perhaps for what you are trying to accomplish it would benefit to exercise a little more control over the animation. For this task I would suggestion using an NSTimer scheduled at whatever interval works best for you (1/30 to 1/60) and just adjust the UIView position every time the timer fires. This way you can always access the X and Y components of the view's position.
Something like:
- (void)timerFired:(NSTimer *)timer {
CGPoint p = view.center;
if (p.y >= 130.0) {
positiveMovement = NO;
}
else if (p.y <= 100.0) {
positiveMovement = YES;
}
if (positiveMovement)
p.y++;
else
p.y--;
view.center = p;
}
positiveMovement would just be a BOOL instance variable. You could also then just directly adjust the X value of the view's position elsewhere with user input and it would update accordingly.