震动效果

本文介绍了一个用于iOS开发的视图震动效果组件,通过Objective-C实现,提供了丰富的配置选项和简便的API调用方式。开发者可以根据需求定制震动的方向、强度、持续时间和结束行为等。

震动效果

 

效果

 

源码

https://github.com/rFlex/SCViewShaker

https://github.com/YouXianMing/Animations

//
//  UIView+Shake.h
//  Animations
//
//  Created by YouXianMing on 16/2/25.
//  Copyright © 2016年 YouXianMing. All rights reserved.
//

#import <UIKit/UIKit.h>

// https://github.com/rFlex/SCViewShaker

#define kDefaultShakeOptions  (SCShakeOptionsDirectionHorizontal | SCShakeOptionsForceInterpolationExpDown | SCShakeOptionsAtEndComplete)
#define kDefaultShakeForce    (0.075)
#define kDefaultShakeDuration (0.5)
#define kDefaultShakeIterationDuration (0.03)

typedef enum : NSUInteger {
    
    SCShakeOptionsDirectionRotate                = 0,
    SCShakeOptionsDirectionHorizontal            = 1,
    SCShakeOptionsDirectionVertical              = 2,
    SCShakeOptionsDirectionHorizontalAndVertical = 3,
    SCShakeOptionsForceInterpolationNone         = 4,
    SCShakeOptionsForceInterpolationLinearUp     = 8,
    SCShakeOptionsForceInterpolationLinearDown   = 16,
    SCShakeOptionsForceInterpolationExpUp        = 32,
    SCShakeOptionsForceInterpolationExpDown      = 64,
    SCShakeOptionsForceInterpolationRandom       = 128,
    SCShakeOptionsAtEndRestart                   = 256,
    SCShakeOptionsAtEndComplete                  = 512,
    SCShakeOptionsAtEndContinue                  = 1024,
    SCShakeOptionsAutoreverse                    = 2048
    
} SCShakeOptions;

typedef void(^ShakeCompletionHandler)();

@interface UIView (Shake)

/**
 *  Returns whether the view is currently shaking or not.
 */
@property (readonly, nonatomic) BOOL isShaking;

/**
 *  Shake the view using the default options. The view will be shaken for a short amount of time.
 */
- (void)shake;

/*
 Shake the view using the specified options.
 |shakeOptions| is an enum of options that can be activated by using the OR operator (like SCShakeOptionsDirectionRotate | SCShakeOptionsForceInterpolationNone).
 
 |force| is the coefficient of force to apply on each shake iteration (typically between 0 and 1 even though nothing prevents you for setting a higher value if you want).
 
 |duration| is the total duration of the shaking motion. This may be ignored depending of the options you set.
 iterationDuration is how long each shake iteration will last. You may want to set a very low value (like 0.02) if you want a proper shake effect.
 
 |completionHandler|, if not null, is the block that will be invoked when the shake finishes.
 */
- (void)shakeWithOptions:(SCShakeOptions)shakeOptions
                   force:(CGFloat)force duration:(CGFloat)duration
       iterationDuration:(CGFloat)iterationDuration
       completionHandler:(ShakeCompletionHandler)completionHandler;

/**
 *  End the current shaking action, if any
 */
- (void)endShake;

@end
//
//  UIView+Shake.m
//  Animations
//
//  Created by YouXianMing on 16/2/25.
//  Copyright © 2016年 YouXianMing. All rights reserved.
//

#import "UIView+Shake.h"
#import <objc/runtime.h>

#define HAS_OPT(options, option) ((options & option) == option)

@interface SCShakeInfo : NSObject

@property (assign, nonatomic)   CGAffineTransform baseTransform;
@property (assign, nonatomic)   BOOL shaking;
@property (assign, nonatomic)   SCShakeOptions options;
@property (assign, nonatomic)   CGFloat force;
@property (assign, nonatomic)   CGFloat duration;
@property (assign, nonatomic)   CGFloat iterationDuration;
@property (assign, nonatomic)   CFTimeInterval startTime;
@property (strong, nonatomic)   ShakeCompletionHandler completionHandler;
@property (assign, nonatomic)   BOOL reversed;
@property (readonly, nonatomic) CGFloat completionRatio;

@end

@implementation SCShakeInfo

- (CGFloat)completionRatio {
    
    return (CACurrentMediaTime() - self.startTime) / self.duration;
}

@end

@implementation UIView (Shake)

static const char *ShakeInfoKey = "ShakeInfo";

- (void)shake {
    
    [self shakeWithOptions:kDefaultShakeOptions
                     force:kDefaultShakeForce
                  duration:kDefaultShakeDuration
         iterationDuration:kDefaultShakeIterationDuration
         completionHandler:nil];
}

- (void)shakeWithOptions:(SCShakeOptions)shakeOptions
                   force:(CGFloat)force
                duration:(CGFloat)duration
       iterationDuration:(CGFloat)iterationDuration
       completionHandler:(ShakeCompletionHandler)completionHandler {
    
    SCShakeInfo *shakeInfo = [self shakeInfo];
    
    shakeInfo.options           = shakeOptions;
    shakeInfo.force             = force;
    shakeInfo.startTime         = CACurrentMediaTime();
    shakeInfo.duration          = duration;
    shakeInfo.iterationDuration = iterationDuration;
    shakeInfo.completionHandler = completionHandler;
    
    if (!shakeInfo.shaking) {
        
        shakeInfo.baseTransform = self.transform;
        shakeInfo.shaking       = YES;
        
        [self _doAnimation:1];
    }
}

- (CGFloat)_getInterpolationRatio:(CGFloat)completionRatio options:(SCShakeOptions)options {
    
    CGFloat (*interpFunc)(CGFloat) = nil;
    
    if (HAS_OPT(options, SCShakeOptionsForceInterpolationRandom)) {
        
        interpFunc =& InterpolateRandom;
        
    } else if (HAS_OPT(options, SCShakeOptionsForceInterpolationExpDown)) {
        
        interpFunc =& InterpolateExpDown;
        
    } else if (HAS_OPT(options, SCShakeOptionsForceInterpolationExpUp)) {
        
        interpFunc =& InterpolateExpUp;
        
    } else if (HAS_OPT(options, SCShakeOptionsForceInterpolationLinearDown)) {
        
        interpFunc =& InterpolateLinearDown;
        
    } else if (HAS_OPT(options, SCShakeOptionsForceInterpolationLinearUp)) {
        
        interpFunc =& InterpolateLinearUp;
        
    } else {
        
        interpFunc =& InterpolateNone;
    }
    
    return interpFunc(completionRatio);
}

- (void)_animate:(CGFloat)force shakeInfo:(SCShakeInfo *)shakeInfo {
    
    CGAffineTransform baseTransform = shakeInfo.baseTransform;
    SCShakeOptions    options       = shakeInfo.options;
    
    if (HAS_OPT(options, SCShakeOptionsDirectionHorizontalAndVertical)) {
        
        if (arc4random_uniform(2) == 1) {
            
            self.transform = CGAffineTransformTranslate(baseTransform, 0, force * self.bounds.size.height);
            
        } else {
            
            self.transform = CGAffineTransformTranslate(baseTransform, force * self.bounds.size.width, 0);
        }
        
    } else if (HAS_OPT(options, SCShakeOptionsDirectionVertical)) {
        
        self.transform = CGAffineTransformTranslate(baseTransform, 0, force * self.bounds.size.height);
        
    } else if (HAS_OPT(options, SCShakeOptionsDirectionHorizontal)) {
        
        self.transform = CGAffineTransformTranslate(baseTransform, force * self.bounds.size.width, 0);
        
    } else {
        
        self.transform = CGAffineTransformRotate(baseTransform, force * M_PI_2);
    }
}

- (void)_doAnimation:(CGFloat)direction {
    
    SCShakeInfo *shakeInfo  = [self shakeInfo];
    SCShakeOptions options  = shakeInfo.options;
    CGFloat completionRatio = shakeInfo.completionRatio;
    
    if (completionRatio > 1) {
        
        completionRatio = 1;
    }
    
    if (shakeInfo.reversed) {
        
        completionRatio = 1 - completionRatio;
    }
    
    CGFloat interpolationRatio = [self _getInterpolationRatio:completionRatio options:options];
    CGFloat force              = shakeInfo.force * interpolationRatio * direction;
    CGFloat iterationDuration  = shakeInfo.iterationDuration;
    
    [UIView animateWithDuration:iterationDuration animations:^{
        
        [self _animate:force shakeInfo:shakeInfo];
        
    } completion:^(BOOL finished) {
        
        if (shakeInfo.shaking) {
            
            BOOL shouldRecurse = YES;
            if (shakeInfo.completionRatio > 1) {
                
                if (HAS_OPT(shakeInfo.options, SCShakeOptionsAutoreverse)) {
                    
                    shakeInfo.reversed = !shakeInfo.reversed;
                }
                
                if (shakeInfo.reversed || HAS_OPT(shakeInfo.options, SCShakeOptionsAtEndRestart)) {
                    
                    shakeInfo.startTime = CACurrentMediaTime();
                    
                } else if (!HAS_OPT(shakeInfo.options, SCShakeOptionsAtEndContinue)) {
                    
                    shouldRecurse = NO;
                    [self endShake];
                }
            }
            
            if (shouldRecurse) {
                
                [self _doAnimation:direction * -1];
            }
        }
    }];
}

- (void)endShake {
    
    SCShakeInfo *shakeInfo = [self shakeInfo];
    
    if (shakeInfo.shaking) {
        
        shakeInfo.shaking                        = NO;
        self.transform                           = shakeInfo.baseTransform;
        ShakeCompletionHandler completionHandler = shakeInfo.completionHandler;
        shakeInfo.completionHandler              = nil;
        
        if (completionHandler != nil) {
            
            completionHandler();
        }
    }
}

- (BOOL)isShaking {
    
    return [self shakeInfo].shaking;
}

- (SCShakeInfo *)shakeInfo {
    
    SCShakeInfo *shakeInfo = objc_getAssociatedObject(self, ShakeInfoKey);
    
    if (shakeInfo == nil) {
        
        shakeInfo = [SCShakeInfo new];
        objc_setAssociatedObject(self, ShakeInfoKey, shakeInfo, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    return shakeInfo;
}

#pragma Interpolations functions

static CGFloat InterpolateLinearUp(CGFloat input) {
    
    return input;
}

static CGFloat InterpolateLinearDown(CGFloat input) {
    
    return 1 - input;
}

static CGFloat Exp(CGFloat a, int power) {
    
    if (a < 0.5f) {
        
        return (float)pow(a * 2, power) / 2;
        
    } else {
    
        return (float)pow((a - 1) * 2, power) / (power % 2 == 0 ? -2 : 2) + 1;
    }
}

static CGFloat InterpolateExpUp(CGFloat input) {
    
    return Exp(input, 4);
}

static CGFloat InterpolateExpDown(CGFloat input) {
    
    return Exp(1 - input, 4);
}

static CGFloat InterpolateNone(CGFloat input) {
    
    return 1;
}

static CGFloat InterpolateRandom(CGFloat input) {
    
    CGFloat randNb = arc4random_uniform(10000);
    return randNb / 10000.0;
}

@end

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值