I'm currently animating between 4 images like this:
UIImageView *tom3BeforeImage;
tom3Images = [NSArray arrayWithObjects: [UIImage imageNamed:#"floortom_before1.png"],[UIImage imageNamed:#"floortom_before2.png"],[UIImage imageNamed:#"floortom_before3.png"],[UIImage imageNamed:#"floortom_before4.png"], nil ];
tom3BeforeImage.animationImages = tom3Images;
tom3BeforeImage.animationDuration = 0.75;
[tom3BeforeImage startAnimating];
It works fine, except that the animation is choppy between the images. I need the duration to be exactly .75 seconds, so speeding it up is not an option.
What's the best way to have the animation be smoother between the images, kind of like blending between each image change?
Thanks!
If you're using frame based UIImageView animation, and the animation must be .75 seconds, then the only way I know of to make it smoother is to create more frames. Try 30 frames/second, or about 22 frames. That should give very smooth motion.
If you want some sort of cross-dissolve between frames then you won't be able to use UIView frame animation. you'll have to use UIView block animation (using animateWithDuration:animations: or its cousins.)
You could create a sequence of cross-dissolves between your frames where the total duration of the sequence is .75 seconds. Have each transition trigger the next transition in it's completion block.
Something like this:
You'll need 2 image views, stacked on top of each other. You'll fade one out and the other in at the same time. You'll need to set the opaque flag to NO on both.
Lets call them tom3BeforeImage1 and tom3BeforeImage2
Add an int instance variable imageCount and make your array of images, tom3Images, and instance variable as well:
- (void) animateImages;
{
CGFloat duration = .75 / ([tom3Images count] -1);
//Start with the current image fully visible in tom3BeforeImage1
tom3BeforeImage1.image = tom3Images[imageCount];
tom3BeforeImage1.alpha = 1.0;
//Get the next image ready, at alpha 0, in tom3BeforeImage2
tom3BeforeImage2.image = tom3Images[imageCount+1];
tom3BeforeImage2.alpha = 0;
imageCount++
[UIView animateWithDuration: duration
delay: 0
options: UIViewAnimationOptionCurveLinear
animations:
^{
//Fade out the current image
tom3BeforeImage1.alpha = 0.0;
//Fade in the new image
tom3BeforeImage2.alpha = 1.0;
}
completion:
^{
//When the current animation step completes, trigger the method again.
if (imageCount < [tom3Images count] -1)
[self animateImages];
}
];
}
Note that I banged out the code above in the forum editor without having had enough coffee. It likely contains syntax errors, and may even have logic problems. This is just to get you thinking about how to do it.
Edit #2:
I'm not sure why, but I decided to flesh this out into a full-blown example project. The code above works passably well after debugging, but since it's fading one image out at the same time it's fading another one in, the background behind both image views shows through.
I reworked it to have logic that only fades the top image in and out. It puts the first frame in the top image view and the second frame in the bottom image view, then fades out the top image view.
The project is up on github, called Animate-Img. (link)
Then it installs the third frame in the top image view and fades it IN,
Then it installs the 4th fame in the bottom image view and fades out the top to expose the bottom, etc, etc.
I ended up creating a generalized method
- (void) animateImagesWithDuration: (CGFloat) totalDuration
reverse: (BOOL) reverse
crossfade: (BOOL) doCrossfade
withCompletionBlock: (void (^)(void)) completionBlock;
It will animate a set of images, into a pair of image views, optionally reversing the animation once it's done. It takes a completion block that gets called once the animation is finished.
The animate button actually calls a method that repeats the whole animation sequence. It's currently set to only run it once, but changing a constant will make the program repeat the whole sequence, if desired.
I do had the requirement to have animation with array of images. Initially when i used animationImages property of imageview, I got the desired animation but the transition between the images were not smooth, I then used CAKeyframeAnimation to achieve the smooth transition, the catch is to use timingFunction along with correct calculationMode. I am not sure this is the exact the answer for the question but this is one way to make the animation smoother,
below is the code for that
For more info on calculation mode please see Apple Documentation
- (void) animate
{
NSMutableArray * imageArray = [[NSMutableArray alloc] init];
int imageCount = 8;
for (int i=0; i<=imageCount; i++) {
[imageArray addObject:(id)[UIImage imageNamed:[NSString stringWithFormat:#“image%d”,i]].CGImage];
}
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"contents"];
animation.calculationMode = kCAAnimationLinear;// Make sure this is kCAAnimationLinear or kCAAnimationCubic other mode doesnt consider the timing function
animation.duration = 8.0;
animation.values = imageArray;
animation.repeatCount = 1; // Change it for repetition
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards; // To keep the last frame when animation ends
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[imageView.layer addAnimation:animation forKey:#"animation"];
}
UPDATE- Swift 3
func animate() {
var imageArray = [CGImage]()
let imageCount: Int = 3
for i in 0...imageCount {
imageArray.append((UIImage(named: String(format:"image\(i)"))?.cgImage!)!)
}
let animation = CAKeyframeAnimation(keyPath: "contents")
animation.calculationMode = kCAAnimationLinear
// Make sure this is kCAAnimationLinear or kCAAnimationCubic other mode doesnt consider the timing function
animation.duration = CFTimeInterval(floatLiteral: 8.0)
animation.values = imageArray
animation.repeatCount = 1
// Change it for repetition
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeForwards
// To keep the last frame when animation ends
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animImageView.layer.add(animation, forKey: "animation")
}
Related
Here's my code:
-(void)play {
animation = [CAKeyframeAnimation animationWithKeyPath: #"contents"];
for (UIImage *image in folder.images) {
[images addObject:(id)image.CGImage];
}
animationArray = [images subarrayWithRange:NSMakeRange(selected, selectedEnd - selected)];
timeBetweenAnimation = 1.0/framesPerSecond;
totalAnimationTime = folder.images.count * timeBetweenAnimation;
animation.duration = totalAnimationTime;
animation.values = animationArray;
animation.delegate = self;
beforeAdd = CACurrentMediaTime();
[displayImage.layer addAnimation:animation forKey:#"contents"];
afterAdd = CACurrentMediaTime();
}
-(void)animationDidStart {
afterStart = CACurrentMediaTime();
}
I am using Core Animation to animate an array of images and I want to get the current image in the animation when I pause the image. I realize that a method of doing this would be to get the image from "animationArray" using timing ratios (played time / total duration time), but I can't capture the begin time. I've tried capturing the begin time, using CACurrentMediaTime(), before and after the call to addAnimation:forKey: and also after animation did start, but it's innaccurate (especially when I have a large array of images). With a larger array of images, the "beforeAdd" and "afterAdd" times are at least a second off. Also, "afterStart" is anywhere between .04 and .08 seconds off independent of the size of my images array.
In the worst case scenario, I could use the "afterStart" begin time. However, if I'm trying to animate 100 images in 1 second, getting the image using this method would be inaccurate by 4 frames. Therefore, I either need a method that doesn't use timing in order to get the current frame of the animation, or I need a way to get a more accurate begin time. Is this possible?
NOTE: I don't want to use UIImageView animation to do this because that animation technique doesn't have smooth transitioning between frames. If there were a way to have smooth transitioning between frames using UIImageView animation I would reconsider, although I'm pretty sure there will still be an issue when calculating the begin time.
I'm trying to make a sequence of animations, I've found in CAAnimationGroup the right class to achieve that object. In practice I'm adding on a view different subviews and I'd like to animate their entry with a bounce effect, the fact is that I want to see their animations happening right after the previous has finished. I know that I can set the delegate, but I thought that the CAAnimationGroup was the right choice.
Later I discovered that the group animation can belong only to one layer, but I need it on different layers on screen. Of course on the hosting layer doesn't work.
Some suggestions?
- (void) didMoveToSuperview {
[super didMoveToSuperview];
float startTime = 0;
NSMutableArray * animArray = #[].mutableCopy;
for (int i = 1; i<=_score; i++) {
NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject: self.greenLeaf];
UIImageView * greenLeafImageView = [NSKeyedUnarchiver unarchiveObjectWithData: archivedData];
greenLeafImageView.image = [UIImage imageNamed:#"greenLeaf"];
CGPoint leafCenter = calculatePointCoordinateWithRadiusAndRotation(63, -(M_PI/11 * i) - M_PI_2);
greenLeafImageView.center = CGPointApplyAffineTransform(leafCenter, CGAffineTransformMakeTranslation(self.bounds.size.width/2, self.bounds.size.height));
[self addSubview:greenLeafImageView];
//Animation creation
CAKeyframeAnimation *bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform.scale"];
greenLeafImageView.layer.transform = CATransform3DIdentity;
bounceAnimation.values = #[
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:1.1],
[NSNumber numberWithFloat:0.8],
[NSNumber numberWithFloat:1.0]
];
bounceAnimation.duration = 2;
bounceAnimation.beginTime = startTime;
startTime += bounceAnimation.duration;
[animArray addObject:bounceAnimation];
//[greenLeafImageView.layer addAnimation:bounceAnimation forKey:nil];
}
// Rotation animation
[UIView animateWithDuration:1 animations:^{
self.arrow.transform = CGAffineTransformMakeRotation(M_PI/11 * _score);
}];
CAAnimationGroup * group = [CAAnimationGroup animation];
group.animations = animArray;
group.duration = [[ animArray valueForKeyPath:#"#sum.duration"] floatValue];
[self.layer addAnimation:group forKey:nil];
}
CAAnimationGroup is meant for having multiple CAAnimation subclasses being stacked together to form an animation, for instance, one animation can perform an scale, the other moves it around, while a third one can rotate it, it's not meant for managing multiple layers, but for having multiple overlaying animations.
That said, I think the easiest way to solve your issue, is to assign each CAAnimation a beginTime equivalent to the sum of the durations of all the previous ones, to illustrate:
for i in 0 ..< 20
{
let view : UIView = // Obtain/create the view...;
let bounce = CAKeyframeAnimation(keyPath: "transform.scale")
bounce.duration = 0.5;
bounce.beginTime = CACurrentMediaTime() + bounce.duration * CFTimeInterval(i);
// ...
view.layer.addAnimation(bounce, forKey:"anim.bounce")
}
Notice that everyone gets duration * i, and the CACurrentMediaTime() is a necessity when using the beginTime property (it's basically a high-precision timestamp for "now", used in animations). The whole line could be interpreted as now + duration * i.
Must be noted, that if a CAAnimations is added to a CAAnimationGroup, then its beginTime becomes relative to the group's begin time, so a value of 5.0 on an animation, would be 5.0 seconds after the whole group starts. In this case, you don't use the CACurrentMediaTime()
If you review the documentation, you will note that CAAnimationGroup inherits from CAAnimation, and that CAAnimation can only be assigned to one CALayer. It's intent is really to make it easy to create and manage multiple animations you wish to apply to a CALayer at the same time, not to manager animations for multiple CALayer objects.
To handle the sequencing of different animations between different CALayer or UIViewobjects, a technique I use is to create an NSOperation for each object/animation, then throw them into a NSOperationQueue to manage the sequencing. This is a bit complicated as you have to use the animation completion callback to tell the NSOperation it is finished, but if you write a good animation management subclass of NSOperation, it can be rather convenient and allow you to create sophisticated sequencing paths. The low-rent way of accomplishing the sequencing goal is to simply set the beginTime property on your CAAnimation object (which comes from it's adoption of the CAMediaTiming protocol) as appropriate to get the timing you want.
With that said, I am going to point you to some code that I wrote and open-sourced to solve the exact same use case you describe. You may find it on github here (same code included). I will add the following notes:
My animation management code allow your to define your animation in a plist by identifying the sequence and timing of image changes, scale changes, position changes, etc. It's actually pretty convenient and cleaner to adjust your animation in a plist file rather than in code (which is why I wrote this).
If the user is not expected to interact with the subviews you creating, it's actually much better (less overhead) to create layer objects that are added as sub-layers to your hosting view's layer.
I am trying to animate a CAEmitterLayer's emitterPosition like this:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"emitterPosition.x"] ;
animation.toValue = (id) toValue ;
animation.removedOnCompletion = NO ;
animation.duration = self.translationDuration ;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut] ;
animation.completion = ^(BOOL finished)
{
[self animateToOtherSide] ;
} ;
[_emitterLayer addAnimation:animation forKey:#"emitterPosition"] ;
CGPoint newEmitterPosition = CGPointMake(toValue.floatValue, self.bounds.size.height/2.0) ;
_emitterLayer.emitterPosition = newEmitterPosition ;
Note that animation.completion is declared in a category that just calls the corresponding CAAnimation delegate method.
The problem is that this doesn't animate at all and shows the emitter in its final position. I was under the impression that once you add the animation to the layer, you should change the actual model behind it to its final position so that when the animation completes the model is in its final state; i.e., to prevent the animation from "snapping back" to its original position.
I have tried placing the last two lines in the animation.completion block, and this does indeed animate as expected. However, when the animation finishes some particles are intermittently emitted at the emitter's original position. If you put the system under load (for example, scrolling a tableview while the animation is playing), this happens more often.
Another solution I was thinking about is to not move the emitterPosition at all but just move the CAEmitterLayer itself, although I haven't tried that yet.
Thanks in advance for any help you can provide.
Perhaps emitterPosition.x is not a valid key path for animation. Try using emitterPosition instead (and so you'll have to provide CGPoint values wrapped up in an NSValue).
I just tried this on my own machine and it works fine:
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"emitterPosition"];
ba.fromValue = [NSValue valueWithCGPoint:CGPointMake(30,100)];
ba.toValue = [NSValue valueWithCGPoint:CGPointMake(200,100)];
ba.duration = 6;
ba.autoreverses = YES;
ba.repeatCount = HUGE_VALF;
[emit addAnimation:ba forKey:nil];
Other things to think about:
You can typically use nil as the key in addAnimation:forKey:, unless you're going to need to find this animation later (e.g. to remove it or override it in some way). The key path is the important thing.
Setting removedOnCompletion to NO is almost always wrong and is typically the last refuge of a scoundrel (i.e. due to not understanding how animation works).
If, as you say, setting _emitterLayer.emitterPosition = newEmitterPosition inside the completion block does animate, then when why are you using CABasicAnimation at all? Why not just call UIView animate... and set the emitterPosition in the animations block? If that works, it will kill two birds with one stone, moving the position and animating it too.
I am using key frame animation to animate a sequence of images. But when I run it for the first time there is a delay before animation begins. After that it runs smoothly. I tried force loading all images. It reduced delay but it is still visible. How can I further reduce the delay.
Apple is notorious for using "lazy" loading techniques, and its quite possible that putting an image retrieved from "[UIImage imageNamed:]" does not in fact create a cached bitmap, just the receipt to create it.
If all else fails try a brute force approach: force the system to render it by rendering the image in a context you then just throw away.
CGSize bigSize; // MAX width and height of your images
UIGraphicsBeginImageContextWithOptions(bigSize, YES, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
for(UIImage *image in arrayOfImages) {
CGContextDrawImage(context, (CGRect){ {0,0}, image.size }, [image CGImage]);
}
UIGraphicsEndImageContext();
Now you not only have a reference to the images, but they have been forced to render, and so hopefully the system keeps that internal bitmap around. Should be an easy test for you to make.
This code is to swift 2.2!
Try to put a final image of you want before the animations end.
It's more like:
// the animation key is for retrive the informations about the array images
func animation(cicleTime:Int, withDuration: Double, animationKey:String){
var images = [UIImage]()
// get all images for animation
for i in 0...cicleTime {
let fileName = String(format: "image%d",i)
let image = UIImage(named: fileName)
images.append(image!)
}
let animation = CAKeyframeAnimation(keyPath: "contents")
animation.setValue(animationKey, forKey: "animationName")
animation.duration = withDuration
animation.repeatCount = 1 // times you want to repeat the animation
animation.values = images.map{$0.CGImage as! AnyObject}
animation.delegate = self
images.removeAll(keepCapacity: false)
YourViewHere.layer.addAnimation(animation, forKey: "contents")
YourViewHere.image = UIImage(named: "nameOfYourLastImageOfAnimation")
}
Well, this works for me.
I had this problem recently and solved it by 'prerunning' the animation as early as possible with a duration of 0 and not removing it on completion. By the time I actually want to run it, the whole sequence is loaded and smooth
let animation: CAKeyframeAnimation = CAKeyframeAnimation(keyPath: "contents")
animation.calculationMode = kCAAnimationDiscrete
animation.duration = 0
animation.values = spritesArray
animation.repeatCount = 1
animation.removedOnCompletion = false
animation.fillMode = kCAFillModeForwards
layer.addAnimation(animation, forKey: "animateIn")
In my case I actually had split the sequence into 2 separate keyframe animations for intro/outro and the outro was always stalling before starting. Preloading the whole thing first in a 0 duration keyframe animation prevented that.
Consider pre-loading your images in a NSArray.
Your delay is most likely caused by the fact that it first has to load the images.
So , basically , let's say you have img1.png , img2.png , etc up to img10.png:
//do this before your keyframe animation.
NSMutableArray *frames = [NSMutableArray array];
for(int i = 1 ; i <= 10 ; i++)
[frames addObject:[UIImage imageNamed:[NSString stringWithFormat:#"img%d.png" , i]]];
//now use this array for the animation
Hope this helps. Cheers!
If I want to animate UITableViewCell so it would bounce from left to right a few times, How can I do that? I'm trying that:
var bounds = activeCell.Bounds;
var originalLocation = bounds.Location;
var loc = originalLocation;
UIView.Animate(0.2,()=>{
loc.X = originalLocation.X + 20;
activeCell.Bounds = new RectangleF (loc, bounds.Size);
loc.X = originalLocation.X - 20;
activeCell.Bounds = new RectangleF (loc, bounds.Size);
});
It animates only the last state (i.e. moves element to the left). I tried to put them in separated Animate blocks - it didn't help. Tried to use different UIAnimationOptions - the same.
Here is a nice article explaining how to make it bounce.
http://khanlou.com/2012/01/cakeyframeanimation-make-it-bounce/
Moreover, there is an explanation the formula used to compute the bounce path.
For my personal use, I've taken the absolute value of the computation to simulate a rebound on ground.
- (void) displayNoCommentWithAnimation{
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position.y"];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.duration = 2;
int steps = 120;
NSMutableArray *values = [NSMutableArray arrayWithCapacity:steps];
double value = 0;
float e = 2.71;
for (int t = 0; t < steps; t++) {
value = 210 - abs(105 * pow(e, -0.025*t) * cos(0.12*t));
[values addObject:[NSNumber numberWithFloat:value]];
}
animation.values = values;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.delegate = self;
[viewThatNeedToBounce.layer addAnimation:animation forKey:nil];
}
- (void) animationDidStop:(CAAnimation *)animation finished:(BOOL)flag {
CAKeyframeAnimation *keyframeAnimation = (CAKeyframeAnimation*)animation;
[viewThatNeedToBounce.layer setValue:[NSNumber numberWithInt:210] forKeyPath:keyframeAnimation.keyPath];
[viewThatNeedToBounce.layer removeAllAnimations];
}
The problem with your approach is that UIView.Animate will record the changes that you make to your view, but only the final state for them.
If you change the Bounds property one hundred times in your animate block, only the last one is the one that will matter from the perspective of the animation framework.
CoreAnimation has a couple of quirks that are explained in the WWDC 2010 and WWDC2011 videos. They have great material and they explain a few of the tricks that are not very obvious.
That being said, animating cells in a UITableView is a complicated matter because you are really poking at a UITableView internals, so expect various strange side effects. You could lift the code from TweetStation that does that animation and deals with various corner cases. But even TweetStation and the Twitter for iOS app do not manage to be perfect, because you are animating things behind the back of a UIView that is constantly updating and making changes to very same properties you are animating.
From the top of my head, the easiest approach would be to put the animation code into a method and call that recursive as often as you want. Code untested, but it should work or at least give you an idea.
// Repeat 10 times, move 20 right and the left and right etc.
FancyAnim(activeCell, activeCell.Bounds.Location, 10, 20);
private void FancyAnim(UITableViewCell activeCell, PointF originalLocation, int repeat, float offset)
{
var bounds = activeCell.Bounds;
var loc = originalLocation;
UIView.Animate(0.2,
delegate
{
// Called when animation starts.
loc.X = originalLocation.X + offset;
activeCell.Bounds = new RectangleF (loc, bounds.Size);
},
delegate
{
// Called when animation ends.
repeat--;
// Call the animation method again but invert the movement.
// If you don't do this too often, you should not run out of memory because of a stack overflow.
if(repeat >= 0)
{
FancyAnim(activeCell, originalLocation, repeat, -offset);
}
});
You can however also use a path animation. You would define a path "20 units right, back to center, 20 units left, back to center" and repeat that animation as often as you like.
This requires you to deal with CAKeyFrameAnimation and will be slightly more code.
This site can get you jump started: http://www.bdunagan.com/2009/04/26/core-animation-on-the-iphone/
Lack of documentation and good samples sometimes really makes even simple tasks so annoyingly challenging.
Here is the solution
Sure code isn't elegant, but it works. Hope it will someday help somebody else, so he or she wouldn't need to spend half a day on something stupidly simple like that
var activeCell = ((Element)sender).GetActiveCell();
var animation =
(CAKeyFrameAnimation)CAKeyFrameAnimation.FromKeyPath ("transform.translation.x");
animation.Duration = 0.3;
animation.TimingFunction = // small details matter :)
CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseOut.ToString());
animation.Values = new NSObject[]{
NSObject.FromObject (20),
NSObject.FromObject (-20),
NSObject.FromObject (10),
NSObject.FromObject (-10),
NSObject.FromObject (15),
NSObject.FromObject (-15),
};
activeCell.Layer.AddAnimation (animation,"bounce");