廢話不多說(shuō)先上圖,看看這個(gè)酷炫的下拉刷新動(dòng)畫(huà):

然后自己動(dòng)手研究了一下,下面講講實(shí)現(xiàn)原理。
水波動(dòng)畫(huà)的關(guān)鍵點(diǎn)就是正余弦函數(shù)
正弦型函數(shù)解析式:y=Asin(ωx+φ)+h
各常數(shù)值對(duì)函數(shù)圖像的影響:
φ(初相位):決定波形與X軸位置關(guān)系或橫向移動(dòng)距離(左加右減)
ω:決定周期(最小正周期T=2π/|ω|)
A:決定峰值(即縱向拉伸壓縮的倍數(shù))
h:表示波形在Y軸的位置關(guān)系或縱向移動(dòng)距離(上加下減)
拆解和分析
我們來(lái)拆解一下這個(gè)動(dòng)畫(huà)吧。兩個(gè)波浪是兩個(gè)正弦函數(shù)的效果疊加。首先我們看看該如何繪制一個(gè)波的曲線,如下圖
??這里寫(xiě)圖片描述

如果要繪制上面這個(gè)曲線,可以觀察:波的峰值是1,周期是2π,初相位是0,h位移也是0。那么計(jì)算各個(gè)點(diǎn)的坐標(biāo)公式就是y = sin(x);獲得各個(gè)點(diǎn)的坐標(biāo)之后,使用CGPathAddLineToPoint這個(gè)函數(shù),把這些點(diǎn)逐一連成線,就可以得到最后的路徑。
接下來(lái)問(wèn)題來(lái)了,我們已經(jīng)繪制了一條靜態(tài)的曲線,如何讓它形成一個(gè)流動(dòng)的波呢?
這就需要設(shè)置上面公式中的φ常量(初相位),假如φ是π/2,那么y=sin(x+φ)在x=0位置的時(shí)候,y的值就不在是0,而是1,就得到一條變化的曲線。通過(guò)上面的分析,我們知道,需要建立一個(gè)時(shí)間和φ的函數(shù)。
我們可以創(chuàng)建一個(gè)定時(shí)器(當(dāng)然做動(dòng)畫(huà)我們肯定不會(huì)使用計(jì)時(shí)器,這里舉個(gè)例子,下面詳解),假設(shè)每秒讓?duì)兆栽靓?2,這樣第4s的時(shí)候,φ等于2π(一個(gè)周期),y=sin(x+2π)和y=sin(x)等效,又回到了初初始狀態(tài),這樣就完成了一個(gè)波動(dòng)周期,往下繼續(xù)加下去,不停的往復(fù)這個(gè)波動(dòng)周期動(dòng)畫(huà)。
如果我們希望波動(dòng)的非常劇烈,也就是波流速很快,那么我們可以讓初相位隨著時(shí)間的函數(shù)波動(dòng)更快,就可以實(shí)現(xiàn)了。
代碼實(shí)現(xiàn):
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];
當(dāng)然僅僅只有一條正弦曲線是模擬不出來(lái)波浪的效果的,還需要一條余弦曲線才可以合成波浪曲線效果:
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];
然后我們可以看見(jiàn)效果是這樣的:

從圖中可以看出,相同參數(shù)下的正弦曲線和余弦曲線并不能很好的合成一個(gè)對(duì)稱(chēng)的曲線,我們想要的效果是正弦曲線的波峰對(duì)應(yīng)余弦曲線的波谷,所以需要將余弦函數(shù)的水平便宜做一個(gè)調(diào)整。
標(biāo)準(zhǔn)的余弦函數(shù)需要在水平方向上向左偏移四分之一周期的距離才能夠跟同參數(shù)的正弦函數(shù)對(duì)稱(chēng)。
CGFloat offsetX = M_PI/cycle/2; // also equal 2*M_PI/_cycle/4;

現(xiàn)在波浪有了,要想讓波浪動(dòng)起來(lái),需要有定時(shí)器每次觸發(fā)的時(shí)候都產(chǎn)生兩條新的曲線(path),然后替換現(xiàn)有曲線,快速替換達(dá)到動(dòng)態(tài)的效果。
先創(chuàng)建定時(shí)器,然后給定時(shí)器綁定上事件:
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTric)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
為了形成動(dòng)態(tài)效果,我們需要每次產(chǎn)生曲線的時(shí)候都有一個(gè)水平方向的偏移量,讓產(chǎn)生的曲線每次都比上次偏移一點(diǎn):
- (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);
}
}
最終可以看到流動(dòng)的波浪產(chǎn)生了:

仔細(xì)觀察,發(fā)現(xiàn)波浪還是不夠逼真,因?yàn)檎鎸?shí)的播放不僅是前進(jìn)的,還是浮動(dòng)的,所以我們的這個(gè)波浪缺少了浮動(dòng)的感覺(jué),前面在正弦函數(shù)的部分提起過(guò),要改變正弦函數(shù)的波動(dòng),需要改變它的振幅,所以需要一個(gè)算法來(lái)動(dòng)態(tài)產(chǎn)生一個(gè)振幅:
- (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);
}
}
主要的思想就是通過(guò)一個(gè)布爾值控制振幅的增長(zhǎng),當(dāng)增長(zhǎng)到了最高值的時(shí)候讓振幅減小,減小到最低值的時(shí)候再增長(zhǎng),以此來(lái)產(chǎn)生一個(gè)動(dòng)態(tài)的振幅,然后就會(huì)看到下面的效果了:

至此,其實(shí)核心的開(kāi)發(fā)已經(jīng)完成了,剩下的就是通過(guò)UIScrollView的偏移量來(lái)計(jì)算出一個(gè)動(dòng)態(tài)的波浪振幅:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat offset = (-scrollView.contentOffset.y-scrollView.contentInset.top);
CGFloat times = offset/10 + 1;
}```
可以用計(jì)算出的 times 變量來(lái)動(dòng)態(tài)控制振幅的變化。
通過(guò)UIScrollView來(lái)動(dòng)態(tài)控制振幅的難點(diǎn)在于不能通過(guò)UIScrollView的代理來(lái)實(shí)現(xiàn)具體的算法,因?yàn)椴荒馨裋iew層的東西冗余到Controller層去,秉承良好的設(shè)計(jì)模式,需要給UIScrollView實(shí)現(xiàn)一個(gè)拓展方法,在拓展方法里面讓我們實(shí)現(xiàn)波浪函數(shù)的View添加為UIScrollView的觀察者,在觀察到UIScrollView的offset每次變化時(shí),動(dòng)態(tài)計(jì)算振幅,具體的實(shí)現(xiàn)還是在源碼中了解吧。
最后,完整的項(xiàng)目地址在這里: [HHPullToRefreshWave](https://github.com/red3/HHPullToRefreshWave)
文/real潘(簡(jiǎn)書(shū)作者)