JazzHands 源码实现分析

前几篇 的文章中,我们已经了解了 JazzHands 这个库的使用方法,这篇文章我们来深入了解 JazzHands 的实现, 看他到底用了什么魔法来制作出这个帧动画库.

JazzHands 支持如下动画类型

  • IFTTTAlphaAnimation
  • IFTTTFrameAnimation
  • IFTTTRotationAnimation
  • IFTTTBackgroundColorAnimation
  • IFTTTCornerRadiusAnimation
  • IFTTTHideAnimation
  • IFTTTScaleAnimation
  • IFTTTTranslationAnimation
  • IFTTTTransform3DAnimation
  • IFTTTTextColorAnimation
  • IFTTTFillColorAnimation
  • IFTTTStrokeStartAnimation
  • IFTTTStrokeEndAnimation
  • IFTTTPathPositionAnimation
  • IFTTTConstraintConstantAnimation
  • IFTTTConstraintMultiplierAnimation
  • IFTTTScrollViewPageConstraintAnimation

支持的动画类型好全! 这些动画的功能都 顾名思义.

只要我们了解其中一个,其它动画的实现方式也会触类旁通.

开始

alphaAnim.gif

我们先来分析这个 Alpha 动画的实现.

第一篇文章的代码略修改

#define PageCount 3
#define ViewSize 30

@interface ViewController ()<UIScrollViewDelegate>
@property (strong, nonatomic) UIScrollView *scrollView;
@property (strong,nonatomic) UIView *v;
@property (strong,nonatomic) IFTTTAnimator *animator;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.animator=[[IFTTTAnimator alloc]init];
    //
    self.scrollView=[[UIScrollView alloc]initWithFrame:self.view.bounds];
    self.scrollView.pagingEnabled=YES;
    self.scrollView.backgroundColor=[UIColor whiteColor];
    self.scrollView.contentSize=CGSizeMake(self.view.boundWidth*PageCount,0);
    self.scrollView.delegate=self;
    [self.view addSubview:self.scrollView];
    //
    self.v=[[UIView alloc]initWithFrame:CGRectMake(100,200, ViewSize, ViewSize)];
    self.v.backgroundColor=[UIColor blueColor];

    [self.scrollView addSubview:self.v];
    
    [self setupAnimation];
}

-(void)setupAnimation{
    IFTTTAlphaAnimation *alpha=[IFTTTAlphaAnimation animationWithView:self.v];
    [self.animator addAnimation:alpha];
    
    [alpha addKeyframeForTime:0 alpha:1.0];
    [alpha addKeyframeForTime:200 alpha:0.0];
}


#pragma mark - 
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
    [self.animator animate:scrollView.contentOffset.x];
}
@end

我们让 scrollView 从 0 滚动到 200 的过程中, 蓝色方块的透明度逐渐变为 0

先来回顾一下上面代码做了什么

  • 首先创建一个 scrollView ,在创建一个蓝色方块 View 添加到 scrollView 中
  • setupAnimation 方法中创建 IFTTTAlphaAnimation 并添加到 IFTTTAnimator
  • 添加2个关键帧 0 - 1.0 和 200 - 0.0
  • 在 scrollView 代理中调用 IFTTTAnimatoranimate: 方法,让 scrollView 滚动的同时,产生改变蓝色方块 Alpha 的动画

IFTTTAlphaAnimation

我们来看一看 IFTTTAlphaAnimation 到底是什么

IFTTTAlphaAnimation : IFTTTViewAnimation <IFTTTAnimatable>

它继承自所有类型的动画的父类 IFTTTViewAnimation ,实现 IFTTTAnimatable protocal

IFTTTAnimatable 只有一个方法

@protocol IFTTTAnimatable <NSObject>
- (void)animate:(CGFloat)time;
@end

所有其他类型的动画都继承自这个类 IFTTTAnimation
它有如下方法:

@interface IFTTTAnimation : NSObject

- (void)addKeyframeForTime:(CGFloat)time value:(id<IFTTTInterpolatable>)value;
- (void)addKeyframeForTime:(CGFloat)time value:(id<IFTTTInterpolatable>)value withEasingFunction:(IFTTTEasingFunction)easingFunction;
- (id<IFTTTInterpolatable>)valueAtTime:(CGFloat)time;
- (BOOL)hasKeyframes;

@end

类扩展中有如下属性:

@interface IFTTTAnimation ()
@property (nonatomic, strong) IFTTTFilmstrip *filmstrip;
@end
  1. 所有的动画在我们调用 [addKeyframeForTime:value:] 来添加动画的关键帧时,会调用父类的 [self.filmstrip setValue: atTime:]; 方法存储我们的关键帧
  2. 每一个动画的关键帧都是一个 IFTTTKeyframe 对象,它存储我们设置好的 时间点,和当前时间点对应的值
  3. IFTTTFilmstrip 类中有一个数组,存储每一个添加的 IFTTTKeyframe 对象, 这个类负责计算在任意时间点时,我们的帧动画对应的值.

我们上面的蓝色方块透明度变化的例子中, 我们创建的是 IFTTTAlphaAnimation ,给他添加了2个关键帧,也就相当于创建了2个 IFTTTKeyframe 对象,他们存储的 时间点 - 值 分别为 0 - 1.0 和 200 - 0.0, 将其添加到 IFTTTFilmstrip 类的数组中保存起来.

IFTTTAnimator

我们所创建的所有动画,比如上面的 IFTTTAlphaAnimation 都要添加到 IFTTTAnimator中,
然后在 scrollView 代理方法调用 [self.animator animate:scrollView.contentOffset.x]; 来让scrollView 滚动的同时,产生动画效果,由此可见 animate:方法是触发动画的地方.

我们先来看看 IFTTTAnimator 这个类

@interface IFTTTAnimator : NSObject
- (void)addAnimation:(id<IFTTTAnimatable>)animation;
- (void)removeAnimation:(id<IFTTTAnimatable>)animation;
- (void)removeAllAnimations;
- (void)animate:(CGFloat)time;
@end

它的类扩展中也有一个数组 @property (nonatomic, strong) NSMutableArray *animations; 保存我们添加的 Animation

然后在 scrollView 滚动时,触发 - (void)animate:(CGFloat)time;
它内部的实现非常简单:

- (void)animate:(CGFloat)time
{
    for (id<IFTTTAnimatable> animation in self.animations) {
        [animation animate:time];
    }
}

只是调用数组中的每一个Animation 的 - (void)animate:(CGFloat)time; 方法.

动画效果的产生

我们来看看每一个动画是如何和应用到 View 上的

我们来看看 IFTTTAlphaAnimation 中的 - (void)animate:(CGFloat)time; 方法

- (void)animate:(CGFloat)time
{    
     //没有设置动画帧,就返回
    if (!self.hasKeyframes) return; 
    //如果设置了动画帧,计算当前时间点时的透明度,设置给 View 
    self.view.alpha = (CGFloat)[(NSNumber *)[self valueAtTime:time] floatValue]; 
}

再来看看 它是怎么计算当前时间点时的透明度的...

[self valueAtTime:time] 又会直接调用 [self.filmstrip valueAtTime:time]; 这个方法,可见 fileStrip 是进行计算任务的.

IFTTTFilmstrip 的计算过程如下,

- (id<IFTTTInterpolatable>)valueAtTime:(CGFloat)time
{
    NSUInteger indexAfter = [self indexOfKeyframeAfterTime:time]; //判断当前时间点对应哪一帧动画
    
    if (indexAfter == 0) {
         //如果时间点没到达第一帧动画发生的时间,就让值为第一帧的值.
        value = ((IFTTTKeyframe *)self.keyframes[0]).value;
        
    } else if (indexAfter < self.keyframes.count) {
         //如果时间点在2帧之间,取出这2帧,
        IFTTTKeyframe *keyframeBefore = (IFTTTKeyframe *)self.keyframes[indexAfter - 1];
        IFTTTKeyframe *keyframeAfter = (IFTTTKeyframe *)self.keyframes[indexAfter];
        
        //根据时间计算现在的动画进行到 这2帧之间的百分比 
        CGFloat progress = [self progressFromTime:keyframeBefore.time toTime:keyframeAfter.time atTime:time withEasingFunction:keyframeBefore.easingFunction];
        
        //根据百分比算出当前时间点对应的值
        value = [keyframeBefore.value interpolateTo:keyframeAfter.value withProgress:progress];
        
        // 上面的 EasingFunction 和 interpolate 后面会说到
    } else {
         //如果时间超过最后一帧动画发生的时间,就让值为最后一帧的值.
        value = ((IFTTTKeyframe *)self.keyframes.lastObject).value;
    }
    return value;
}

比如我们上面例子中的2个时间点

  • 0 - 1.0
  • 200 - 0.0

1. 当scrollView 滚动0 px 时,没到达发生动画的时间,view 保持第一帧的值, Alpha = 1.0

2. 当 scrollView 滚动 > 200 px 时,时间超过最后一帧动画发生的时间,不管怎么滚动,其 Alpha 保持 0.0

3. 当 scrollView 滚动 在 0 ~ 200 之间时,比如 滚动了 40 px,

  • 首先取出离当前时间点之前和之后的2帧
  • 然后 [progressFromTime: toTime: atTime:withEasingFunction:] 方法计算当前进行到这2帧之间的百分比

这个方法的实现是:

- (CGFloat)progressFromTime:(CGFloat)fromTime toTime:(CGFloat)toTime atTime:(CGFloat)atTime withEasingFunction:(IFTTTEasingFunction)easingFunction
{
    CGFloat duration = toTime - fromTime;  // duration = 200 - 0 = 200; 
    if (duration == 0.f) return 0.f;
    CGFloat timeElapsed = atTime - fromTime;  // timeElapsed = 40 - 0 = 40;  
    
    // 当前时间 / 这2帧的时间差 得出 "进行到这2帧之间的百分比"
    return easingFunction(timeElapsed / duration);  // return (40/200) = 0.2 
}

也就是 scrollView 滚动 40px 时, "进行到这2帧之间的百分比" 是 0.2

  • 最后根据 "进行到这2帧之间的百分比" 计算 当前时间点对应的值
    因为不同的值计算方法不一样,比如说颜色的渐变 要计算 UIColor 的值, Frame 动画要计算 CGRect 的值.
    我们这里先看最简单的 CGFloat 值的计算 :
+ (CGFloat)interpolateCGFloatFrom:(CGFloat)fromValue to:(CGFloat)toValue withProgress:(CGFloat)progress
{
    CGFloat totalChange = toValue - fromValue; // totalChange = 0 - 1 = -1;
    CGFloat currentChange = totalChange * progress; // currentChange= -1 * 0.2 = -0.2;
    return fromValue + currentChange;      // return 1.0 + (-0.2) = 0.8;
}

也就是 当前时间点 40px 时,对应的View 的 Alpha 值为0.8,将其赋值给 View self.view.alpha = 0.8
这样每次 scrollView 滚动距离变化一点, IFTTTFilmstrip 都会计算当前滚动距离下 view的 Alpha.

这样就产生了根据 scrollView 滚动距离,改变 View Alpha 的帧动画.

IFTTTEasingFunction

我们非常熟悉 [UIView animateWithDuration: delay: options: animations: completion:] 方法,
其中它的 option 可以设置为

    typedef NS_ENUM(NSInteger, UIViewAnimationCurve) {
        UIViewAnimationCurveEaseInOut,  //先慢慢加速,然后匀速,最后慢慢减速,符合现实运动规律
        UIViewAnimationCurveEaseIn,     // 先慢后快,慢慢加速
        UIViewAnimationCurveEaseOut,    // 先快后满,慢慢减速
        UIViewAnimationCurveLinear      // 匀速
    };

同样我们的 CAMediaTimingFunction 也有

    NSString * const kCAMediaTimingFunctionLinear;
    NSString * const kCAMediaTimingFunctionEaseIn;
    NSString * const kCAMediaTimingFunctionEaseOut;
    NSString * const kCAMediaTimingFunctionEaseInEaseOut;
    NSString * const kCAMediaTimingFunctionDefault; //效果最接近 kCAMediaTimingFunctionEaseInEaseOut

在 JazzHands 中有这些

    typedef CGFloat (^IFTTTEasingFunction) (CGFloat t);
    
    IFTTTEasingFunction const IFTTTEasingFunctionLinear;
    
    IFTTTEasingFunction const IFTTTEasingFunctionEaseInQuad;
    IFTTTEasingFunction const IFTTTEasingFunctionEaseOutQuad;
    IFTTTEasingFunction const IFTTTEasingFunctionEaseInOutQuad;
    
    IFTTTEasingFunction const IFTTTEasingFunctionEaseInCubic;
    IFTTTEasingFunction const IFTTTEasingFunctionEaseOutCubic;
    IFTTTEasingFunction const IFTTTEasingFunctionEaseInOutCubic;
    
    IFTTTEasingFunction const IFTTTEasingFunctionEaseInBounce;  //Bounce 小球自由落体到地面并反复弹起的效果
    IFTTTEasingFunction const IFTTTEasingFunctionEaseOutBounce;

在上面我们计算 进行到这2帧之间的百分比 时,这个方法 :
- (CGFloat)progressFromTime: toTime: atTime: withEasingFunction:
会调用 easingFunction(timeElapsed / duration);
来使用上面所说的 easingFunction 让我们的动画的变化更加平缓舒适

这些 easingFunction 产生的效果如下:

easingFunction.png
  • 如果你真的想研究这些数学公式或者实现具体实现,可以参考 iOS-Core-Animation-Advanced-Techniques这本书.
  • Github 上还有这本书的译文
  • 也可以在 这个网站 看更多的函数图像,
  • 如果你需要自定义 easingFunction :( 可以参考 JazzHands 的源码 和 Robert Penner's Easing Functions

IFTTTInterpolatable

IFTTTInterpolatable 是一个 protocal :

@protocol IFTTTInterpolatable <NSObject>
- (id)interpolateTo:(id)toValue withProgress:(CGFloat)progress;
@end

它是用来计算 当前时间点对应的值 的.

在上面的例子中,我们的 scrollView 移动40px,progress 为 0.2, fromValue = 1.0, toValue = 0.0

+ (CGFloat)interpolateCGFloatFrom:(CGFloat)fromValue to:(CGFloat)toValue withProgress:(CGFloat)progress
{
    CGFloat totalChange = toValue - fromValue; // totalChange = 0 - 1 = -1;
    CGFloat currentChange = totalChange * progress; // currentChange= -1 * 0.2 = -0.2;
    return fromValue + currentChange;      // return 1.0 + (-0.2) = 0.8;
}

计算的出我们的 view.alpha = 0.8

其他的动画 比如 Translation ,Scale ,ConstraintConstant ,CornerRadius 的值都是 CGFloat
只不过经过上面的计算之后赋值给不同的属性而已,

在他们各自的 - (void)animate:(CGFloat)time 方法中

self.constraint.constant = [self valueAtTime:time]; //IFTTTConstraintConstantAnimation

self.view.layer.cornerRadius = [self valueAtTime:time]; //IFTTTCornerRadiusAnimation

Translation 和 Scale Rotation 动画都是修改 view.transform 属性

    CGFloat scale = (CGFloat)[(NSNumber *)[self valueAtTime:time] floatValue];
    
    CGAffineTransform scaleTransform = CGAffineTransformMakeScale(scale, scale);
    self.view.iftttScaleTransform = [NSValue valueWithCGAffineTransform:scaleTransform];
    CGAffineTransform newTransform = scaleTransform;
    if (self.view.iftttRotationTransform) {
        newTransform = CGAffineTransformConcat(newTransform, [self.view.iftttRotationTransform CGAffineTransformValue]);
    }
    if (self.view.iftttTranslationTransform) {
        newTransform = CGAffineTransformConcat(newTransform, [self.view.iftttTranslationTransform CGAffineTransformValue]);
    }
    
    self.view.transform = newTransform;

先计算对应时间点的值,然后将所有 transformCGAffineTransformConcat 拼接起来 ,赋值给 View

其他 IFTTTInterpolatable

如果我们要做改变 View 背景色或者 Frame 的动画呢,计算方式就不同了.

计算任意时间点对应的 Color

- (UIColor *)interpolateTo:(UIColor *)toValue withProgress:(CGFloat)progress
{
    CGFloat startRed, startBlue, startGreen, startAlpha;
    CGFloat endRed, endBlue, endGreen, endAlpha;
    UIColor *interpolatedColor = self;
    
    if ([self.class iftttGetRed:&startRed green:&startGreen blue:&startBlue alpha:&startAlpha fromColor:self] &&
        [self.class iftttGetRed:&endRed green:&endGreen blue:&endBlue alpha:&endAlpha fromColor:toValue]) {
        CGFloat red = [NSNumber interpolateCGFloatFrom:startRed to:endRed withProgress:progress];
        CGFloat green = [NSNumber interpolateCGFloatFrom:startGreen to:endGreen withProgress:progress];
        CGFloat blue = [NSNumber interpolateCGFloatFrom:startBlue to:endBlue withProgress:progress];
        CGFloat alpha = [NSNumber interpolateCGFloatFrom:startAlpha to:endAlpha withProgress:progress];
        interpolatedColor = [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
    }
    return interpolatedColor;
}

我们先把 from 和 to Color 的 R,G,B,A 分别取出来,然后作为 CGFloat 值分别计算,最后拼接成 UIColor 即可.

Frame 的计算:

+ (CGRect)interpolateCGRectFrom:(CGRect)fromValue to:(CGRect)toValue withProgress:(CGFloat)progress
{
    CGPoint originBetween = [self interpolateCGPointFrom:fromValue.origin to:toValue.origin withProgress:progress];
    CGSize sizeBetween = [self interpolateCGSizeFrom:fromValue.size to:toValue.size withProgress:progress];
    return CGRectMake(originBetween.x, originBetween.y, sizeBetween.width, sizeBetween.height);
}

也很好理解,拆出其 origin 和 size 分别计算

  • origin 含有 x, y ,也就是2个 CGFloat 计算
  • size 含有 height 和 width 也是 2个 CGFloat
  • 最后将所有计算完的 CGFloat 值拼成 CGRect 作为 当前时间的 Frame 值返回

完成

读到这里,你不仅对 JazzHands 的实现有了深入的了解,也对动画思想有了了解.
在 JavaScript,Android 等语言或平台上,他们的语法不尽相同,但是实现动画效果的思路都是类似的.
比如 EasingFunction ,他在各个语言中的实现都是类似的.

JazzHands 是 一个完善易用,功能强大的帧动画库, Thanks to IFTTT 将其开源

Ref

android动画(一)Interpolator
iOS-Core-Animation-Advanced-Techniques

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,066评论 4 62
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,846评论 25 707
  • 纪念逝去的青春8090 1.可爱女人 MV 来自网易视频 2.完美主义MV来自酷我音乐 3.星晴MV 来...
    SunStill阅读 554评论 0 0
  • 当清晨一束白光射进寝室的时候,我以在床上静静的无眠了一晚。深夜中的黑暗赋予我一颗沉寂的心,我在沉思,我沉思着这...
    凹凸蛮阅读 185评论 0 1
  • 生活从来就不是容易的,成功也不是一蹴而就的,世界从来就不缺少一个你。但你可以选择过什么样的生活,并愿意为之努...
    菜叶子喲哟阅读 276评论 0 1