How to reflect a rotated image view - ios

I am building an iOS app and I need a way to reflect a rotating (by CABasicAnimation) image to the surface below, like a translucent material effect. Here is my code for the images named indicator and indicatorReflection to initialize:
#define rotation_reflected(ANG) CGAffineTransformMakeRotation(M_PI/2 - (ANG * M_PI / 180.0))
#define rotation(ANG) CGAffineTransformMakeRotation(-M_PI/2 - (ANG * M_PI / 180.0))
[self rotateIndicator:0];
-(void)rotateIndicator:(float)degrees{
self.indicatorView.transform = rotation(degrees);
self.indicatorReflectionView.transform = rotation_reflected(degrees);
}
I animate them using the following code, afterwards:
-(void)startWanderingIndicator{
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform"];
CATransform3D xform = CATransform3DMakeAffineTransform(rotation(180));
anim.toValue = [NSValue valueWithCATransform3D:xform];
anim.duration = 4.0f;
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
[self.indicatorView.layer addAnimation:anim forKey:#"rotation"];
anim = [CABasicAnimation animationWithKeyPath:#"transform"];
xform = CATransform3DMakeAffineTransform(rotation_reflected(180));
anim.toValue = [NSValue valueWithCATransform3D:xform];
anim.duration = 4.0f;
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
[self.indicatorReflectionView.layer addAnimation:anim forKey:#"rotation"];
}
There is no problem with the first one. The problem begins with the reflected view. I've tried almost all the +/- PI/2 and +/- ANGLE combinations, but I can never make the reflected view to follow the correct path of reflection. I'm not a trigonometry guy, but this should be something very trivial to anyone who knows some math, and besides that, I've tried all the combinations that are possible, and one of them should be the correct answer anyway. Is there a problem with my rotation calculation code, or is it something to do with the animation/transform methods?
Thanks,
Can.

Your functions should probably be:
#define rotation_reflected(ANG) CGAffineTransformMakeRotation(M_PI/2 + (ANG * M_PI / 180.0))
#define rotation(ANG) CGAffineTransformMakeRotation(-M_PI/2 - (ANG * M_PI / 180.0))
Note the + sign in the first line; you want the two objects to rotate in the opposite directions. You're still not going to have the right appearance, though, unless you flip one of your views (mirroring can't be simulated by rotations alone). Try making a subview which has a scale transform of -1 in, say, the y axis.
This might not do what you want, though, because there's no way for the transform to know which direction you're trying to rotate in. (Imagine you were rotating from noon to 6 o'clock; you'd specify from up to down, but the CABasicAnimation doesn't know if you mean clockwise or counter-clockwise; there's no "sign" to a transform, so it can't tell 180 degrees from -180 degrees.)
The way to get the desired effect is to use CAValueFunction. Rather than specifying the from and to transforms, you specify what you want to do (rotate around the Z axis) and from what angle you want to rotate from and to (in this case, it will respect the sign). To quote the CAValueFunction docs:
You use a value transform function that rotates from 0° to 180° around
the z-axis by creating a CAValueTransform function specifying the
kCAValueFunctionRotateZ and then creating an animation with a
fromValue of 0, a toValue of M_PI, and set the animation’s
valueTransform property to the value transform instance.
So, you'd want something like:
-(void)startWanderingIndicator{
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform"];
anim.valueFunction = [CAValueFunction functionWithName:kCAValueFunctionRotateZ];
anim.toValue = rotation(180);
anim.duration = 4.0f;
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
[self.indicatorView.layer addAnimation:anim forKey:#"rotation"];
anim = [CABasicAnimation animationWithKeyPath:#"transform"];
anim.valueFunction = [CAValueFunction functionWithName:kCAValueFunctionRotateZ];
anim.toValue = rotation_reflected(180);
anim.duration = 4.0f;
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
[self.indicatorReflectionView.layer addAnimation:anim forKey:#"rotation"];
}

Related

Changing the speed of CALayer in CABasicAnimation while rotating a wheel causes jerk effect

I develop an app which requires a wheel to be rotated around z axis with increasing or decreasing the speed of the wheel steadily over time. I use CABasicAnimation & my code is as follows. While i change the speed property of the layer at particular interval, it causes "Jerk" effect to the wheel.
/****/
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.toValue = [NSNumber numberWithFloat:-2*M_PI];
animation.duration = 4.0f;
animation.repeatCount = INFINITY;
[animation setValue:#"left" forKey:#"side"];
[animation setDelegate:self];
animation.removedOnCompletion=NO;
animation.fillMode = kCAFillModeForwards;
animation.cumulative = YES;
imageLeft.layer.beginTime = CACurrentMediaTime();
/************/
In a timer I vary the speed of the CALayer of the imageview as follows where dPlayedPercentage is a variable.
imageLeft.layer.speed=1.0+dPlayedPercentage;
[imageLeft.layer addAnimation:animation forKey:#"SpinAnimation"];
I think it is due to the position resets while changing the speed property of CALayer. What should i do to rectify this. Or any other way to do this animation?
Adding the following code has rectified the jerk in the animation.
imageLeft.layer.timeOffset = [imageLeft.layer convertTime:CACurrentMediaTime() fromLayer:nil];
imageLeft.layer.beginTime = CACurrentMediaTime();
imageLeft.layer.speed=1.0+dPlayedPercentage;
For more dynamic speed change, I has some issue with the previous answer (layer not drawing at all) since the timeOffset needed to be calculated with the new speed in mind.
(source https://coveller.com/2016/05/core_animation_timing)
The base formula for the timeOffset is:
timeOffset = CACurrentMediaTime() - ((convertTime - beginTime) x speed)
In code:
theLayer.speed = newSpeed
let mediaTime = CACurrentMediaTime()
let converedTime = theLayer.convertTime(mediaTime, to: nil)
theLayer.beginTime = mediaTime
let offset = mediaTime - ((converedTime - theLayer.beginTime) * Double(newSpeed))
theLayer.timeOffset = offset

Move UIImage across screen using core animation

DO NOT POST ANSWERS ABOUT UIVIEW ANIMATIONS I understand UIView, I am learning core-animation. Im trying to get an image to move 50 units to the right but I am having many issues. First when the animation is called the image jumps to a new location, runs, then jumps back to the original location. I want it to simply move 50 units to the right, stop, move again if the button is pressed. I have spent a lot of time researching and I can't seem to find the problem. My Code:
-(IBAction)preform:(id)sender{
CGPoint point = CGPointMake(imView.frame.origin.x, imView.frame.origin.y);
imView.layer.position = point;
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.fromValue = [NSValue valueWithCGPoint:point];
anim.toValue = [NSValue valueWithCGPoint:CGPointMake(point.x + 50, point.y)];
anim.duration = 1.5f;
anim.repeatCount =1;
anim.removedOnCompletion = YES;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[imView.layer addAnimation:anim forKey:#"position.x"];
imView.layer.position = point;
}
I see several problems in your code.
First, you're grabbing the frame origin like this:
CGPoint point = CGPointMake(imView.frame.origin.x, imView.frame.origin.y);
Although it's not really a problem, you could simply do this:
CGPoint point = imView.frame.origin;
The problem is that you're setting the layer's position to its frame's origin. But by default, the position controls the center of the view, and the frame's origin is its upper-left corner. You probably want to just pick up the layer's position in the first place:
CGPoint point = imView.layer.position; // equivalent to imView.center
Second, you're using the position.x key path, which wants a CGFloat value, but you're providing a CGPoint value. This doesn't appear to cause a problem in the iOS 6.1 simulator, but it's probably a bad idea to assume it will always work.
Third, you need to understand that an animation does not change the properties of your layer! Each of the layers you normally manipulate (technically called a model layer) has an associated presentation layer. The presentation layer's properties control what is on the screen. When you change a model layer's property, Core Animation usually sets up an animation (from the old value to the new value) on the presentation layer automatically. This is called an implicit animation. A UIView suppresses implicit animations on its layer.
When the animation on the presentation layer ends and is removed, the presentation layer's properties revert to its model layer's values. So to make the change permanent, you need to update the model layer's properties. Generally it's best to update the model layer's properties first, then add the animation, so that your explicit animation overwrites the implicit animation (if one was created).
As it happens, although you can animate position.x, you need to set position on the model layer to make it stick. I tested this to work:
- (IBAction)perform:(id)sender {
CGPoint point0 = imView.layer.position;
CGPoint point1 = { point0.x + 50, point0.y };
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.fromValue = #(point0.x);
anim.toValue = #(point1.x);
anim.duration = 1.5f;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
// First we update the model layer's property.
imView.layer.position = point1;
// Now we attach the animation.
[imView.layer addAnimation:anim forKey:#"position.x"];
}
(Note that I called my method perform:. The method in the original post seems to be misspelled.)
If you want to really understand Core Animation, I highly recommend watching the Core Animation Essentials video from WWDC 2011. It's an hour long and contains a ton of useful information.
This is what you should need to get it working. The position on the layer is already set so you can use that as the fromValue and just modify it to get the toValue. The other important step is to set the layers position to be endPos so that when the animation finishes, the image view will stay at the correct position.
-(IBAction)preform:(id)sender
{
CGPoint startPos = imView.layer.position;
CGPoint endPos = CGPointMake(point.x + 50, point.y);
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.fromValue = [NSValue valueWithCGPoint:startPos];
anim.toValue = [NSValue valueWithCGPoint:endPos];
anim.duration = 1.5f;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[imView.layer addAnimation:anim forKey:#"position.x"];
imView.layer.position = endPos;
}
jumping back is caused by the fact that you remove animation on completion
anim.removeOnCompletion = NO;
EDIT #3: just copied and tried your code here is the fix for all of your issues, pretty much self explanatory :
CGPoint point = imView.center;
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.fromValue = [NSNumber numberWithFloat:point.x];
anim.toValue = [NSNumber numberWithFloat:point.x+50.0];//[NSValue valueWithCGPoint:CGPointMake(point.x + 50, point.y)];
anim.duration = 1.5f;
anim.repeatCount =1;
anim.removedOnCompletion = NO;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
anim.fillMode=kCAFillModeForwards;
[imView.layer addAnimation:anim forKey:#"position.x"];
[sender.layer addAnimation:anim forKey:#"position.x"];
sender.layer.position=CGPointMake(point.x+50.0,sender.layer.position.y);
That fact that it jumps back even though removedOnCompletion is set to no is a head scratcher. Still playing around with it.

Cannot get current position of CALayer during animation

I am trying to achieve an animation that when you hold down a button it animates a block down, and when you release, it animates it back up to the original position, but I cannot obtain the current position of the animating block no matter what. Here is my code:
-(IBAction)moveDown:(id)sender{
CGRect position = [[container.layer presentationLayer] frame];
[movePath moveToPoint:CGPointMake(container.frame.origin.x, position.y)];
[movePath addLineToPoint:CGPointMake(container.frame.origin.x, 310)];
CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
moveAnim.path = movePath.CGPath;
moveAnim.removedOnCompletion = NO;
moveAnim.fillMode = kCAFillModeForwards;
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim, nil];
animGroup.duration = 2.0;
animGroup.removedOnCompletion = NO;
animGroup.fillMode = kCAFillModeForwards;
[container.layer addAnimation:animGroup forKey:nil];
}
-(IBAction)moveUp:(id)sender{
CGRect position = [[container.layer presentationLayer] frame];
UIBezierPath *movePath = [UIBezierPath bezierPath];
[movePath moveToPoint:CGPointMake(container.frame.origin.x, position.y)];
[movePath addLineToPoint:CGPointMake(container.frame.origin.x, 115)];
CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
moveAnim.path = movePath.CGPath;
moveAnim.removedOnCompletion = NO;
moveAnim.fillMode = kCAFillModeForwards;
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim, nil];
animGroup.duration = 2.0;
animGroup.removedOnCompletion = NO;
animGroup.fillMode = kCAFillModeForwards;
[container.layer addAnimation:animGroup forKey:nil];
}
But the line
CGRect position = [[container.layer presentationLayer] frame];
is only returning the destination position not the current position. I need to basically give me the current position of the container thats animating once I release the button, so I can perform the next animation. What I have now does not work.
I haven't analyzed your code enough to be 100% sure why [[container.layer presentationLayer] frame] might not return what you expect. But I see several problems.
One obvious problem is that moveDown: doesn't declare movePath. If movePath is an instance variable, you probably want to clear it or create a new instance each time moveDown: is called, but I don't see you doing that.
A less obvious problem is that (judging from your use of removedOnCompletion and fillMode, in spite of your use of presentationLayer) you apparently don't understand how Core Animation works. This turns out to be surprisingly common, so forgive me if I'm wrong. Anyway, read on, because I will explain how Core Animation works and then how to fix your problem.
In Core Animation, the layer object you normally work with is a model layer. When you attach an animation to a layer, Core Animation creates a copy of the model layer, called the presentation layer, and the animation changes the properties of the presentation layer over time. An animation never changes the properties of the model layer.
When the animation ends, and (by default) is removed, the presentation layer is destroyed and the values of the model layer's properties take effect again. So the layer on screen appears to “snap back” to its original position/color/whatever.
A common, but wrong way to fix this is to set the animation's removedOnCompletion to NO and its fillMode to kCAFillModeForwards. When you do this, the presentation layer hangs around, so there's no “snap back” on screen. The problem is that now you have the presentation layer hanging around with different values than the model layer. If you ask the model layer (or the view that owns it) for the value of the animated property, you'll get a value that's different than what's on screen. And if you try to animate the property again, the animation will probably start from the wrong place.
To animate a layer property and make it “stick”, you need to change the model layer's property value, and then apply the animation. That way, when the animation is removed, and the presentation layer goes away, the layer on screen will look exactly the same, because the model layer has the same property values as its presentation layer had when the animation ended.
Now, I don't know why you're using a keyframe to animate straight-line motion, or why you're using an animation group. Neither seems necessary here. And your two methods are virtually identical, so let's factor out the common code:
- (void)animateLayer:(CALayer *)layer toY:(CGFloat)y {
CGPoint fromValue = [layer.presentationLayer position];
CGPoint toValue = CGPointMake(fromValue.x, y);
layer.position = toValue;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:fromValue];
animation.toValue = [NSValue valueWithCGPoint:toValue];
animation.duration = 2;
[layer addAnimation:animation forKey:animation.keyPath];
}
Notice that we're giving the animation a key when I add it to the layer. Since we use the same key every time, each new animation will replace (remove) the prior animation if the prior animation hasn't finished yet.
Of course, as soon as you play with this, you'll find that if you moveUp: when the moveDown: is only half finished, the moveUp: animation will appear to be at half speed because it still has a duration of 2 seconds but only half as far to travel. We should really compute the duration based on the distance to be travelled:
- (void)animateLayer:(CALayer *)layer toY:(CGFloat)y withBaseY:(CGFloat)baseY {
CGPoint fromValue = [layer.presentationLayer position];
CGPoint toValue = CGPointMake(fromValue.x, y);
layer.position = toValue;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:fromValue];
animation.toValue = [NSValue valueWithCGPoint:toValue];
animation.duration = 2.0 * (toValue.y - fromValue.y) / (y - baseY);
[layer addAnimation:animation forKey:animation.keyPath];
}
If you really need it to be a keypath animation in an animation group, your question should show us why you need those things. Anyway, it works with those things too:
- (void)animateLayer:(CALayer *)layer toY:(CGFloat)y withBaseY:(CGFloat)baseY {
CGPoint fromValue = [layer.presentationLayer position];
CGPoint toValue = CGPointMake(fromValue.x, y);
layer.position = toValue;
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:fromValue];
[path addLineToPoint:toValue];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animation.path = path.CGPath;
animation.duration = 2.0 * (toValue.y - fromValue.y) / (y - baseY);
CAAnimationGroup *group = [CAAnimationGroup animation];
group.duration = animation.duration;
group.animations = #[animation];
[layer addAnimation:group forKey:animation.keyPath];
}
You can find the full code for my test program in this gist. Just create a new Single View Application project and replace the contents of ViewController.m with the contents of the gist.

CABasicAnimation returns to the original position before the next animation

I want to implement a method to rotate a UIButton based on user inputs. Upon the first input, it should rotate 45 degrees to the left. Upon the second input, it should rotate for another 45degrees from the position it stopped after the first rotation.
But the button goes back to its very original position before starting the 2nd animation. Following is the method I use.
- (void)spinLayer:(CALayer *)inLayer duration:(CFTimeInterval)inDuration
direction:(int)direction
{
CABasicAnimation* rotationAnimation;
// Rotate about the z axis
rotationAnimation =
[CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
// Rotate 360 degress, in direction specified
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 1/2 * direction];
// Perform the rotation over this many seconds
rotationAnimation.duration = inDuration;
rotationAnimation.removedOnCompletion = NO;
rotationAnimation.fillMode = kCAFillModeForwards;
// Set the pacing of the animation
rotationAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
// Add animation to the layer and make it so
[inLayer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}

rotate a UIView around its center but several times

I'm trying to rotate some UIView around its center, so the simple code goes something like
(in pseudocode):
[UIView beginAnimations:#"crazyRotate" context:nil];
[UIView setAnimationDuration:1.0];
someview.transform = CGAffineTransformMakeRotation(angle);
[UIView commitAnimations]
now if I set angle to say M_PI/2 the thing rotates nicely.
if I set it to 2*M_PI, well it does "nothing". I can understand that the matrix translates to something that does nothing (rotating 360 means "stay" in a sense),
yet, I want to rotate it 5 times (think of a newspaper rotate scale coming at you effect -- I'm not great at describing, hope someone understands).
So, I tried adding setting angle to 180 deg (M_PI) and add a nested animatationBlock.
but I guess that since I'm setting the same property (someview.transition) again it ignores it somehow).
I tried setting repeat count of the animation to 2 with angle M_PI but it seems to simply rotate 180, going back to straight position and then initiating the rotate again.
So, I'm a little out of ideas,
any help appreciated!
--t
You can use the following animation on your UIView's layer property. I've tested it.
Objective-C
UIView *viewToSpin = ...;
CABasicAnimation* spinAnimation = [CABasicAnimation
animationWithKeyPath:#"transform.rotation"];
spinAnimation.toValue = [NSNumber numberWithFloat:5*2*M_PI];
[viewToSpin.layer addAnimation:spinAnimation forKey:#"spinAnimation"];
Swift 5.0
let viewToSpin = UIView() // However you have initialized your view
let spinAnimation = CABasicAnimation.init(keyPath: "transform.rotation")
spinAnimation.toValue = NSNumber(value: 5.0 * 2.0 * Float.pi)
viewToSpin.layer.add(spinAnimation, forKey: "spinAnimation")
As Brad Larson indicated, you can do this with a CAKeyframeAnimation. For instance,
CAKeyframeAnimation *rotationAnimation;
rotationAnimation =
[CAKeyframeAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.values = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0 * M_PI],
[NSNumber numberWithFloat:0.75 * M_PI],
[NSNumber numberWithFloat:1.5 * M_PI],
[NSNumber numberWithFloat:2.0 * M_PI], nil];
rotationAnimation.calculationMode = kCAAnimationPaced;
rotationAnimation.removedOnCompletion = NO;
rotationAnimation.fillMode = kCAFillModeForwards;
rotationAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
rotationAnimation.duration = 10.0;
CALayer *layer = [viewToSpin layer];
[layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
You can control the duration of the total animation with the rotationAnimation.duration property, and the acceleration and deceleration (and calculation of steps in between) with the rotationAnimation.timingFunction property.
Getting a continuous spinning effect is a little tricky, but I describe a means to do it here. Yes, Core Animation seems to optimize transforms to the closest ending position within the unit circle. The method I describe there chains a few half-rotation animations together to make full rotations, although you do notice a slight stutter in the handoff from one animation to the next.
Perhaps a CAKeyframeAnimation constructed with these half-rotation values would be the right way to go. Then you could also control acceleration and deceleration.
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat: 2*M_PI];
animation.duration = 8.0f;
animation.repeatCount = INFINITY;
[self.myView.layer addAnimation:animation forKey:#"SpinAnimation"];

Resources