目录
Ⅰ 前言
当作者从 WPF 转 Avalonia 时,瞬间爱上 Avalonia 过渡语法,但是 WPF 的一些特点又是我不舍得抛下的,那每次在 WPF / Avalonia 间转来转去的也是相当折磨人了,所以,我能不能自己构建一套抽象层,使其支持在任何 .NET 平台快速搭载过渡系统呢?这样,我们仅需要一套自定义的 API 规则即可在【动画】这块概念上无视框架差异了。答案当然是,可以!并且作者已经写出了一个雏形。
Ⅱ 项目简介
项目可以在 NuGet 或 github 找到,这里贴一个主页
Ⅲ 过渡系统概述
(1) Fluent API
以下 API 可同时在 WPF / Avalonia / MAUI 使用,它会自行检查调用时的线程并安全执行更新,支持生命周期事件与缓动函数,在特定平台,如 WPF / Avalonia ,还可选择 UI 更新操作的优先级。
(2) 插值器
对于不同的平台,一些属性的插值计算存在差异是无法避免的,例如 WPF 与 MAUI 的 Brush 在插值计算方面,能否使用 DrawingBrush 实现透明度混合是一个重要的区分点 ( 不过我不了解 MAUI,如果 MAUI 支持DrawingBrush 当我没说 ),这时候需要注册一些插值器来处理计算,我已经做了基础的实现,你可继续注册新的插值器来支持对应属性的动画
这里再给一个插值器的定义方式,你需要实现 IValueInterpolator 接口,Interpolate 方法描述了从 start 到 end 计算 steps 个插值的过程,需要手动实现
当然,还有一种更高级的定义方式,直接声明自定义类为可插值的,也就是为你的自定义类实现 IInterpolable 接口,接口同样有 Interpolate 方法需要实现,于是,你便不再需要特意为它注册一个插值器了
Ⅳ 可扩展性
得益于我在 VeloxDev.Core 中设计的全抽象层,当我想在一个新的框架中实现过渡系统时,会非常轻松,现在,我将为你展示一整完套的构建流程,你看完后,完全可以不使用 VeloxDev.WPF 等二次封装,而是仅使用 VeloxDev.Core 构建属于你自己的过渡系统。
( 不过这段流程也挺长的,如果懒得看,那直接用 VeloxDev.WPF / VeloxDev.Avalonia / VeloxDev.MAUI 就可以了 )
(1) 效果参数定义
TransitionEffectCore 是 VeloxDev.Core 核心组件之一,DispatcherPriority 则指明当前框架中用于描述 UI 调度操作优先级的方式,可以不指定 DispatcherPriority ,例如在 MAUI 中这基本是没必要的,除非你要自己实现优先级,这一点我后面也会说到.
(2) 线程检查器
为了确保更新发生在 UI 线程 ,我们必须定义一个检查器来实现,以下是 WPF 的实现,不过这个实现我还没测试过,理论上应该没问题,有问题的话可以评论区留言一下
(3) 动画帧输出
这个组件用于接收插值器的输出,并应用这些插值,这里你就可以充分理解为什么需要 线程检查器 和 DispatcherPriority 了,此处框架内部已经返回了 线程检查器 的运行结果 isUIAccess,于是,我们可以决定如何调度更新,DispatcherPriority则是先前指定的由于描述 优先级 的结构,此处可在 InvokeAsync 中派上用场.
(4) 状态记录器
继承一下即可,无需任何其它内容,它用于描述一个实例在某一时刻的状态,具体包含多个属性所对应的值
(5) 插值器
这一点上面已经介绍过了,继承一下,然后注册实现的插值器即可
(6) 动画帧控制器
这个组件用于决策如何根据 过渡效果参数 来控制动画帧的应用,不需要你实现具体逻辑,继承核心部件即可,其中,Core<> 包含的若干泛型就是我们刚才定义过的一些组件
(7) 全局调度器
好 !距离成功仅一步之遥 ,我们最终需要实现一个用于调度动画启动、终结的组件,而方法也很简单,我们进行一个简单的继承,然后在 Core<> 的泛型参数中,将我们刚才实现的部分模块填充进去,于是构建就完成了 !
(8) Fluent API 构建
当然,如果想要实现我在文章一开始展示的那种语法,需要最后一层封装,这一步我在 VeloxDev.Core 中没有做抽象层,目的就是为了允许用户完全自由地定义 API。但是完全自由亦有代价,这段代码会很长,不过也不用担心,因为我已经建立了自己的 API 规则,那我其实只需要写一次,然后直接粘贴在其它框架中就可以直接完工了。
public static class Transition
{
public static StateSnapshot<T> Create<T>()
where T : class
{
var value = new StateSnapshot<T>();
value.root = value;
return value;
}
public static StateSnapshot<T> Create<T>(T target)
where T : class
{
var value = new StateSnapshot<T>()
{
targetref = new(target)
};
value.root = value;
return value;
}
public class StateSnapshot<T>
where T : class
{
internal WeakReference<T>? targetref = null;
internal State state = new();
internal StateSnapshot<T>? root;
internal StateSnapshot<T>? next = null;
internal TransitionEffect effect = TransitionEffects.Empty;
internal TimeSpan delay = TimeSpan.Zero;
internal Interpolator interpolator = new();
internal CancellationTokenSource? cts = null;
public StateSnapshot<T> Property(Expression<Func<T, double>> propertyLambda, double newValue)
{
state.SetValue<T, double>(propertyLambda, newValue);
return this;
}
public StateSnapshot<T> Property(Expression<Func<T, Brush>> propertyLambda, Brush newValue)
{
state.SetValue<T, Brush>(propertyLambda, newValue);
return this;
}
public StateSnapshot<T> Property(Expression<Func<T, Transform>> propertyLambda, ICollection<Transform> newValue)
{
var transformGroup = new TransformGroup()
{
Children = [.. newValue]
};
state.SetValue<T, Transform>(propertyLambda, transformGroup);
return this;
}
public StateSnapshot<T> Property(Expression<Func<T, Point>> propertyLambda, Point newValue)
{
state.SetValue<T, Point>(propertyLambda, newValue);
return this;
}
public StateSnapshot<T> Property(Expression<Func<T, CornerRadius>> propertyLambda, CornerRadius newValue)
{
state.SetValue<T, CornerRadius>(propertyLambda, newValue);
return this;
}
public StateSnapshot<T> Property(Expression<Func<T, Thickness>> propertyLambda, Thickness newValue)
{
state.SetValue<T, Thickness>(propertyLambda, newValue);
return this;
}
public StateSnapshot<T> Property<TInterpolable>(Expression<Func<T, TInterpolable>> propertyLambda, TInterpolable newValue)
where TInterpolable : IInterpolable
{
state.SetValue<T, TInterpolable>(propertyLambda, newValue);
return this;
}
public StateSnapshot<T> Effect(TransitionEffect effect)
{
this.effect = effect;
return this;
}
public StateSnapshot<T> Effect(Action<TransitionEffect> effectSetter)
{
var newEffect = new TransitionEffect();
effectSetter.Invoke(newEffect);
effect = newEffect;
return this;
}
public StateSnapshot<T> Await(TimeSpan timeSpan)
{
delay += timeSpan;
return this;
}
public StateSnapshot<T> Then()
{
var newNode = new StateSnapshot<T>
{
root = this.root,
targetref = this.targetref
};
next = newNode;
return newNode;
}
public StateSnapshot<T> AwaitThen(TimeSpan timeSpan)
{
var newNode = new StateSnapshot<T>
{
root = this.root,
targetref = this.targetref,
delay = timeSpan
};
next = newNode;
return newNode;
}
public StateSnapshot<T> Then(StateSnapshot<T> next)
{
next.root = root;
next.targetref = targetref;
this.next = next;
return next;
}
public StateSnapshot<T> AwaitThen(TimeSpan timeSpan, StateSnapshot<T> next)
{
next.root = root;
next.targetref = targetref;
next.delay = timeSpan;
this.next = next;
return next;
}
public async void Start()
{
if (root is not null) await root.StartAsync();
}
public async void Start(T target)
{
if (root is not null) await root.StartAsync(target);
}
internal async Task StartAsync()
{
if (targetref?.TryGetTarget(out var target) ?? false)
{
var cts = RefreshCts();
var scheduler = TransitionScheduler<T>.FindOrCreate(target);
scheduler.Exit();
await Task.Delay(delay, cts.Token);
var copyEffect = effect.Clone();
copyEffect.Completed += (s, e) =>
{
next?.StartAsync();
};
scheduler.Execute(interpolator, state, copyEffect);
}
}
internal async Task StartAsync(T target)
{
var cts = RefreshCts();
var scheduler = TransitionScheduler<T>.FindOrCreate(target);
scheduler.Exit();
var copyEffect = effect.Clone();
copyEffect.Completed += (s, e) =>
{
next?.StartAsync(target);
};
await Task.Delay(delay, cts.Token);
scheduler.Execute(interpolator, state, copyEffect);
}
internal CancellationTokenSource RefreshCts()
{
var newCts = new CancellationTokenSource();
if (root is not null)
{
var oldCts = Interlocked.Exchange(ref root.cts, newCts);
if (oldCts is not null && !oldCts.IsCancellationRequested) oldCts.Cancel();
}
return newCts;
}
}
}
Ⅴ 总结
到这里,我们介绍了 VeloxDev.Core 提供的核心能力之一 :【 多框架过渡系统快速搭载 】,并且为你演示了具体的搭载流程,可以说,除了最后的 Fluent API 需要写不少东西,其它地方那真是简单到不能再简单了 ~ 这个可以算作是锻炼一下作者的抽象思维能力,我说真的,设计那个抽象层的时候,除了脑袋尖尖,别无他感 ( 笑
但我还是想问问,MAUI 真的不能用类似 WPF 的 DrawingBrush 实现复杂画刷的 【透明度混合】过渡吗,还有还有,MAUI 的 Transform 怎么算插值 …… 跨框架确实存在一些挑战,特别是对于我这种没什么实战经验的学生,不过嘛,只要有 VeloxDev.Core 提供的全抽象层,这些问题也许能由那些更有经验的人员完成 ?
文章就先写到这里吧,VeloxDev.Core 其实还有对 AOP 编程的支持,甚至计划未来将 【拖拽式工作流构建器】也发展到多个平台,宗旨就一条 : " 多框架一致的快速开发模式 "