前言
之前在iOS開發(fā)干貨 第1期中提到過一個(gè)挺有意思的數(shù)字轉(zhuǎn)變動(dòng)畫NumberMorphView , 如下圖:

我將通過幾篇文章對(duì)這個(gè)開源庫做一些分析,當(dāng)然,這篇文章不會(huì)對(duì)它做全面的解析,而是利用這個(gè)庫的一些技術(shù)概念來做一些簡單的示例,也算是一個(gè)引子,后面會(huì)抽時(shí)間再寫一篇對(duì)這個(gè)庫的代碼分析,敬請(qǐng)期待。
要做些什么
我們將會(huì)使用CADisplayLink + CAShapeLayer + UIBezierPath結(jié)合制作一個(gè)毫秒級(jí)的畫圓動(dòng)畫,不同的是,這個(gè)動(dòng)畫具有彈性效果,下面先來看看制作的效果:

開始
準(zhǔn)備工作
- 先新建一個(gè)Single View Application項(xiàng)目,在項(xiàng)目中添加類RRCircleAnimationView,繼承于UIView。
- 打開Main.storyboard,將唯一的一個(gè)ViewController的view custom class修改為RRCircleAnimationView。
至此,準(zhǔn)備工作已經(jīng)完成。
動(dòng)手來畫個(gè)圓
先來個(gè)簡單任務(wù),我們來實(shí)現(xiàn)畫圓動(dòng)畫。
第一步,為RRCircleAnimationView添加屬性:
@implementation RRCircleAnimationView
{
CADisplayLink *_displayLink; // CADisplayLink可以確保系統(tǒng)渲染每一幀的時(shí)候我們的方法都被調(diào)用, 從而保證了動(dòng)畫的流暢性,毫秒級(jí)動(dòng)畫就靠他。
UIBezierPath *_path; // 用于創(chuàng)建基于矢量的路徑
CGPoint _beginPoint; // 開始觸摸位置
CGPoint _endPoint; // 觸摸結(jié)束的位置
CAShapeLayer *_shapeLayer; // 可以結(jié)合UIBezierPath進(jìn)行繪畫
}
接著初始化實(shí)例變量,由于我們用的是storyboard進(jìn)行加載,所以可以在awakeFromNib方法里面初始化
// 注意這里我們是直接從xib加載當(dāng)前view。
- (void)awakeFromNib
{
_shapeLayer = [CAShapeLayer layer];
[self.layer addSublayer:_shapeLayer];
_shapeLayer.fillColor = [UIColor colorWithRed:0.400 green:0.400 blue:1.000 alpha:1.000].CGColor;
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateFrame)];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
接下來實(shí)現(xiàn)上面CADisplayLink要不停調(diào)用的updateFrame方法,我們?cè)诖朔椒▋?nèi)不斷地畫圓。
- (void)updateFrame {
// 畫圓
_path = [UIBezierPath bezierPathWithArcCenter:_beginPoint radius:[self getRadius] startAngle:0 endAngle:M_PI*2 clockwise:YES];
_shapeLayer.path = _path.CGPath;
}
上面我們用開始觸摸的點(diǎn)的位置作為圓心的位置,再根據(jù)特定的半徑進(jìn)行繪制一個(gè)圓,這個(gè)半徑是根據(jù)我們觸摸的開始點(diǎn)和結(jié)束點(diǎn)進(jìn)行計(jì)算出來的,開始觸摸點(diǎn)到結(jié)束點(diǎn)的距離就是這個(gè)圓的半徑。
我們先把觸摸的起始和結(jié)束點(diǎn)給找到:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
_beginPoint = point;
_endPoint = point;
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
_endPoint = point;
}
最后計(jì)算用上我們中學(xué)的數(shù)學(xué)知識(shí),根據(jù)兩點(diǎn)坐標(biāo)距離公式

可以得到我們起始和結(jié)束兩點(diǎn)的距離,也就是圓的半徑是:
- (CGFloat)getRadius
{
CGFloat result = sqrt(pow(_endPoint.x - _beginPoint.x, 2) + pow(_endPoint.y - _beginPoint.y, 2));
return result;
}
到這里畫圓動(dòng)畫完成。
加入彈性效果
上面只是的畫圓動(dòng)畫看起來是沒什么問題了,不過總感覺缺少動(dòng)感,接下來我們來幫他加入些活力!
-
添加一下成員變量到RRCircleAnimationView類中。
BOOL _isTouchEnd; // 觸摸結(jié)束標(biāo)志 int _currentFrame; // 當(dāng)前的幀數(shù) -
在
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event方法內(nèi)添加以下代碼:_isTouchEnd = NO; //重置觸摸狀態(tài) _currentFrame = 1; //重置當(dāng)前的幀數(shù) -
添加以下方法:
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { _isTouchEnd = YES; //觸摸結(jié)束,更新觸摸狀態(tài) } -
將方法
- (CGFloat)getRadius修改如下:- (CGFloat)getRadius { CGFloat result = sqrt(pow(_endPoint.x-_beginPoint.x, 2)+pow(_endPoint.y-_beginPoint.y, 2)); if (_isTouchEnd) { CGFloat animationDuration = 1.0; // 彈簧動(dòng)畫持續(xù)的時(shí)間 int maxFrames = animationDuration / _displayLink.duration; _currentFrame++; if (_currentFrame <= maxFrames) { CGFloat factor = [self getSpringInterpolation:(CGFloat)(_currentFrame) / (CGFloat)(maxFrames)]; //根據(jù)公式計(jì)算出彈簧因子 return MAX_RADIUS + (result - MAX_RADIUS) * factor; // 根據(jù)彈簧因子計(jì)算當(dāng)前幀的圓半徑 }else { return MAX_RADIUS; } } return result; } -
最后加入神奇的公式:
- (CGFloat)getSpringInterpolation:(CGFloat)x { CGFloat tension = 0.3; // 張力系數(shù) return pow(2, -10 * x) * sin((x - tension / 4) * (2 * M_PI) / tension); }
這個(gè)公式用數(shù)學(xué)符號(hào)表達(dá)出來是:

可以用Mac OS X自帶的軟件叫Grapher畫出此函數(shù)的的圖像,如下圖:

這個(gè)函數(shù)的作用其實(shí)就是通過x值,也就是當(dāng)前幀數(shù)除以允許的最大幀數(shù)。
(CGFloat)(_currentFrame) / (CGFloat)(maxFrames)
因此,x的值的范圍也就是(0, 1]。
我們所要的動(dòng)畫效果是把圓拉大到超過或者小于設(shè)定的目標(biāo)半徑MAX_RADIUS時(shí),需要一個(gè)彈性動(dòng)畫逐漸回到設(shè)定好的目標(biāo)半徑。
回頭再看一下實(shí)時(shí)計(jì)算動(dòng)畫半徑的公式:
MAX_RADIUS + (result - MAX_RADIUS) * factor
為了讓x = 1的時(shí)候,半徑 = MAX_RADIUS,所以這時(shí)factor就應(yīng)該為0,也就是f(1) = 0。
再看看剛才的函數(shù)圖像,在x = 0到1之前振動(dòng),隨著x的增加振幅逐漸減少,當(dāng)x = 1的時(shí)候,y值為0。
最后
這篇文章講述了如何自己實(shí)現(xiàn)具有彈性的幀動(dòng)畫,如果能理解好這種動(dòng)畫制作原理,對(duì)動(dòng)畫效果開發(fā)是很有幫助的,后面有時(shí)間會(huì)繼續(xù)寫其他的一些動(dòng)畫制作的方法,實(shí)現(xiàn)更多的動(dòng)畫效果。
差點(diǎn)忘了說了,目前這個(gè)動(dòng)畫已經(jīng)放到github上面,傳送門:RRongAnimation

The End