Logarithmic scale for animation objective-c - ios

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.

Related

How to reduce the opacity of an UIButton gradually?

In my project i have an UIButton which on clicking zooms. Everything is fine till here but i require the opacity of the button to gradually decrease from 1 to 0. I mean it should decrease in 1,0.9,0.8,0.7,0.6,0.5,0.4,0.3,0.2,0.1,0.0 sequence. My current code is
-(void)roundButtonAnimation
{
self.buttonZoom = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
self.buttonZoom.duration = 0.8f;
self.buttonZoom.values = #[[NSValue valueWithCATransform3D:CATransform3DMakeScale(1, 1, 1)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(2,2,2)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(3,3,3)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(5,5,5)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(6,6,6)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(7,7,7)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(9,9,9)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(10,10,10)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(11,11,11)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(12,12,12)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(13,13,13)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(14,14,14)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(15,15,15)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(16,16,16)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(17,17,17)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(18,18,18)],[NSValue valueWithCATransform3D:CATransform3DMakeScale(50,50,50)]];
self.buttonZoom.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
self.roundButton.transform = CGAffineTransformMakeScale(0,0);
self.roundButton.alpha = 0.3;
[self.roundButton.layer addAnimation:self.buttonZoom forKey:#"buttonScale"];
[self.CLSpinnerView1 stopAnimating];
//[self performSelector:#selector(secondView) withObject:self afterDelay:0.3];
[self performSelector:#selector(postNotification) withObject:self afterDelay:0.6];
}
[UIView animateWithDuration:1.0
delay:0.0
options: UIViewAnimationCurveLinear
animations:^{
self.roundButton.transform = CGAffineTransformMakeScale(50.0f, 50.0f);
self.roundButton.alpha = 0.0;
}
completion:^(BOOL finished){
NSLog(#"Done!");
}];
}
Let's explain a little, in this animation block you say that the animation should take place in one second, during this second based on a linear curve (thus always equal increment/decrement) the button should zoom 50 times and alpha should be animate to 0.
I do not understand why you are using a more complicated CAKeyframeAnimation over that simple animation block.
Keyframe animation are useful when you want more power over timings and animate over a set of values.
UIView animateWithDuration:animations: should serve your purpose here.
[UIView animateWithDuration:1.0 animations:^{
button.alpha = 0.0;
}];
Will gradually change the button's alpha to 0 over one second (...WithDuration:1.0...).
See the The UIView Class Reference for more information.
Hope this helps.
Try this
[button.layer removeAnimationForKey:#"opacity"];

Card flipping animation

I am making a card game and would like to use a Card flipping animation.
Currently I'm using this code with two images, front and back:
card, is a uiview with two UIImageView propertys
[UIView transitionWithView:card duration:0.65f
options:UIViewAnimationOptionTransitionFlipFromRight animations:^{
card.backImageView.image = card.frontImageView.image;
card.layer.shadowOpacity = 0.8;
} completion:^(BOOL finished){
card.layer.shadowOpacity = 0.0;
}];
In order to create a basic card flipping animation like the one in the video you've linked to, I suggest putting frontImageView and the backImageView directly on top of each other on the UIView you intend to flip. To start, set their images to front and back accordingly; and, in this particular case, hide the frontImageView and show the backImageView.
Assuming "card" is the UIView you intend to flip, to perform the flip try:
[UIView transitionWithView:card duration:0.65f
options:UIViewAnimationOptionTransitionFlipFromRight animations:^{
frontImageView.hidden = NO;
backImageView.hidden = YES;
} completion:^(BOOL finished) {
// whatever you'd like to do immediately after the flip completes
}];
Edit:
And to handle the shadow, first off, it appears in the video you posted that the shadow grows in length moreso than it just fades in. And it seems as if (and makes logical sense that) the shadow reaches its peak during the middle of the animation as the card is lifted at its highest point. Since the shadow grows then shrinks during the course of the flip animation, it doesn't make sense to include the shadow animation within the same animation block as the flip since they're on different time schedules.
Secondly with regard to the shadow, to animate the layer property, you have to use Core Animations.
Perhaps you can run the two animations concurrently, i.e. while the above animation is performing, also do something like:
CABasicAnimation *shadowAnimation = [CABasicAnimation animationWithKeyPath:#"shadowRadius"];
shadowAnimation.delegate = self;
[shadowAnimation setFromValue:[NSNumber numberWithFloat:3.0]];
[shadowAnimation setToValue:[NSNumber numberWithFloat:10.0]];
[shadowAnimation setDuration:0.65f];
shadowAnimation.autoreverses = YES;
[[card layer] addAnimation:shadowAnimation forKey:#"shadowRadius"];
The last portion has been adapted from this code and takes advantage of the autoreverse property to automatically reverse the shadow's growth.
This is how to perform a card flipping animation in swift 3:
UIView.transition(from: frontImageView,
to: backImageView,
duration: 0.65,
options: .transitionFlipFromRight,
completion: nil)
i tested a few things, and Compromise with this:
_tempTransform = card.transform;
[card loadFront];//loads the front image.
card.frontImageView.hidden=YES;
[UIView animateWithDuration:0.02f
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0 / -500;
transform = CATransform3DRotate(transform, 15.0f * M_PI / 180.0f, 0, -1, 0.0f);
card.layer.transform = transform;
}completion:^(BOOL done){
card.layer.shadowOpacity=0.1f;
card.transform=CGAffineTransformScale(card.transform, 1.2f, 1.2f);
[UIView transitionWithView:card duration:0.4f
options:UIViewAnimationOptionTransitionFlipFromRight animations:^{
card.frontImageView.hidden=NO;
card.backImageView.hidden=YES;
} completion:^(BOOL finished){
card.layer.shadowOpacity=0.0f;
card.transform=_tempTransform;
}];
}];

iPhone: Animate circle with UIKit

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!

Smooth scaling of vector graphics in UIView

So I have subclassed UIView and added some drawing code. I am scaling the resulting view up and down.
I would like this view to be resolution independent so that it is legible at any size, and I won't need to manage multiple images etc. etc.
As a test I made up a bit of drawing code that looks like this.
It creates concentric ovals that fit within whatever frame size the UIView has.
Actually the outside ring is a little smaller than the frame so it isn't clipped. Fine for this. The actual graphic will be more complex and will contain text which must be readable at small sizes and things of that nature.
- (void)drawRect:(CGRect)rect
{
UIColor* color = [UIColor colorWithRed: 0.833 green: 0.833 blue: 0.833 alpha: 1];
float width = self.bounds.size.width;
float height = self.bounds.size.height;
float scalePercent = 0.8;
for(int i = 0; i < 10; i++){
width = width * scalePercent;
height = height * scalePercent;
float x = (self.bounds.size.width - width) / 2;
float y = (self.bounds.size.height - height) / 2;
UIBezierPath* ovalPath = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(x, y, width, height)];
[color setStroke];
ovalPath.lineWidth = 2;
[ovalPath stroke];
}
}
Now here's the scaling:
- (void) makeBig{
[UIView animateWithDuration:0.5
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:(void (^)(void)) ^{
self.transform = CGAffineTransformMakeScale(2, 2);
}
completion:^(BOOL finished){
}];
}
When you run this the view zooms up, but it is pixelated. It's pixelated because the view has doubled in size but it's resolution has not changed.
So, here's how not to do it.
- (void) makeBig{
[UIView animateWithDuration:0.5
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:(void (^)(void)) ^{
self.transform = CGAffineTransformMakeScale(2, 2);
}
completion:^(BOOL finished){
CGRect targetFrame = self.frame;
self.transform = CGAffineTransformIdentity;
self.frame = targetFrame;
[self setNeedsDisplay];
}];
}
This works, but the fix is visible at the end of the animation when the resolution snaps back to screen resolution.
I could try pre-scaling the view up and pre-drawing at the final size then scaling it down and then running the animation to scale it back up again, but for various reasons that I can think of that sounds totally stupid. I suppose I could be wrong and it's the brightest idea since fire. I kind of doubt it though.
So how is a smooth scale of vector content done best?
View-based animation is really handy, but if I'm not mistaken it uses CABasicAnimation, which only uses a single keyframe. It'll be a little more code, but if you use a CAKeyframeAnimation instead, Core Animation will redraw the contents of the animated layer for each keyframe (you get to specify when those occur), so you can avoid the appearance of pixelation.

Mimic UIAlertView Bounce?

Whats the best way to mimic the bouncing animation from the UIAlertView on the iPhone? Is there some built-in mechanism for this? The UIAlertView itself won't work for my needs.
I looked into animation curves but from what I can tell the only ones they provide are easeIn, easeOut, and linear.
UIAlertView uses a more sophisticated animation:
scale to larger than 100%
scale to smaller than 100%
scale to 100%
Here's an implementation using a CAKeyFrameAnimation:
view.alpha = 0;
[UIView animateWithDuration:0.1 animations:^{view.alpha = 1.0;}];
CAKeyframeAnimation *bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform.scale"];
bounceAnimation.values = #[#0.01f, #1.1f, #0.8f, #1.0f];
bounceAnimation.keyTimes = #[#0.0f, #0.5f, #0.75f, #1.0f];
bounceAnimation.duration = 0.4;
[view.layer addAnimation:bounceAnimation forKey:#"bounce"];
I investigated how animations are added to UIAlertView's layer by swizzling -[CALayer addAnimation:forKey:]. Here are the values I got for the scale transform animations it performs:
0.01f -> 1.10f -> 0.90f -> 1.00f
with durations
0.2s, 0.1s, 0.1s.
All the animations use an ease in/ease out timing function. Here is a CAKeyframeAnimation that encapsulates this logic:
CAKeyframeAnimation *bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
bounceAnimation.fillMode = kCAFillModeBoth;
bounceAnimation.removedOnCompletion = YES;
bounceAnimation.duration = 0.4;
bounceAnimation.values = #[
[NSValue valueWithCATransform3D:CATransform3DMakeScale(0.01f, 0.01f, 0.01f)],
[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.1f, 1.1f, 1.1f)],
[NSValue valueWithCATransform3D:CATransform3DMakeScale(0.9f, 0.9f, 0.9f)],
[NSValue valueWithCATransform3D:CATransform3DIdentity]];
bounceAnimation.keyTimes = #[#0.0f, #0.5f, #0.75f, #1.0f];
bounceAnimation.timingFunctions = #[
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
I believe UIAlertView also performs a simple opacity animation from 0.0f to 1.0f over the total duration of the transform animation (0.4).
You can use 2 animations, one to pop up to very large, and the other one to rescale back to normal size.
(This is the approach use by UIAlertView internally.)
Alternatively, you can use the lower-level CAAnimation and use +[CAMediaTimingFunction functionWithControlPoints::::] to make your own curve.
Here's how I did it for an app I'm working on. The effect I was going for was bouncing when you pressed the view. Experiment with the values to suit your taste and the desired speed of the effect.
- (void) bounceView:(UIView*)bouncer
{
// set duration to whatever you want
float duration = 1.25;
// use a consistent frame rate for smooth animation.
// experiment to your taste
float numSteps = 15 * duration;
// scale the image up and down, halving the distance each time
[UIView animateKeyframesWithDuration:duration
delay:0
options:UIViewKeyframeAnimationOptionCalculationModeCubic
animations:^{
float minScale = 0.50f; // minimum amount of shrink
float maxScale = 1.75f; // maximum amount of grow
for(int i = 0; i< numSteps*2; i+=2)
{
// bounce down
[UIView addKeyframeWithRelativeStartTime:duration/numSteps * i
relativeDuration:duration/numSteps
animations:^{
bouncer.layer.transform = CATransform3DMakeScale(minScale, minScale, 1);
}];
// bounce up
[UIView addKeyframeWithRelativeStartTime:duration/numSteps * (i+1)
relativeDuration:duration/numSteps
animations:^{
bouncer.layer.transform = CATransform3DMakeScale(maxScale, maxScale, 1);
}];
// cut min scale halfway to identity
minScale = minScale + (1.0f - minScale) / 2.0f;
// cut max scale halfway to identity
maxScale = 1.0f + (maxScale - 1.0f) / 2.0f;
}
} completion:^(BOOL finished) {
// quickly smooth out any rounding errors
[UIView animateWithDuration:0.5*duration/numSteps animations:^{
bouncer.layer.transform = CATransform3DIdentity;
}];
}];
}

Resources