學(xué)習(xí)計(jì)劃(11) - 動(dòng)畫 - 貝塞爾曲線(2) - 下拉和加載動(dòng)畫

在貝塞爾曲線(1)中,我們介紹了貝塞爾曲線的繪制,但是那是固定的,運(yùn)用場(chǎng)景很少,運(yùn)用更多的是一些動(dòng)畫效果。
而前端主要的功能就是負(fù)責(zé)貌美如花,所以掌握一定的動(dòng)畫技巧還是有必要的。
所以呢,我就開始從簡(jiǎn)單的慢慢研究吧。

下拉動(dòng)畫

首先是如下的效果:


下拉動(dòng)畫.gif

首先需要分析幾個(gè)問題:

  1. 如何繪制二階曲線
  2. 如何讓曲線跟隨著下拉進(jìn)行變化

問題的解決有很多種,我寫下的只是我的解決思路:

1. 創(chuàng)建一個(gè)視圖,重寫它的drawRect:方法

UIBezierPath *bezier = [UIBezierPath bezierPath];
UIColor *color = hexStrColor(@"#FF8C69");
[color set];
bezier.lineWidth = 1.0;
[bezier moveToPoint:(CGPoint){0,0}];
[bezier addQuadCurveToPoint:(CGPoint){self.width,0} controlPoint:(CGPoint){self.width/2,self.offsetY}];
[bezier fill];

通過上面的方法,我們繪制了二階曲線,那么就是解決第二個(gè)問題了。

想讓我們的表格下拉的時(shí)候,視圖也跟著變化,那么肯定需要知道我們表格下拉的具體數(shù)值,所以需要實(shí)現(xiàn)UIScrollViewDelegate的scrollViewDidScroll方法

- (void)scrollViewDidScroll:(UIScrollView *)scrollView{}

通過該方法我們實(shí)時(shí)的知道表格的Y軸偏移量,所以我們還需要在之前的視圖中添加一個(gè)變量:
@property (nonatomic,readwrite,unsafe_unretained)CGFloat offsetY;
通過該變量來實(shí)時(shí)的控制二階曲線的controlPoint參數(shù)的Y值。

接下來,我們?cè)趕crollViewDidScroll方法中寫下如下代碼。

self.headerView.offsetY = -scrollView.contentOffset.y;
[self.headerView setNeedsDisplay];
[self.headerView setFrame:(CGRect){0,statusBarHeight,kSCREENWIDTH,- scrollView.contentOffset.y}];

因?yàn)槲覀冃枰诨瑒?dòng)的時(shí)候?qū)崟r(shí)的改變曲線的拐點(diǎn),然后生成對(duì)應(yīng)的曲線。也就是我們需要實(shí)時(shí)的重新繪制視圖,也就是調(diào)用drawRect: 方法.所以我們需要調(diào)用setNeedsDisplay方法。通過在外部調(diào)用setNeedsDisplay可以是視圖重新調(diào)用drawRect方法

setNeedsLayout會(huì)默認(rèn)調(diào)用layoutSubViews, setNeedsDisplay會(huì)調(diào)用drawRect:方法

加載動(dòng)畫

接下來我們?cè)僮鰝€(gè)加載動(dòng)畫,具體的效果如下:


加載動(dòng)畫.gif

這是個(gè)根據(jù)路徑變化的動(dòng)畫,也是我在很多軟件上經(jīng)??吹降囊环N加載動(dòng)畫。所以就自己試試能不能做不出來。
先創(chuàng)建三個(gè)球,哈哈,先把我們要玩的球畫出來:

CGFloat radius = 20;
CGFloat marginLeft = 10;
//第一顆球
CAShapeLayer *fLayer = [CAShapeLayer new];
fLayer.backgroundColor =  getColor(102, 170, 238, 1).CGColor;
[fLayer setFrame:(CGRect){self.width/2-(radius*2+marginLeft*2)/2,self.height/2,radius,radius}];
fLayer.cornerRadius = radius/2;
//第二顆球
CAShapeLayer *sLayer = [CAShapeLayer new];
sLayer.backgroundColor = getColor(102, 170, 238, 0.5).CGColor;
[sLayer setFrame:(CGRect){marginLeft(fLayer)+marginLeft,self.height/2,radius,radius}];
sLayer.cornerRadius = radius/2;
//第三顆球
CAShapeLayer *tLayer = [CAShapeLayer new];
tLayer.backgroundColor = getColor(102, 170, 238, 0.2).CGColor;
[tLayer setFrame:(CGRect){marginLeft(sLayer)+marginLeft,self.height/2,radius,radius}];
tLayer.cornerRadius = radius/2;
    
    
self.layer_list = [NSArray arrayWithObjects:fLayer,sLayer,tLayer,nil];
[self.layer addSublayer:fLayer];
[self.layer addSublayer:sLayer];
[self.layer addSublayer:tLayer];

球畫好了我們就要開始研究它的運(yùn)動(dòng)軌跡了。通過一些觀察我們分析得出了下面的運(yùn)動(dòng)軌跡:


第一課球的運(yùn)動(dòng)動(dòng)畫.gif

第二顆球運(yùn)動(dòng)動(dòng)畫.gif

第三顆球運(yùn)動(dòng)動(dòng)畫.gif

通過觀察,第一顆球的運(yùn)動(dòng)軌跡其實(shí)就是一個(gè)上半圓加上一個(gè)下半圓的路徑動(dòng)畫。那么只需要畫兩個(gè)半圓就搞定的事咯?我們來試一下

繪制圓的參考圖:


弧線參考圖.png

第一個(gè)半圓是以兩個(gè)圓的中心點(diǎn)為圓心繪制的半圓,
第一個(gè)半圓是 π -> 0° 順時(shí)針運(yùn)動(dòng),然后π -> 0°逆時(shí)針運(yùn)動(dòng)

第一顆球的運(yùn)動(dòng)軌跡.png

CAShapeLayer *fLayer = self.layer_list[0];
CAShapeLayer *testLayer = [CAShapeLayer new];
testLayer.fillColor = [UIColor clearColor].CGColor;
testLayer.borderWidth = 1.0f;
testLayer.strokeColor = hexStrColor(@"#AA8C69").CGColor;

UIBezierPath *semicirclePath = [UIBezierPath bezierPath];
//上半圓
[semicirclePath addArcWithCenter:(CGPoint){fLayer.frame.origin.x+25,fLayer.frame.origin.y+10}
                           radius:15
                       startAngle:M_PI
                         endAngle:0
                        clockwise:YES];
//下半圓
[semicirclePath addArcWithCenter:(CGPoint){sLayer.frame.origin.x+25,sLayer.frame.origin.y+10}
                           radius:15
                       startAngle:M_PI
                         endAngle:0
                        clockwise:NO];
testLayer.path = semicirclePathA.CGPath;
[self.layer addSublayer:testLayer];

第一顆球的軌跡我們清楚之后我們來設(shè)置第二顆球的軌跡:
0° -> π 順時(shí)針運(yùn)動(dòng)

第二顆球的運(yùn)動(dòng)軌跡.png

    CAShapeLayer *fLayer = self.layer_list[0];
    CAShapeLayer *sLayer = self.layer_list[1];
    //第二段動(dòng)畫
    CAShapeLayer *testLeftLayer = [CAShapeLayer new];
    testLeftLayer.fillColor = [UIColor clearColor].CGColor;
    testLeftLayer.borderWidth = 1.f;
    testLeftLayer.strokeColor = [UIColor redColor].CGColor;
    UIBezierPath *leftPath = [UIBezierPath bezierPath];
    [leftPath addArcWithCenter:(CGPoint){fLayer.frame.origin.x+25,fLayer.frame.origin.y+10} 
                        radius:15 startAngle:0 endAngle:M_PI clockwise:YES];
    testLeftLayer.path = leftPath.CGPath;
    [self.layer addSublayer:testLeftLayer];

我們?cè)賮砜纯吹谌w球:
0° -> π 逆時(shí)針運(yùn)動(dòng)

第三顆球的運(yùn)動(dòng)軌跡.png

    CAShapeLayer *sLayer = self.layer_list[1];
    CAShapeLayer *tLayer = self.layer_list[2];
    //第三段動(dòng)畫
    CAShapeLayer *testRightLayer = [CAShapeLayer new];
    testRightLayer.fillColor = [UIColor clearColor].CGColor;
    testRightLayer.borderWidth = duration;
    testRightLayer.strokeColor = [UIColor blueColor].CGColor;
    
    UIBezierPath *rightPath = [UIBezierPath bezierPath];
    [rightPath addArcWithCenter:(CGPoint){sLayer.frame.origin.x+25,tLayer.frame.origin.y+10} 
                         radius:15 startAngle:0 endAngle:M_PI clockwise:NO];
    testRightLayer.path = rightPath.CGPath;
    [self.layer addSublayer:testRightLayer];

最后的靜態(tài)效果圖如下:


靜態(tài)效果圖.png

設(shè)定好軌跡之后我們就來設(shè)置它們的動(dòng)畫效果,動(dòng)畫效果就要用到我們上一篇中提到的關(guān)鍵幀動(dòng)畫了.
由于三個(gè)動(dòng)畫具有相同的屬性設(shè)置,所以我們最好是封裝一下,提取出它們會(huì)有變化的數(shù)值。
例如,動(dòng)畫的依賴視圖會(huì)變,它們的運(yùn)動(dòng)path會(huì)變,所以提取出這兩個(gè)參數(shù)。

- (void)keyFrameAnimationWithLayer:(CALayer *)layer path:(UIBezierPath *)path;
    CAKeyframeAnimation *keyAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    keyAnimation.path = path.CGPath;
    keyAnimation.fillMode = kCAFillModeForwards;
    keyAnimation.calculationMode = kCAAnimationPaced;
    keyAnimation.removedOnCompletion = NO;
    keyAnimation.duration = duration;
    keyAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    keyAnimation.rotationMode = kCAAnimationRotateAuto;
    keyAnimation.repeatCount = MAXFLOAT;
    keyAnimation.calculationMode = kCAAnimationCubic;
    [layer addAnimation:keyAnimation forKey:@"keyFrameAnimation"];

具體的參數(shù)信息已經(jīng)在上一篇中提過了,這里就不說了。
然后我們開始調(diào)用:

//調(diào)用第一顆球
[self keyFrameAnimationWithLayer:fLayer path:semicirclePath];
第一顆球卡頓的動(dòng)畫.gif

我們運(yùn)行之后會(huì)發(fā)現(xiàn)在兩個(gè)圓的交界處,卡頓了一下下(關(guān)于這個(gè)問題,我嘗試調(diào)整了動(dòng)畫的timingFunction屬性還有calculationMode都無法很好的解決這個(gè)問題,我猜想是由于兩個(gè)路徑是分開繪制的原因,具體的原因還在研究當(dāng)中。)
不過我還是找到了解決辦法:
設(shè)置兩個(gè)路徑,然后將路徑進(jìn)行合并成一個(gè)路徑:

    UIBezierPath *semicirclePathA = [UIBezierPath bezierPath];
    [semicirclePathA addArcWithCenter:(CGPoint){fLayer.frame.origin.x+25,fLayer.frame.origin.y+10} radius:15 startAngle:M_PI endAngle:0 clockwise:YES];
    UIBezierPath *semicirclePathB = [UIBezierPath bezierPath];
    [semicirclePathB addArcWithCenter:(CGPoint){sLayer.frame.origin.x+25,sLayer.frame.origin.y+10} radius:15 startAngle:M_PI endAngle:0 clockwise:NO];
    [semicirclePathA appendPath:semicirclePathB];
    testLayer.path = semicirclePathA.CGPath;
    [self.layer addSublayer:testLayer];

運(yùn)行之后,可以看到效果,如巧克力般絲滑的感覺:


第一課球的運(yùn)動(dòng)動(dòng)畫.gif

畫好第一個(gè)球之后,我們?cè)賮懋嬈渌膬蓚€(gè)球,基本就相同了,只要調(diào)用

- (void)keyFrameAnimationWithLayer:(CALayer *)layer path:(UIBezierPath *)path;

方法就可以了。

效果如下:


三顆球運(yùn)動(dòng)動(dòng)畫.gif

但是實(shí)現(xiàn)的效果并不如意,它們的顏色變化并不是漸變的,看起來并不好,所以我們還需要添加顏色變換的動(dòng)畫
觀察原來的效果總結(jié)如下:

  1. 第一顆球從開始到轉(zhuǎn)到最后的時(shí)候,顏色從深色變?yōu)榱藴\色。所以我們可以設(shè)定它的變化值為 1 -> 0.2.
  2. 第二顆球原本是淺色然后旋轉(zhuǎn)到第一的位置時(shí)是深色。所以我們可以設(shè)定它的變化值為 0.5 -> 1.
  3. 第三顆球原本是最淺的然后旋轉(zhuǎn)到第二的位置時(shí),顏色變深了一點(diǎn)。所以我們?cè)O(shè)定它的變化值 0.2 -> 0.5.

動(dòng)畫因?yàn)橹皇莾蓚€(gè)值得變化 , FromValue -> toValue , 我們采用CABasicAnimation就可以輕松實(shí)現(xiàn)效果.
我們同樣來封裝該動(dòng)畫:

- (void)colorGradientAnimationWithLayer:(CALayer *)layer fromValue:(id)fromValue toValue:(id)toValue{
    CABasicAnimation *alphaAnimation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
    alphaAnimation.fromValue = fromValue;
    alphaAnimation.toValue = toValue;
    alphaAnimation.duration = duration;
    alphaAnimation.repeatCount = MAXFLOAT;
    [layer addAnimation:alphaAnimation forKey:@"anmiationAlpha"];
}

調(diào)用:

//第一顆球的變化
[self colorGradientAnimationWithLayer:fLayer
                                    fromValue:(__bridge id _Nullable)(getColor(102, 170, 238, 1)).CGColor
                                      toValue:(__bridge id _Nullable)(getColor(102, 170, 238, 0.2)).CGColor];
//第二顆球的變化
[self colorGradientAnimationWithLayer:sLayer
                                    fromValue:(__bridge id _Nullable)(getColor(102, 170, 238, 0.5)).CGColor
                                      toValue:(__bridge id _Nullable)(getColor(102, 170, 238, 0.1)).CGColor];
//第三顆球的變化
[self colorGradientAnimationWithLayer:tLayer
                                fromValue:(__bridge id _Nullable)(getColor(102, 170, 238, 0.2)).CGColor
                                  toValue:(__bridge id _Nullable)(getColor(102, 170, 238, 0.5)).CGColor];

在制作過程中出現(xiàn)的情況:

1.動(dòng)畫閃爍
keyAnimation.fillMode = kCAFillModeForwards; 可以添加該屬性,該屬性表示保留動(dòng)畫結(jié)束時(shí)的效果。
但添加之后發(fā)現(xiàn)還是不行,最后發(fā)現(xiàn)其實(shí)動(dòng)畫結(jié)束的坐標(biāo)點(diǎn)有問題。

__bridge 主要是因?yàn)槲覀冊(cè)诰帉懗绦虻臅r(shí)候還會(huì)用到 CoreFoundation(CF) 框架的對(duì)象,CF和 OC 對(duì)象之間的類型轉(zhuǎn)換就需要用到__bridge

最后就是我們需要的效果了。


加載動(dòng)畫.gif

DEMO:
https://github.com/yanggenwei/GWAnimation/tree/master

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 又是一年畢業(yè)季,今年終于輪到我了,最近一邊忙著公司的項(xiàng)目,一邊趕著畢設(shè)和論文,還私下和朋友搞了些小外包,然后還要抽...
    李晨瑋閱讀 7,497評(píng)論 5 65
  • 【Android 動(dòng)畫】 動(dòng)畫分類補(bǔ)間動(dòng)畫(Tween動(dòng)畫)幀動(dòng)畫(Frame 動(dòng)畫)屬性動(dòng)畫(Property ...
    Rtia閱讀 6,392評(píng)論 1 38
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,175評(píng)論 25 708
  • 中午跟同事吃飯,提到2018年個(gè)人OKR時(shí),感覺自己成長(zhǎng)最大的時(shí)間段是2008-2010年和過去的2017年,...
    liuxinamy閱讀 280評(píng)論 5 3
  • 走進(jìn)流動(dòng)的空氣中踏入漆黑的夜開始一個(gè)人行走的旅程城市的燈火輝煌照映遠(yuǎn)方是否有一盞明亮的燈伴著飄寂的雪花點(diǎn)綴這無芳的...
    昊水長(zhǎng)天閱讀 183評(píng)論 2 2

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