I have an iOS App that loads a set of images and a duration into an array then I have a timer that displays the images like so:
- (void)fireTimer:(NSTimer *)theTimer
{
self.image = [frames objectAtIndex:currentFrame];
NSTimeInterval time = [[durations objectAtIndex:currentFrame] floatValue];
self.timer = [NSTimer scheduledTimerWithTimeInterval:time target:self selector:#selector(fireTimer:) userInfo:nil repeats:NO];
currentFrame++;
if (currentFrame >= [frames count])
{
currentFrame = 0;
[timer invalidate];
}
}
To start the animation I call fireTimer and cycle through the images then when all the images are processed I call [timer invalidate] to stop the animation. I cannot use startAnimation because I need different durations for each image.
Right know I am NOT doing this on a background thread so the animation is choppy because other processing is happening whilst the image animates.
What is the best way to animate this in the background? Can I simply put this call to fireTimer in a block?
I know this may not be the best way to animate an image on iOS, but I do not want to do a lot of refactoring on the code right now.
Thanks for any suggestions or examples for a better solution!!!
I would suggest you use UIImageView to animate your images : it is made to do this task. If, like you say, some images needs to remains longer then others, then you can just display them many times. Let's say that image2 needs to be displayed twice longer than image1 and image3, just initialize the array animationImages on your UIImageView like : #[image1, image2, image2, image3].
Instead of using a timer, you could use performSelector: withObject: afterDelay:
-(void)loopBackground:(int)index
{
if(index < [self.durations count]{
self.image = [self.frames objectAtIndex:index];
[self performSelector:#selector(loopBackground:) withObject:index++ afterDelay:[[self.durations objectAtIndex:index] floatValue]];
}
}
Related
I'm developing an app for Apple Watch using WatchKit and I need to implement a circular progress while recording audio, similar to Apple demos.
I don't know if WatchKit includes something to do it by default so I have created my own images (13 for 12 seconds [209 KB in total]) that I change with a timer.
This is my source code:
Button action to start/stop recording
- (IBAction)recordAction {
NSLogPageSize();
NSLog(#"Recording...");
if (!isRecording) {
NSLog(#"Start!");
isRecording = YES;
_timerStop = [NSTimer scheduledTimerWithTimeInterval:12.0
target:self
selector:#selector(timerDone)
userInfo:nil
repeats:NO];
_timerProgress = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(progressChange)
userInfo:nil
repeats:YES];
} else {
NSLog(#"Stop :(");
[self timerDone];
}
}
Action when timer is finished or if tap again on the button
-(void) timerDone{
NSLog(#"timerDone");
[_timerProgress invalidate];
_timerProgress = nil;
[recordButtonBackground setBackgroundImageNamed:[NSString stringWithFormat:#"ic_playing_elipse_12_%d", 12]];
counter = 0;
isRecording = NO;
}
Method to change progress
- (void)progressChange
{
counter++;
NSLog(#"%d", counter);
NSString *image = [NSString stringWithFormat:#"ic_playing_elipse_12_%d", counter];
[recordButtonBackground setBackgroundImageNamed:image];
NSLog(image);
}
This is a gif showing the bug. It starts to show but change the image randomly until it gets a pair of seconds. (Note: I have already checked that the first images have the right progress)
Update (other solution): using startAnimating
There is a new way to show a circle progress using startAnimatingWithImagesInRange:duration:repeatCount
You need to specify the images range and the duration for your animation. See an example below:
[self.myElement startAnimatingWithImagesInRange:NSMakeRange(0, 360) duration:myDuration repeatCount:1];
I believe this is a side-effect of the way WatchKit interprets image names.
Image names ending in numbers are used to represent frames in an animated image, so "ic_playing_elipse_12_10" is being interpreted as the first frame of the image named "ic_playing_eclipse_12_1" and your 10th image gets displayed when you want it to display your first one.
You should be able to just change your image names so the number isn't the last part of the image name and it will fix it.
I know that this question is answered but I would like to explain better the solution of startAnimating.
[myElement startAnimatingWithImagesInRange:NSMakeRange(imageIdBegin, imageIdEnd) duration:duration repeatCount:repetitions];
In order to use this, you need a suite of images named like you want but with sequential ids. For example: progress_0.png, progress_1.png, progress_2.png, .., progress_n.png
You can decide the begin index and the end index using that values in NSMakeRange(imageIdBegin, imageIdEnd) just indicating the number id (not the full image name)
Of course, you will need to indicate the duration of the animation in seconds, WatchKit will interpolate for you to animate your images between init index and end index.
And finally, you can indicate if you want to repeat this animation (for example for a loading action) or not. If you don't want to repeat, you have to indicate 1.
I hope this can help more people to perform circle progress or another animations inside their AppleWatch applications :)
I have an interface which contains several UIImageView components on a view and i would like to update the animation images in run-time.
The main idea is to change the animation images into a thread with a delay of 2 seconds. The code below works fine. However, when i introduce a sleep time (with the commented NSThread sleepForTime); it no longer works. I am sure there is a logical explanation but i cannot identify it.
It's important to note that the animation is already running with another set of animated images.
Any help or tip is more than welcome :)
dispatch_async(allowToTouchThread, ^{
//[NSThread sleepForTimeInterval:2.0];
int randomnReward = 0;
WizzReward* currentReward = [rewardListForPages objectAtIndex:randomnReward];
WizzAnimalModel* animalModel = [gameLevelManager getAnimalModelByCategoryId: [currentReward getAnimalId]];
NSMutableArray* expressionsForAnimals = [animalModel getArrayForClosedEyesFaceExpression];
float animationDuration = [animalModel getStandardRewardAnimationDuration];
pageContentViewController.imagesArrayFileLevel1 = expressionsForAnimals;
pageContentViewController.animationLevel1Duration = animationDuration;
}
});
What about replacing your dispatch_async to a dispatch_after and set a delay of 2 seconds:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
int randomnReward = 0;
WizzReward* currentReward = [rewardListForPages objectAtIndex:randomnReward];
WizzAnimalModel* animalModel = [gameLevelManager getAnimalModelByCategoryId: [currentReward getAnimalId]];
NSMutableArray* expressionsForAnimals = [animalModel getArrayForClosedEyesFaceExpression];
float animationDuration = [animalModel getStandardRewardAnimationDuration];
pageContentViewController.imagesArrayFileLevel1 = expressionsForAnimals;
pageContentViewController.animationLevel1Duration = animationDuration;
});
Changing the animated images without any timer, could be done directly by modifying the array of images.
However, changing the same animated images with a timer, needed 3 steps:
first the modification of the image arrays
second the actual re-association of the UIView AnimationImages to the modified array of images;
and finally startAnimating the UIView eventhough it hasn't been stopped beforehand
The source code below now works fine with a timer:
WizzReward* currentReward = [rewardListForPages objectAtIndex:0];
WizzAnimalModel* animalModel = [gameLevelManager getAnimalModelByCategoryId: [currentReward getAnimalId]];
NSMutableArray* expressionsForAnimals = [animalModel getArrayForClosedEyesFaceExpression];
pageContentViewController.imagesArrayFileLevel1 = expressionsForAnimals;
pageContentViewController.reward1CurrentPage.animationImages = pageContentViewController.imagesArrayFileLevel1;
[pageContentViewController.reward1CurrentPage startAnimating];
I did find neither "time vs no-timer difference"information in the literature nor actual reason why it should be done like that; but it works every time. Hope it helps anyone who runs into the same problem.
I am creating an animation using the below code:
NSMutableArray *dashBoy = [NSMutableArray array];
for (int i = 1; i<= 20; i++) {
butterfly = [NSString stringWithFormat:#"5c_%d.jpg", i];
if ((image = [UIImage imageNamed:butterfly]))
[dashBoy addObject:image];
}
[stgImageView setAnimationImages:dashBoy];
[stgImageView setAnimationDuration:7.0f];
[stgImageView setAnimationRepeatCount:-1];
[stgImageView startAnimating];
My requirement is if dashBoy is 5c_10.jpg, then pause the image for about 5 sec and then resume animation and another if dashBoy is 5c_20.jpg, then pause the image again for about 5 sec and resume again. Is it possible?
You can't use variable speeds for UIImageView animations like this.
There are a couple of ways around it though.
1. Do it all yourself.
For this you would use something like an NSTimer and have it fire a method repeatedly to change the images for you. Using this you can time each image individually (you could even create a data object that holds an image and the length of time to show it and then create and array of these.
2. Manipulate the current method.
If you have 20 images and you want to show them all in 7 seconds then thats... 0.35 seconds per image. So a 5 second pause is about 14 images worth.
So instead of adding each image once you can add 1-9 once and then add 10 fourteen times. 11-19 once and then 20 fourteen times.
This will then swap like it is doing but when it reaches 10 it will swap it for another copy of the same image so it will look like it's pausing.
You will then have to increase the duration... to 17 seconds to get a similar duration for each image.
Which I'd do?
Although it sounds like a hack (because it is) I think I'd give the second method a go first. It's a lot easier to get working so if it does fail you haven't spent a long time setting it up.
The first method is a lot more work to set up but will allow for greater control of the animation.
Quick and dirty example for method 1
Create an object something like...
MyTimedImage
------------
UIImage *image
CGFloat duration
So for instance...
// probably want this as a property
NSMutableArray *timedImages = [NSMutableArray array];
MyTimedImage *timedImage = [MyTimedImage new];
timedImage.image = [UIImage imageNamed:[NSString stringWithFormat:#"5c_%d.jpg", i]];
timedImage.duration = 0.4;
[timedImages addObject:timedImage];
Then you want a way of displaying them...
//set up properties something like this...
#property (nonatomic, assign) NSInteger currentIndex;
#property (nonatomic, assign) BOOL paused;
#property (nonatomic, assign) NSTimer *imageTimer;
- (void)displayNextImage
{
if (self.paused) {
return;
}
NSInteger nextIndex = self.currentIndex + 1;
if (nextIndex == [self.timedImages count]) {
nextIndex = 0;
}
MyTimedImage *nextImage = self.timedImages[nextIndex];
self.currentIndex = nextIndex;
self.imageView.image = nextImage.image;
self.imageTimer = [NSTimer scheduledTimerWithInterval:nextImage.duration target:self selector:#selector(displayNextImage) userInfo:nil repeats:NO];
}
Using the properties that you've exposed you can pause the image view on the current image from a button press (for example).
To start the process just run [self displayNextImage]; you can start anywhere in the loop of images too.
using UIImageView animation, this is not possible, you may need to create your own animation logic like loading all UIImages in array and through timer, switch to next animation frame and when you desire to pause, invalidate the timer.
I want to add a photo captured from my app and display on the iPad for 2 seconds. I have a self timing photo app that counts down from 4 to 1 then takes the photo. I have it set to display a PNG image when it reaches zero. But I want to display the photo that was taken immediately after the PNG image. I am stumped.
This is part of the code that saves the image.
- (void)captureEnded:(CameraView *)camView {
NSLog(#"%f, %f", [camView capturedImage].size.width, [camView capturedImage].size.height);
UIImage *img = [camView capturedImage];
This is part of the code that displays the countdown. When the timer reaches zero "smile3" displays on screen for .2 seconds. I want the UI Image above to display following that smile3 for 2 seconds. Any thoughts?
if (mTimer-counter+1 == 0) {
[[AppDelegate sharedAppDelegate] playShutterSound];
lblRemainPhoto.hidden = YES;
[btnTimeCounter setBackgroundImage:[UIImage imageNamed:#"smile3"] forState:UIControlStateNormal];
[NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:#selector(anim1) userInfo:nil repeats:NO];
}
I'm trying to make a photo slider similar to iOS's photos app. I've seen PhotoScroller, and I'm using initWithContentsOfFile.
CATiledLayer seems like a good idea, except I have no way of pre-generating tiles. The tiles also take up a lot of space. Images are part of a document bundle synced up with iCloud. Photos are typically JPEG. From hours of reading it seems like generating the tiles on the fly is slower than just loading the whole image.
It seems like a majority of the time is spent in decompressing the image anyway. And moving it to a background queue and displaying a smaller image should work well. So that's what I'm trying to do. And it works to a point, but if I slide without waiting for the image to load there's still somewhat of a stutter, which sometimes causes the scroll view to hang momentarily (when paging).
This is the function that sets the image:
- (void)setImage:(UIImage *)image placeholder:(UIImage *)placeholder
{
_image = image;
self.zoomScale = 1.0;
imageView.image = nil;
imageSize = image.size;
imageView.frame = CGRectMake(0, 0, image.size.width, image.size.height);
MachTimer *timer = [MachTimer new];
[timer start];
imageView.image = placeholder;
NSLog(#"placeholder: %f", [timer elapsedSeconds]);
//imageView.layer.contents = (id)placeholder.CGImage;
self.contentSize = image.size;
[self setMaxMinZoomScalesForCurrentBounds];
self.zoomScale = self.minimumZoomScale;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
MachTimer *timer = [MachTimer new];
[timer start];
UIGraphicsBeginImageContext(CGSizeMake(1, 1));
[image drawAtPoint:CGPointZero];
UIGraphicsEndImageContext();
NSLog(#"decode: %f", [timer elapsedSeconds]);
dispatch_sync(dispatch_get_main_queue(), ^{
if(_image == image) {
MachTimer *timer = [MachTimer new];
[timer start];
imageView.image = image;
NSLog(#"display: %f", [timer elapsedSeconds]);
//imageView.layer.contents = (id)image.CGImage;
}
});
});
}
My "placeholder" times are about 0.00005 - 0.00006 seconds (decompress and display), and they're 480px tall. My "decode" times (for full image) are about 0.8 to 1.2 seconds. "display" is about 0.0001 seconds (which is about 0.1 milliseconds).
So with those times the UI should be smooth as butter, but it isn't.
I've even tried to go as far as setting contents of a regular UIView to the CGImage. I've tried iOS 6.0's drawsAsynchronously, and that seems to make it a little worse.
What am I doing wrong?
EDIT:
I've pushed my sample project to GitHub:
https://github.com/lukescott/PhotoScroller
I would create a background thread for hard work (photo load or decode) and when its done then delegate UI work (draw or whatever) to main thread using one of tools (queues, msgs, etc).