同时支持 WPF / Avalonia / MAUI 的过渡系统

目录

Ⅰ 前言

Ⅱ 项目简介

Ⅲ 过渡系统概述

(1) Fluent API 

(2) 插值器

Ⅳ 可扩展性

(1) 效果参数定义 

(2) 线程检查器

(3) 动画帧输出

(4) 状态记录器 

(5) 插值器

(6) 动画帧控制器

(7) 全局调度器 

(8) Fluent API 构建

Ⅴ 总结


Ⅰ 前言

当作者从 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 编程的支持,甚至计划未来将 【拖拽式工作流构建器】也发展到多个平台,宗旨就一条 : " 多框架一致的快速开发模式 "

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值