一个优雅的水波动画

今天整理文件的时候发现以前写的一个动画,感觉还不错,就和大家分享一下。

如何实现一个如下图的动画呢?

20170811-1

下面我们就来一步步的实现它。

动画拆解

做一个复杂的东西的时候,我们需要将它拆解成若干个容易实现的细节,之后在将这些细节有序的累加起来就可以实现我们最初的复杂的结构,没准当你完成的时候自己都会发出It’s amazing的感叹。

来看这个动画,我们发现有两个主要元素,一个是晃动水波线,一个是由于水深造成的颜色渐变。那我们得到了第一个容易实现的细节点:

1.水深的颜色渐变

晃动的水波线对我们来说似乎并不是一个简单的元素,那我们再来拆解,或许你注意到了这是一个正弦曲线的水平移动,只是这个正弦曲线稍微有点特殊,加入了一些浮动波峰和波谷的参数。那么我们得到了第二个细节点:

2.水波正弦曲线

同时也确定了第三个细节点:

3.正弦曲线模拟波浪参数方式

然后我们还有一个工作要做,就是以上元素以何种方式组织起来,这也是第四个点:

4.以上元素的组织方式

动画实现

要实现这个动画的话我们先要来谈谈第4点,因为你不能将你分解的元素有效的组织起来的话我们的动画就不能实现。其实在动画拆解的时候,我们心里就要对第4点心中有数了,这是一个慢慢积累经验的过程,当然我们可以参考别人。

针对于第4点,我们的实现方式是用第2点和第3点实现的曲线设置成一个mask,去遮罩一个颜色有渐变的矩形区域(第1点),这样就得到了我们想要的波浪。想要波浪动起来,我们让波浪曲线随着时间发生变化即可。要做成效果当中的三条波浪,只需要将这些元素放置3份即可,只是设置参数不同罢了。

下面我们来分别实现上面拆解的细节点:

针对第1点,我们采用CAGradientLayer来实现,你不仅设置它从某个颜色渐变到另外一个颜色,而且还可以设置它的渐变方向等等。下面几行代码就可以实现我们想要的效果。

_firstGradientLayer = [CAGradientLayer layer];
_firstGradientLayer.frame = CGRectMake(0, sc_screenSize.height-(FirstWaveCenterHeight*BEI6+FirstWaveAmplitude*BEI6), sc_screenSize.width, FirstWaveCenterHeight*BEI6+FirstWaveAmplitude*BEI6);
[_firstGradientLayer setColors:[NSArray arrayWithObjects:(id)[[UIColor colorWithRed:122.0f/255.0f green:95.0f/255.0f blue:233.0f/255.0f alpha:1] CGColor],(id)[[UIColor colorWithRed:70.0f/255.0f green:221.0f/255.0f blue:220.0f/255.0f alpha:1] CGColor], nil]];
[_firstGradientLayer setStartPoint:CGPointMake(0, 0)];
[_firstGradientLayer setEndPoint:CGPointMake(0, 1)];
[self.view.layer addSublayer:_firstGradientLayer];

效果如下:

20170811-2

对于第2点,就要用到我们高中的知识了,对于屏幕上的每一个x值,计算出对应的y值,并将它们连接起来。其中FirstWaveSpeed*BEI6*_firstWaveTime是为了让波浪曲线随时间变化,产生向右平移运动的效果,_firstVariable这个属性是第3点当中要实现的更好的模拟波浪效果的参数,我们下面再进行介绍。

- (CGMutablePathRef)getFirstCurrentWavePath
{
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathMoveToPoint(path, nil, 0, FirstWaveCenterHeight*BEI6);

    float y = 0.0f;

    for (float x = 0.0f; x <=  sc_screenSize.width ; x++) {
        // 正弦波浪公式
        y = FirstWaveAmplitude*BEI6 + FirstWaveAmplitude*BEI6* sin(_firstWaveCircle*x-FirstWaveSpeed*BEI6*_firstWaveTime)*_firstVariable;
        CGPathAddLineToPoint(path, nil, x, y);
    }

    CGPathAddLineToPoint(path, nil, sc_screenSize.width, sc_screenSize.height);
    CGPathAddLineToPoint(path, nil, 0, sc_screenSize.height);
    CGPathCloseSubpath(path);
    return path;
}

下面我们来看3点,为了更好的模拟波浪的效果,我们需要一个曲线振幅的变化,我这里只做了简单的随时间在设定的范围内增大和变小的操作,如果想要更好的效果,可以添加更多参数来控制,当然这也需要一些物理学的知识。

-(void)animateFirstWave
{
    if (_firstIncrease) {
        _firstVariable += 0.01;
    }else{
        _firstVariable -= 0.01;
    }

    if (_firstVariable<=0.4) {
        _firstIncrease = YES;
    }

    if (_firstVariable>=0.8) {
        _firstIncrease = NO;
    }
}

以上基本元素都已经准备好了,那我们来做第4点,将上述元素整合起来。我们用CAShapeLayer来为第1点实现的渐变色提供提供遮罩。

_firstWaveLayer = [CAShapeLayer layer];
_firstWaveLayer.fillColor = [UIColor whiteColor].CGColor;

[_firstGradientLayer setMask:_firstWaveLayer];

//当然别忘记把我们计算好的曲线给设置上去
_firstWaveLayer.path = [self getFirstCurrentWavePath];

这样我们就得到了一个静态的波浪

20170811-3

想要波浪动起来,我们要为它加入上面提到的时间变量_firstWaveTime,一般情况下我们加入时间变量都会采用NSTimer来进行计时,但是此处我们使用CADisplayLink来进行定时操作。为什么使用CADisplayLink呢,因为CADisplayLink的调用频率是和屏幕的刷新频率一致的,这样的话不会出现NSTimer那种由于和屏幕刷新时间冲突造成的渲染滞后画面卡顿的现象。

- (void)initTimer
{
    _firstWaveDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(setWaveLayer)];
    [self setWaveLayer];
}

- (void)setWaveLayer
{
    [self firstWaveTimeCount];
    [self animateFirstWave];
    _firstWaveLayer.path = [self getFirstCurrentWavePath];
}

//_firstCircleTime是波浪的一个时间周期,超过一个时间周期,重复上个周期的操作。
//为了了方便设置波浪的方便设置设置波浪的的速度和周期引入的此参数。
- (void)firstWaveTimeCount
{
    _firstWaveTime++;
    _firstWaveTime = _firstWaveTime%_firstCircleTime;
}

这样我们就得到了第一个流动的波浪

20170811-4

下面只要在重复添加两条线就可以得到我们想要的效果了,不过,需要注意的是第三条波浪的颜色渐变是从左到右的哦,当然这是一个小问题。

动画调整

大部分情况下,我们写出来的复杂动画都会和设计师想要的样子有一定差距,当然也不排除能拿到精准参数的情况,但毕竟让一个设计师告诉你正弦曲线的起始位置不太现实,这个时候我们需要把能控制这个动画的一些参数单独写出来,不要用写死的值,其他的参数依赖于这些值,由这些值计算出来。这样我们就可以方便的调整动画的显示了,甚至可以让设计师自己动手调整出想要的样子。

const static float FirstWaveCenterHeight = 162;
const static float FirstWaveAmplitude = 20.0f;//振幅比例
const static float FirstWaveNum = 1;//容纳曲线循环个数
const static float FirstWaveSpeed = 0.05f;//波浪速度

源码:https://github.com/yangzq007/SHDemo->水波动画