ios 水波动画实现阿里的蚂蚁聚宝app中很酷炫的下拉刷新效果

废话不多说先上图,看看这个酷炫的下拉刷新动画:

ZvqyiuA.gif

然后自己动手研究了一下,下面讲讲实现原理。
水波动画的关键点就是正余弦函数

正弦型函数解析式:y=Asin(ωx+φ)+h
各常数值对函数图像的影响:
φ(初相位):决定波形与X轴位置关系或横向移动距离(左加右减)
ω:决定周期(最小正周期T=2π/|ω|)
A:决定峰值(即纵向拉伸压缩的倍数)
h:表示波形在Y轴的位置关系或纵向移动距离(上加下减)
拆解和分析

我们来拆解一下这个动画吧。两个波浪是两个正弦函数的效果叠加。首先我们看看该如何绘制一个波的曲线,如下图
  这里写图片描述


20160722102903317.jpeg

如果要绘制上面这个曲线,可以观察:波的峰值是1,周期是2π,初相位是0,h位移也是0。那么计算各个点的坐标公式就是y = sin(x);获得各个点的坐标之后,使用CGPathAddLineToPoint这个函数,把这些点逐一连成线,就可以得到最后的路径。

接下来问题来了,我们已经绘制了一条静态的曲线,如何让它形成一个流动的波呢?
这就需要设置上面公式中的φ常量(初相位),假如φ是π/2,那么y=sin(x+φ)在x=0位置的时候,y的值就不在是0,而是1,就得到一条变化的曲线。通过上面的分析,我们知道,需要建立一个时间和φ的函数。

我们可以创建一个定时器(当然做动画我们肯定不会使用计时器,这里举个例子,下面详解),假设每秒让φ自增π/2,这样第4s的时候,φ等于2π(一个周期),y=sin(x+2π)和y=sin(x)等效,又回到了初初始状态,这样就完成了一个波动周期,往下继续加下去,不停的往复这个波动周期动画。

如果我们希望波动的非常剧烈,也就是波流速很快,那么我们可以让初相位随着时间的函数波动更快,就可以实现了。
代码实现:

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 100, self.view.frame.size.width, 200)];
view.backgroundColor = [UIColor whiteColor];
[self.view addSubview:view];
CAShapeLayer *firstWaveLayer = [CAShapeLayer layer];
firstWaveLayer.fillColor = [UIColor lightGrayColor].CGColor;
CGMutablePathRef path = CGPathCreateMutable();
CGFloat y = 50;
CGPathMoveToPoint(path, nil, 0, y);
CGFloat waveWidth = self.view.frame.size.width;
CGFloat cycle = 6 * M_PI / self.view.frame.size.width;
CGFloat offsetX = 0;
for (float x = 0.0f; x <=  waveWidth ; x++) {
    y = 8 * sin(cycle * x + 0) + 70 ;
    CGPathAddLineToPoint(path, nil, x, y);
}
CGPathAddLineToPoint(path, nil, waveWidth, 100);
CGPathAddLineToPoint(path, nil, 0, 100);
CGPathCloseSubpath(path);
firstWaveLayer.path = path;
CGPathRelease(path);
[view.layer addSublayer:firstWaveLayer];

当然仅仅只有一条正弦曲线是模拟不出来波浪的效果的,还需要一条余弦曲线才可以合成波浪曲线效果:

CAShapeLayer *secondWaveLayer = [CAShapeLayer layer];
secondWaveLayer.fillColor = [UIColor redColor].CGColor;
CGMutablePathRef path = CGPathCreateMutable();
CGFloat y = 50;
CGPathMoveToPoint(path, nil, 0, y);
for (float x = 0.0f; x <=  waveWidth ; x++) {
    y = 8 * cos(cycle * x + offsetX) + 70 ;
    CGPathAddLineToPoint(path, nil, x, y);
}
CGPathAddLineToPoint(path, nil, waveWidth, 100);
CGPathAddLineToPoint(path, nil, 0, 100);
CGPathCloseSubpath(path);
secondWaveLayer.path = path;
CGPathRelease(path);
[view.layer addSublayer:secondWaveLayer];

然后我们可以看见效果是这样的:

正余弦波形图

从图中可以看出,相同参数下的正弦曲线和余弦曲线并不能很好的合成一个对称的曲线,我们想要的效果是正弦曲线的波峰对应余弦曲线的波谷,所以需要将余弦函数的水平便宜做一个调整。
标准的余弦函数需要在水平方向上向左偏移四分之一周期的距离才能够跟同参数的正弦函数对称。

CGFloat offsetX = M_PI/cycle/2;  // also equal 2*M_PI/_cycle/4;
波形图

现在波浪有了,要想让波浪动起来,需要有定时器每次触发的时候都产生两条新的曲线(path),然后替换现有曲线,快速替换达到动态的效果。
先创建定时器,然后给定时器绑定上事件:

displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTric)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    为了形成动态效果,我们需要每次产生曲线的时候都有一个水平方向的偏移量,让产生的曲线每次都比上次偏移一点:
- (void)displayLinkTric {
    
    static CGFloat offsetX = 0;
    offsetX += 0.07;
    
    CGFloat waveWidth = self.view.frame.size.width;
    CGFloat cycle = 6 * M_PI / self.view.frame.size.width;
    
    {
        CGMutablePathRef path = CGPathCreateMutable();
        CGFloat y = 50;
        CGPathMoveToPoint(path, nil, 0, y);
        
        
        for (float x = 0.0f; x <=  waveWidth ; x++) {
            y = 8 * sin(cycle * x + offsetX) + 70 ;
            CGPathAddLineToPoint(path, nil, x, y);
        }
        CGPathAddLineToPoint(path, nil, waveWidth, 100);
        CGPathAddLineToPoint(path, nil, 0, 100);
        CGPathCloseSubpath(path);
        firstWaveLayer.path = path;
        CGPathRelease(path);
    }
    
    {
        CGMutablePathRef path = CGPathCreateMutable();
        CGFloat y = 50;
        CGFloat forword = M_PI/cycle/2;  // also equal 2*M_PI/_cycle/4
        CGPathMoveToPoint(path, nil, 0, y);
        for (float x = 0.0f; x <=  waveWidth ; x++) {
            y = 8 * cos(cycle * x + offsetX + forword) + 70 ;
            CGPathAddLineToPoint(path, nil, x, y);
        }
        CGPathAddLineToPoint(path, nil, waveWidth, 100);
        CGPathAddLineToPoint(path, nil, 0, 100);
        CGPathCloseSubpath(path);
        secondWaveLayer.path = path;
        CGPathRelease(path);
    }
}
    最终可以看到流动的波浪产生了:
平移波形图

仔细观察,发现波浪还是不够逼真,因为真实的播放不仅是前进的,还是浮动的,所以我们的这个波浪缺少了浮动的感觉,前面在正弦函数的部分提起过,要改变正弦函数的波动,需要改变它的振幅,所以需要一个算法来动态产生一个振幅:

- (void)displayLinkTric {
    
    static CGFloat offsetX = 0;
    offsetX += 0.05;
    
    static CGFloat amplitude = 8;
    static BOOL increase = YES;
    
    if (increase) {
        amplitude += 0.04;
    } else {
        amplitude -= 0.04;
    }
    
    if (amplitude >= 12) {
        increase = NO;
    }
    if (amplitude <= 4) {
        increase = YES;
    }
    
    CGFloat waveWidth = self.view.frame.size.width;
    CGFloat cycle = 2 * M_PI / self.view.frame.size.width;
    
    {
        CGMutablePathRef path = CGPathCreateMutable();
        CGFloat y = 50;
        CGPathMoveToPoint(path, nil, 0, y);
        
        
        for (float x = 0.0f; x <=  waveWidth ; x++) {
            y = amplitude * sin(cycle * x + offsetX) + 70 ;
            CGPathAddLineToPoint(path, nil, x, y);
        }
        CGPathAddLineToPoint(path, nil, waveWidth, 100);
        CGPathAddLineToPoint(path, nil, 0, 100);
        CGPathCloseSubpath(path);
        firstWaveLayer.path = path;
        CGPathRelease(path);
    }
    
    {
        CGMutablePathRef path = CGPathCreateMutable();
        CGFloat y = 50;
        CGFloat forword = M_PI/cycle/2;  // also equal 2*M_PI/_cycle/4
        CGPathMoveToPoint(path, nil, 0, y);
        for (float x = 0.0f; x <=  waveWidth ; x++) {
            y = amplitude * cos(cycle * x + offsetX - forword) + 70 ;
            CGPathAddLineToPoint(path, nil, x, y);
        }
        CGPathAddLineToPoint(path, nil, waveWidth, 100);
        CGPathAddLineToPoint(path, nil, 0, 100);
        CGPathCloseSubpath(path);
        secondWaveLayer.path = path;
        CGPathRelease(path);
    }
}

    主要的思想就是通过一个布尔值控制振幅的增长,当增长到了最高值的时候让振幅减小,减小到最低值的时候再增长,以此来产生一个动态的振幅,然后就会看到下面的效果了:
水波曲线

至此,其实核心的开发已经完成了,剩下的就是通过UIScrollView的偏移量来计算出一个动态的波浪振幅:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    
        CGFloat offset = (-scrollView.contentOffset.y-scrollView.contentInset.top);
        CGFloat times = offset/10 + 1;
}```
可以用计算出的 times 变量来动态控制振幅的变化。

通过UIScrollView来动态控制振幅的难点在于不能通过UIScrollView的代理来实现具体的算法,因为不能把View层的东西冗余到Controller层去,秉承良好的设计模式,需要给UIScrollView实现一个拓展方法,在拓展方法里面让我们实现波浪函数的View添加为UIScrollView的观察者,在观察到UIScrollView的offset每次变化时,动态计算振幅,具体的实现还是在源码中了解吧。
最后,完整的项目地址在这里: [HHPullToRefreshWave](https://github.com/red3/HHPullToRefreshWave)
文/real潘(简书作者)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • 类似淘宝的效果:  我们知道,计算机不可能绘制出一条完美的曲线,如果放大到像素的级别,可以看到这些曲线其实都是栅格...
    CholMay阅读 1,070评论 0 7
  • 类似淘宝个人信息状态栏,京东金融等双波浪动画 主要方法:通过自定义View,利用正弦函数与余弦函数的效果. 一.相...
    FTC陳阅读 5,619评论 23 164
  • 去年夏季日剧最强话题作《昼颜》的最终话,最后的画面暗下那刻,我呼出一口长长的气,如释重负,念头完满。 作为我第一个...
    七君阅读 9,085评论 29 95
  • 最近整理自己的书单,看到2015年之间大部分偏术(产品,设计,开发技能)。15年买了Kindle之后,阅读量增加,...
    L萧越阅读 302评论 0 6
  • 2012年6月24日 佳节端午,惊闻噩耗。 慈祥郑老,身陨魂消。 风雨飘摇,难分昏晓。 天地相隔,阴阳两遥。 音容...
    疏影06阅读 144评论 0 2