在 前几篇 的文章中,我们已经了解了 JazzHands
这个库的使用方法,这篇文章我们来深入了解 JazzHands 的实现, 看他到底用了什么魔法来制作出这个帧动画库.
JazzHands 支持如下动画类型
- IFTTTAlphaAnimation
- IFTTTFrameAnimation
- IFTTTRotationAnimation
- IFTTTBackgroundColorAnimation
- IFTTTCornerRadiusAnimation
- IFTTTHideAnimation
- IFTTTScaleAnimation
- IFTTTTranslationAnimation
- IFTTTTransform3DAnimation
- IFTTTTextColorAnimation
- IFTTTFillColorAnimation
- IFTTTStrokeStartAnimation
- IFTTTStrokeEndAnimation
- IFTTTPathPositionAnimation
- IFTTTConstraintConstantAnimation
- IFTTTConstraintMultiplierAnimation
- IFTTTScrollViewPageConstraintAnimation
支持的动画类型好全! 这些动画的功能都 顾名思义.
只要我们了解其中一个,其它动画的实现方式也会触类旁通.
开始
我们先来分析这个 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 代理中调用
IFTTTAnimator
的animate:
方法,让 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
- 所有的动画在我们调用 [addKeyframeForTime:value:] 来添加动画的关键帧时,会调用父类的
[self.filmstrip setValue: atTime:];
方法存储我们的关键帧 - 每一个动画的关键帧都是一个
IFTTTKeyframe
对象,它存储我们设置好的 时间点,和当前时间点对应的值 -
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
产生的效果如下:
- 如果你真的想研究这些数学公式或者实现具体实现,可以参考
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;
先计算对应时间点的值,然后将所有 transform
用 CGAffineTransformConcat
拼接起来 ,赋值给 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