牛頓擺的實(shí)現(xiàn)

本文由我們團(tuán)隊(duì)的剛畢業(yè)的帥小伙真帥童鞋在學(xué)校寫的一個(gè)小動(dòng)畫。發(fā)出來分享一下。


做了一個(gè)小動(dòng)畫,放上來請(qǐng)大家拍磚指教。

牛頓擺大家應(yīng)該都不陌生的吧,其實(shí)用代碼實(shí)現(xiàn)起來,挺簡單的,下邊我們就來實(shí)際的寫一下這個(gè)動(dòng)畫。

?效果圖

0 動(dòng)畫拆分

這個(gè)動(dòng)畫看起來并不是很難,由6根線,5個(gè)圓組成,其實(shí)拆分來看的話,其中中間的三根線和最上邊的一根線是不用動(dòng)的,做動(dòng)畫的只是兩邊的兩條線和兩個(gè)圓,其中,左邊的動(dòng)畫來說,就是先擺動(dòng)起來,然后回落,當(dāng)落下來的時(shí)候,右邊的開始向上擺動(dòng),然后回落,當(dāng)落下來的時(shí)候,左邊的開始擺動(dòng),以此循環(huán)。

也就是說,我們只需要控制兩個(gè)動(dòng)畫,然后讓其交替進(jìn)行即可。

單個(gè)動(dòng)畫拆分

對(duì)于左邊動(dòng)畫來說,是由兩個(gè)動(dòng)畫組成的:線的動(dòng)畫,圓的動(dòng)畫

  • 線的動(dòng)畫:對(duì)于左邊的額線來說,只是繞著上方的端點(diǎn)旋轉(zhuǎn)了45°

  • 圓的動(dòng)畫:對(duì)于左邊的圓來說,也只是沿著一個(gè)以左邊線為長度的弧,走了45°

兩者結(jié)合起來就形成了,線和球是在一起擺動(dòng)的視覺效果,但其實(shí)在實(shí)現(xiàn)的過程中,他們是分開的

1 動(dòng)畫的實(shí)現(xiàn)

在該例子中,所有的圖形都是由CAShaplayer繪制的。

1.1 繪制線

首選,我們需要繪制6條線,來構(gòu)成牛頓擺的基本圖形,下邊我們來看代碼,

CAShapeLayer * layer = [CAShapeLayer layer]; //創(chuàng)建一個(gè)layer
CGMutablePathRef path = CGPathCreateMutable(); //初始化一個(gè)路徑
CGPathMoveToPoint(path, nil, startPoint.x, startPoint.y); //移動(dòng)到開始的地方
CGPathAddLineToPoint(path, nil, endPoint.x, endPoint.y); //劃線,畫到結(jié)束的點(diǎn)
layer.path = path; // 指定layer的Path
layer.lineCap = kCALineCapRound; //設(shè)置線端點(diǎn)的形狀
layer.lineWidth = self.lineWithd; //設(shè)置線寬
layer.strokeColor = [UIColor redColor].CGColor; //設(shè)置線的顏色 

這是繪制一條線我們所需要做的工作,其中,我們要指定這個(gè)線從哪開始,到哪結(jié)束,其中關(guān)于lineCap,這里有張圖可以很好理解這是個(gè)什么玩意

Line Cap

其實(shí)我們可以封裝一個(gè)方法循環(huán)來創(chuàng)建這些個(gè)線條,這樣,我們的界面會(huì)看起來像這個(gè)樣子

Line

1.2 繪制圓

同上邊繪制線一樣,我們需要繪制5個(gè)圓,看代碼

    CAShapeLayer * cycleLayer = [CAShapeLayer layer]; //創(chuàng)建一個(gè)layer
    CGMutablePathRef path = CGPathCreateMutable(); //初始化一個(gè)路徑
    CGPathAddArc(path, nil, center.x, center.y, radius, 0, M_PI * 2, YES); // 畫一個(gè)圓
    cycleLayer.path = path; //指定layer的path
    cycleLayer.fillColor = [UIColor grayColor].CGColor; //填充layer的顏色

對(duì)于CGPathAddArc來說,參數(shù)分別是,路徑,形變,圓心的X,圓心的Y,圓的半徑,開始角度,結(jié)束角度,是否為順時(shí)針

畫完之后,我們的界面看起來像是這個(gè)樣子

這樣我們的牛頓擺就畫完了,當(dāng)然,你可以適當(dāng)?shù)募右恍╆幱伴_始這個(gè)形狀看起來更有立體感。下邊我們就可以來做動(dòng)畫了。

2 動(dòng)畫

就像上邊我們所說的一樣,我們拆開來做這些動(dòng)畫,首先我們先做左邊的動(dòng)畫。

2.1.1 左邊線的動(dòng)畫

左邊的線的動(dòng)畫,就是讓左邊的線,圍繞著上邊的而端點(diǎn),順時(shí)針擺動(dòng)45°,我們先來看代碼。

_leftLineBaseAnimation = [CABasicAnimation animation]; //初始化一個(gè)動(dòng)畫
_leftLineBaseAnimation.keyPath = @"transform.rotation.z"; //動(dòng)畫運(yùn)動(dòng)的方式,現(xiàn)在指定的是圍繞Z軸旋轉(zhuǎn)
_leftLineBaseAnimation.duration = 0.4; //動(dòng)畫持續(xù)時(shí)間
_leftLineBaseAnimation.fromValue = [NSNumber numberWithFloat:0]; //開始的角度
_leftLineBaseAnimation.toValue = [NSNumber numberWithFloat:M_PI_4/2]; //結(jié)束的角度
_leftLineBaseAnimation.timingFunction =[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut]; //動(dòng)畫的運(yùn)動(dòng)方式,
_leftLineBaseAnimation.autoreverses = YES; //是否反向移動(dòng)動(dòng)畫
_leftLineBaseAnimation.delegate = self; //動(dòng)畫的代理
_leftLineBaseAnimation.fillMode = kCAFillModeForwards;//動(dòng)畫結(jié)束后的狀態(tài)

  • 首先我們先來看keyPath這個(gè)屬性,這個(gè)是行是一個(gè)字符串,指定了你要讓那個(gè)CALayer或CALayer的子類的某個(gè)可以做動(dòng)畫的屬性的值,關(guān)于這個(gè)值,大家可以在這里

  • fromeValuetoValue都好理解,從一個(gè)值,到另一值,這里從0°到45°

  • 我們來看timingFunction這個(gè)屬性,一圖省千言

這里我們能夠看出,也就是運(yùn)動(dòng)的速度曲線,先快后慢,先慢后快等等,我們也可以用貝塞爾曲線來定制我們自己的運(yùn)動(dòng)速度曲線,這樣可以實(shí)現(xiàn)更加優(yōu)美的動(dòng)畫。

  • autoreverses 這個(gè)就是是否反向動(dòng)畫,也就是,逆序在播放一遍動(dòng)畫,因?yàn)檫@里我們要的效果是先擺上去,然后在擺下來,正好是動(dòng)畫播放一遍然后在反向播放一遍,所以我們指定為YES。

  • fillMode 這個(gè)屬性是提供了一個(gè)動(dòng)畫結(jié)束后的狀態(tài),是應(yīng)該移除,還是繼續(xù)保持,反向等等。

2.1.2 左邊圓的動(dòng)畫

我們來看左邊圓的動(dòng)畫

CGMutablePathRef path  = CGPathCreateMutable(); // 創(chuàng)建一個(gè)路徑
CGPathAddArc(path, nil, 0, 10, self.height - 20, M_PI_2, M_PI_2+M_PI_4/2, NO); //這里的圓心我們制定左邊第一條線的上邊的端點(diǎn),長為線的長度,角度為45°
_leftCycleKeyframeAnimation = [CAKeyframeAnimation animation]; //創(chuàng)建一個(gè)動(dòng)畫
_leftCycleKeyframeAnimation.keyPath = @"position"; //我們要移動(dòng)小球的位置,所以keyPath中我們要改變小球的position
_leftCycleKeyframeAnimation.path = path; //制定動(dòng)畫路徑
_leftCycleKeyframeAnimation.duration = 0.4f; //動(dòng)畫持續(xù)時(shí)間
_leftCycleKeyframeAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; //動(dòng)畫的速度曲線
_leftCycleKeyframeAnimation.autoreverses = YES; // 是否反向運(yùn)動(dòng)
_leftCycleKeyframeAnimation.fillMode = kCAFillModeForwards; //動(dòng)畫結(jié)束后的狀態(tài)
_leftCycleKeyframeAnimation.delegate = self;
[_leftCycleKeyframeAnimation setValue:@"left" forKey:@"left"]; //key-value,用于在代理中監(jiān)聽動(dòng)畫

關(guān)鍵幀動(dòng)畫,可以說是指定了一個(gè)動(dòng)畫路徑,使得動(dòng)畫按照一個(gè)路徑來進(jìn)行動(dòng)畫,相對(duì)于基礎(chǔ)動(dòng)畫來說,關(guān)鍵幀動(dòng)畫更加細(xì)膩,基礎(chǔ)動(dòng)畫只能提供一個(gè)點(diǎn)到另一個(gè)點(diǎn)的動(dòng)畫,中間過程要系統(tǒng)自動(dòng)補(bǔ)全,但關(guān)鍵幀動(dòng)畫可以完整的定制動(dòng)畫的運(yùn)動(dòng)軌跡,使得動(dòng)畫更加細(xì)膩平滑,效果也更加好看

右邊的動(dòng)畫和左邊的動(dòng)畫類似,詳細(xì)的就不說了,貼一下代碼。

右邊線

_rightLineBaseAnimation = [CABasicAnimation animation];
_rightLineBaseAnimation.keyPath = @"transform.rotation.z";
_rightLineBaseAnimation.duration = 0.4;
_rightLineBaseAnimation.fromValue = [NSNumber numberWithFloat:0];
_rightLineBaseAnimation.toValue = [NSNumber numberWithFloat:-M_PI_4/2];
_rightLineBaseAnimation.timingFunction =[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut];
_rightLineBaseAnimation.autoreverses = YES;
_rightLineBaseAnimation.fillMode = kCAFillModeForwards;
_rightLineBaseAnimation.delegate = self;

右邊圓

CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, nil, 90, 10, self.height - 20, M_PI_2,M_PI_2-M_PI_4/2, YES);
_rightCycleKeyframeAnimation = [CAKeyframeAnimation animation];
_rightCycleKeyframeAnimation.keyPath = @"position";
_rightCycleKeyframeAnimation.path = path;
_rightCycleKeyframeAnimation.duration = 0.4f;
_rightCycleKeyframeAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
_rightCycleKeyframeAnimation.autoreverses = YES;
_rightCycleKeyframeAnimation.fillMode = kCAFillModeForwards;
_rightCycleKeyframeAnimation.delegate = self;
[_rightCycleKeyframeAnimation setValue:@"right" forKey:@"right"];

來來來,我們把左右動(dòng)畫結(jié)合起來,讓左邊動(dòng)畫先開始,然后結(jié)束了之后,在開始右邊的動(dòng)畫,右邊的動(dòng)畫結(jié)束了,在開始左邊的動(dòng)畫,然后循環(huán)進(jìn)行,那我們?cè)趺茨軝z測(cè)到動(dòng)畫結(jié)束呢,還記得我們?cè)陉P(guān)鍵幀動(dòng)畫中設(shè)置的代理么,那么在動(dòng)畫結(jié)束后,代理方法- animationDidStop: finished: 會(huì)被調(diào)用,在這里,我們可以動(dòng)過kvc的方式拿到我們?cè)谏鲜鲎笥谊P(guān)鍵幀動(dòng)畫中設(shè)置的相關(guān)參數(shù),然后來控制左右動(dòng)畫的運(yùn)行,代碼看起來是這個(gè)樣子的

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    if ([[anim valueForKey:@"left"] isEqualToString:@"left"]) {
        
        [self doRightAnimation];
    } else if([[anim valueForKey:@"right"] isEqualToString:@"right"])
    {
        [self doLeftAnimation];
    }
}

好了,現(xiàn)在動(dòng)畫寫完了,我們來運(yùn)行一下,看看效果怎么樣,來,喝口水,深呼吸,按下command + r 盯著模擬器,然后出現(xiàn)了這么個(gè)玩意

這這這,這是啥,這是啥,有瑕疵啊

一定是有什么不對(duì)的地方,恩,一定有什么不對(duì) ( ˙-˙ )

2.3 anchorPoint & bounds & position & frame的關(guān)系

上邊之所以會(huì)出現(xiàn)這種情況,主要是我們沒有認(rèn)清楚 anchorPoint bounds position frame三者之間的關(guān)系導(dǎo)致的,可以從上述代碼中看到,我們并沒有設(shè)置四者中的任何一個(gè)值。但我們的動(dòng)畫,和這四個(gè)值,關(guān)系還是很大的,下邊我們就來一一分析這四者之間的關(guān)系

  • anchorPoint

錨點(diǎn),這是個(gè)什么玩意,舉個(gè)例子,桌子上有一張紙,你用一個(gè)手指頭按著他,當(dāng)他不動(dòng)的時(shí)候,他就在那里,不增不減,當(dāng)你要旋轉(zhuǎn)他的時(shí)候,那就要看你按住的那個(gè)點(diǎn)的位置。動(dòng)畫中的旋轉(zhuǎn)也是這個(gè)樣子,這個(gè)錨點(diǎn),決定了你的旋轉(zhuǎn)。

在這個(gè)圖中我們可以看到,默認(rèn)的錨點(diǎn)是(0.5,0.5),也就是layer的中心位置,當(dāng)圍繞著錨點(diǎn)旋轉(zhuǎn)的時(shí)候,不同的錨點(diǎn),旋轉(zhuǎn)效果是不一樣的

  • bounds

The bounds rectangle is the origin and size of the layer in its own coordinate space. When you create a new standalone layer, the default value for this property is an empty rectangle, which you must change before using the layer. The values of each coordinate in the rectangle are measured in points.

官方文檔是這么說的,bounds這個(gè)屬性想必大家都不陌生,我們把position和frame看完,在說他們的關(guān)系

  • position

The layer’s position in its superlayer’s coordinate space. Animatable.

The value of this property is specified in points and is always specified relative to the value in the anchorPoint property. For new standalone layers, the default position is set to (0.0, 0.0). Changing the frame property also updates the value in this property.

也就是說,position是這個(gè)layer在superLayer中的位置,創(chuàng)建一個(gè)layer的時(shí)候,默認(rèn)值是0 和frame anchorPoint都有關(guān)系,我們?cè)趤砜磃rame

  • frame

The frame rectangle is position and size of the layer specified in the superlayer’s coordinate space. For layers, the frame rectangle is a computed property that is derived from the values in thebounds, anchorPoint and position properties. When you assign a new value to this property, the layer changes its position and bounds properties to match the rectangle you specified. The values of each coordinate in the rectangle are measured in points.

官方文檔說,這個(gè)屬性也是在superlayer‘s坐標(biāo)系中的位置,對(duì)于layer來說,其是一個(gè)計(jì)算屬性,由bounds anchorPoint position 計(jì)算而來,當(dāng)指定一個(gè)frame的時(shí)候,系統(tǒng)會(huì)重新計(jì)算position和bounds來改變layer的位置,所以說,frame bounds anchorPoint position 這幾個(gè)值是有非常緊密的聯(lián)系的,下邊我們就來探究一下,究竟有什么聯(lián)系

我們回過頭先看anchorPoint的第一張圖

第一組數(shù)據(jù)

frame = (40, 60, 120, 80);

bounds = (0, 0, 120, 80);

anchorPoint = (0.5, 0.5);

position = (100, 100);

第二組數(shù)據(jù)

frame = (40, 60, 120, 80);

bounds = (0, 0, 120, 80);

anchorPoint = (0, 0);

position = (40, 60);

上邊說,四者是有聯(lián)系的,而且frame和position都是在父坐標(biāo)系中的位置,而bounds的寬和高是和frame的寬高一樣的,那就很好解釋了

frame.x = position.x - archonPoint.x * bounds.size.with

frame.y = position.y - archonPoint.y * bounds.size.height

那我們這樣的想法對(duì)不對(duì)呢,我們還是寫代碼來看一下

CALayer * aLayer = [CALayer layer];
aLayer.backgroundColor = [UIColor redColor].CGColor;
aLayer.position = CGPointMake(200, 100);
aLayer.anchorPoint = CGPointMake(0.2, 0.2);
aLayer.bounds = CGRectMake(0, 0, 100, 150);
[self.view.layer addSublayer:aLayer];
NSLog(@"%@",NSStringFromCGRect(aLayer.frame));

當(dāng)我們這樣寫的時(shí)候,根據(jù)上邊的公式,應(yīng)該能計(jì)算出來,frame.x = 180 frame.y = 70。那結(jié)果到底對(duì)不對(duì)呢,運(yùn)行一下結(jié)果還真是。其實(shí)在日常開發(fā)過程中,我們一般都是先寫好布局,然后設(shè)置錨點(diǎn),在做動(dòng)畫,這樣的時(shí)候,一運(yùn)行就會(huì)發(fā)現(xiàn),到處各種界面元素到處亂飛,根本就不是想要結(jié)果,這個(gè)時(shí)候,我們可以這么做,先設(shè)置錨點(diǎn),然后在重設(shè)一遍frame,這樣系統(tǒng)就能自動(dòng)計(jì)算出position,這樣的話,極大的簡化了我們?cè)趧?dòng)畫中的布局問題

就拿上邊的豎線擺動(dòng)的話,我們可以看出,左邊的動(dòng)畫和左邊的圓,簡單的話我們都可以直接把frame設(shè)置成(0, 0, superLayer.width, superLayer.height) 這樣的話,我們想怎么運(yùn)動(dòng),直接設(shè)置相應(yīng)的錨點(diǎn)即可。

self.leftLine.anchorPoint = CGPointMake(0.1, 0);
self.leftLine.frame = CGRectMake(0, 0, self.width, self.height);
    
self.leftCycle.anchorPoint = CGPointMake(0, 0.9);
self.leftCycle.frame = CGRectMake(0, 0, self.width, self.height);
    
self.rightLine.anchorPoint = CGPointMake(0.9, 0);
self.rightLine.frame = CGRectMake(0, 0, self.width, self.height);
    
self.rightCycle.anchorPoint = CGPointMake(0.9, 0.9);
self.rightCycle.frame = CGRectMake(0, 0, self.width, self.height);

我們重新設(shè)置一遍anchorPointframe 把他們放在我們想要的位置,以及想要的錨點(diǎn)上,我們?cè)賮碓囈幌?/p>

?效果圖

嗯,終于達(dá)到了我們想要的結(jié)果。

總結(jié)

其實(shí),只要理解了 anchorPoint bounds position frame的關(guān)系,把我們想要做動(dòng)畫的控件擺放在合理的位置,在加上強(qiáng)大的CAAnimation,把復(fù)雜的動(dòng)畫拆分來看,在把簡單的動(dòng)畫組合起來,我們可以做出很多很酷炫的動(dòng)畫。

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

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

  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果,實(shí)現(xiàn)這些動(dòng)畫的過程并不復(fù)雜,今天將帶大家一窺iOS動(dòng)畫全貌。在這里你可以看...
    F麥子閱讀 5,275評(píng)論 5 13
  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果,實(shí)現(xiàn)這些動(dòng)畫的過程并不復(fù)雜,今天將帶大家一窺ios動(dòng)畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,699評(píng)論 6 30
  • 轉(zhuǎn)載:http://m.itdecent.cn/p/32fcadd12108 每個(gè)UIView有一個(gè)伙伴稱為l...
    F麥子閱讀 6,602評(píng)論 0 13
  • 原文來自:http://www.cnblogs.com/benbenzhu/p/3615516.html?utm_...
    小如99閱讀 486評(píng)論 0 0
  • 最愛的西瓜,再加上我最喜歡的蘇打,就變成了西瓜蘇打。今天只畫了西瓜,是因?yàn)樘K打不會(huì)畫。為什么不會(huì)畫?因?yàn)楹軓?fù)雜。為...
    西瓜蘇打閱讀 174評(píng)論 0 0

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