iOS水波紋動畫詳解
水波動畫的關鍵點就是正餘弦函式
正弦型函式解析式:y=Asin(ωx+φ)+h
各常數值對函式影象的影響:
φ(初相位):決定波形與X軸位置關係或橫向移動距離(左加右減)
ω:決定週期(最小正週期T=2π/|ω|)
A:決定峰值(即縱向拉伸壓縮的倍數)
h:表示波形在Y軸的位置關係或縱向移動距離(上加下減)
拆解和分析
我們來拆解一下這個動畫吧。兩個波浪是兩個正弦函式的效果疊加。首先我們看看該如何繪製一個波的曲線,如下圖
我們知道,計算機不可能繪製出一條完美的曲線,如果放大到畫素的級別,可以看到這些曲線其實都是柵格的畫素點組成。我們只能最大化的接近曲線,達到肉眼無法分辨的程度。如果想繪製出來一條正弦函式曲線,可以沿著假想的曲線繪製許多個點,然後把點逐一用直線連在一起,如果點足夠多,就可以得到一條滿足需求的曲線,這也是一種微分的思想。而這些點的位置可以通過正弦函式的解析式求得。
如果要繪製上面這個曲線,可以觀察:波的峰值是1,週期是2π,初相位是0,h位移也是0。那麼計算各個點的座標公式就是y = sin(x);獲得各個點的座標之後,使用CGPathAddLineToPoint這個函式,把這些點逐一連成線,就可以得到最後的路徑。
接下來問題來了,我們已經繪製了一條靜態的曲線,如何讓它形成一個流動的波呢?
可以這麼思考:初始的曲線如上面所示,1s之後,希望曲線能成為下個形態:
接著,2s、3s…,曲線分別在不停的變化,如下圖:
那麼隨著時間的流逝,這個曲線在不停的起伏變化,就形成了波動的效果。我們認真的想想,波動其實就是每一個點的y座標都在不停的做著週期變化,想要實現上圖1s之後的曲線形態,需要設定上面公式中的φ常量(初相位),假如φ是π/2,那麼y=sin(x+φ)在x=0位置的時候,y的值就不在是0,而是1,就得到一條變化的曲線。通過上面的分析,我們知道,需要建立一個時間和φ的函式。
我們可以建立一個定時器(當然做動畫我們肯定不會使用計時器,這裡舉個例子,下面詳解),假設每秒讓φ自增π/2,這樣第4s的時候,φ等於2π(一個週期),y=sin(x+2π)和y=sin(x)等效,又回到了初初始狀態,這樣就完成了一個波動週期,往下繼續加下去,不停的往復這個波動週期動畫。
如果我們希望波動的非常劇烈,也就是波流速很快,那麼我們可以讓初相位隨著時間的函式波動更快,就可以實現了。
程式碼實現
把上面的原理落實到我們需要製作的動畫上面。首先要總結出一個公式,確定正弦型函式解析式:y=Asin(ωx+φ)+h中各個常數的值。這裡需要注意UIKit的座標系統y軸是向下延伸。
1、我們的容器高度是100,我希望波的整體高度,固定在容器的一個相對的位置。
這裡設定h = 30;也就是說,當Asin(ωx+φ)計算為0的時候,這個時候y的位置是30;
2、決定波起伏的高度,我們設定波峰是5,波峰越大,曲線越陡峭;
3、決定波的寬度和週期,比如,我們可以看到上面的例子中是一個週期的波曲線,
一個波峰、一個波谷,如果我們想在0到2π這個距離顯示2個完整的波曲線,那麼週期就是π。
我們這裡設定波的寬度是容器的寬度_waveWidth,希望能展示2.5個波曲線,週期就是_waveWidth/2.5。
那麼ω常量就可以這樣計算:2.5*M_PI/_waveWidth。
4、一共有兩個波曲線,形成一個落差,也就是設定不同的φ(初相位),我們這裡設定落差是M_PI/4。
5、時間和初相位的函式關係:我們在計時器的函式中一直呼叫_offset += _speed;
可以看到,如果我們設定波的速度speed越大,波的震動將會越快。
最後我們的公式如下:
CGFloat y = _waveHeight*sinf(2.5*M_PI*i/_waveWidth + 3*_offset*M_PI/_waveWidth + M_PI/4) + _h;
這些引數都可以自己調整,得到一個符合要求的效果。
現在我們解決了專案中最有難度的問題,剩下的事情就非常簡單了。兩個波是兩個CAShapeLayer。我們使用CADisplayLink而不是計時器來驅動動畫,因為CADisplayLink觸發的時機是每隔一幀執行一次,而NSTimer不是很精確,會有阻塞的情況,照成動畫卡頓的現象。
- (void)wave {
/*
*建立兩個layer
*/
self.waveShapeLayer = [CAShapeLayer layer];
self.waveShapeLayer.fillColor = self.waveColor.CGColor;
[self.layer addSublayer:self.waveShapeLayer];
self.waveShapeLayerT = [CAShapeLayer layer];
self.waveShapeLayerT.fillColor = self.waveColor.CGColor;
[self.layer addSublayer:self.waveShapeLayerT];
/*
*CADisplayLink是一個能讓我們以和螢幕重新整理率相同的頻率將內容畫到螢幕上的定時器。我們在應用中建立一個新的 CADisplayLink 物件,把它新增到一個runloop中,並給它提供一個 target 和selector 在螢幕重新整理的時候呼叫。
*/
self.waveDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(getCurrentWave)];
[self.waveDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
//CADispayLink相當於一個定時器 會一直繪製曲線波紋 看似在運動,其實是一直在繪畫不同位置點的餘弦函式曲線
- (void)getCurrentWave {
//offsetX決定x位置,如果想搞明白可以多試幾次
self.offsetX += self.waveSpeed;
//宣告第一條波曲線的路徑
CGMutablePathRef path = CGPathCreateMutable();
//設定起始點
CGPathMoveToPoint(path, nil, 0, self.waveHeight);
CGFloat y = 0.f;
//第一個波紋的公式
for (float x = 0.f; x <= self.waveWidth ; x++) {
y = self.waveAmplitude * sin((300 / self.waveWidth) * (x * M_PI / 180) - self.offsetX * M_PI / 270) + self.waveHeight*1;
CGPathAddLineToPoint(path, nil, x, y);
x++;
}
//把繪圖資訊新增到路徑裡
CGPathAddLineToPoint(path, nil, self.waveWidth, self.frame.size.height);
CGPathAddLineToPoint(path, nil, 0, self.frame.size.height);
//結束繪圖資訊
CGPathCloseSubpath(path);
self.waveShapeLayer.path = path;
//釋放繪圖路徑
CGPathRelease(path);
/*
* 第二個
*/
self.offsetXT += self.waveSpeed;
CGMutablePathRef pathT = CGPathCreateMutable();
CGPathMoveToPoint(pathT, nil, 0, self.waveHeight+100);
CGFloat yT = 0.f;
for (float x = 0.f; x <= self.waveWidth ; x++) {
yT = self.waveAmplitude*1.6 * sin((260 / self.waveWidth) * (x * M_PI / 180) - self.offsetXT * M_PI / 180) + self.waveHeight;
CGPathAddLineToPoint(pathT, nil, x, yT-10);
}
CGPathAddLineToPoint(pathT, nil, self.waveWidth, self.frame.size.height);
CGPathAddLineToPoint(pathT, nil, 0, self.frame.size.height);
CGPathCloseSubpath(pathT);
self.waveShapeLayerT.path = pathT;
CGPathRelease(pathT);
}
我們可以看到,兩個波曲線不但初相位不同,形成一個落差,而且相位隨著時間的改變速度也不同,帶來兩個波的流速不同的視覺差異。CADisplayLink每幀都會呼叫wave方法,wave不停的改變著offset的值,也就是改變著初相位,最後形成了波動動畫。