網(wǎng)易新聞客戶端的這個(gè)水波效果出來很久了,我考慮了很長時(shí)間該如何實(shí)現(xiàn),但是都沒有很好的辦法室埋,幸好在一個(gè)動畫牛人daixunry的文章里,知道了實(shí)現(xiàn)這個(gè)動畫最關(guān)鍵的點(diǎn)伊约。他的blog上面有許多優(yōu)秀的動畫案例姚淆,非常值的學(xué)習(xí),blog的地址是http://www.reibang.com/p/272aa1f26c62
這個(gè)動畫的關(guān)鍵點(diǎn)就是正余弦函數(shù)屡律。在聽到這個(gè)的時(shí)候腌逢,我非常的震驚,原因是正余弦我們當(dāng)初在高中的時(shí)候?qū)W習(xí)的知識疹尾,不過從來沒有想過這些高中書本的知識竟然運(yùn)用到了實(shí)際上忍,非常的佩服kittenyang,同時(shí)感覺非常羞愧的纳本,高中的知識都還給老師了窍蓝,連正余弦的公式都忘記了。不熟悉的同學(xué)也可以去復(fù)習(xí)一下繁成。
正弦型函數(shù)解析式:y=Asin(ωx+φ)+h
各常數(shù)值對函數(shù)圖像的影響:
φ(初相位):決定波形與X軸位置關(guān)系或橫向移動距離(左加右減)
ω:決定周期(最小正周期T=2π/|ω|)
A:決定峰值(即縱向拉伸壓縮的倍數(shù))
h:表示波形在Y軸的位置關(guān)系或縱向移動距離(上加下減)
拆解和分析
好了吓笙,我們還是來拆解一下這個(gè)動畫吧。兩個(gè)波浪是兩個(gè)正弦函數(shù)的效果疊加巾腕。首先我們看看該如何繪制一個(gè)波的曲線面睛,如下圖:
我們知道絮蒿,計(jì)算機(jī)不可能繪制出一條完美的曲線,如果放大到像素的級別叁鉴,可以看到這些曲線其實(shí)都是柵格的像素點(diǎn)組成土涝。我們只能最大化的接近曲線,達(dá)到肉眼無法分辨的程度幌墓。如果想繪制出來一條正弦函數(shù)曲線但壮,可以沿著假想的曲線繪制許多個(gè)點(diǎn),然后把點(diǎn)逐一用直線連在一起常侣,如果點(diǎn)足夠多蜡饵,就可以得到一條滿足需求的曲線,這也是一種微分的思想胳施。而這些點(diǎn)的位置可以通過正弦函數(shù)的解析式求得溯祸。
如果要繪制上面這個(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)逐一連成線压状,就可以得到最后的路徑仆抵。
接下來問題來了,我們已經(jīng)繪制了一條靜態(tài)的曲線种冬,如何讓它形成一個(gè)流動的波呢镣丑?
可以這么思考:初始的曲線如上面所示,1s之后娱两,希望曲線能成為下個(gè)形態(tài):
接著莺匠,2s、3s...十兢,曲線分別在不停的變化趣竣,如下圖:
那么隨著時(shí)間的流逝,這個(gè)曲線在不停的起伏變化旱物,就形成了波動的效果遥缕。我們認(rèn)真的想想,波動其實(shí)就是每一個(gè)點(diǎn)的y坐標(biāo)都在不停的做著周期變化宵呛,想要實(shí)現(xiàn)上圖1s之后的曲線形態(tài)单匣,需要設(shè)置上面公式中的φ常量(初相位),假如φ是π/2,那么y=sin(x+φ)在x=0位置的時(shí)候户秤,y的值就不在是0码秉,而是1,就得到一條變化的曲線鸡号。通過上面的分析转砖,我們知道,需要建立一個(gè)時(shí)間和φ的函數(shù)鲸伴。
我們可以創(chuàng)建一個(gè)定時(shí)器(當(dāng)然做動畫我們肯定不會使用計(jì)時(shí)器堪藐,這里舉個(gè)例子,下面詳解)挑围,假設(shè)每秒讓φ自增π/2,這樣第4s的時(shí)候糖荒,φ等于2π(一個(gè)周期)杉辙,y=sin(x+2π)和y=sin(x)等效,又回到了初初始狀態(tài)捶朵,這樣就完成了一個(gè)波動周期蜘矢,往下繼續(xù)加下去,不停的往復(fù)這個(gè)波動周期動畫综看。
如果我們希望波動的非常劇烈品腹,也就是波流速很快,那么我們可以讓初相位隨著時(shí)間的函數(shù)波動更快红碑,就可以實(shí)現(xiàn)了舞吭。
代碼實(shí)現(xiàn)
把上面的原理落實(shí)到我們需要制作的動畫上面。首先要總結(jié)出一個(gè)公式析珊,確定正弦型函數(shù)解析式:y=Asin(ωx+φ)+h中各個(gè)常數(shù)的值羡鸥。這里需要注意UIKit的坐標(biāo)系統(tǒng)y軸是向下延伸。
1忠寻、我們的容器高度是100惧浴,我希望波的整體高度,固定在容器的一個(gè)相對的位置奕剃。
這里設(shè)置h = 30衷旅;也就是說,當(dāng)Asin(ωx+φ)計(jì)算為0的時(shí)候纵朋,這個(gè)時(shí)候y的位置是30柿顶;
2、決定波起伏的高度倡蝙,我們設(shè)置波峰是5九串,波峰越大,曲線越陡峭;
3猪钮、決定波的寬度和周期品山,比如,我們可以看到上面的例子中是一個(gè)周期的波曲線烤低,
一個(gè)波峰肘交、一個(gè)波谷,如果我們想在0到2π這個(gè)距離顯示2個(gè)完整的波曲線扑馁,那么周期就是π涯呻。
我們這里設(shè)置波的寬度是容器的寬度_waveWidth,希望能展示2.5個(gè)波曲線腻要,周期就是_waveWidth/2.5复罐。
那么ω常量就可以這樣計(jì)算:2.5*M_PI/_waveWidth。
4雄家、一共有兩個(gè)波曲線效诅,形成一個(gè)落差,也就是設(shè)置不同的φ(初相位)趟济,我們這里設(shè)置落差是M_PI/4乱投。
5、時(shí)間和初相位的函數(shù)關(guān)系:我們在計(jì)時(shí)器的函數(shù)中一直調(diào)用_offset += _speed;
可以看到顷编,如果我們設(shè)置波的速度speed越大戚炫,波的震動將會越快。
最后我們的公式如下:
CGFloat y = _waveHeight*sinf(2.5*M_PI*i/_waveWidth + 3*_offset*M_PI/_waveWidth + M_PI/4) + _h;
這些參數(shù)都可以自己調(diào)整媳纬,得到一個(gè)符合要求的效果双肤。
現(xiàn)在我們解決了項(xiàng)目中最有難度的問題,剩下的事情就非常簡單了钮惠。兩個(gè)波是兩個(gè)CAShapeLayer杨伙。我們使用CADisplayLink而不是計(jì)時(shí)器來驅(qū)動動畫,因?yàn)镃ADisplayLink觸發(fā)的時(shí)機(jī)是每隔一幀運(yùn)行一次萌腿,而NSTimer不是很精確限匣,會有阻塞的情況,照成動畫卡頓的現(xiàn)象毁菱。
- (void)wave
{
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(doAni)];
[_link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)doAni
{
_offset += _speed;
//設(shè)置第一條波曲線的路徑
CGMutablePathRef pathRef = CGPathCreateMutable();
//起始點(diǎn)
CGFloat startY = _waveHeight*sinf(_offset*M_PI/_waveWidth);
CGPathMoveToPoint(pathRef, NULL, 0, startY);
//第一個(gè)波的公式
for (CGFloat i = 0.0; i < _waveWidth; i ++) {
CGFloat y = 1.1*_waveHeight*sinf(2.5*M_PI*i/_waveWidth + _offset*M_PI/_waveWidth) + _h;
CGPathAddLineToPoint(pathRef, NULL, i, y);
}
CGPathAddLineToPoint(pathRef, NULL, _waveWidth, 40);
CGPathAddLineToPoint(pathRef, NULL, 0, 40);
CGPathCloseSubpath(pathRef);
//設(shè)置第一個(gè)波layer的path
_layer.path = pathRef;
_layer.fillColor = [UIColor lightGrayColor].CGColor;
CGPathRelease(pathRef);
//設(shè)置第二條波曲線的路徑
CGMutablePathRef pathRef2 = CGPathCreateMutable();
CGFloat startY2 = _waveHeight*sinf(_offset*M_PI/_waveWidth + M_PI/4);
CGPathMoveToPoint(pathRef2, NULL, 0, startY2);
//第二個(gè)波曲線的公式
for (CGFloat i = 0.0; i < _waveWidth; i ++) {
CGFloat y = _waveHeight*sinf(2.5*M_PI*i/_waveWidth + 3*_offset*M_PI/_waveWidth + M_PI/4) + _h;
CGPathAddLineToPoint(pathRef2, NULL, i, y);
}
CGPathAddLineToPoint(pathRef2, NULL, _waveWidth, 40);
CGPathAddLineToPoint(pathRef2, NULL, 0, 40);
CGPathCloseSubpath(pathRef2);
_layer2.path = pathRef2;
_layer2.fillColor = [UIColor lightGrayColor].CGColor;
CGPathRelease(pathRef2);
}
我們可以看到米死,兩個(gè)波曲線不但初相位不同,形成一個(gè)落差贮庞,而且相位隨著時(shí)間的改變速度也不同峦筒,帶來兩個(gè)波的流速不同的視覺差異。CADisplayLink每幀都會調(diào)用wave方法窗慎,wave不停的改變著offset的值物喷,也就是改變著初相位卤材,最后形成了波動動畫。