There are many situations where you need to "shake" a UIView.
(For example, "draw child user's attention to a control", "connection is slow", "user enters bad input," and so on.)
Would it be possible to do this using UIKit Dynamics?
So you'd have to ..
take the view, say at 0,0
add a spring concept
give it a nudge, say to the "left"
it should swing back and fore on the spring, ultimately settling again to 0,0
Is this possible? I couldn't find an example in the Apple demos. Cheers
Please note that as Niels astutely explains below, a spring is not necessarily the "physics feel" you want for some of these situations: in other situations it may be perfect. As far as I know, all physics in iOS's own apps (eg Messages etc) now uses UIKit Dynamics, so for me it's worth having a handle on "UIView bouncing on a spring".
Just to be clear, of course you can do something "similar", just with an animation. Example...
But that simply doesn't have the same "physics feel" as the rest of iOS, now.
-(void)userInputErrorShake
{
[CATransaction begin];
CAKeyframeAnimation * anim =
[CAKeyframeAnimation animationWithKeyPath:#"transform"];
anim.values = #[
[NSValue valueWithCATransform3D:
CATransform3DMakeTranslation(-4.0f, 0.0f, 0.0f) ],
[NSValue valueWithCATransform3D:
CATransform3DMakeTranslation(4.0f, 0.0f, 0.0f) ]
];
anim.autoreverses = YES;
anim.repeatCount = 1.0f;
anim.duration = 0.1f;
[CATransaction setCompletionBlock:^{}];
[self.layer addAnimation:anim forKey:nil];
[CATransaction commit];
}
If you want to use UIKit Dynamics, you can:
First, define a governing animator:
#property (nonatomic, strong) UIDynamicAnimator *animator;
And instantiate it:
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
Second, add attachment behavior to that animator for view in current location. This will make it spring back when the push is done. You'll have to play around with damping and frequency values.
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:viewToShake attachedToAnchor:viewToShake.center];
attachment.damping = 0.5;
attachment.frequency = 5.0;
[self.animator addBehavior:attachment];
These values aren't quite right, but perhaps it's a starting point in your experimentation.
Apply push behavior (UIPushBehaviorModeInstantaneous) to perturb it. The attachment behavior will then result in its springing back.
UIPushBehavior *push = [[UIPushBehavior alloc] initWithItems:#[viewToShake] mode:UIPushBehaviorModeInstantaneous];
push.pushDirection = CGVectorMake(100, 0);
[self.animator addBehavior:push];
Personally, I'm not crazy about this particular animation (the damped curve doesn't feel quite right to me). I'd be inclined use block based animation to move it one direction (with UIViewAnimationOptionCurveEaseOut), upon completion initiate another to move it in the opposite direction (with UIViewAnimationOptionCurveEaseInOut), and then upon completion of that, use the usingSpringWithDamping rendition of animateWithDuration to move it back to its original spot. IMHO, that yields a curve that feels more like "shake if wrong" experience.
Related
I was looking around for a solution on how to wrap a view in a dialog in iOS when I came across this post, which has this line:
vc.modalPresentationStyle = UIModalPresentationCurrentContext;
It solves my problem of basically creating/imitating a dialog but it does not animate upon transition as mentioned in the post. So what is the simplest way to get the slide up animation?
ps.I would ask this as a sub question in that post but I do not have 50 rep comment :(
Well, once your view has been shown, you can do pretty much any animation you want in it. You can do a simple [UIView animateWithDuration] kind of deal, but I would personally use a CATransition for this, it's relatively simple.
The Way of the QuartzCore
First, I'm gonna assume that the view you're presenting is transparent, and there's another view inside that behaves as the dialog. The view controller that will be presented, let's call it PresentedViewController and holds the dialog property for the view within.
PresentedViewController.m
(Needs to be link against QuartzCore.h)
#import <QuartzCore/QuartzCore.h>
#implementation PresentedViewController
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (animated)
{
CATransition *slide = [CATransition animation];
slide.type = kCATransitionPush;
slide.subtype = kCATransitionFromTop;
slide.duration = 0.4;
slide.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
slide.removedOnCompletion = YES;
[self.dialog.layer addAnimation:slide forKey:#"slidein"];
}
}
Getting Fancy
The good thing about this, is that you can create your own custom animations, and play around with other properties.
CABasicAnimation *animations = [CABasicAnimation animationWithKeyPath:#"transform"];
CATransform3D transform;
// Take outside the screen
transform = CATransform3DMakeTranslation(0, self.view.bounds.size.height, 0);
// Rotate it
transform = CATransform3DRotate(transform, M_PI_4, 0, 0, 1);
animations.fromValue = [NSValue valueWithCATransform3D:transform];
animations.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
animations.duration = 0.4;
animations.fillMode = kCAFillModeForwards;
animations.removedOnCompletion = YES;
animations.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0 :0.0 :0 :1];
[self.dialog.layer addAnimation:animations forKey:#"slidein"];
Here, the view will be moved outside of screen by the translation, then rotated, and it will slide in, back to its original transform. I also modified the timing function to provide a smoother curve.
Consider that I just scraped the surface of what's possible with CoreAnimation, I've been on this road for three years now, and I've grown to like CAAnimation for all the things it does.
Storyboard pro-tip: You can wrap this up very nicely if you build your own custom UIStoryboardSegue subclass.
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 am trying to animate a CAEmitterLayer's emitterPosition like this:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"emitterPosition.x"] ;
animation.toValue = (id) toValue ;
animation.removedOnCompletion = NO ;
animation.duration = self.translationDuration ;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut] ;
animation.completion = ^(BOOL finished)
{
[self animateToOtherSide] ;
} ;
[_emitterLayer addAnimation:animation forKey:#"emitterPosition"] ;
CGPoint newEmitterPosition = CGPointMake(toValue.floatValue, self.bounds.size.height/2.0) ;
_emitterLayer.emitterPosition = newEmitterPosition ;
Note that animation.completion is declared in a category that just calls the corresponding CAAnimation delegate method.
The problem is that this doesn't animate at all and shows the emitter in its final position. I was under the impression that once you add the animation to the layer, you should change the actual model behind it to its final position so that when the animation completes the model is in its final state; i.e., to prevent the animation from "snapping back" to its original position.
I have tried placing the last two lines in the animation.completion block, and this does indeed animate as expected. However, when the animation finishes some particles are intermittently emitted at the emitter's original position. If you put the system under load (for example, scrolling a tableview while the animation is playing), this happens more often.
Another solution I was thinking about is to not move the emitterPosition at all but just move the CAEmitterLayer itself, although I haven't tried that yet.
Thanks in advance for any help you can provide.
Perhaps emitterPosition.x is not a valid key path for animation. Try using emitterPosition instead (and so you'll have to provide CGPoint values wrapped up in an NSValue).
I just tried this on my own machine and it works fine:
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"emitterPosition"];
ba.fromValue = [NSValue valueWithCGPoint:CGPointMake(30,100)];
ba.toValue = [NSValue valueWithCGPoint:CGPointMake(200,100)];
ba.duration = 6;
ba.autoreverses = YES;
ba.repeatCount = HUGE_VALF;
[emit addAnimation:ba forKey:nil];
Other things to think about:
You can typically use nil as the key in addAnimation:forKey:, unless you're going to need to find this animation later (e.g. to remove it or override it in some way). The key path is the important thing.
Setting removedOnCompletion to NO is almost always wrong and is typically the last refuge of a scoundrel (i.e. due to not understanding how animation works).
If, as you say, setting _emitterLayer.emitterPosition = newEmitterPosition inside the completion block does animate, then when why are you using CABasicAnimation at all? Why not just call UIView animate... and set the emitterPosition in the animations block? If that works, it will kill two birds with one stone, moving the position and animating it too.
I've been searching around, but couldn't find an answer that seems to address my specific issue. In my app, I have a custom UIView that animates indefinitely. It's a piece of seaweed, and the animation is very subtle, to make it look like it's swaying in water. I do this with CAKeyframeAnimation objects on the transform.rotation.z and position keys. These are added to a CAAnimationGroup, which is added as a layer to my UIView, like so:
animGroup = [CAAnimationGroup animation];
animGroup.fillMode = kCAFillModeForwards;
[animGroup setAnimations:[NSArray arrayWithObjects:posAnim, rotateAnim, nil]];
animGroup.duration = prpAnim.duration;
animGroup.repeatCount = prpAnim.repeat;
animGroup.delegate = self;
[animGroup setValue:self forKey:[NSString stringWithFormat:#"paper.rot.pos.%d",objID]];
[self.layer addAnimation:animGroup forKey:[NSString stringWithFormat:#"rot.pos.%d",objID]];
I want to further rotate that UIView image (piece of seaweed) when tilting the iPad without disturbing the core animation. I can do this when it's not animated by keyframe, but when I try to combine the two, it doesn't work and I can't figure it out.
I've tried animating the layer using something like this:
CATransform3D rotatePiece3D = CATransform3DMakeRotation(-(tiltRadians), 0.0, 0.0, 1.0);
seaweedPiece.layer.transform = rotatePiece3D;
But that doesn't work either - only when the animation group is turned off. It's a 2D app, so I just want it rotate around the z-axis when tilting left or right. Any ideas how to do this?
You apparently can't alter the rotation separately while it's being animated, so what I was trying to do won't work. I ended up changing the animation key to path, which allowed me to rotate the view separately using the accelerometer.
I have a CALayer that contains few other subLayers (CATextLayer actually)
I want to apply some transformation on that layer when user do usual gesture on the ipad but it doesn't seem to be working properly. The goal of using the CALayer was to apply the transformation only to that Layer so that all of my sub-textLayer would be affected at the same time with the same transformation.
What's happening is that the transformation seem to flicker between previous and current position. I don't really get what could be the problem... When I do a 2 fingers panning gesture for example, CaTextLayer positions flickers all the time during my gesture and at the end they are all correctly placed at their new translated position.
So everything seems to work fine except that flickering thing that is bothering me a lot.
Do I need to set some property I don't know about ? I'm thinking it might have something to do with bounds and frame too....
Here's how I create my CATextLayer (This is done only once at creation and it works properly):
_textString = [[NSString alloc] initWithString: text];
_position = position;
attributedTextLayer_ = [[CATextLayer alloc] init];
attributedTextLayer_.bounds = frameSize;
//.. Set the font
attributedTextLayer_.string = attrString;
attributedTextLayer_.wrapped = YES;
CFRange fitRange;
CGRect textDisplayRect = CGRectInset(attributedTextLayer_.bounds, 10.f, 10.f);
CGSize recommendedSize = [self suggestSizeAndFitRange:&fitRange
forAttributedString:attrString
usingSize:textDisplayRect.size];
[attributedTextLayer_ setValue:[NSValue valueWithCGSize:recommendedSize] forKeyPath:#"bounds.size"];
attributedTextLayer_.position = _position;
This is how I add them to my Super CALayer
[_layerMgr addSublayer:t.attributedTextLayer_];
[[_drawDelegate UI_GetViewController].view.layer addSublayer:_layerMgr];
And here's how I apply my transformation :
_layerMgr.transform = CATransform3DMakeAffineTransform(_transform);
After lots of reading and testing...I've found my own solution.
It looks like the CoreAnimation uses default animation when you do transformation or operation on any layers. It's very recommended that when you do such CALayer operation that you go through what they call "Transactions".
I've found all about that in the CoreAnimation Programming guide, under the section : transactions.
My solution was then to implement such a transaction, and preventing any animation while doing CALayer operation.
This is what I do when applying my transformation (which prevents flickering) :
-(void)applyTransform
{
if (! CGAffineTransformIsIdentity(_transform))
{
[CATransaction begin];
//This is what prevents all animation during the transaction
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
_layerMgr.transform = CATransform3DMakeAffineTransform(_transform);
[CATransaction commit];
}
}