This is my method to move a player (aka ball synchronized with label):
int x2 = self.ball.frame.origin.x;
int y2 = self.ball.frame.origin.y;
double distance = sqrt(pow((x2 - coords.x), 2.0) + pow((y2 - coords.y), 2.0));
// 140 is just an arbitrary multiplier
NSTimeInterval time = distance / 140;
animating = YES;
[UIView animateWithDuration:time delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
[self.ball setFrame:CGRectMake(coords.x, coords.y, 48, 48)];
[self.label setFrame:CGRectMake(coords.x - 30, coords.y - 25, 108, 20)];
} completion:^(BOOL finished) {
animating = NO;
}];
This code works on one tap, but when a player redirects his move (touches somewhere else while still animating, hence the UIViewAnimationOptionBeginFromCurrentState), the distance is wrong. This is because the frame is set to the end point of the first touch, instead of where the animation ended off.
You probably want to get the starting frame position from the object layer.presentationLayer property.
The docs say:
The layer object returned by this method provides a close
approximation of the layer that is currently being displayed onscreen.
While an animation is in progress, you can retrieve this object and
use it to get the current values for those animations.
Related
I'm trying to make a circle expand quickly for the first 1-2 seconds and then decrease in speed by which it grows. I thought that a logarithmic scale would be best suited for this, but I don't know how to create one. I'm using the following code to animate the circle:
// Create a view with a corner radius as the circle
self.circle = [[UIView alloc] initWithFrame:CGRectMake(currentPos.x, currentPos.y, 10, 10)];
[self.circle.layer setCornerRadius:self.circle.frame.size.width / 2];
[self.circle setBackgroundColor:[UIColor clearColor]];
self.circle.layer.borderColor = [UIColor redColor].CGColor;
self.circle.layer.borderWidth = .5f;
UIPanGestureRecognizer *move = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[self.circle addGestureRecognizer:move];
[self.view addSubview:self.circle];
[UIView animateWithDuration:5 delay:0 options:UIViewAnimationOptionAllowUserInteraction animations:^(void){
// Animate it to double the size
const CGFloat scale = 2;
[self.circle setTransform:CGAffineTransformMakeScale(scale, scale)];
} completion:nil];
The easiest way is to use the built in animation options, you can set the animation ease (UIViewAnimationOptionCurveEaseOut)
[UIView animateWithDuration:2 delay:0 options:UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction animations:^(void){
// Animate it to double the size
const CGFloat scale = 2;
[self.circle setTransform:CGAffineTransformMakeScale(scale, scale)];
} completion:nil];
These are the built in ease types:
If you wanted something different to this then you'd need to do it yourself I believe.
Thanks to this post for the ease images
How to create custom easing function with Core Animation?
I would use animateKeyframesWithDuration:. This lets you set the scale at different points during the animation (so it can be non-linear). You do this with separate 'addKeyFrameWithRelativeStartTime:` calls. For example:
double total_duration = 5;
[UIView animateKeyframesWithDuration:total_duration delay:0 options:UIViewKeyframeAnimationOptionAllowUserInteraction animations:^(void){
const CGFloat final_scale = 2;
double acceleration = 1000; // the bigger this number, the bigger the initial acceleration
double multiplier = (final_scale - 1) / (logf(1+ (1/acceleration)) - logf(1/acceleration));
double addon = 1 - multiplier * logf(1/acceleration);
double segments = 20;
for (int segment = 0 ; segment < segments ; segment++) {
[UIView addKeyframeWithRelativeStartTime:(segment/segments) relativeDuration:(1.0/segments) animations:^(void){
double scale = multiplier * logf(segment/segments + (1/acceleration)) + addon;
self.circle.transform = CGAffineTransformMakeScale(scale,scale);
}];
}
} completion:nil];
achieves roughly what you want (though forgive the messy maths, it can probably be simplified)!
I don't think view animations allow any curves other than linear and the "ease" style animations.
As I recall, Core Animation allows you to define a custom timing function using a cubic bezier curve. You should be able to create a bezier curve that approximates a log curve.
See the docs on CAMediaTimingFunction for info on creating custom timing functions for Core Animation methods.
Be warned, though, that Core Animation is a pretty involved subject. Core Animation methods are not nearly as easy to use as UIView animations like the code you posted.
I am trying to reproduce an effect like the iOS 7 app switcher where I can scroll between smaller, transformed views. My current obstacle is getting the contentOffset
In order to do this, I have added a scrollView to my view, and sized it to the bounds of the view. I then create a container view that holds on to all the 'cards' -- it is this container view to which I apply the transform.
Anyway, the issue I am having is with adjusting where a particular card appears after it has the transform applied -- right now, I am unable to adjust the contentOffset during the transform in a manner that is smooth.
Anyway, code would help! Here it is:
NSInteger idx = selectedCardIndex;
if (TransformApplied)
{
[UIView animateWithDuration:0.5 animations:^{
[self.container setTransform:CGAffineTransformIdentity];
[self.container setFrame:CGRectOffset(self.container.frame, (1-ScaleFactor)/2 * NumCards * ScreenWidth, 0)];
[self.scrollView setContentSize:self.container.frame.size];
[self.scrollView setContentOffset:CGPointMake(ScreenWidth * idx, 0) animated:NO];
[self.scrollView setPagingEnabled:YES];
} completion:^(BOOL finished) {
TransformApplied = NO;
}];
}
else
{
[UIView animateWithDuration:0.5 animations:^{
[self.container setTransform:CGAffineTransformMakeScale(ScaleFactor, ScaleFactor)];
[self.container setFrame:CGRectOffset(self.container.frame, (-1)*((1-ScaleFactor)/2) * NumCards * ScreenWidth, 0)];
[self.scrollView setContentSize:CGSizeMake(ScreenWidth * ScaleFactor * NumCards, ScreenHeight * ScaleFactor)];
[self.scrollView setContentOffset:CGPointMake(ScaleFactor * ScreenWidth * idx, 0) animated:NO];
[self.scrollView setPagingEnabled:NO];
} completion:^(BOOL finished) {
TransformApplied = YES;
}];
}
More
I am noticing that everything is smooth when applying the identity transform, but not when applying the smaller transform, I get a weird 'jump' in the frame before the animation.
If i comment out the content offset adjustment in the scale transform (the bottom block) then everything runs smoothly, but the contentOffset is wrong after the scale transform.
Lastly, calling setContentOffset:animated: results in a wonky animation so that is a no-go.
Any help greatly appreciated!!!!
UPDATE
I've looked in to anchor point/ position properties on CALayer, and I've got the animation effects working perfectly with this:
NSInteger idx = selectedCardIndex;
if (TransformApplied)
{
[UIView dd_AnimateWithDuration:0.5 animations:^{
[self.container setTransform:CGAffineTransformIdentity];
[self.scrollView setContentOffset:CGPointMake(ScreenWidth * idx, 0) animated:NO];
[self.scrollView setPagingEnabled:YES];
} completion:^(BOOL finished) {
TransformApplied = NO;
}];
}
else
{
CGPoint anchor = CGPointMake((idx * ScreenWidth + ScreenWidth/2)/CGRectGetWidth(self.container.frame), 0.5f);
[self.container.layer setAnchorPoint:anchor];
CGPoint position = CGPointMake((anchor.x) * self.container.frame.size.width, self.container.layer.position.y);
[self.container.layer setPosition:position];
[UIView dd_AnimateWithDuration:0.5 animations:^{
[self.container setTransform:CGAffineTransformMakeScale(ScaleFactor, ScaleFactor)];
[self.scrollView setPagingEnabled:NO];
} completion:^(BOOL finished) {
TransformApplied = YES;
}];
}
However, now i've got to figure out a way to get rid of all the extra space in the scrollview without allowing the position of the container to change....
I have had a similar issue a long time ago about the position of a view. After a long research, I ended up with the UIView documentation. It says about transform and frame properties of a UIView with a warning:
Warning:
If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.
Changing the frame rectangle automatically redisplays the receiver without invoking the drawRect: method. If you want the drawRect: method invoked when the frame rectangle changes, set the contentMode property to UIViewContentModeRedraw.
Changes to this property can be animated. However, if the transform property contains a non-identity transform, the value of the frame property is undefined and should not be modified. In that case, you can reposition the view using the center property and adjust the size using the bounds property instead.
So, the basic idea is that, you shouldn't rely on or change frame when your view's transform contains a non-identity transform.
And the more important idea, always give a chance to the official documentation before looking elsewhere.
I hope this helps.
I am using a CircleView class that basically inherits off UIView and implements drawRect to draw a circle. This all works, hurrah!
What I cannot figure out though is how to make it so when I touch it (touch code is implemented) the circle grows or pops. Normally I'd use the UIKit animation framework to do this but given I am basically overriding the drawRect function to directly draw the circle. So how do I animate this?
- (void)drawRect:(CGRect)rect{
CGContextRef context= UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, _Color.CGColor);
CGContextFillEllipseInRect(context, CGRectMake(0, 0, self.frame.size.width, self.frame.size.height));
}
- (void)handleSingleTap:(UITapGestureRecognizer *)recognizer {
// Animate?
}
The answers depends on what you mean by "grows or pops". When I hear "pop" I assume that the view scales up over a short period of time ans scales back down again to the original size. Something that "grows" on the other hand would scale up but not down again.
For something that scales up and down again over a short period of time I would use a transform to scale it. Custom drawing or not, UIView has build in support for animating a simple transform. If this is what you are looking for then it's not more then a few lines of code.
[UIView animateWithDuration:0.3
delay:0.0
options:UIViewAnimationOptionAutoreverse // reverse back to original value
animations:^{
// scale up 10%
yourCircleView.transform = CGAffineTransformMakeScale(1.1, 1.1);
} completion:^(BOOL finished) {
// restore the non-scaled state
yourCircleView.transform = CGAffineTransformIdentity;
}];
If on the other hand you want the circle to grow a little bit more on every tap then this won't do it for you since the view is going to look pixelated when it scales up. Making custom animations can be tricky so I would still advice you to use a scaling transform for the actual animation and then redraw the view after the animation.
[UIView animateWithDuration:0.3
animations:^{
// scale up 10%
yourCircleView.transform = CGAffineTransformMakeScale(1.1, 1.1);
} completion:^(BOOL finished) {
// restore the non-scaled state
yourCircleView.transform = CGAffineTransformIdentity;
// redraw with new value
yourCircleView.radius = theBiggerRadius;
}];
If you really, really want to do a completely custom animation then I would recommend that you watch Rob Napiers talk on Animating Custom Layer Properties, his example is even the exact thing you are doing (growing a circle).
If you want an animation that expands the ellipse from the centre, try this. In the header, define 3 variables:
BOOL _isAnimating;
float _time;
CGRect _ellipseFrame;
Then implement these 3 methods in the .m file:
- (void)drawRect:(CGRect)rect; {
[super drawRect:rect];
CGContextRef context= UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, _Color.CGColor);
CGContextFillEllipseInRect(context, _ellipseFrame);
}
- (void)expandOutward; {
if(_isAnimating){
_time += 1.0f / 30.0f;
if(_time >= 1.0f){
_ellipseFrame = self.frame;
_isAnimating = NO;
}
else{
_ellipseFrame = CGRectMake(0.0f, 0.0f, self.frame.size.width * _time, self.frame.size.height * _time);
_ellipseFrame.center = CGPointMake(self.frame.size.width / 2.0f, self.frame.size.height / 2.0f);
[self setNeedsDisplay];
[self performSelector:#selector(expandOutward) withObject:nil afterDelay:(1.0f / 30.0f)];
}
}
}
- (void)handleSingleTap:(UITapGestureRecognizer *)recognizer; {
if(_isAnimating == NO){
_time = 0.0f;
_isAnimating = YES;
[self expandOutward];
}
}
This is the most basic way you can animate the circle expanding from the centre. Look into CADisplayLink for a constant sync to the screen if you want more detailed animations. Hope that Helps!
I have a UIView at the bottom left of its superview, i.e.its frame is like
[myView setFrame:CGRectMake(0,superView.bounds.size.height-myViewHeight,myViewWidth,myViewHeight)];
I want to animate its size such that its bottom left point remains fixed at its position. I am not sure how to play with anchor point property of layers.
Currently I am increasing its size using the following lines of code.
[UIView animateWithDuration:0.2 animations:^{
[bottomLbl setFrame:CGRectMake(0, bottomView.bounds.size.height-250, 300, 250)];
This works fine but when I try to bring back to its original size, its bottom point gets lifted at the start of animation and settles down at the end of animation.
First define the point for the bottom left corner...
CGPoint bottomLeft = CGPointMake(0, 480); // this can be anything.
Then you need to define the sizes (big and small).
CGSize bigSize = CGSizeMake(280, 280);
CGSize smallSize = CGSizeMake(50, 50); // again, these can be anything.
Then animate them...
// create the rect for the frame
CGRect bigFrame = CGRectMake(bottomLeft.x, bottomLeft.y - bigSize.height, bigSize.width, bigSize.height);
CGRect smallFrame = CGRectMake(bottomLeft.x, bottomLeft.y - smallSize.height, smallSize.width, smallSize.height);
[UIView animateWithDuration:0.2
animations:^{
// set the rect onto the view
bottomLbl.frame = bigFrame;
// or
bottomLbl.frame = smallFrame;
}];
As long as you set the frame and the calculated end frames are correct and it's all done in one animation then the bottom left corner won't move.
If this doesn't help can you please post a video of what is happening (and all your animation code).
EDIT
[UIView animateWithDuration:0.2
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
// set the rect onto the view
bottomLbl.frame = bigFrame;
// or
bottomLbl.frame = smallFrame;
}
completion:nil];
I am having an issue with running two animations in iOS. First i am trying to move the image from the bottom of the screen to the top by changing frame in the first animation set, then on completion i am trying to extend the length of the object by transforming the scale. But for some reason once the first animation is finished the object goes back to the original place at the bottom of the screen and the growing transformation happens off the screen. Can anyone help me with this? Thanks in advance for your help, here is my code:
CGRect fullview = CGRectMake(59, 102, 650, 150);
CGRect iconView = CGRectMake(59, 102, 290, 150);
CGRect textView = CGRectMake(349, 102, 360, 150);
[_profileBtn setEnabled:FALSE];
[_profileBtn setHidden:TRUE];
[UIView animateWithDuration:1 animations:^{ // animate the following:
_profileIcon.frame = iconView; // move to new location
_profileText.frame = textView; // move to new location
_profileBackground.frame =fullview;
}completion:^(BOOL finished){
[UIView animateWithDuration:1 animations:^{ // animate the following:
_profileBackground.transform = CGAffineTransformMakeScale(1, 5.64);
}];
}];
Check out the documentation for the UIView transform property. Specifically, what happens to the object's frame if you adjust the transform. I think instead of performing the second animation on the UIView's transform, you should apply it to the scale of its layer's transform.
I figure out a solution that was close to what i wanted. This will do the translation and the movement at the same time:
CGRect iconView = CGRectMake(59, 102, 290, 150);
CGRect textView = CGRectMake(349, 102, 360, 150);
_profileIcon.frame = iconView; // move to new location
_profileText.frame = textView; // move to new location
CGAffineTransform scale = CGAffineTransformMakeScale(1, 5.00);
CGRect fullview = CGRectMake(59, 40, 650, 150);
_profileBackground.frame = CGRectApplyAffineTransform(fullview,scale);