NullLib.TickAnimation 用于实现对一个对象的属性(Property), 以指定的计时函数, 在一定时间内从一个值, 平滑的过渡到另一个值. NullLib.TickAnimation.Drawing 基于 NullLib.TickAnimation, 并提供了关于绘画类型的过渡动画, 例如 点, 尺寸, 矩形区域, 颜色
例如你可以使用它将一个 WinForm 窗体的 Bounds 从一个值过渡到另一个值以实现窗体位置与尺寸的过渡动画. 下面是一个简单示例:
快速开始
首先, 创建一个 WinForm 项目用于测试, 向主窗体中添加一个按钮, 并为按钮的点击事件添加事件处理器
在解决方案管理器中右击项目, 选择 “管理 nuget 程序包”, 然后安装 NullLib.TickAnimation.Drawing (它会自动安装依赖项 NullLib.TickAnimation)
转到主窗体的代码, 在 using 部分添加
using NullLib.TickAnimation
, 这样我们就可以直接使用关于动画的一些类了编辑刚刚添加的按钮的点击事件处理器代码, 添加以下内容:
Rectangle workingArea = Screen.PrimaryScreen.WorkingArea; // 获取主屏幕的工作区矩形 DrawingTickAnimator animator = new DrawingTickAnimator(new SineTicker(), this, nameof(Bounds)); // 创建动画控制器实例 animator.SetPropertySetter((setAction) => Invoke(setAction)); // 解决窗体程序的跨线程资源访问问题 animator.SetTickDelay(1); // 在每一帧后进行 1ms 的延时 animator.Animate(workingArea, 200); // 进行动画, 时间是 200ms
运行程序, 点击按钮, 查看效果
本库已在 GitHub 开源, github.com/SlimeNull/NullLib.TickAnimation
基本原理
在 NullLib.TickAnimator 中, ITickAnimator 是最基本的, 用于运行动画的接口, 它依赖于 ITicker 来提供计时函数, ITIcker 可表示运动曲线, 不同的类有不同的特征, 例如使用 BackTicker 的 ITickAnimator 进行动画时将具有回弹效果, 使用 BounceTicker 的 ITickAnimator 进行动画时将具有弹跳效果.
其中的技术细节, 可参考 CSS3 过渡中的 timing-function, WPF 窗体程序中的 EasingFunction. 另外, 也推荐对贝塞尔曲线作基本了解, 这样, 你可以通过库中提供的 CubicBezierTicker 来创建自定义的三次贝塞尔曲线计时函数, 进而实现自定义动画效果.
推荐的在线三次贝塞尔曲线编辑工具: Cubic-Bezier
类型
- ITicker
ITicker 是动画计时函数, 它分为两种, 贝塞尔曲线函数以及原生函数, 其中贝塞尔曲线是可以由用户指定控制点从而控制曲线形状的, 原生函数是一些定义好的, 遵循 ITicker 接口的函数
贝塞尔曲线函数: 分两种, 三次贝塞尔曲线(CubicBezierTicker)与二次贝塞尔曲线(QuadraticBezierTicker), 你可以手动指定控制点, 也可以使用默认提供的一些曲线, 例如 Ease, EaseIn, EaseOut, EaseInOut, InSine, OutSine 等.
指定 EaseInOut 的方式是: 指定 CubicBezierCurve.Ease 与 EasingMode.EaseInOut, 同理, 指定 InOutBack 的方式是: CubicBezierCurve.Back 与 EasingMode.EaseInOut, 需要知道的是, 这些曲线都是源自于 CSS3 内置曲线(Ease, EaseIn, EaseOut, EaseInOut, Linear) 以及 Microsoft Edge 浏览器调试工具中的预设曲线(InSIne, OutSine, InOutSine, InBack, OutBack, …)
原生函数: 它们都是从 WPF 中移植过来的, 例如 BounceTicker, 它来自 WPF 中的 BounceEase, 并且与它算法完全一致, 关于它们的使用, 我建议查阅 WPF 动画缓动函数的相关文档.
最简单的, 使用 BackTicker 函数就是直接实例化一个对象, 在这里需要提到的是, 关于 BackTicker 实例的属性默认值也是与 WPF 一致的, 虽然 BackTicker 和 CubicBezierCurve.Back 都是表示带有回弹效果的曲线, 但是两者并不相等, 贝塞尔曲线函数和原生函数是截然不同的.
关于 ITicker 的更多信息, 我建议查阅源码, 或者在 Visual Studio 中使用对象浏览器概览所有类型.
- ITickAnimator
ITickAnimator 是驱动动画的最主要的类, 默认包含的 TickAnimator 中支持 double float int 三种数字类型的动画处理, 它其中还包括了对于动画同时进行时的中断处理, 当然, 如果你要使用针对于绘画的动画处理, 例如点, 尺寸, 矩形区域, 颜色这类, 可以使用 DrawingTickAnimator (nuget包: NullLib.TickAnimator.Drawing, 命名空间: NullLib.TickAnimator)
TickAnimator 的动画处理中包含这样一点: 当一个动画还未结束就启动另一个动画, 那么第一个动画会自动停止以保证不会造成冲突, 这是因为一个 TickAnimator 应该针对于唯一一个属性, 但针对同一个属性的两个动画同时启动, 将会造成严重的冲突问题. (这里需要提一下, 如果你要同时对一个控件进行位置过渡和尺寸过渡, 我不推荐你为 Location 和 Size 属性分别创建一个 ITickAnimator 实例, WinForm 的内部实现问题仍然会导致冲突, 更安全的做法是为 Bounds 属性创建 ITickAnimator 并且使用针对 Rectangle 的过渡)
- TickAnimationProc
TickAnimationProc 是 NullLib.TickAnimation 中对于动画驱动的最核心部分, 它提供了最为基本的动画实现方式, 通过指定 ITicker(计时函数), Fucn<double, T>(tick picker用于从动画进度中取得对应类型的值), TimeSpan(动画的时间间隔), Func<T, bool>(动画每一帧的回调函数, 返回值表示动画是否应该继续进行), 这四个参数来运行最基本的动画. 但是一般的不推荐使用这个, 因为其中没有像 TickAnimator 的互斥措施.
- TickAnimatorBase
TickAnimator 的基类, 其中包含了 TickAnimator 基础功能的实现, 例如对于值过渡的方法封装
自定义类型
如果你要自定义 ITicker 实现类或者 ITickAnimator 实现类, 则需要遵守下面的一些规范:
对于 ITicker 的实现, 必须保证对于方法 CalcTick(double x), x 从 0 到 1 时, 返回值也是从 0 到 1, 即: CalcTick(0) 应该返回 0, CalcTick(1) 应该返回 1.
并且, 如果你要创建更高阶的贝塞尔曲线函数, 建议继承 BezierTickerBase, 如果你要创建一些遵循 WPF 原生过渡函数规则的自定义函数, 必须继承 FuncTickerBase 并在字类中重写 CalcInTick(double x) 方法.
对于 ITickAnimator 的实现, 推荐的方式是直接继承 TickAnimator, 并且在实现 Animate 和 SyncAnimate 方法时, 应该通过调用 TickAnimatorBase 的 Animate 和 SyncAnimate 方法来实现动画, 而获取对应属性的当前值, 不应该使用 prop.GetValue 方法, 而是使用 TickAnimatorBase 的 GetPropertyValue 方法. 下面是 TickAnimator 中 double 的动画实现:
public Task Animate(float start, float end, int dur) { double diff = end - start; return Animate((t) => (float)(start + diff * t), TimeSpan.FromMilliseconds(dur)); // 内部调用基类 TickAnimatorBase 的方法 } public Task Animate(double end, int dur) => Animate(GetPropertyValue<double>(), end, dur); // 从当前值开始执行动画, 按照规 public ITickAnimator<double> SyncAnimate(double start, double end, int dur) // 范, 使用 GetPropertyValue 方法 { double diff = end - start; SyncAnimate((t) => (double)(start + diff * t), TimeSpan.FromMilliseconds(dur)); // SyncAnimate 也一样 return this; } public ITickAnimator<double> SyncAnimate(double end, int dur) => SyncAnimate(GetPropertyValue<double>(), end, dur); // 一致