I am trying to animate a button. When I do so (using button.layer addAnimation) the button becomes disabled. Is there any way to allow user interaction when animating? I also tried wrapping everything in a block using animateWithDuration passing in the option UIViewAnimationOptionAllowUserInteraction, but it still doesn't work.
EDIT: It's odd. If I click in the upper corner (where I placed my button) it fires off the event. It's almost like the frame of the button does not follow the animation.
EDIT: What I ended up doing is create an event that fires every 0.1 seconds that sets the button.frame equal to the [[button.layer presentationLayer] frame]. That seemed to do the trick.
Use the following UIView method with UIViewAnimationOptionAllowUserInteraction in the UIViewAnimationOptions parameter:
+(void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion
the best solution is to change the position of the layer after you call the animation:
[button.layer addAnimation:animation forKey:#"animation"];
button.layer.position = endPosition;
What I ended up doing is create an event that fires every 0.1 seconds that sets the button.frame equal to the button.layer
To me the event firing seemed a bit hacky.
However your mentioning of the [[button.layer presentationLayer] frame] brought me onto the right track :-)
So by attaching a gesture recognizer to the superview of the moving view I can do the following detection:
CGPoint tapLocation = [gestureRecognizer locationInView:self.view];
for (UIView* childView in self.view.subviews)
{
CGRect frame = [[childView.layer presentationLayer] frame];
if (CGRectContainsPoint(frame, tapLocation))
{
....
}
}
I wanted the button to zoom in. So I scale down the layer before I start the animation:
layer.transform = CATransform3DScale (layer.transform, 0.01, 0.01, 0.01);
// some animation code
After the animation I scaled it back.
// some animation code
CATransform3D endingScale = CATransform3DScale (layer.transform, 100, 100, 100);
// some animation code
[layer addAnimation:animation forKey:#"transform"];
layer.transform = endingScale;
Looks like if you directly assign it to the layer the frame will change. However using an animation won't change the frame.
Related
I've a screen where following kind of animation has to too be implemented.
The circles unfurl out of the bloom and then spread out to become the individual buttons. Slightly slower the first time it animates (1 sec), slight faster for the second time (.75s), and then faster for all subsequent times (.5s).
But I could not figure out what kind of animation has to be implemented. Any help would be appreciated.
What you describe is a complicated animation. What you should probably do is just break it down into its constituent parts:
Create circular buttons, e.g:
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.backgroundColor = [UIColor ...];
button.frame = CGRectMake(...); // probably define this to be square, just off screen to the bottom
button.layer.cornerRadius = button.frame.size.height / 2.0;
button.layer.borderColor = [UIColor ...].CGColor; // white or black as appropriate
button.layer.borderWidth = 2.0;
[self.view insertSubview:button atIndex:0];
[buttons addObject:button];
Animate their appearance on the screen by animating each of the buttons from off screen to the appropriate vertical position using animateWithDuration. You could move them all together, or you might stagger them so they appear to fall into place, using a rendition that includes delay so you can delay each button a little more. Perhaps something like:
for (NSInteger idx = 0; idx < count; idx++) {
[UIView animateWithDuration:duration * (count - idx) / count
delay:duration * idx / count
options:0
animations:^{
UIButton *button = buttons[idx];
button.center = CGPointMake(button.center.x, idx * 50.0 + 100.0);
}
completion:^(BOOL finished) {
//do next step
}];
}
Unfurl the buttons by animating the frame of the button to be the full width that that row will eventually occupy. Again, you can just use animateWithDuration.
Animate the unrounding of the corners so they're square, like your final buttons. This is the one animation that you cannot easily do with animateWithDuration, but rather you might use CAKeyFrameAnimation:
[buttons enumerateObjectsUsingBlock:^(UIView *button, NSUInteger idx, BOOL *stop) {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"cornerRadius"];
animation.values = #[#(button.layer.cornerRadius), #(0.0)];
animation.duration = duration;
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
if (idx == 0)
animation.delegate = self;
[button.layer addAnimation:animation forKey:nil];
}];
Your delegate's animationDidStop can initiate the next step of the animation.
Replace the wide button with one that is broken up into the right number of individual buttons for that row. You might want to make these be lined up edge to edge.
Do a final frame adjustment of the buttons so they end up with a little space in between them. Again, you can use animateWithDuration.
Replace the previously blank labels on the buttons with the final text strings. Note, the label on a button is not generally an animatable property, so if you'd like it to fade in nicely, you should use transitionWithView, specifying the common superview for all of those buttons and use an options value of UIViewAnimationOptionTransitionCrossDissolve.
If you look at each one of these steps, you can see that none is terribly complicated. Just break your complicated animation into separate steps, write one method for each animation, and have each trigger the next step of the animation.
UIButtons don't have a rounded rectangle look to them any more in iOS 7. Are you going to create a custom subclass of UIButton?
If so, I would suggest using CAAnimation. You should be able to attach a CALayer to each button and set it's borderColor, backgroundColor, borderWidth, and cornerRadius. When you want to animate the layer, just change the bounds of the layer and it will animate to the larger size.
To change the animation's duration or timing you could enclose your changes in a CATransaction. Something like this:
[CATransaction begin];
[CATransaction setAnimationDuration: 1.0];
CAMediaTimingFunction *linearTiming =
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionLinear];
[CATransaction setAnimationTimingFunction: linearTiming]
myButton1Layer.bounds = newButton1ounds;
myButton2Layer.bounds = newButton2ounds;
[CATransaction commit];
I'm scrolling through a scroll view dynamically, it works using
-(IBAction) animateTestingTwo{
[UIView animateWithDuration:4 animations:^{scroller.contentOffset = CGPointMake(0, 2000);}];
}
However the animation curve is not linear, it needs to be so I'm using this:
-(IBAction) animateTestingTwo{
[UIView animateWithDuration:4 options:UIViewAnimationOptionCurveLinear animations:^{scroller.contentOffset = CGPointMake(0, 2000);}];
}
However it isn't working. Any ideas?
Thanks!!
If the duration doesn't matter to you, this can be used:
[scrollview setContentOffset:CGPointMake(10, 200) animated:YES];
Acconding to the documentation:
animated
YES to animate the transition at a constant velocity to the new offset, NO to make the transition immediate.
You can also try to do this inside an animation block to see what happens.
I have a scrollview with multiple views on it. All these views have text field on them and once clicked the keyboard pops up. In certain cases the keyboard may hide this subview and I want to determine if this can be calculated in advance. If yes than how can I do it..? Can the scrollRectToVisible of UIScrollView method be used here to any use.. ? you look at the attached image for further clarification. Thanks for any ideas..
EDIT:
These subviews are dynamically drawn so I can't determine and hard code this.
In Landscape mode :
Height of iPad : 768 pixels.
Height of Keyboard : 352 pixels.
Which means keyboard x co-ordinates : 0.0 and y co-ordinates : 416
Whenever your controls y co-ordinates are greater then 416. Understand it'll be hidden when keyboard will pop-up.
Yeah , you need to shift the subview a little up so that it can be visible, you can use this animation to shift it to any point. Just pass the layer and destination point. and once user pressed return on keyboard, shift it again to the previous point :)
-(void)moveLayer:(CALayer*)layer to:(CGPoint)point
{
// Prepare the animation from the current position to the new position
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [layer valueForKey:#"position"];
animation.duration = 0.2;
animation.toValue = [NSValue valueWithCGPoint:point];
// Update the layer's position so that the layer doesn't snap back when the animation completes.
layer.position = point;
// Add the animation, overriding the implicit animation.
[layer addAnimation:animation forKey:#"position"];
}
Register for the following notification:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardDidChangeFrame:)
name:UIKeyboardDidChangeFrameNotification
object:nil];
Make sure you handle this:
- (void)keyboardDidChangeFrame:(NSNotification *)notification
{
CGRect keyboardEndFrame;
[[notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardEndFrame];
CGRect keyboardFrame = [self.view convertRect:keyboardEndFrame fromView:nil];
// Add your code to check if the keyboard is hiding your views.
// CGRectIntersectsRect should help you here.
// For each view you are worried about hiding, run CGRectIntersectsRect to check
// if an intersection occurs. If it does, then you can
// move your view using UIScrollView's setContentOffset: animated: method.
}
I'm trying to animate a button which moves around the screen. At any point, the user can push the button. But the button doesn't respond to touches. I've tried an animation block but the button simply moves immediately to its end coordinates, while the frame shows the animation (the button is called bubble):
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:5.0];
CGAffineTransform newTransform = CGAffineTransformMakeScale(3, 3);
bubble.transform = CGAffineTransformTranslate(newTransform, 0, -460);
[UIView commitAnimations];
So then I tried Core Animation with the help of some sample code (the path isn't important, it's just an example):
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath,NULL,15.0f,15.f);
CGPathAddCurveToPoint(thePath,NULL,
15.f,250.0f,
295.0f,250.0f,
295.0f,15.0f);
CAKeyframeAnimation *theAnimation=[CAKeyframeAnimation animationWithKeyPath:#"position"];
theAnimation.path=thePath;
CAAnimationGroup *theGroup = [CAAnimationGroup animation];
theGroup.animations=[NSArray arrayWithObject:theAnimation];
theGroup.timingFunction=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
theGroup.duration=15.0;
CFRelease(thePath);
[bubble.layer addAnimation:theGroup forKey:#"animatePosition"];
But the buttons still don't respond to touches. Btw, I have several of these 'bubble' buttons on screen at once so having several NSTimers concurrently active wouldn't be optimal.
Can anyone suggest another approach? Should I perhaps animate UIImageViews and make them repsond to touches? Or will I have the same problem? This has been puzzling me for a couple of days so any help much appreciated.
Thanks :)
Michael
When animating a GUI element like the UIButton, the actual position only changes when the animation is done. This means any touch events during the animation will only work at the starting point of the button. You can get the current displayed position of the button by accessing it's presentationLayer. You'll probably need to implement your own event handling, look at the position when the user touches the screen, and compare that to the bounds you get from the presentationLayer.
Well, for anyone interested, here's my solution which works perfectly well:
- (void)startMinigame {
makeBubbles = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:#selector(generateRandomBubble) userInfo:nil repeats:YES];
floatBubbles = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0 target:self selector:#selector(floatBubbles) userInfo:nil repeats:YES];
}
- (void)generateRandomBubble {
//Get a random image for the bubble
int randomIndex = arc4random()%kNumberBubbles;
UIImage *bubbleImage = [bubbleImages objectAtIndex:randomIndex];
//Create a new bubble object and retrieve the UIButton
Bubble *bubble = [[Bubble alloc]initWithImage:bubbleImage andIndex:randomIndex];
UIButton *bubbleButton = bubble.bubbleButton;
//Configure the button
[bubbleButton addTarget:self action:#selector(bubbleBurst:) forControlEvents:UIControlEventTouchDown];
//Add the bubble to the scene
[self.view insertSubview:bubbleButton belowSubview:bathTub];
//Add the bubble to an array of currently active bubbles:
[self.bubbles addObject:bubble];
[bubble release];
}
- (void)floatBubbles {
//Move every active bubble's frame according to its speed.
for (int i = 0; i < [bubbles count]; ++i) {
Bubble *bubble = [bubbles objectAtIndex:i];
int bubbleSpeed = bubble.speed;
CGRect oldFrame = bubble.bubbleButton.frame;
CGRect newFrame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y - bubbleSpeed, oldFrame.size.width+0.1, oldFrame.size.height+0.1);
bubble.bubbleButton.frame = newFrame;
if (bubble.bubbleButton.frame.origin.y < -100.0) {
[bubbles removeObject:bubble];
[bubble.bubbleButton removeFromSuperview];
}
}
}
This sounds a bit too complicated for UIKit and CoreAnimation. It seems like you're developing something like a game. Why not use a global animation timer for let's say ~60 fps and do your drawing in Quartz2D? Then for each touch you could quickly check any hits between Quartz refreshes.
Try animating the uiview's frame property.
So I've got a problem with buttons and animations. Basically, I'm animating a view using the UIView animations while also trying to listen for taps on the button inside the view. The view is just as large as the button, and the view is actually a subclass of UIImageView with an image below the button. The view is a subview of a container view placed in Interface Builder with user interaction enabled and clipping enabled. All the animation and button handling is done in this UIImageView subclass, while the startFloating message is sent from a separate class as needed.
If I do no animation, the buttonTapped: message gets sent correctly, but during the animation it does not get sent. I've also tried implementing the touchesEnded method, and the same behavior occurs.
UIImageView subclass init (I have the button filled with a color so I can see the frame gets set properly, which it does):
- (id)initWithImage:(UIImage *)image {
self = [super initWithImage:image];
if (self != nil) {
// ...stuffs
UIButton *tapBtn = [UIButton buttonWithType:UIButtonTypeCustom];
tapBtn.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
[tapBtn addTarget:self action:#selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
tapBtn.backgroundColor = [UIColor cyanColor];
[self addSubview:tapBtn];
self.userInteractionEnabled = YES;
}
return self;
}
Animation method that starts the animation (if I don't call this the button works correctly):
- (void)startFloating {
[UIView beginAnimations:#"floating" context:nil];
[UIView setAnimationDelegate:self];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
[UIView setAnimationDuration:10.0f];
self.frame = CGRectMake(self.frame.origin.x, -self.frame.size.height, self.frame.size.width, self.frame.size.height);
[UIView commitAnimations];
}
So, to be clear:
Using the UIView animation effectively disables the button.
Disabling the animation causes the button to work.
The button is correctly sized and positioned on screen, and moves along with the view correctly.
This resolves the issue:
[UIView animateWithDuration:20 delay: 0.0 options: UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction
...
The animation is just eye candy. The animation lags behind the actual movement of the view. The button is already at the destination point when the animation starts. You just see a movie of the view/button moving.
If you want a button to be clickable during the animation, you'll have to make the animation yourself.
... was experiencing this same problem because my code was doing one large animation per block. I made an NSTimer based solution, like the one suggested above, and it worked... yet the movement was jerky (unless I inserted animation within every timer event trigger).
So, since animation was required anyway, I found a solution which requires no timer. It animates only a short distance and thus the button click is still accurate, with only a small error which is my case is very unnoticeable in the UI, and can be reduced depending on your params.
Note below that the error at any given time is < 15.0, which can be reduced for more accuracy depending on your animation speed requirements. You can also reduce the duration time for more speed.
- (void)conveyComplete:(UIView*)v
{
[self convey:v delay:0];
}
- (void)convey:(UIView*)v delay:(int)nDelay
{
[UIView animateWithDuration:.5
delay:nDelay
options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction)
animations: ^
{
CGRect rPos = v.frame;
rPos.origin.x -= 15.0;
v.frame = rPos;
}
completion: ^(BOOL finished)
{
[self conveyComplete:v];
}];
}