I am writing an iPad app that presents user documents similar to the way Pages presents them (as large icons of the actual document). I also want to mimic the jiggling beha
For completeness, here is how I animated my CALayer subclass — inspired by the other answers — using an explicit animation.
-(void)stopJiggle
{
[self removeAnimationForKey:@"jiggle"];
}
-(void)startJiggle
{
const float amplitude = 1.0f; // degrees
float r = ( rand() / (float)RAND_MAX ) - 0.5f;
float angleInDegrees = amplitude * (1.0f + r * 0.1f);
float animationRotate = angleInDegrees / 180. * M_PI; // Convert to radians
NSTimeInterval duration = 0.1;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
animation.duration = duration;
animation.additive = YES;
animation.autoreverses = YES;
animation.repeatCount = FLT_MAX;
animation.fromValue = @(-animationRotate);
animation.toValue = @(animationRotate);
animation.timeOffset = ( rand() / (float)RAND_MAX ) * duration;
[self addAnimation:animation forKey:@"jiggle"];
}
@mientus Original Apple Jiggle code in Swift 4, with optional parameters to adjust the duration (i.e. speed), displacement (i.e. position change) and degrees (i.e. rotation amount).
private func degreesToRadians(_ x: CGFloat) -> CGFloat {
return .pi * x / 180.0
}
func startWiggle(
duration: Double = 0.25,
displacement: CGFloat = 1.0,
degreesRotation: CGFloat = 2.0
) {
let negativeDisplacement = -1.0 * displacement
let position = CAKeyframeAnimation.init(keyPath: "position")
position.beginTime = 0.8
position.duration = duration
position.values = [
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
NSValue(cgPoint: CGPoint(x: 0, y: 0)),
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)),
NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
]
position.calculationMode = "linear"
position.isRemovedOnCompletion = false
position.repeatCount = Float.greatestFiniteMagnitude
position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
position.isAdditive = true
let transform = CAKeyframeAnimation.init(keyPath: "transform")
transform.beginTime = 2.6
transform.duration = duration
transform.valueFunction = CAValueFunction(name: kCAValueFunctionRotateZ)
transform.values = [
degreesToRadians(-1.0 * degreesRotation),
degreesToRadians(degreesRotation),
degreesToRadians(-1.0 * degreesRotation)
]
transform.calculationMode = "linear"
transform.isRemovedOnCompletion = false
transform.repeatCount = Float.greatestFiniteMagnitude
transform.isAdditive = true
transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
self.layer.add(position, forKey: nil)
self.layer.add(transform, forKey: nil)
}
In case anyone needs the same code in Swift
class Animation {
static func wiggle(_ btn: UIButton) {
btn.startWiggling()
}
}
extension UIView {
func startWiggling() {
let count = 5
let kAnimationRotateDeg = 1.0
let leftDegrees = (kAnimationRotateDeg * ((count%2 > 0) ? +5 : -5)).convertToDegrees()
let leftWobble = CGAffineTransform(rotationAngle: leftDegrees)
let rightDegrees = (kAnimationRotateDeg * ((count%2 > 0) ? -10 : +10)).convertToDegrees()
let rightWobble = CGAffineTransform(rotationAngle: rightDegrees)
let moveTransform = rightWobble.translatedBy(x: -2.0, y: 2.0)
let concatTransform = rightWobble.concatenating(moveTransform)
self.transform = leftWobble
UIView.animate(withDuration: 0.1,
delay: 0.1,
options: [.allowUserInteraction, .repeat, .autoreverse],
animations: {
UIView.setAnimationRepeatCount(3)
self.transform = concatTransform
}, completion: { success in
self.layer.removeAllAnimations()
self.transform = .identity
})
}
}
Just Call
Animation.wiggle(viewToBeAnimated)
It is always best to write a wrapper over the functions you are calling so that even if you have to change the function arguments or may be the name of the function, it does not take you to rewrite it everywhere in the code.
Check out the openspringboard project.
In particular, setIconAnimation:(BOOL)isAnimating in OpenSpringBoard.m. That should give you some ideas on how to do this.
So I'm sure I'll get yelled at for writing messy code (there are probably simpler ways to do this that I am not aware of because I'm a semi-beginner), but this is a more random version of Vic320's algorithm that varies the amount of rotation and translation. It also decides randomly which direction it will wobble first, which gives a much more random look if you have multiple things wobbling simultaneously. If efficiency is a big problem for you, do not use. This is just what I came up with with the way that I know how to do it.
For anyone wondering you need to #import <QuartzCore/QuartzCore.h>
and add it to your linked libraries.
#define degreesToRadians(x) (M_PI * (x) / 180.0)
- (void)startJiggling:(NSInteger)count {
double kAnimationRotateDeg = (double)(arc4random()%5 + 5) / 10;
double kAnimationTranslateX = (arc4random()%4);
double kAnimationTranslateY = (arc4random()%4);
CGAffineTransform leftWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg * (count%2 ? +1 : -1 ) ));
CGAffineTransform rightWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg * (count%2 ? -1 : +1 ) ));
int leftOrRight = (arc4random()%2);
if (leftOrRight == 0){
CGAffineTransform moveTransform = CGAffineTransformTranslate(rightWobble, -kAnimationTranslateX, -kAnimationTranslateY);
CGAffineTransform conCatTransform = CGAffineTransformConcat(rightWobble, moveTransform);
self.transform = leftWobble; // starting point
[UIView animateWithDuration:0.1
delay:(count * 0.08)
options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse
animations:^{ self.transform = conCatTransform; }
completion:nil];
} else if (leftOrRight == 1) {
CGAffineTransform moveTransform = CGAffineTransformTranslate(leftWobble, -kAnimationTranslateX, -kAnimationTranslateY);
CGAffineTransform conCatTransform = CGAffineTransformConcat(leftWobble, moveTransform);
self.transform = rightWobble; // starting point
[UIView animateWithDuration:0.1
delay:(count * 0.08)
options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse
animations:^{ self.transform = conCatTransform; }
completion:nil];
}
}
- (void)stopJiggling {
[self.layer removeAllAnimations];
self.transform = CGAffineTransformIdentity; // Set it straight
}
Here is the Swift 4.2 version of @mientus' code (which is itself an update of Paul Popiel's version), as an extension of CALayer:
extension CALayer {
private enum WigglingAnimationKey: String {
case position = "wiggling_position_animation"
case transform = "wiggling_transform_animation"
}
func startWiggling() {
let duration = 0.25
let displacement = 1.0
let negativeDisplacement = displacement * -1
let rotationAngle = Measurement(value: 2, unit: UnitAngle.degrees)
// Position animation
let positionAnimation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
positionAnimation.beginTime = 0.8
positionAnimation.duration = duration
positionAnimation.values = [
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
NSValue(cgPoint: CGPoint.zero),
NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
]
positionAnimation.calculationMode = .linear
positionAnimation.isRemovedOnCompletion = false
positionAnimation.repeatCount = .greatestFiniteMagnitude
positionAnimation.beginTime = CFTimeInterval(Float(Int.random(in: 0...25)) / 100)
positionAnimation.isAdditive = true
// Rotation animation
let transformAnimation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.transform))
transformAnimation.beginTime = 2.6
transformAnimation.duration = duration
transformAnimation.valueFunction = CAValueFunction(name: .rotateZ)
transformAnimation.values = [
CGFloat(rotationAngle.converted(to: .radians).value * -1),
CGFloat(rotationAngle.converted(to: .radians).value),
CGFloat(rotationAngle.converted(to: .radians).value * -1)
]
transformAnimation.calculationMode = .linear
transformAnimation.isRemovedOnCompletion = false
transformAnimation.repeatCount = .greatestFiniteMagnitude
transformAnimation.isAdditive = true
transformAnimation.beginTime = CFTimeInterval(Float(Int.random(in: 0...25)) / 100)
self.add(positionAnimation, forKey: WigglingAnimationKey.position.rawValue)
self.add(transformAnimation, forKey: WigglingAnimationKey.transform.rawValue)
}
func stopWiggling() {
self.removeAnimation(forKey: WigglingAnimationKey.position.rawValue)
self.removeAnimation(forKey: WigglingAnimationKey.transform.rawValue)
}
}
Usage (where anyLayer
is a CALayer):
// Start animating.
anyLayer.startWiggling()
// Stop animating.
anyLayer.stopWiggling()