FlightAnimator 0.9.8

FlightAnimator 0.9.8

测试已测试
Lang语言 SwiftSwift
许可证 MIT
发布最后发布2017年5月
SPM支持 SPM

Anton Doudarev 维护。



  • Anton Doudarev

FlightAnimator

alt tag

迁移至 Swift 3.1 支持

  • 对于 Swift 3.1 - 使用 标签 版本 0.9.7
  • 以下 安装说明 以进行说明

介绍

FlightAnimator 提供了一种非常简单的基于块的动画定义语言,允许您动态地创建、配置、分组、序列化、缓存和重用属性动画。

与使用单个缓动曲线同时动画化多个属性的 CAAnimationGroupsUIViewAnimations 不同,FlightAnimator 允许为每个单独的属性动画进行配置和同步独特的缓动曲线。

特性

  • [x] 46+ 参数曲线、衰减和弹簧
  • [x] 构建复杂动画的块语法
  • [x] 链接和序列动画
  • [x] 对每个属性动画应用独特的缓动
  • [x] 高级多曲线分组同步
  • [x] 定义、缓存和重用动画

在以下视频中的 FlightAnimator 项目演示
体验 FlightAnimator 的所有不同功能。

FlightAnimator Demo

安装

交流

  • 如果您 发现了一个错误有功能请求,请创建一个 issue。
  • 如果您 需要帮助有一般问题,请使用 Stack Overflow。(标签 'flight-animator')
  • 如果您 想要贡献,请查看 贡献指南 并提交一个 pull request。

基本使用

FlightAnimator 提供了一种非常灵活的语法,用于简化动画定义,可以从简单到复杂。遵循基于块的构建方法,您可以轻松定义一个动画组,以及它的属性动画。

在底层,构建的动画是带有多个自定义的 CAKeyframeAnimationCAAnimationGroup。一旦动画时间到来,FlightAnimator 将动态同步所有动画相对于当前 presentationLayer 的值的剩余进度,然后继续动画到最后的状态。

简单动画

要真正了解 FlightAnimator 的强大功能,我们首先使用 CoreAnimation 定义一个动画,然后使用框架基于块的语法重新定义它。下面的动画使用了 CAAnimationGroup 将3个单独的 CABasicAnimations(alpha、边界和位置)组合在一起。

let alphaAnimation             = CABasicAnimation(keyPath: "position")
alphaAnimation.toValue          = 0.0
alphaAnimation.fromValue        = 1.0
alphaAnimation.fillMode                 = kCAFillModeForwards
alphaAnimation.timingFunction           = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)

let boundsAnimation           = CABasicAnimation(keyPath: "bounds")
boundsAnimation.toValue         = NSValue(CGRect : toBounds)
boundsAnimation.fromValue       = NSValue(CGRect : view.layer.bounds)
boundsAnimation.fillMode                = kCAFillModeForwards
boundsAnimation.timingFunction          = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)

let positionAnimation             = CABasicAnimation(keyPath: "position")
positionAnimation.toValue       = NSValue(CGPoint : toPosition)
positionAnimation.fromValue         = NSValue(CGPoint : view.layer.position)
positionAnimation.fillMode              = kCAFillModeForwards
positionAnimation.timingFunction        = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)
    
let progressAnimation             = CABasicAnimation(keyPath: "animatableProgress")
progressAnimation.toValue       = 1.0
progressAnimation.fromValue         = 0
progressAnimation.fillMode              = kCAFillModeForwards
progressAnimation.timingFunction        = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut)

let animationGroup            = CAAnimationGroup()
animationGroup.duration         = 0.5
animationGroup.removedOnCompletion      = true
animationGroup.animations       = [alphaAnimation,  boundsAnimation, positionAnimation, progressAnimation]

view.layer.addAnimation(animationGroup, forKey: "PositionAnimationKey")
view.frame = toFrame

现在我们已经看到了上面的示例,让我们重新定义 FlightAnimator 的基于块的语法

view.animate {  [unowned self] (animator) in
    animator.alpha(toAlpha).duration(0.5).easing(.OutCubic)
    animator.bounds(toBounds).duration(0.5).easing(.OutCubic)
        animator.position(toPosition).duration(0.5).easing(.OutCubic)
        animator.value(toProgress, forKeyPath : "animatableProgress").duration(0.5).easing(.OutCubic)
}

view 上调用 animate(:) 开始于 FAAnimationGroup 的创建过程。在创建的闭包内部,animator 构造、配置,并将自定义动画追加到新创建的父组中。通过调用一个 预定义属性设置器,定义每个单独的属性动画,或者为任何其他可动画属性调用 func value(:, forKeyPath:) -> PropertyAnimator 方法。

一旦启动属性动画,递归配置 PropertyAnimator 以添加持续时间、缓动和/或主要设计,从而创建最终的 FABasicAnimation,并将其添加到父组中。

func duration(duration : CGFloat) -> PropertyAnimator
func easing(easing : FAEasing) -> PropertyAnimator
func primary(primary : Bool) -> PropertyAnimator

一旦函数调用退出闭包,FlightAnimator 将执行以下操作

  1. 将新创建的 FAAnimationGroup 添加到调用 view 的层中,
  2. 根据调用 view 的呈现层值同步组内 FABasicAnimations
  3. 应用分组动画的 toValue 到调用 view 的层中来触发动画。

动画链式调用

在 FlightAnimator 中将动画链式调用起来非常简单。

起始触发

在 secondaryView 上创建的动画将在 primaryView 的动画开始时触发。

primaryView.animate { [unowned self] (animator) in
    ....
        
    animator.triggerOnStart(onView: self.secondaryView, animator: { (animator) in
         ....
    })
}

完成触发

在 secondaryView 上创建的动画将在 primaryView 的动画完成时触发。

primaryView.animate { [unowned self] (animator) in
    ....
        
    animator.triggerOnCompletion(onView: self.secondaryView, animator: { (animator) in
         ....
    })
}

时间进度触发

当主视图上的驱动动画达到相对的中途时,secondaryView 上创建的动画将被触发。

primaryView.animate { [unowned self] (animator) in
    ....
                
    animator.triggerOnProgress(0.5, onView: self.secondaryView, animator: { (animator) in
         ....
    })
}

值进度触发

当驱动动画达到主视图动画 fromValue 和 toValue 间的相对一半距离时,secondaryView 上创建的动画将被触发。

primaryView.animate { [unowned self] (animator) in
    ....
        
    animator.triggerOnValueProgress(0.5, onView: self.secondaryView, animator: { (animator) in
         ....
    })
}

动画触发嵌套

有内置的支持在触发器内嵌套触发器以安排动画,并相对于父动画的范围附加多类型触发器。

primaryView.animate { [unowned self] (animator) in
    ....
        
    animator.triggerOnStart(onView: self.secondaryView, animator: { (animator) in
         -> Relative to primaryView animation
             
    animator.triggerOnCompletion(onView: self.tertiaryView, animator: { (animator) in
        -> Relative to secondaryView animation
            
            animator.triggerOnProgress(0.5, onView: self.quaternaryView, animator: { (animator) in
            -> Relative to tertiaryView animation
            })
        })
            
        animator.triggerOnValueProgress(0.5, onView: self.quinaryView, animator: { (animator) in
                -> Relative to secondaryView animation
        })
    })
        
    animator.triggerOnStart(onView: self.senaryView, animator: { (animator) in
            -> Relative to primaryView animation 
    })
}

CAAnimationDelegate 回调

有时需要通过响应 CAAnimationDelegate 方法在动画开始或结束时执行一些逻辑。

view.animate { (animator) in
    ....
    
    animator.setDidStartCallback({ (animator) in
         // Animation Did Start
    })
    
    animator.setDidStopCallback({ (animator, complete) in
         // Animation Did Stop   
    })
}

这些可以像动画触发器一样嵌套,并且可以通过动画器将动画器应用于动画创建闭包中定义的动画范围内的组。

缓存和重用动画

FlightAnimator 允许在最初注册动画(即状态),使用唯一的动画密钥。一旦定义,可以随时在应用的流程中使用注册时使用的动画密钥手动触发。

当应用动画时,如果视图处于飞行中,它将自动与当前呈现层的值同步,并动画到最后目的地。

注册/缓存动画

要注册一个动画,调用一个全局定义的方法,并在制作块中创建一个动画,就像以前示例中所定义的那样。以下示例显示了如何注册和缓存指定视图上的动画键。

struct AnimationKeys {
    static let CenterStateFrameAnimation  = "CenterStateFrameAnimation"
}
...

view.registerAnimation(forKey : AnimationKeys.CenterStateFrameAnimation) { (animator) in
      animator.bounds(newBounds).duration(0.5).easing(.OutCubic)
      animator.position(newPositon).duration(0.5).easing(.OutCubic)
})

此动画仅进行缓存,直到手动触发才会执行。

应用已注册动画

要触发动画,请调用以下代码

view.applyAnimation(forKey: AnimationKeys.CenterStateFrameAnimation)

要应用最终值而不使视图动起来,重写默认的动画标志为假,并将所有最终值应用到相关视图的模型层。

view.applyAnimation(forKey: AnimationKeys.CenterStateFrameAnimation, animated : false)

高级使用

时间调整

由于框架的动态特性,可能需要进行一些调整以获得理想的动画。

FlightAnimator有几个选项,可以更精确地控制时间同步。

  • 时间优先级 - 调整在整体动画同步过程中时间的选择方式
  • 主要驱动器 - 定义在整体动画同步过程中影响时间同步的动画

时间优先级

首先是一个简单的背景,框架基本上通过优先考虑最大剩余时间来同步时间,如果在飞行过程中被重新定向。

让我们看以下示例,在组动画上设置时间优先级为 .MaxTime,这是FlightAnimator的默认值。

func animateView(toFrame : CGRect) {
    
    let newBounds = CGRectMake(0,0, toFrame.width, toFrame.height)
    let newPosition = CGPointMake(toFrame.midX, toFrame.midY)
    
    view.animate(.MaxTime) { (animator) in
            animator.bounds(newBounds).duration(0.5).easing(.OutCubic)
            animator.position(newPositon).duration(0.5).easing(.InSine)
    }
}

就像演示应用程序一样,此方法由不同的按钮调用,并采用触发该方法的按钮的帧值。让我们假设动画已经被触发,并且正在飞行中。当在飞行中另一个按钮被点击时,应用一个新的动画,位置发生变化,但边界保持不变。

框架内部将根据上次动画的当前进度确定进度,并从分组属性动画的持续时间数组中选择最长持续时间值。

假设边界没有变化,因此假设动画的持续时间为0.0。新的动画将根据进度与位置动画的持续时间同步,并自动根据 .MaxTime 时间优先级成为最长持续时间。

时间优先级还可以用于 triggerAtTimeProgress()triggerAtValueProgress()。现在让我们继续下一个主题,那就是主标志。

在组内具有越多的属性动画,调整时间应用方式的必要性越大。为此,可以选择4种时间优先级:

  • .MaxTime
  • .MinTime
  • .Median
  • .Average

主标志

就像之前的示例中提到的,动画可能非常复杂,组内的属性动画越多,动画在时间上的颠簸可能性越大,尤其是在同步不同曲线和持续时间的4个以上的动画时。

为此,可以在单个属性动画上设置主标志,并将它们指定为主持续时间驱动器。默认情况下,如果没有属性动画设置为主,在同步期间,FlightAnimator将使用时间优先级设置从所有动画中找到进度同步后的相应值。

如果只需要定义进度的某些特定属性动画,并将其作为主要驱动因素,请将主要标志设置为 true,这将排除所有其他未标记为主要的动画,不考虑它们。

以下是一个示例,演示了一个简单的视图如何使用边界和位置从当前位置动画过渡到一个新帧。

view.animate(.MaxTime) { (animator) in
      animator.bounds(newBounds).duration(0.5).easing(.OutCubic).primary(true)
      animator.position(newPositon).duration(0.5).easing(.InSine).primary(true)
      animator.alpha(0.0).duration(0.5).easing(.OutCubic)
      animator.transform(newTransform).duration(0.5).easing(.InSine)
}

就是这样简单,现在当视图在动画过程中被重新定位时,只有边界和位置动画将被考虑作为定时同步的一部分。

.SpringDecay 带初速度

当使用 UIPanGestureRecognizer 通过调整视图位置在其屏幕上移动视图时,例如,需要平滑地将视图动画化到最终位置,正好在用户释放手势时。这时 .SpringDecay 缓动就派上用场了。.SpringDecay 缓动会让视图轻松减速并进入位置,只需配置初始速度,它就会根据速度自行计算前往目的地的相对时间。

以下是处理交云并使用 .SpringDecay(velocity: velocity) 缓动执行动画的示例。

func respondToPanRecognizer(recognizer : UIPanGestureRecognizer) {
    switch recognizer.state {
    ........
    
    case .Ended:
        let currentVelocity = recognizer.velocityInView(view)
        
        view.animate { (animator) in
            animator.bounds(finalBounds).duration(0.5).easing(.OutCubic)
            animator.position(finalPositon).duration(0.5).easing(.SpringDecay(velocity: velocity))
        }
    default:
        break
    }
}

参考

支持的参数曲线

CALayer 支持的可动性属性

当前版本说明

贡献指南

框架演示应用

项目包含一个高度可配置的演示应用,允许进行实验以探索 FlightAnimator 支持的无尽配置产生的效果。

包含的演示功能

  • 将视图动画到屏幕上的不同位置
  • 拖动释放视图以将衰减缓解应用于最终目的地
  • 调整边界、位置、alpha 和转换的定时曲线。
  • 启用二级视图,该视图跟随主视图到达其最后位置
  • 调整组定时优先级以测试同步
  • 调整二级视图的时间触发/值触发的进度

许可证

FlightAnimator 采用了 MIT 许可证发布。有关详情,请参阅 许可证