JazzHands使用

作者: HF_K | 来源:发表于2018-03-22 20:39 被阅读0次

    一、 简介

    JazzHands是UIKit一个简单的关键帧基础动画框架。可以通过手势、scrollView、KVO或者RectiveCocoa控制动画从而实现一些交互式动画。常见用途:制作一些炫酷的引导页;

    下载地址及安装(可以通过CocoaPods安装)

    OC版

    pod "JazzHands"
    

    Swift版

    pod "RazzleDazzle"
    

    二、 基础类

    IFTTTAnimatior:导演类。里面有一个管理所有动画的添加、删除、执行的数组(IFTTTAnimation)。

    IFTTTAnimatable:执行动画类,定义了核心协议(接口),animate:(CGFloat)time; 计算每一个时间点当前对象的值.每一种动画都要实现这个接口.如IFTTTAlphaAnimation类,计算一个时间点对应的对象的alpha值。

    IFTTTAnimation:动画基类,定义了一个对象的动画.仅仅只是对类中的IFTTTFilemstrip进行一个简单的封装。

    IFTTTFilmstrip:胶片类,有一个关键帧(IFTTTKeyframe)数组,添加/修改/获取对应关键帧(IFTTTKeyframe)的值。

    IFTTTKeyframe:关键帧类,描述每一个关键帧的时间对应的值。

    三、 动画类型

    IFTTTAlphaAnimationalpha属性(实现淡入淡出效果)

    IFTTTRotationAnimation:旋转,通过改变view的transform从而实现旋转动画

    IFTTTBackgroundColorAnimation:背景色,backgroundColor属性.

    IFTTTCornerRadiusAnimation:圆角,改变视图的layer.cornerRadius属性

    IFTTTHideAnimation:显示隐藏,改变view的显示及隐藏,view.hidden

    IFTTTScaleAnimation:缩放变换,修改view.transform属性

    IFTTTTranslationAnimation:平移变换,改变view.transform属性

    IFTTTTransform3DAnimationlayer.transform属性 (是3D变换)。

    IFTTTTextColorAnimation:UILabel的textColor属性,修改Label的textColor

    IFTTTFillColorAnimation:CAShapeLayer的fillColor属性

    IFTTTStrokeStartAnimation:CAShapeLayer的strokeStart属性

    IFTTTStrokeEndAnimation:CAShapeLayer的strokeEnd属性

    IFTTTPathPositionAnimation:UIView的layer.position属性
    IFTTTConstraintConstantAnimation:修改AutoLayout Constraint的constant属性

    IFTTTConstraintMultiplierAnimation:修改 AutoLayoutMultiplier属性

    IFTTTScrollViewPageConstraintAnimation:用 AutoLayout 修改 ScrollView 和 View 的位置关系

    IFTTTFrameAnimation:修改 View 的frame属性

    四、 应用一

    通过其中一个类(IFTTTAlphaAnimation)了解JazzHands,

    JazzHands_Demo.gif
    @interface AlphaViewController ()<UIScrollViewDelegate>
    @property (strong, nonatomic) UIScrollView *scrollView;
    @property (strong,nonatomic) UIView *alphaView;
    @property (strong,nonatomic) IFTTTAnimator *animator;
    @end
    
    
    @implementation AlphaViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        self.view.backgroundColor=[UIColor whiteColor];
        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.frame.size.width*2,0);
        self.scrollView.delegate=self;
        [self.view addSubview:self.scrollView];
    
        self.alphaView=[[UIView alloc]initWithFrame:CGRectMake(300,200, 40, 40)];
        self.alphaView.backgroundColor=[UIColor blackColor];
        [self.scrollView addSubview:self.alphaView];
        [self setupAlphaAnimation];
    }
    
    -(void)setupAlphaAnimation{
    
        IFTTTAlphaAnimation *alpha=[IFTTTAlphaAnimation animationWithView:self.alphaView];
        [self.animator addAnimation:alpha];
        [alpha addKeyframeForTime:0 alpha:1.0];
        [alpha addKeyframeForTime:200 alpha:0.0];
    }
    
    -(void)scrollViewDidScroll:(UIScrollView *)scrollView{
        [self.animator animate:scrollView.contentOffset.x];
    }
    
    - (void)didReceiveMemoryWarning {
        [super didReceiveMemoryWarning];
        // Dispose of any resources that can be recreated.
    }
    
    @end
    

    1.实现效果:中间黑色方块随着scrollView滚动透明度逐渐变为0;

    1. 创建一个scrollView并且在上面添加一个黑色方块.
    2. 创建IFTTTAlphaAnimation并且添加到IFTTTAnimator中,同时添加两个关键帧0 - 1.0 和 200 - 0.0.
    3. 再在scrollView的代理方法中调用IFTTTAnimatoranimate:方法,从而实现相应透明度变化的动画.

    2.IFTTTAlphaAnimation

    IFTTTAlphaAnimation : IFTTTViewAnimation <IFTTTAnimatable>
    IFTTTAlphaAnimation继承于动画父类IFTTTViewAnimation,实现IFTTTAnimatableprotocal,而 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:]方法进行存储关键帧,每个动画的关键帧都是单独的一个IFTTTKeyframe对象,它包括设置的时间点以及相应时间点对应的值.
    2. IFTTTFilmstrip 类中有一个数组,存储每一个添加的 IFTTTKeyframe 对象, 这个类负责计算在任意时间点时,我们的帧动画对应的值.

    3.IFTTTAnimator

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

    类扩展中如下:

     @interface IFTTTAnimator ()
    
     @property (nonatomic, strong) NSMutableArray *animations;
    
     @end
    

    IFTTTAnimator通过animations保存我们添加的animation,然后在 scrollView 滚动时,触发 - (void)animate:(CGFloat)time;它内部的实现非常简单:

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

    动画效果的产生,每个动画是如何和应用到 View 上的

    IFTTTAlphaAnimation 中的- (void)animate:(CGFloat)time;方法

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

    然后调用

    - (id<IFTTTInterpolatable>)valueAtTime:(CGFloat)time
    {
        if (self.filmstrip.isEmpty) return nil;
        return [self.filmstrip valueAtTime:time];
    }
    

    可见 fileStrip 是进行计算任务的.

    IFTTTFilmstrip 的计算过程如下:

    - (id<IFTTTInterpolatable>)valueAtTime:(CGFloat)time
    {
        NSAssert(!self.isEmpty, @"At least one KeyFrame must be set before animation begins.");
        id value;
        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];
        } else {
             //如果时间超过最后一帧动画发生的时间,就让值为最后一帧的值.
            value = ((IFTTTKeyframe *)self.keyframes.lastObject).value;
        }
        return value;
    }
    

    上面Demo中:

    • 0-1.0 : scrollView滚动 0 px 时,没到达发生动画的时间,view保持第一帧的值,即 Alpha = 1.0

    • 200 - 0.0 : scrollView 滚动scrollview.contentOffset > 200 px 时,时间超过最后一帧动画发生的时间, Alpha 保持 0.0

    • 在 0 - 200 之间(例如 40) : 首先取出离当前时间点之前和之后的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
         }
    

    最后根据 "进行到这2帧之间的百分比" 计算:

         + (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 滚动距离,改变 View Alpha 的帧动画.

    4. IFTTTEasingFunction

    [UIView animateWithDuration: delay: options: animations: completion:] 方法中,option 可以设置为:

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

    同样 CAMediaTimingFunction(速度控制函数)也有(如果想研究这些数学公式或者实现具体实现):

    可以参照iOS-Core-Animation-Advanced-Techniques

        NSString * const kCAMediaTimingFunctionLinear;//(线性):匀速,给你一个相对静态的感觉
        NSString * const kCAMediaTimingFunctionEaseIn;//(渐进):动画缓慢进入,然后加速离开
        NSString * const kCAMediaTimingFunctionEaseOut;//(渐出):动画全速进入,然后减速的到达目的地
        NSString * const kCAMediaTimingFunctionEaseInEaseOut;//(渐进渐出):动画缓慢的进入,中间加速,然后减速的到达目的地。这个是默认的动画行为
        NSString * const kCAMediaTimingFunctionDefault;//效果最接近 kCAMediaTimingFunctionEaseInEaseOut
    

    在 JazzHands 中有这些:

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

    CAMediaTimingFunction 产生的效果如下:

    JazzHands_CAMediaTimingFunction

    5. 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

    6.其他 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主要应用于一些动画引导页,主要用scrollviews,JazzHands封装了IFTTTAnimatedPagingScrollViewController更加方便使用了,JazzHands框架是基于关键帧的动画,其概念类似于Core Animation,但是JazzHands动画驱动主要是坐标驱动。JazzHands中以scrollview.contentOffset作为动画的time。

    JazzHands_Time
    然后在[scrollview didScroll]代理中来进行计算相关视图的位置。
    在JazzHands中
    - (void)keepView:(UIView *)view onPages:(NSArray *)pages atTimes:(NSArray *)times withAttribute:(IFTTTHorizontalPositionAttribute)attribute;
    

    view表示需要加动画的视图

    pages、offsets和attribute控制动画的显示。

    其中pages和offsets的数量必须相同,否则会产生crash

    pages、offsets控制动画:

    view.frame.size.x = page * pageWidth
    scrollview.contentOffset = time * pageWidth
    

    例如:

    [self keepView:self.iftttPresents onPages:@[@(0), @(-1)] atTimes:@[@(0), @(1)]];
    

    当time=0时,即scrollview在初始位置,这时scrollview.contentOffset=(pageWidth * 1)。当time=1时,即scrollview移动到下个scrollview.contentOffset,这时scrollview.contentOffset=(pageWidth * 1);相当于scrollview滚动到第二页时,我们必须设置时间time所对应的view的位置,view对象进行时间0到1的动画。每一个动画(animation)都拥有一个胶卷(filmstrip),每一个胶卷都包含该了该动作的所有关键帧(keyframe)。随着屏幕拖拽,JazzHands会根据约束和时间(contentOffset)计算对象的位置.只要刷新的频率只够高。我们人眼就看不出是重新画上去,而是连续的动画了。

    相关文章

      网友评论

        本文标题:JazzHands使用

        本文链接:https://www.haomeiwen.com/subject/bhjzqftx.html