I have a button on my UI that I would like to flash (turn on and then off again) every 800ms, once the button has been pressed. I do that with the following code:
- (void)flickEmergencyButton {
// Check whether an emergency is in progress...
if (model.emergencyInProgress) {
// ...and if so, flick the state
self.emergencyButton.selected = !self.emergencyButton.selected;
// Make this method be called again in 800ms
[self performSelector:@selector(flickEmergencyButton) withObject:nil afterDelay:0.8];
} else {
// ...otherwise, turn the button off
self.emergencyButton.selected = NO;
}
}
...which works really well, except: There is a UIScrollView on the UI as well and while the user has his finger down on it and is scrolling around, the button freezes. While I completely understand why that is, I am not sure what to do about it.
The performSelector:withObject:afterDelay
message schedules the message to be send on the current thread, which is the main thread, ie. the UI tread and hence does not get to process the message until all other UI activity has come to an end. Correct? But I need to do this on the UI thread as I cannot select/un-select the button on any other thread, right? So what is the solution here?
I would recommend using Core Animation. Try something like this:
-(void) flash{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.3f];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
if( emergency ){
// Start flashing
[UIView setAnimationRepeatCount:1000];
[UIView setAnimationRepeatAutoreverses:YES];
[btn setAlpha:0.0f];
}else{
// Stop flashing
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationRepeatCount:1];
[btn setAlpha:1.0f];
}
emergency = !emergency;
[UIView commitAnimations];
}
Where btn is declared as
@property(nonatomic, retain) IBOutlet UIButton *btn;
and emergency is a simple BOOL variable.
Call flash to start and to stop the animation.
In this example we animate the alpha attribute for simplicity, but you can do the same with the button backcolor, as Sam said in his answer, or whatever attribute you like.
Hope it helps.
UPDATE:
Regarding making the transition between two images, try calling imageFlash
instead of flash
:
-(void) imageFlash{
CABasicAnimation *imageAnimation = [CABasicAnimation animationWithKeyPath:@"contents"];
[btn setImage:normalState forState:UIControlStateNormal];
if( emergency ){
imageAnimation.duration = 0.5f;
imageAnimation.repeatCount = 1000;
}else{
imageAnimation.repeatCount = 1;
}
imageAnimation.fromValue = (id)normalState.CGImage;
imageAnimation.toValue = (id)emergencyState.CGImage;
[btn.imageView.layer addAnimation:imageAnimation forKey:@"animateContents"];
[btn setImage:normalState forState:UIControlStateNormal]; // Depending on what image you want after the animation.
emergency = !emergency;
}
Where normalState
and emergencyState
are the images you want to use:
Declared as:
UIImage *normalState;
UIImage *emergencyState;
Assigning the images:
normalState = [UIImage imageNamed:@"normal.png"];
emergencyState = [UIImage imageNamed:@"alert.png"];
Good luck!
While this feels like a job for CoreAnimation
(perhaps animating the backgroundColor
of a custom UIControl
) you could accomplish this with an NSTimer
running in the appropriate run loop mode.
e.x.
NSTimer *timer = [NSTimer timerWithTimeInterval:0.8f target:self selector:@selector(flicker:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
and when you want to stop animation:
[timer invalidate], timer = nil;
button.selected = NO;
By adding the timer to all NSRunLoopCommonModes
, you not only add it to the default run loop mode but the mode it is in while user interaction is being continuously processed (UITrackingRunLoopMode
.)
Apple's docs give a more thorough explanation of run loop modes and run loops in general.
来源:https://stackoverflow.com/questions/6893181/how-to-flash-a-button-on-ui-thread