水波表现进度的动画效果实现

2016/01/08 iOS

最近有一个需求,用小球表示进度,小球里水的高度表示进度完成的比例,设置比例参数时,水面会有一个从0开始波动上升的过程,水面上升到指定高度后,水波慢慢平缓最后静止。


网上看了一些示例,大体上有两种实现方式:

  1. 切一张波浪形的图片,进行不断循环的位置变化的动画
  2. 绘制波浪形的线条,并进行线条变化的重绘更新

需求的实现采用的是第二种思路,通过CAShapeLayer绘制波形曲线,并不断改变垂直位置,来达到水面波动并上升的动画效果。

代码实现:

自定义一个UIView,命名为YSWaterWaveView

YSWaterWaveView.m 实现文件中变量的声明:

@interface YSWaterWaveView ()

@property (nonatomic, strong) CADisplayLink *displayLink;

@property (nonatomic, strong) CAShapeLayer *waveLayer;
@property (nonatomic, strong) CAGradientLayer gradientLayer

...

CADisplayLink是一个和屏幕刷新率同步定时器类,通过创建一个CADisplayLink类displayLink,把它添加到runloop,并给它提供一个方法,在屏幕刷新时调用

waveLayer用来绘制波形曲线,并作为gradientLayermaskgradientLayer用来呈现背景的渐变色,若不需要渐变色,可以只用waveLayer来实现效果


@property (nonatomic, strong) NSArray *colors;  
@property (nonatomic, assign) CGFloat percent;  

...

colors为渐变色需要用到的颜色数组,percent为整个小球的进度比例

@property (nonatomic, assign) CGFloat waveAmplitude;   
@property (nonatomic, assign) CGFloat waveCycle;
@property (nonatomic, assign) CGFloat offsetX;   
@property (nonatomic, assign) CGFloat currentWavePointY; 

...        

绘制波形的变量定义,使用波形曲线y=Asin(ωx+φ)+k进行绘制

waveAmplitude,波纹振幅,A

waveCycle波纹周期,T = 2π/ω

offsetX,波浪x位移,φ

currentWavePointY,当前波浪高度,k

@property (nonatomic, assign) CGFloat waveSpeed;      
@property (nonatomic, assign) CGFloat waveGrowth;    
... 

waveSpeed波纹速度,用来累加到相位φ上,达到波纹水平移动的效果

waveGrowth波纹上升速度,累加到k上,达到波浪高度上升的效果

@property (nonatomic, assign) BOOL bWaveFinished;
@property (nonatomic, assign) BOOL increase;
@property (nonatomic, assign) CGFloat variable;

@end

其他的用来标记的变量

函数方法

初始化属性值

- (void)defaultConfig
{
	self.waveCycle = 1.66 * M_PI / CGRectGetWidth(self.frame);    		self.currentWavePointY = CGRectGetHeight(self.frame); 
	
    self.waveGrowth = 1.0;
	self.waveSpeed = 0.4 / M_PI;

	self.offsetX = 0;
}

waveCycle的值影响波长,waveGrowth为水波上升的速度,waveSpeed为水波波动的速度,currentWavePointY设为视图高度以保证水波从最低处开始上升。offsetX为0,正弦曲线相位从0开始进行累加不断变化。所以的系数均为经验值,可根据实际效果适当调节。

动画开始

- (void)startWaveToPercent:(CGFloat)percent
{
    self.percent = percent;
    
    [self resetProperty];
    [self resetLayer];
    
    if (self.displayLink)
    {
        [self.displayLink invalidate];
        self.displayLink = nil;
    }
    
    self.bWaveFinished = NO;

    // 启动同步渲染绘制波纹
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(setCurrentWave:)];
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

percent为进度比例参数,resetProperty方法在动画开始前重置一些属性,resetLayer重置waveLayergradientLayer并做一些相关的设置。通过displayLink启动同步渲染绘制波纹,绘制波纹的函数为setCurrentWave:

重置属性

- (void)resetProperty
{
    self.currentWavePointY = CGRectGetHeight(self.frame);
    self.offsetX = 0;
    
    self.variable = 1.6;
    self.increase = NO;
}

variableincrease的值影响水波的高度,见后续代码详情

重置layer

- (void)resetLayer
{
    if (self.waveLayer)
    {
        [self.waveLayer removeFromSuperlayer];
        self.waveLayer = nil;
    }
    self.waveLayer = [CAShapeLayer layer];
    
    // 设置渐变
    if (self.gradientLayer)
    {
        [self.gradientLayer removeFromSuperlayer];
        self.gradientLayer = nil;
    }
    self.gradientLayer = [CAGradientLayer layer];
    
    self.gradientLayer.frame = [self gradientLayerFrame];
    [self setupGradientColor];
    
    [self.gradientLayer setMask:self.waveLayer];
    [self.layer addSublayer:self.gradientLayer];
}

设置gradientLayer的frame值并添加到当前类的layer上,设置渐变色相关的一些属性。gradientLayerwaveLayer设为mask作为呈现波形效果的图层。

设置gradientLayer的渐变色属性

- (void)setupGradientColor
{
    // gradientLayer设置渐变色
    if ([self.colors count] < 1)
    {
        self.colors = [self defaultColors];
    }
    
    self.gradientLayer.colors = self.colors;
    
    NSInteger count = [self.colors count];
    CGFloat d = 1.0 / count;
    
    NSMutableArray *locations = [NSMutableArray array];
    for (NSInteger i = 0; i < count; i++)
    {
        NSNumber *num = @(d + d * i);
        [locations addObject:num];
    }
    NSNumber *lastNum = @(1.0f);
    [locations addObject:lastNum];
    
    self.gradientLayer.locations = locations;
    
    self.gradientLayer.startPoint = CGPointMake(0, 0);
    self.gradientLayer.endPoint = CGPointMake(0, 1);
}

- (NSArray *)defaultColors
{
    UIColor *color0 = [UIColor colorWithRed:164 / 255.0 green:216 / 255.0 blue:222 / 255.0 alpha:1];
    UIColor *color1 = [UIColor colorWithRed:105 / 255.0 green:192 / 255.0 blue:154 / 255.0 alpha:1];
    
    NSArray *colors = @[(__bridge id)color0.CGColor, (__bridge id)color1.CGColor];
    return colors;
}

设置渐变方向为从上往下,颜色进行平均分割,若self.colors为空,则设为默认的渐变色,(注意:self.colors为进行(__bridge id)转换过的CGColor数组)

设置gradientLayer的frame值

- (CGRect)gradientLayerFrame
{
    CGFloat gradientLayerHeight = CGRectGetHeight(self.frame) * self.percent + 20;  // 加上20保证gradientLayer高度比waveLayer达到波峰时高度要高
    
    if (gradientLayerHeight > CGRectGetHeight(self.frame))
    {
        gradientLayerHeight = CGRectGetHeight(self.frame);
    }
    
    CGRect frame = CGRectMake(0, CGRectGetHeight(self.frame) - gradientLayerHeight, CGRectGetWidth(self.frame), gradientLayerHeight);
    
    return frame;
}

gradientLayer在上升完成之后的frame值,如果gradientLayer在水波上升过程中不断变化frame值,将会导致一开始绘制前有几秒钟明显的卡顿,所以gradientLayer的frame只进行一次赋值

绘制波形

- (void)setCurrentWave:(CADisplayLink *)displayLink
{
    if ([self waveFinished])
    {
        self.bWaveFinished = YES;
        [self amplitudeReduce];
        
        if (self.waveAmplitude <= 0)
        {
            [self stopWave];
            return;
        }
    }
    else
    {
        [self amplitudeChanged];
        self.currentWavePointY -= self.waveGrowth;
    }
    
    self.offsetX += self.waveSpeed;
    [self setCurrentWaveLayerPath];
} **displayLink**在屏幕刷新过程中不断调用的方法,若水波上升到指定位置,则波动渐渐平缓并最终静止;若未到达指定位置,则继续上升。

amplitudeReduce函数在上升完成后开始不断减小波形振幅,当振幅减小为0时,水波动画效果停止,此时将displayLink取消。

amplitudeChanged函数在一定范围内轻微调整波形振幅,使得水波波动效果更真实,不断改变currentWavePointY的值,使得水波上升。

setCurrentWaveLayerPath函数用来绘制波形曲线

函数判断水波上升是否已完成

- (BOOL)waveFinished
{
    return self.currentWavePointY <= (CGRectGetHeight(self.frame) * (1 - self.percent));
}

不断减小波形振幅

- (void)amplitudeReduce
{
    self.waveAmplitude -= 0.066;
}

振幅减小的速度可通过系数调整

水波静止,动画停止

- (void)stopWave
{
    [self.displayLink invalidate];
    self.displayLink = nil;
}

水波在上升过程中振幅的轻微变化

- (void)amplitudeChanged
{
    if (self.increase)
    {
        self.variable += 0.01;
    }
    else
    {
        self.variable -= 0.01;
    }
    
    // 变化的范围
    if (self.variable <= 1)
    {
        self.increase = YES;
    }
    
    if (self.variable >= 1.6)
    {
        self.increase = NO;
    }
    
    self.waveAmplitude = self.variable * 5;
}

振幅在一定范围内做增大减小的循环变化,变化范围可设置,variable的值决定振幅的大小。

波形绘制方法

- (void)setCurrentWaveLayerPath
{
    CGMutablePathRef path = CGPathCreateMutable();
    CGFloat y = self.currentWavePointY;
    
    CGPathMoveToPoint(path, nil, 0, y);
    CGFloat width = CGRectGetWidth(self.frame);
    for (float x = 0.0f; x <= width; x++)
    {
        // 正弦曲线公式
        y = self.waveAmplitude * sin(self.waveCycle * x + self.offsetX) + self.currentWavePointY;
        CGPathAddLineToPoint(path, nil, x, y);
    }
    
    CGPathAddLineToPoint(path, nil, width, CGRectGetHeight(self.frame));
    CGPathAddLineToPoint(path, nil, 0, CGRectGetHeight(self.frame));
    CGPathCloseSubpath(path);
    
    self.waveLayer.path = path;
    CGPathRelease(path);
}

通过正弦曲线公式y=Asin(ωx+φ)+k,绘制在每个时刻的波形图以达到水波动画的效果。

基本效果代码实现如上。

若不需要渐变色,可将gradientLayer去掉,直接将waveLayer添加到当前视图的layer上,毕竟做渐变色渲染比较消耗系统的性能。

还有一种实现方法,就是同时绘制两个波形图,让它们彼此间错开,下层的波形图层设置一定的透明度,两层水波交替波动时会有更好的视觉效果。大致实现思路为:使用两个 CAShapeLayer作为绘制(不使用CAGradientLayer),在波形绘制方法中,另一个CAShapeLayer用余弦曲线公式进行绘制,以达到两条曲线波峰错开的效果。

本文完成后效果如下,gif不会弄,直接截了张图

image1

不只是圆形,也可以是其他形状。

具体实现见源码

转载请保留原文地址:https://moshuqi.github.io/ios/2016/01/08/水波表现进度的动画效果实现.html

<完。>

想留言却没看到评论框?点这里。

Post Directory