I am trying to rotate an UIImageView periodically using CGAffineTranformationMakeRotation() function.....
Following is the code....
- (void)showSpinnerAnimation
{
rotateAngle += 3.14f;
[UIView animateWithDuration:0.18 animations:^{
self.spinnerImageView.transform = CGAffineTransformMakeRotation(rotateAngle);
} completion:^(BOOL finished) {
[self showSpinnerAnimation];
}];
}
But what i am observing is
Along with the rotating UIImageView. It is also shifting from its center.
I tried with previous SO Questions regarding this problem and thereby i tried the following methods
1. Set the center of the UIImageView in the completion code (by copying the center in a CGPoint variable).
2. Tried with setting autolayout. But it will break other constraints which i already set for other UI elements in IB.
Please note that the self.spinnerImageView is an IBOutlet UIImageView reffered from IB.
Give this a try
- (void) rotateView:(UIView *)theView
{
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue =[NSNumber numberWithFloat: -M_PI];
rotationAnimation.duration = 0.7;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = HUGE_VALF;
[theView.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}
And to stop rotating remove the animation from the layer
-(void)stopRotating:(UIView *)theView
{
[theView.layer removeAnimationForKey:#"rotationAnimation"];
// or
//[theView.layer removeAllAnimations];
}
Pass your UIImageView to the method.
[self rotateView:self.spinnerImageView]
Related
How would I implement a loading animation like the one found in the Weather Channel iOS App?
Specifically, I have a rounded UIButton that I want a spinning circle around when the user has tapped it, and something is loading.
Preferably, I'd want to make this using a built-in iOS framework, and as few 3rd party libraries as possible.
Here's how the Weather Channel Application looks like (couldn't get a gif, so to see it in action download the application from the App Store):
It doesn't necessarily have to look exactly like this, a solid color would be a good start. But the concept should be the same. I don't believe it should be hard to make, but sadly I have no idea where to start.
Thanks for all help!
EDIT 1:
I should point out that a solid color is good enough, and it doesn't need a gradient or a glow.
I have been working on some simple code that might be in the right direction. Please feel free to use that as a starting point:
- (void)drawRect:(CGRect)rect {
// Make the button round
self.layer.cornerRadius = self.frame.size.width / 2.0;
self.clipsToBounds = YES;
// Create the arc
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2) radius:(self.frame.size.width / 2) startAngle:M_PI_2 endAngle:M_PI clockwise:NO].CGPath;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor redColor].CGColor;
circle.lineWidth = 10;
// Create the animation
// Add arc to button
[self.layer addSublayer:circle];
}
As pointed out elsewhere, you can just add a view, and the rotate it. If you want to rotate with block-based animation, it might look like:
- (void)rotateView:(UIView *)view {
[UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
[UIView animateKeyframesWithDuration:1.0 delay:0.0 options:UIViewKeyframeAnimationOptionRepeat animations:^{
for (NSInteger i = 0; i < 4; i++) {
[UIView addKeyframeWithRelativeStartTime:(CGFloat) i / 4.0 relativeDuration:0.25 animations:^{
view.transform = CGAffineTransformMakeRotation((CGFloat) (i + 1) * M_PI / 2.0);
}];
}
} completion:nil];
} completion:nil];
}
Frankly, while I generally prefer block-based animation, the silliness of wrapping an animation with an animation (which is necessary to get no ease-in/ease-out) makes the block-based approach less compelling. But it illustrates the idea.
Alternatively, in iOS 7 and later, you could use UIKit Dynamics to spin it:
Define a property for the animator:
#property (nonatomic, strong) UIDynamicAnimator *animator;
Instantiate the animator:
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
Add rotation to an item:
UIDynamicItemBehavior *rotationBehavior = [[UIDynamicItemBehavior alloc] initWithItems:#[self.imageView]];
[rotationBehavior addAngularVelocity:2.0 forItem:self.imageView];
[self.animator addBehavior:rotationBehavior];
Maybe I miss something but I think you only need to rotate a custom spin asset.
You can easily achieve this with a method like:
- (void) runSpinAnimationOnView:(UIView*)view duration:(CGFloat)duration repeat:(float)repeat;
{
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0 * duration ];
rotationAnimation.duration = duration;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = repeat;
[view.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}
You need to repeat it indefinitely. So put HUGE_VALF as repeat parameter.
And if you want to create a custom spin image according to your button size, you can use bezier path. For example, you can do something like:
-(UIImageView*)drawBezierPathInView:(UIView*)view{
UIGraphicsBeginImageContext(CGSizeMake(view.frame.size.width, view.frame.size.height));
//this gets the graphic context
CGContextRef context = UIGraphicsGetCurrentContext();
//you can stroke and/or fill
CGContextSetStrokeColorWithColor(context, [UIColor colorWithRed:0.5f green:0.5f blue:0.5f alpha:1.f].CGColor);
CGFloat lineWidth = 8.f;
UIBezierPath *spin = [UIBezierPath bezierPath];
[spin addArcWithCenter:CGPointMake(view.frame.size.width * 0.5f, view.frame.size.height * 0.5f) radius:view.frame.size.width * 0.5f - lineWidth * 0.5f startAngle:-M_PI_2 endAngle:2 * M_PI_2 clockwise:YES];
[spin setLineWidth:lineWidth];
[spin stroke];
//now get the image from the context
UIImage *bezierImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *bezierImageView = [[UIImageView alloc]initWithImage:bezierImage];
return bezierImageView;
}
And you create your button like:
// myButton is your custom button. spinView is an UIImageView, you keep reference on it to apply or to stop animation
self.spinView = [self drawBezierPathInView:mybutton];
[mybutton addSubview:self.spinView];
[self runSpinAnimationOnView:self.spinView duration:3 repeat:HUGE_VALF];
There are a lot of related questions, but this situation does not seem to be addressed by any the existing questions.
I have created a view with a custom layer so that one of the properties can be animated. Using the CABasicAnimation class, the animation works correctly.
However, I need a little more control over the animation, such as the ease in and ease out and sequential animations and tried to switch to using block animations. However, when I do that, the animation completes immediately rather than animating over time.
How can I get this block animation to work correctly?
Working animation code:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"inputValue"];
animation.duration = DEFAULT_ANIMATION_DURATION;
if (flipped) {
animation.fromValue = [NSNumber numberWithDouble:0.0];
animation.toValue = [NSNumber numberWithDouble:1.0];
self.myLayer.inputValue = 1.0;
} else {
animation.fromValue = [NSNumber numberWithDouble:1.0];
animation.toValue = [NSNumber numberWithDouble:0.0];
self.myLayer.inputValue = 0.0;
}
[self.layer addAnimation:animation forKey:#"animateInputValue"];
Animation that incorrectly completes immediately, but finished is YES:
[UIView animateWithDuration:10.0 delay:0.0 options:0 animations:^{
self.myLayer.inputValue = 1.0;
} completion:^(BOOL finished) {
NSLog(#"done %#", finished?#"and finished":#", but not finished");
}];
The CALayer being animated:
#import "UViewLayer.h"
#import "YoYouStyleKit.h"
#implementation UViewLayer
+ (BOOL)needsDisplayForKey:(NSString *)key {
if( [key isEqualToString:#"inputValue"] )
return YES;
return [super needsDisplayForKey:key];
}
- (void)setInputValue:(CGFloat)inputValue {
_inputValue = inputValue;
[self setNeedsDisplay];
}
- (void)drawInContext:(CGContextRef)context {
UIGraphicsPushContext(context);
[YoYouStyleKit drawUShapeWithFrame:self.bounds input:self.inputValue];
UIGraphicsPopContext();
}
Adding #dynamic inputValue; in the custom layer seems to make no difference.
Do not mix UIKit and Core Animation animations.
Implement like this:
[CATransaction begin];
[CATransaction setCompletionBlock:^
{
NSLog(#"done");
}];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"inputValue"];
animation.duration = DEFAULT_ANIMATION_DURATION;
if (flipped)
{
animation.fromValue = [NSNumber numberWithDouble:0.0];
animation.toValue = [NSNumber numberWithDouble:1.0];
self.myLayer.inputValue = 1.0;
}
else
{
animation.fromValue = [NSNumber numberWithDouble:1.0];
animation.toValue = [NSNumber numberWithDouble:0.0];
self.myLayer.inputValue = 0.0;
}
[self.layer addAnimation:animation forKey:#"animateInputValue"];
[CATransaction commit];
In addition to Leo Natan's answer, as mentioned in the Apple docs :
Custom layer objects ignore view-based animation block parameters and
use the default Core Animation parameters instead.
What's more tricky is that you can animate the UIView's own layer properties, provided you change one of the animatable properties.
For custom properties like your layer inputValue, you can provide the CABasicAnimation in your layer (id<CAAction>)actionForKey:(NSString *)key but it will not use the UIView animation parameters (duration...).
This animation will play when you change the layer property in the UIView block animation, but it will also play when you simply set the value.
The code provided by Leo Natan is the easiest when to animate your layer from the UIView.
What is the best way to rotate a view and move its location at the same time?
I want this view to rotate 1 full revolution while sliding to the left, then rotate the other direction while sliding back to its original position. I got the first part of this working (rotate while sliding to left), but when I try to rotate while sliding back to the right, the view jumps back to its original spot without animating.
What am I doing wrong?
// step 1 (this works)
[UIView animateWithDuration:0.3f animations:^{
self.number.transform = CGAffineTransformMakeRotation(DegreesToRadians(180));
self.number.transform = CGAffineTransformMakeTranslation(-160, 0);
self.number.center = CGPointMake(self.number.center.x - 160, self.number.center.y);
} completion:nil];
// step 2 (this doesn't work)
[UIView animateWithDuration:0.2f animations:^{
self.number.transform = CGAffineTransformMakeRotation(DegreesToRadians(180));
self.number.transform = CGAffineTransformMakeTranslation(160, 0);
self.number.center = CGPointMake(self.number.center.x + 160, self.number.center.y);
} completion:nil];
I got it to work using the following code. My view had a constraint to the left side of the superview, to which I added the IBOutlet, leftCon. It's initial value was 160, so the translation moves it all the way to the left edge of the screen.
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#define animationTime 1.0
#interface ViewController ()
#property (nonatomic) CGFloat leftConstant;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.leftConstant = self.leftCon.constant;
}
- (IBAction)rotate:(UIButton *)sender {
if (self.leftCon.constant == self.leftConstant) {
[self.view removeConstraint:self.leftCon];
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = #(-M_PI * 2.0);
rotationAnimation.duration = animationTime;
CABasicAnimation *translationAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
translationAnimation.fromValue = [NSValue valueWithCGPoint:self.number.center];
translationAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(self.number.frame.size.width/2.0, self.number.center.y)];
translationAnimation.duration = animationTime;
[self.number.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
[self.number.layer addAnimation:translationAnimation forKey:#"translationAnimation"];
self.leftCon.constant = 0;
[self.view addConstraint:self.leftCon];
}else{
[self.view removeConstraint:self.leftCon];
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = #(M_PI * 2.0);
rotationAnimation.duration = animationTime;
CABasicAnimation *translationAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
translationAnimation.fromValue = [NSValue valueWithCGPoint:self.number.center];
translationAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(self.leftConstant+ self.number.frame.size.width/2.0, self.number.center.y)];
translationAnimation.duration = animationTime;
[self.number.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
[self.number.layer addAnimation:translationAnimation forKey:#"translationAnimation"];
self.leftCon.constant = self.leftConstant;
[self.view addConstraint:self.leftCon];
}
}
You probably want to place your second animation as the completion block in the first animation.
I've been implementing a simple FlipView in iOS : A UIView that contains two subviews, displaying one at a time, and when you click on it, it flips them.
I'm using the following to animate the flipping.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
#synchronized(self){
if(!self.flipping){
self.flipping = YES;
UIView *toView = self.currentView == self.primaryView ? self.secondaryView : self.primaryView;
[UIView transitionFromView:self.currentView toView:toView duration:self.speed options:UIViewAnimationOptionTransitionFlipFromLeft|UIViewAnimationOptionCurveEaseInOut completion:^(BOOL finished) {
[self.currentView removeFromSuperview];
self.currentView = toView;
self.flipping = NO;
}];
}
}
}
Pretty straight forward, right ?
But what bugs me is that, while the views are flip, the flipped content is darkened. Which shows, against a light background.
Would anyone knows a solution to have the exact same animation, but without the darkening (<= is that even a word ?)
Thanks in advance !
PS : I'm targeting IOS 5 and above.
I recently had a problem with similar symptoms and I was adding a subview over and over again else where in my code whenever I committed a certain action. Maybe you are doing something similar? When your touches end, are you doing something else to your flipped content? You probably need to remove the subviews being added IF that is your problem.
I succeeded, getting inspiration in the code I found here http://www.mycodestudio.com/blog/2011/01/10/coreanimation/ (and he, himself, took inspiration from http://www.mentalfaculty.com/mentalfaculty/Blog/Entries/2010/9/22_FLIPPIN_OUT_AT_NSVIEW.html)
Anyway, what I do spin between two views.
- (void)flip{
#synchronized(self){
if(!self.flipping){
self.flipping = YES;
UIView *bottomView = self.currentView == self.primaryView ? self.secondaryView : self.primaryView;
CALayer *top = self.currentView.layer;
CALayer *bot = bottomView.layer;
CAAnimation *topAnimation = [self flipAnimationWithDuration:self.speed/2.0 forLayerBeginningOnTop:YES scaleFactor:1];
CAAnimation *bottomAnimation = [self flipAnimationWithDuration:self.speed/2.0 forLayerBeginningOnTop:NO scaleFactor:1];
CGFloat zDistance = 1500.0f;
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1. / zDistance;
top.transform = perspective;
bot.transform = perspective;
topAnimation.delegate = self;
[CATransaction setCompletionBlock:^{
[top removeAllAnimations];
[self.currentView removeFromSuperview];
self.currentView = bottomView;
[self addSubview:bottomView];
[CATransaction setCompletionBlock:^{
self.flipping = NO;
[bot removeAllAnimations];
}];
[CATransaction begin];
[bot addAnimation:bottomAnimation forKey:#"flip"];
[CATransaction commit];
}];
[CATransaction begin];
[top addAnimation:topAnimation forKey:#"flip"];
[CATransaction commit];
}
}
}
-(CAAnimation *)flipAnimationWithDuration:(NSTimeInterval)aDuration forLayerBeginningOnTop:(BOOL)beginsOnTop scaleFactor:(CGFloat)scaleFactor
{
// Rotating halfway (pi radians) around the Y axis gives the appearance of flipping
CABasicAnimation *flipAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
CGFloat startValue = beginsOnTop ? 0.0f : M_PI/2;
CGFloat endValue = beginsOnTop ? -M_PI/2 : 0.0f;
flipAnimation.fromValue = [NSNumber numberWithDouble:startValue];
flipAnimation.toValue = [NSNumber numberWithDouble:endValue];
// Shrinking the view makes it seem to move away from us, for a more natural effect
// Can also grow the view to make it move out of the screen
CABasicAnimation *shrinkAnimation = nil;
if (scaleFactor != 1.0 ) {
shrinkAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
shrinkAnimation.toValue = [NSNumber numberWithFloat:scaleFactor];
// We only have to animate the shrink in one direction, then use autoreverse to "grow"
shrinkAnimation.duration = aDuration * 0.5;
shrinkAnimation.autoreverses = YES;
}
// Combine the flipping and shrinking into one smooth animation
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.animations = [NSArray arrayWithObjects:flipAnimation, shrinkAnimation, nil];
// As the edge gets closer to us, it appears to move faster. Simulate this in 2D with an easing function
animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:beginsOnTop?kCAMediaTimingFunctionEaseIn:kCAMediaTimingFunctionEaseOut];
animationGroup.duration = aDuration;
// this really means keep the state of the object at whatever the anim ends at
// if you don't do this then it reverts back to the original state (e.g. brown layer)
animationGroup.fillMode = kCAFillModeForwards;
animationGroup.removedOnCompletion = NO;
return animationGroup;
}
The two views are named primaryView and secondaryView. You can use any view, (ImageView, text view...)
I have a array of CALayers that i'm looping and trying to move.
Tile is a CALayer subclass and has a CGRect property named originalFrame where i store the frame i want to animate to.
When i'm useing the code below everything is instant moved to the correct possition and there is no 4 sec animation. How can i animate these CALayer?
for (int i = 0; i < [tileArray count]; i++) {
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDelay:i];
[UIView setAnimationDuration:4];
Tile *currentCard = (Tile*)[tileArray objectAtIndex:i];
currentCard.frame = currentCard.originalFrame;
[UIView commitAnimations];
}
You have two problems: the first is that you're trying to animate the layer's frame directly. Since this is a derived property, you can't do that. Instead, you have to animate the position property. http://developer.apple.com/library/mac/#qa/qa1620/_index.html
Second, you're using UIView's +beginAnimations API, but you say your Tile objects are CALayers, not UIViews. So you don't need to use +beginAnimations. Instead you need to use a CAAnimation object, like CABasicAnimation (untested):
for (Tile *tile in tileArray)
{
static NSString * const kProperty = #"position";
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:kProperty];
animation.duration = 4.0f;
animation.fromValue = [tile valueForKey:kProperty];
animation.toValue = [NSValue valueWithCGRect:tile.originalFrame];
[tile addAnimation:animation forKey:kProperty];
}