一个水滴动画的实现

由于项目需求,最近做了一个水滴落下的加载动画。最简单的实现当然是用UIImageView直接播放,但是考虑到动画的帧数太多,会导致整个应用程序包增大,还有水滴落下和水面碰撞的效果不够随机,最后直接用纯代码实现。

波纹的实现

波浪动画用CAShapeLayer和正弦曲线来模拟,刷新频率为屏幕刷新频率的一半,即30帧每秒。屏幕的刷新用到CADisplayLink,CADisplayLink的精确度优于NSTimer,非常适合这种和屏幕刷新相关的操作。

加载波形层

- (void)loadWaveLayers{

if (!_waveLayers) 
{
    self.waveLayers = [[NSMutableArray alloc] initWithCapacity:3];

    UIColor *whiteColor = [UIColor whiteColor];
    UIColor *lightGrayColor = [UIColor colorWithRed:207/255.0f
                                              green:207/255.0f
                                               blue:207/255.0f
                                              alpha:1.0f];
    UIColor *darkGrayColor = [UIColor colorWithRed:159/255.0f
                                             green:159/255.0f
                                              blue:159/255.0f
                                             alpha:1.0f];

    NSArray *colors = @[darkGrayColor, lightGrayColor, whiteColor];

    for (int i = 0; i < 3; ++i) {
        CAShapeLayer *waveLayer = [CAShapeLayer layer];
        waveLayer.fillColor = ((UIColor *)colors[i]).CGColor;
        [self.layer addSublayer:waveLayer];

        [_waveLayers addObject:waveLayer];
    }
}
}

设置定时器

_frameTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateVibrations:)];
_frameTimer.frameInterval = 2;
[_frameTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

更新数值

- (void)updateVibrations:(CADisplayLink *)displayLink
{
if (_increase) {
    _variable += 1 / 30.0f;
}else{
    _variable -= 1 / 30.0f;
}

if (_variable <= 1) {
    _increase = YES;
}

if (_variable >= 1.6) {
    _increase = NO;
}

_offsetX += _waveSpeed;

[self drawWaveWithLayer:_waveLayers[0] amplitude:_variable * 3 waveCycle:1.5* M_PI / _waveWidth offsetY:20.0f];    // dark
[self drawWaveWithLayer:_waveLayers[1] amplitude:_variable * 5 waveCycle:1.1 * M_PI / _waveWidth offsetY:22.0f];   // light
[self drawWaveWithLayer:_waveLayers[2] amplitude:_variable * 10 waveCycle:1 * M_PI / _waveWidth offsetY:22.0f];    // white
}

绘制正弦曲线

 - (void)drawWaveWithLayer:(CAShapeLayer *)waveLayer
           amplitude:(CGFloat)amplitude
           waveCycle:(CGFloat)waveCycle
             offsetY:(CGFloat)offsetY
{
   CGMutablePathRef path = CGPathCreateMutable();
   CGFloat y = self.bounds.size.height * 0.5f;
   CGPathMoveToPoint(path, nil, 0, y);

   for (float x = 0.0f; x <=  _waveWidth ; x++) {
       y = amplitude * sin(waveCycle * x + _offsetX) + self.frame.size.height * 0.5f + offsetY;   // 正弦波
       CGPathAddLineToPoint(path, nil, x, y);
   }

   CGPathAddLineToPoint(path, nil, _waveWidth, self.frame.size.height);
   CGPathAddLineToPoint(path, nil, 0, self.frame.size.height);
   CGPathCloseSubpath(path);

   waveLayer.path = path;
   CGPathRelease(path);
}

绘制水滴

绘制水滴的视图

自定义一个View继承自UIView,在drawRect中实现:

- (void)drawRect:(CGRect)rect {
// Drawing code

CGContextRef context = UIGraphicsGetCurrentContext();

CGContextMoveToPoint(context, self.bounds.size.width * 0.5f, 0);

[[UIColor whiteColor] set];
CGContextSetLineWidth(context, 1.0);

CGContextAddCurveToPoint(context,
                         0,
                         self.bounds.size.height,
                         self.bounds.size.width,
                         self.bounds.size.height,
                         self.bounds.size.width * 0.5f,
                         0);

CGContextSetFillColorWithColor(context,[UIColor whiteColor].CGColor);
CGContextFillPath(context);

CGContextStrokePath(context);
}

这里要保证水滴视图的宽高比为2:1,这样贝塞尔曲线绘制出来的才是水滴形状。

给水滴添加重力和碰撞检测

这里用到了UIKit动力学。

UIGravityBehavior *gravityBeahvior = [[UIGravityBehavior alloc] initWithItems:@[_dripView]];
[_animator addBehavior:gravityBeahvior];

UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[_dripView]];
collisionBehavior.translatesReferenceBoundsIntoBoundary = NO;

CGPoint rightEdge = CGPointMake(_barrierLine.frame.origin.x + _barrierLine.frame.size.width,
                                _barrierLine.frame.origin.y);

[collisionBehavior addBoundaryWithIdentifier:@"barrier"
                            fromPoint:_barrierLine.frame.origin
                              toPoint:rightEdge];
collisionBehavior.collisionDelegate = (id)self;
[_animator addBehavior:collisionBehavior];


UIDynamicItemBehavior *itemBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:@[_dripView, _barrierLine]];
itemBehaviour.elasticity = 0.0;
[_animator addBehavior:itemBehaviour];

水滴和波浪碰撞后的喷溅效果

创建圆形水珠

- (void)splashWater
{
if (!_dripLayers) {
    _dripLayers = [[NSMutableArray alloc] initWithCapacity:kDripCount];

    for (int i = 0; i < kDripCount; i++) {

        CALayer *dripLayer = [CALayer layer];
        [_dripLayers addObject:dripLayer];

    }
}

for (int i = 0; i < kDripCount; i++) {

    [self performSelector:@selector(addAnimationToDrip:) withObject:_dripLayers[i] afterDelay:i * 0.01];
}
}

给水珠添加抛物线动画

- (void)addAnimationToDrip:(CALayer *)dripLayer
{
CGFloat width = arc4random() % 15 + 1;
dripLayer.frame = CGRectMake((self.bounds.size.width - width)* 0.5f, self.bounds.size.height * 0.5f + 40, width, width);
dripLayer.cornerRadius = dripLayer.frame.size.width * 0.5f;
dripLayer.backgroundColor = [UIColor whiteColor].CGColor;

[self.layer addSublayer:dripLayer];

CGFloat x3 = arc4random() % ((int)self.bounds.size.width) + 1;
CGFloat y3 = self.bounds.size.height * 0.5f + 40;

CGFloat height = arc4random() % ((int)(self.bounds.size.height * 0.5f));

[self throwDrip:dripLayer
           from:dripLayer.position
             to:CGPointMake(x3, y3)
         height:height
       duration:0.7f];
 }


 - (void)throwDrip:(CALayer *)drip
         from:(CGPoint)start
           to:(CGPoint)end
       height:(CGFloat)height
     duration:(CGFloat)duration
{

CGMutablePathRef path = CGPathCreateMutable();

CGPathMoveToPoint(path, NULL, start.x, start.y);
CGPathAddQuadCurveToPoint(path, NULL, (end.x + start.x) * 0.5f, -height, end.x, end.y);

CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
[animation setPath:path];
animation.duration = duration;
CFRelease(path);
path = nil;Demotic

[drip addAnimation:animation forKey:@"position"];
}

项目地址

WaterDropDemo