Shake visual effect on iPhone (NOT shaking the dev

2019-01-10 01:14发布

问题:

On login failure, I'd prefer to avoid showing an alert, it's too fleeting. Showing the alert and then showing the text somewhere on the login screen seems like duplication.

So I'd like for it to graphically shake my login view when the user enters the wrong user ID and password like the Mac login screen does.

Anyone know if there's a way to pull this off, or have any suggestions for another effect I could use?

回答1:

I think this is a more efficient solution:

Swift:

let anim = CAKeyframeAnimation( keyPath:"transform" )
anim.values = [
    NSValue( CATransform3D:CATransform3DMakeTranslation(-5, 0, 0 ) ),
    NSValue( CATransform3D:CATransform3DMakeTranslation( 5, 0, 0 ) )
]
anim.autoreverses = true
anim.repeatCount = 2
anim.duration = 7/100

viewToShake.layer.addAnimation( anim, forKey:nil )

Obj-C:

CAKeyframeAnimation * anim = [ CAKeyframeAnimation animationWithKeyPath:@"transform" ] ;
anim.values = @[ 
    [ NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-5.0f, 0.0f, 0.0f) ], 
    [ NSValue valueWithCATransform3D:CATransform3DMakeTranslation( 5.0f, 0.0f, 0.0f) ] 
] ;
anim.autoreverses = YES ;
anim.repeatCount = 2.0f ;
anim.duration = 0.07f ;

[ viewToShake.layer addAnimation:anim forKey:nil ] ;

Only one animation object is created and it's all performed at the CoreAnimation level.



回答2:

Using iOS 4+ block based UIKit animations (and loosely based on on jayccrown's answer):

- (void)shakeView:(UIView *)viewToShake
{
    CGFloat t = 2.0;
    CGAffineTransform translateRight  = CGAffineTransformTranslate(CGAffineTransformIdentity, t, 0.0);
    CGAffineTransform translateLeft = CGAffineTransformTranslate(CGAffineTransformIdentity, -t, 0.0);

    viewToShake.transform = translateLeft;

    [UIView animateWithDuration:0.07 delay:0.0 options:UIViewAnimationOptionAutoreverse|UIViewAnimationOptionRepeat animations:^{
        [UIView setAnimationRepeatCount:2.0];
        viewToShake.transform = translateRight;
    } completion:^(BOOL finished) {
        if (finished) {
            [UIView animateWithDuration:0.05 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
                viewToShake.transform = CGAffineTransformIdentity;
            } completion:NULL];
        }
    }];
}


回答3:

I had seen some wobble animation and changed it to shake a view t pixels upright and downleft:

- (void)earthquake:(UIView*)itemView
{
    CGFloat t = 2.0;

    CGAffineTransform leftQuake  = CGAffineTransformTranslate(CGAffineTransformIdentity, t, -t);
    CGAffineTransform rightQuake = CGAffineTransformTranslate(CGAffineTransformIdentity, -t, t);

    itemView.transform = leftQuake;  // starting point

    [UIView beginAnimations:@"earthquake" context:itemView];
    [UIView setAnimationRepeatAutoreverses:YES]; // important
    [UIView setAnimationRepeatCount:5];
    [UIView setAnimationDuration:0.07];
    [UIView setAnimationDelegate:self];
    [UIView setAnimationDidStopSelector:@selector(earthquakeEnded:finished:context:)];

    itemView.transform = rightQuake; // end here & auto-reverse

    [UIView commitAnimations];
}

- (void)earthquakeEnded:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context 
{
    if ([finished boolValue]) 
    {
        UIView* item = (UIView *)context;
        item.transform = CGAffineTransformIdentity;
    }
}


回答4:

Here's a tutorial that details how to do it in Cocoa. Should be the same for the iPhone (or at least quite similar).

http://www.cimgf.com/2008/02/27/core-animation-tutorial-window-shake-effect/



回答5:

Simply changing the X coordinate of the center property of your view might do the trick. If you haven't done any core animation before it's pretty straight-forward.

First, start an animation right, then listen for it to finish, and then move back to the left, and so on. Getting the timing down so it "feels right" might take a while.

- (void)animationFinishCallback:(NSString *)animationID finished:(BOOL)finished context:(void *)context
{
  if ([animationID isEqualToString:@"MoveRight"]) {
    [UIView beginAnimations:@"MoveLeft" context:NULL];
    [UIView setAnimationDuration:1.0];
    [UIView setAnimationDelay: UIViewAnimationCurveEaseIn];
    [UIView setAnimationDelegate:self];
    [UIView setAnimationDidStopSelector:@selector(animationFinishCallback:finished:context:)];

    myView.center = CGRectMake(newX, newY);
    [UIView commitAnimations];
  }
}


回答6:

This UIView category snippet worked for me. It's using 3 CABasingAnimations applied to view's layer.

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>

#define Y_OFFSET 2.0f
#define X_OFFSET 2.0f
#define ANGLE_OFFSET (M_PI_4*0.1f)

@interface UIView (shakeAnimation)

-(BOOL)isShakeAnimationRunning;
-(void)startShakeAnimation;
-(void)stopShakeAnimation;

@end



@implementation UIView (shakeAnimation)

-(BOOL)isShakeAnimationRunning{
     return [self.layer animationForKey:@"shake_rotation"] != nil;
}

-(void)startShakeAnimation{
    CFTimeInterval offset=(double)arc4random()/(double)RAND_MAX;
    self.transform=CGAffineTransformRotate(self.transform, -ANGLE_OFFSET*0.5);
    self.transform=CGAffineTransformTranslate(self.transform, -X_OFFSET*0.5f, -Y_OFFSET*0.5f);

    CABasicAnimation *tAnim=[CABasicAnimation animationWithKeyPath:@"position.x"];
    tAnim.repeatCount=HUGE_VALF;
    tAnim.byValue=[NSNumber numberWithFloat:X_OFFSET];
    tAnim.duration=0.07f;
    tAnim.autoreverses=YES;
    tAnim.timeOffset=offset;
    [self.layer addAnimation:tAnim forKey:@"shake_translation_x"];

    CABasicAnimation *tyAnim=[CABasicAnimation animationWithKeyPath:@"position.y"];
    tyAnim.repeatCount=HUGE_VALF;
    tyAnim.byValue=[NSNumber numberWithFloat:Y_OFFSET];
    tyAnim.duration=0.06f;
    tyAnim.autoreverses=YES;
    tyAnim.timeOffset=offset;
    [self.layer addAnimation:tyAnim forKey:@"shake_translation_y"];

    CABasicAnimation *rAnim=[CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    rAnim.repeatCount=HUGE_VALF;
    rAnim.byValue=[NSNumber numberWithFloat:ANGLE_OFFSET];
    rAnim.duration=0.15f;
    rAnim.autoreverses=YES;
    rAnim.timeOffset=offset;
    [self.layer addAnimation:rAnim forKey:@"shake_rotation"];
}
-(void)stopShakeAnimation{
    [self.layer removeAnimationForKey:@"shake_translation_x"];
    [self.layer removeAnimationForKey:@"shake_translation_y"];
    [self.layer removeAnimationForKey:@"shake_rotation"];
    [UIView animateWithDuration:0.2f animations:^{
        self.transform=CGAffineTransformRotate(self.transform, ANGLE_OFFSET*0.5);
        self.transform=CGAffineTransformTranslate(self.transform, X_OFFSET*0.5, Y_OFFSET*0.5f);
    }];
}

@end

Hope it helpes someone :)



回答7:

In iOS 7.0 or later, UIKit keyframe animation is available.

[UIView animateKeyframesWithDuration:0.5 delay:0.0 options:0 animations:^{
    [UIView setAnimationCurve:UIViewAnimationCurveLinear];

    NSInteger repeatCount = 8;
    NSTimeInterval duration = 1.0 / (NSTimeInterval)repeatCount;

    for (NSInteger i = 0; i < repeatCount; i++) {
        [UIView addKeyframeWithRelativeStartTime:i * duration relativeDuration:duration animations:^{
            CGFloat dx = 5.0;
            if (i == repeatCount - 1) {
                viewToShake.transform = CGAffineTransformIdentity;
            } else if (i % 2) {
                viewToShake.transform = CGAffineTransformTranslate(CGAffineTransformIdentity, -dx, 0.0);
            } else {
                viewToShake.transform = CGAffineTransformTranslate(CGAffineTransformIdentity, +dx, 0.0);
            }
        }];
    }
} completion:completion];


回答8:

very easy shake categorie for UIVoew

https://github.com/jonasschnelli/UIView-I7ShakeAnimation



回答9:

I know the question is already answered, but since I have already implemented something like this previously, I feel it can't hurt to add it:

CAKeyframeAnimation *shakeAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"];
NSArray *transformValues = [NSArray arrayWithObjects:
                        [NSNumber numberWithFloat:((M_PI)/64)],
                        [NSNumber numberWithFloat:(-((M_PI)/64))],
                        [NSNumber numberWithFloat:((M_PI)/64)],
                        [NSNumber numberWithFloat:(-((M_PI)/64))],
                        [NSNumber numberWithFloat:((M_PI)/64)],
                        [NSNumber numberWithFloat:(-((M_PI)/64))],
                        [NSNumber numberWithFloat:0],                                
                        nil];

[shakeAnimation setValues:transformValues];

NSArray *times = [NSArray arrayWithObjects:
                  [NSNumber numberWithFloat:0.14f],
                  [NSNumber numberWithFloat:0.28f],
                  [NSNumber numberWithFloat:0.42f],
                  [NSNumber numberWithFloat:0.57f],
                  [NSNumber numberWithFloat:0.71f],
                  [NSNumber numberWithFloat:0.85f],
                  [NSNumber numberWithFloat:1.0f], 
                  nil];

[shakeAnimation setKeyTimes:times];

shakeAnimation.fillMode = kCAFillModeForwards;
shakeAnimation.removedOnCompletion = NO;
shakeAnimation.duration = 0.6f;

[self.viewToShake.layer addAnimation:shakeAnimation forKey:@"anim"];

Also, since you want the shaking to indicate that the user failed to log in, you might also consider adding this animation that tints the screen red while the screen shakes:

//Put this in the header (.h)
@property (nonatomic, strong) UIView *redView;

//Put this in the implementation (.m)
@synthesize redView;

//Put this in viewDidLoad
self.redView = [[UIView alloc] initWithFrame:self.view.frame];
self.redView.layer.opacity = 0.0f;
self.redView.layer.backgroundColor = [[UIColor redColor] CGColor];

//Put this wherever you check if the login failed
CAKeyframeAnimation *redTint = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
NSArray *transformValues = [NSArray arrayWithObjects:
                           [NSNumber numberWithFloat:0.2f],
                           [NSNumber numberWithFloat:0.0f],                                
                           nil];

[redTint setValues:transformValues];

NSArray *times = [NSArray arrayWithObjects:
                  [NSNumber numberWithFloat:0.5f],
                  [NSNumber numberWithFloat:1.0f], 
                  nil];

[redTint setKeyTimes:times];

redTint.fillMode = kCAFillModeForwards;
redTint.removedOnCompletion = NO;
redTint.duration = 0.6f;

[self.redView.layer addAnimation:shakeAnimation forKey:@"anim"];

Hope this helps!



回答10:

Using Auto Layout, I adapted Chris Miles' answer but animated NSLayoutConstraints like this:

NSLayoutConstraint *left  = ...
NSLayoutConstraint *right = ...

[UIView animateWithDuration:0.08 delay:0.0 options:UIViewAnimationOptionAutoreverse|UIViewAnimationOptionRepeat animations:^{
    [UIView setAnimationRepeatCount:3];
    left.constant  = 15.0;
    right.constant = 25.0;
    [self.view layoutIfNeeded];
} completion:^(BOOL finished) {
    if (finished) {
        [UIView animateWithDuration:0.08 animations:^{
            left.constant  = 20.0;
            right.constant = 20.0;
            [self.view layoutIfNeeded];
        } completion:NULL];
    }
}];


回答11:

A solution I used for constraints which I set in my storyboard. Without using animateWithDuration.

@IBOutlet var balloonHorizontalConstraint: NSLayoutConstraint!

NSTimer.scheduledTimerWithTimeInterval(0.04, target: self, selector: "animateBalloon", userInfo: nil, repeats: true)

func animateBalloon() {
    switch balloonHorizontalConstraint.constant {
    case -46:
        balloonHorizontalConstraint.constant = -50
    default:
        balloonHorizontalConstraint.constant = -46
    }
}

In my case the animation just kept on going, but I pop my viewcontroller after a duration of a few seconds, this stops my timer aswell.