iOS進(jìn)階之編寫彈性動(dòng)畫

前言

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

NumberMorphView
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)畫具有彈性效果,下面先來看看制作的效果:

效果預(yù)覽

開始

準(zhǔn)備工作

  1. 先新建一個(gè)Single View Application項(xiàng)目,在項(xiàng)目中添加類RRCircleAnimationView,繼承于UIView。
  2. 打開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)距離公式

兩點(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)感,接下來我們來幫他加入些活力!

  1. 添加一下成員變量到RRCircleAnimationView類中。

        BOOL _isTouchEnd; // 觸摸結(jié)束標(biāo)志
        int _currentFrame; // 當(dāng)前的幀數(shù)
    
  2. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event方法內(nèi)添加以下代碼:

        _isTouchEnd = NO; //重置觸摸狀態(tài)
        _currentFrame = 1; //重置當(dāng)前的幀數(shù)
    
  3. 添加以下方法:

    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        _isTouchEnd = YES; //觸摸結(jié)束,更新觸摸狀態(tài)
        
    }
    
  4. 將方法- (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;
    }
    
  5. 最后加入神奇的公式:

    - (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á)出來是:

Math

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

Grapher

這個(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

RRongAnimation
RRongAnimation

The End

最后編輯于
?著作權(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)容

  • http://www.itdecent.cn/p/cf9f600a5342
    sfandy閱讀 269評(píng)論 0 0
  • 前言 本文只要描述了iOS中的Core Animation(核心動(dòng)畫:隱式動(dòng)畫、顯示動(dòng)畫)、貝塞爾曲線、UIVie...
    GitHubPorter閱讀 3,741評(píng)論 7 11
  • Core Animation Core Animation,中文翻譯為核心動(dòng)畫,它是一組非常強(qiáng)大的動(dòng)畫處理API,...
    45b645c5912e閱讀 3,157評(píng)論 0 21
  • Android動(dòng)畫的分類:三種,屬性動(dòng)畫,補(bǔ)間動(dòng)畫,幀動(dòng)畫 補(bǔ)間動(dòng)畫:縮放,平移,旋轉(zhuǎn),透明度 API:Anima...
    EvanPoison閱讀 234評(píng)論 0 0
  • 臺(tái)詞“直白”,一般人對(duì)話不會(huì)有這樣的思考吧。那是壓抑了很久,想表達(dá)好多東西。才會(huì)對(duì)一個(gè)人說這樣的臺(tái)詞,富有思想,當(dāng)...
    RingRun閱讀 337評(píng)論 0 0

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