I'm working on and iPhone app, and have an issue with a gesture recognizer.
I've added an UITapGestureRecognizer on a view, then I transform the layer related to this view with CABasicAnimation. After this transformation, the gesture recognizer only works in the area occupied by the view before the transformation.
Hope this little description of my problem is understandable..
Here is some code :
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(myViewTapped:)];
[self.myView addGestureRecognizer:tapGestureRecognizer];
CABasicAnimation * animation = [CABasicAnimation animationWithKeyPath:#"position.y"];
[animation setFromValue:[NSNumber numberWithFloat:0]];
[animation setToValue:[NSNumber numberWithFloat: - 100]];
[animation setDuration:.3];
[animation setTimingFunction:[CAMediaTimingFunction functionWithControlPoints:.55 :-0.25 :.30 :1.4]];
animation.additive = YES;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
[self.myView.layer addAnimation:animation forKey:nil];
How can I handle this issue ?
Thanks !
You are animating only the graphical part of the view (the CALayer), not the part responsable for user interaction (the UIView itself).
Your code move the layer and made it been drown elsewhere, but don't change the frame (or the bound+center).
You have 3 option (well maybe more, just I can think of these 3):
1) use the UIView-based animations [UIView animation...]
2) struct with you code but also relocare the view after the animation take place (but this may rise issues cause your layer will be moved also).
3) use your animation but put the gesture recognizer on a parent (bigger) view and then check the events there...
Related
I will start off by explaining that I have seen many questions and answers regarding this type of feature, but I am still having problems implementing it myself. I am using ARC, and am not using auto-layout or storyboard. I define my layouts with constraints in code, so the way I have been trying to implement my animation is a little different. Lastly, this is an iPad application.
To the specific problem at hand, I have a subview that starts off hidden but appears when an action takes place. I would like this subview to use the hidden feature, but slide in and out after it appears and before it is hidden. So far, I have gotten halfway there and am able to get the view to slide in without issue. Below is the code that accomplishes this.
detailView.hidden = NO;
// Perform Animation - Slide In
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.duration = kAnimationTimeout;
animation.removedOnCompletion = NO;
animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(800.0, 0.0, 1.0)];
[detailView.layer addAnimation:animation forKey:nil];
However, I have been unsuccessful with trying getting the view to slide out before it is hidden. Below is the code that I added to attempt at completing this feature.
// Perform Animation - Slide Out
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.duration = kAnimationTimeout;
animation.removedOnCompletion = NO;
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-800.0, 0.0, 1.0)];
[detailView.layer addAnimation:animation forKey:nil];
detailView.hidden = YES;
The result I get is that the view simply disappears like it was hidden, which it always did. Do I need to remove one animation that is added to a view before I add a different animation? Or is my CATransform3DMakeTranslation incorrectly defined?
Turns out detailView.hidden was being called before the animation started. I resolved this by adding a selector with a delay that contained a method to hide my view.
[self performSelector:#selector(hideDetailView) withObject:nil afterDelay:.40];
I have a UIView containing a UITapGestureRecognizer that triggers a method called handleLeftTap.
-(void)handleLeftTap {
self.player.direction = LEFT;
if(!self.isAnimating && self.toTile.isWalkable) {
for(UIView *currentView in self.subviews) {
CABasicAnimation *moveAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
[moveAnimation setDuration:MOVE_ANIMATION_DURATION];
[moveAnimation setDelegate:self];
[moveAnimation setRemovedOnCompletion:NO];
[moveAnimation setFillMode:kCAFillModeForwards];
moveAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(currentView.center.x+TILE_WIDTH, currentView.center.y)];
[currentView.layer addAnimation:moveAnimation forKey:nil];
}
NSLog(#"animated\n");
}
}
When I tap on the screen for the first time, this animation works perfectly; every subview moves to the right by TILE_WIDTH pixels. However, if I tap the screen again, the animation doesn't work at all; no view moves. I stepped through this code with breakpoints and verified that this animation is being added to the layers. It's just that the animation isn't being applied or something like that. Is there a way to fix this?
Not quite sure, but are you sure that the second animation is a different location than the location after the first animation?
I have a simple animation setup in a Mac App where a page control swaps out some views. I am very close to getting the paging-like animation I want, but there is a slight problem. The paging works and the new page animates into place correctly, but the new page also replaces the initial page before the animation. I wish to ultimately have the view that is being pushed out remain the same instead of changing to the new page early. Here is a video showing the problem, slowed down to a 3 second animation duration. Here is the code:
UASourceView *previousSourceView = [self.sourceViewContainer.subviews objectAtIndex:0];
NSInteger previousIndex = [self.sourceViews indexOfObjectIdenticalTo:previousSourceView];
CATransition *pushTransition = [CATransition animation];
[pushTransition setType:kCATransitionPush];
[pushTransition setSubtype:(previousIndex < index) ? kCATransitionFromRight : kCATransitionFromLeft];
[pushTransition setDuration:PANEL_PAGE_SWIPE_ANIMATION_DURATION];
[pushTransition setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[pushTransition setRemovedOnCompletion:YES];
[CATransaction begin];
[self.sourceViewContainer replaceSubview:previousSourceView with:sourceView];
[CATransaction commit];
[sourceView.layer addAnimation:pushTransition forKey:#"push"];
There is a fixed container view self.sourceViewContainer that gets a new subview replaced on every animation. The problem, as seen above is that the previousSourceView gets replaced immediately by the sourceView and gets pushed in as well. Please help me stop the immediate replacement. Where am I going wrong?
*Note, iOS tag is added because this code is platform independent.
I solved similar issues using CAAnimationBlocks (this is my fork of the original, which the author has moved in a direction I didn't much care for).
The idea is to set removedOnCompletion to NO and fillMode to kCAFillModeForwards and perform the actual switch (without animation) in the completion block. This has been tested under OSX only (you might need a different solution for iOS, which already supports completion blocks).
Here's an example usage from my code, which moves a piece on a chess board, and back again:
CABasicAnimation *moveAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
moveAnimation.fromValue = [NSValue valueWithPoint:[self _pointForSquare:move.from()]];
moveAnimation.toValue = [NSValue valueWithPoint:[self _pointForSquare:move.to()]];
moveAnimation.duration = _animateSpeed / 1000.0;
moveAnimation.autoreverses = YES;
moveAnimation.removedOnCompletion = NO;
moveAnimation.fillMode = kCAFillModeForwards;
moveAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
moveAnimation.completion = ^(BOOL finished)
{
[pieceLayer removeAnimationForKey:#"movePiece"];
[self _setPieceLayer:pieceLayer toPiece:piece];
[pieceLayer setNeedsDisplay];
};
[pieceLayer addAnimation:moveAnimation forKey:#"movePiece"];
I am trying to add a cool animation effect to the UINavigationController, however I found once I do it it will mess up the other views displayed on top of other views which suppose to be on top of it.
e.g. navivc is a UINavigationController
overlayview is a subview of navivc's superview, and placed on top of navivc.
...
CATransform3D t1 = CATransform3DIdentity;
t1.m34 = 1.0/-900;
t1 = CATransform3DScale(t1, 0.7, 0.7, 0.3);
t1 = CATransform3DRotate(t1, 15.0f*M_PI/180.0f, 1, 0, 0);
CABasicAnimation *move = [CABasicAnimation animationWithKeyPath:#"transform" ];
//[move setFromValue:[NSNumber numberWithFloat:0.0f]];
[move setToValue:[NSNumber valueWithCATransform3D:t1]];
[move setDuration:0.3f];
move.removedOnCompletion = NO;
move.fillMode = kCAFillModeForwards;
[move setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
CAAnimationGroup *group = [CAAnimationGroup animation];
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
[group setDuration:0.3f];
[group setAnimations:[NSArray arrayWithObjects:move, nil]];
[navivc.view.layer addAnimation:move forKey:#"coolanimation"];
...
The animation is normal and correct, however now navivc's view show on top of overlayview.
Seemed the problem only happen on "transform" path, I tried other key paths, e.g. position, scale, translation etc., all those seemed to be correct. But the 3D transform messed up the view on top.
I would advise against animating a view controller's content view. Instead, create a container view inside the view controller's content view and animate that.
It isn't clear from your post where overlayView is in your view hierarchy, so it's hard to give you specific instructions on how to prevent your animation from covering your overlay view. You might try changing the zPosition of the overlayView's layer to make it appear above the layer you are animating.
i'm rotating a CALayer using CABasicAnimation and works fine. The problem is, when I try to rotate the same layer, it returns back to its original position before it will rotate. My expected output is that, for the next rotation, it should start from where it has ended. Here's my code:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
animation.fromValue = 0;
animation.toValue = [NSNumber numberWithFloat:3.0];
animation.duration = 3.0;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.autoreverses = NO;
[calayer addAnimation:animation forKey:#"rotate"];
Is there anything missing on my code? thanks
What's happening is that you're seeing the animation in the presentation layer. However, that doesn't update the actual position of your layer. So, once the animation finishes, you see the layer as it was because it hasn't changed.
It's really worth reading the "Core Animation Rendering Architecture". Otherwise this can be very confusing.
To fix it, set a delegate to your CABasicAnimation as follows:
[animation setDelegate:self];
Then, create a method to set your target properties that you want when the animation completes. Now, here's the confusing part. You should do this on animationDidStart not animationDidStop. Otherwise, the presentation layer animation will finish, and you'll get a flicker as you see the calayer in the original position then it jumps - without animation - to the target position. Try it with animationDidStop and you'll see what I mean.
I hope that's not too confusing!
- (void)animationDidStart:(CAAnimation *)theAnimation
{
[calayer setWhateverPropertiesExpected];
}
EDIT:
I later discovered that Apple recommend a much better way to do this.
Oleg Begemann has a nice description of the correct technique in his blog post Prevent Layers from Snapping Back to Original Values When Using Explicit CAAnimations
Basically what you do is before you start the animation, you take a note of the layer's current value, i.e., the original value:
// Save the original value
CGFloat originalY = layer.position.y;
Next, set the toValue on the layer's model. Therefore the layer model has the final value of whatever animation you are about to do:
// Change the model value
layer.position = CGPointMake(layer.position.x, 300.0);
Then, you set the animation up with the animation fromValue being that original value that you noted above:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position.y"];
// Now specify the fromValue for the animation because
// the current model value is already the correct toValue
animation.fromValue = #(originalY);
animation.duration = 1.0;
// Use the name of the animated property as key
// to override the implicit animation
[layer addAnimation:animation forKey:#"position"];
Note that code in edit above was copy/pasted from Ole Begemann's blog for clarity
If you want the animation to start from where it has ended, then set the fromValue property to the CALayer's current rotation.
Obtaining that value is tricky, but this SO post shows you how: https://stackoverflow.com/a/6706604/1072846