ios 水波動(dòng)畫(huà)實(shí)現(xiàn)阿里的螞蟻聚寶app中很酷炫的下拉刷新效果

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

ZvqyiuA.gif

然后自己動(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ě)圖片描述


20160722102903317.jpeg

如果要繪制上面這個(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ū)作者)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 類(lèi)似淘寶的效果: ?我們知道,計(jì)算機(jī)不可能繪制出一條完美的曲線,如果放大到像素的級(jí)別,可以看到這些曲線其實(shí)都是柵格...
    CholMay閱讀 1,363評(píng)論 0 7
  • 類(lèi)似淘寶個(gè)人信息狀態(tài)欄,京東金融等雙波浪動(dòng)畫(huà) 主要方法:通過(guò)自定義View,利用正弦函數(shù)與余弦函數(shù)的效果. 一.相...
    FTC陳閱讀 5,807評(píng)論 23 164
  • 去年夏季日劇最強(qiáng)話題作《晝顏》的最終話,最后的畫(huà)面暗下那刻,我呼出一口長(zhǎng)長(zhǎng)的氣,如釋重負(fù),念頭完滿(mǎn)。 作為我第一個(gè)...
    七君閱讀 9,304評(píng)論 29 95
  • 最近整理自己的書(shū)單,看到2015年之間大部分偏術(shù)(產(chǎn)品,設(shè)計(jì),開(kāi)發(fā)技能)。15年買(mǎi)了Kindle之后,閱讀量增加,...
    L蕭越閱讀 345評(píng)論 0 6
  • 2012年6月24日 佳節(jié)端午,驚聞噩耗。 慈祥鄭老,身隕魂消。 風(fēng)雨飄搖,難分昏曉。 天地相隔,陰陽(yáng)兩遙。 音容...
    疏影06閱讀 194評(píng)論 0 2

友情鏈接更多精彩內(nèi)容