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.
Related
Sorry for my poor english.
In my work, We made a app which show a high resolution image into a UIScrollVIew. Besides, the user can play a MP3 that animate the position and zoom of UIScrollView from one second with a specific duration showing the detail of image that MP3 is talking about. Currently, the audio play from second 0 to the end sequently. User can´t interactuate with UIScrollView when the MP3 is playing
Now we want that the user can change the current time when is playing, I´m not sure how calculate the position when the user select a second placed in the midst of one animation.
Example:
Second 1: Duration 4; Second 8: Duration 2; Second 20 Duration 10; Second 31 Duration 3;
If the user select the second 24, we would want animate during 6 seconds, but keeping in mind the original position that UIScrollView should have since second 20 to 24 from original animation.
We are using uiview animation (animateWithDuration) with zoomToRect (animated:NO) method from UIScrollView
I´m not sure how calculate those intermediate values.
Thanks.
I do not know, whether I really get you right or wrong. But you can set the animations progress to a progress value that is in relation to the start point and the total duration of the animation. I. e. in your case
float animationStartPoint = 20.0; // Start of the animation
float duration = 10.0; // Its duration
float partialStartPoint = 24.0; // user selected start point
NSAnimation *animation = …;
…
animation.currentProgress = (partialStartPoint-animationStartPoint)/duration;
[animation startAnimation];
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")
}
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 have an UIImageView that runs across the screen when a button is pressed and held. When the button is pressed is changes the UIImage of the UIImageView and when the button is let go I change it to its original UIImage. When ever the image changes back it snaps back to the location that the image started.
This Timer is called when the button is pressed:
//This is the image that changes when the button is pressed.
imView.image = image2;
runTimer = [NSTimer scheduledTimerWithTimeInterval:0.04
target:self
selector:#selector(perform)
userInfo:nil
repeats:YES];
This is called When the button stops being held:
- (IBAction)stopPerform:(id)sender{
[runTimer invalidate];
//THIS IS WHAT SNAPS THE ANIMATION BACK:
//Without this the animation does not snap back
imView.image = image1;
}
- (void)performRight{
CGPoint point0 = imView.layer.position;
CGPoint point1 = { point0.x + 4, point0.y };
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.fromValue = #(point0.x);
anim.toValue = #(point1.x);
anim.duration = 0.2f;
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"];
}
Do I need to add the change in images to the animation? If so how? Im really confused.
Core Animation uses different sets of properties to represent an object:
From Core Animation Programming Guide:
model layer tree (or simply “layer tree”) are the ones your app interacts with the most. The objects in this tree are the model objects that store the target values for any animations. Whenever you change the property of a layer, you use one of these objects.
presentation tree contain the in-flight values for any running animations. Whereas the layer tree objects contain the target values for an animation, the objects in the presentation tree reflect the current values as they appear onscreen. You should never modify the objects in this tree. Instead, you use these objects to read current animation values, perhaps to create a new animation starting at those values.
So when you animate the properties you change the presentation layer, but once the animation is finished the object reverts back to its model property values.
What you need to do to fix this is use the [CAAnimation animationDidStop:finished:] delegate method to set the final property value and anything else you would like to do. I think you could use this to dump that horrible NSTimer code you are using and one small part of the world will be that much better.
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");