• Avalonia中的动画


    作者:王先荣
    内置动画 在Avalonia中,常用的动画有以下几种:关键帧动画(KeyFrame Animation)、过渡动画(Transition Animation)和合成动画(Composition Animations)。它们在使用方式和适用场景有所不同,官方文档的总结如下表所示(https://docs.avaloniaui.net/docs/graphics-animation/animations)。
    Type
    Description
    Use case
    Keyframe Animations
     
    Change one or more properties over a timeline with multiple keyframes.
     
    Complex, multi-step animations triggered by style selectors.
     
    Control Transitions
     
    Animate a single property when its value changes.
     
    Smooth visual feedback for property changes (opacity, color, size).
     
    Composition Animations
     
    Code-driven animations that run on the render thread.
     
    Performance-sensitive or programmatic animations controlled from C#.
    严格来说,它们都是插值动画(Interpolated Animations)。那么怎样才能实现离散动画呢?例如打字机效果的动画,游戏中的精灵动画。 Avalonia官方的自定义动画示例 Avalonia的官方示例项目(RenderDemo https://github.com/AvaloniaUI/Avalonia/tree/master/samples/RenderDemo)中,实现了一个名为CustomStringAnimator(https://github.com/AvaloniaUI/Avalonia/blob/master/samples/RenderDemo/Pages/CustomStringAnimator.cs)的动画类,它继承自InterpolatingAnimator,并重写了Interpolate方法来实现打字机效果的动画。以下是该动画类的代码:
    public class CustomStringAnimator : InterpolatingAnimator<string>
    {
        public override string Interpolate(double progress, string oldValue, string newValue)
        {
            if (newValue.Length == 0) return "";
            var step = 1.0 / newValue.Length;
            var length = (int)(progress / step);
            var result = newValue.Substring(0, length + 1);
            return result;
        }
    }
    自定义动画

    使用起来跟普通的动画几乎一样,需要指定Animator,代码如下(https://github.com/AvaloniaUI/Avalonia/blob/master/samples/RenderDemo/Pages/CustomAnimatorPage.xaml):
    <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">
      <TextBlock.Styles>
        <Style Selector="TextBlock">
          <Style.Animations>
            <Animation Duration="0:0:1" IterationCount="Infinite">
              <KeyFrame Cue="0%">
                <Setter Property="Text" Value="">
                  <Animation.Animator>
                    <pages:CustomStringAnimator/>
                  Animation.Animator>
                Setter>
              KeyFrame>
              <KeyFrame Cue="100%">
                <Setter Property="Text" Value="0123456789" >
                  <Animation.Animator>
                    <pages:CustomStringAnimator/>
                  Animation.Animator>
                Setter>
              KeyFrame>
            Animation>
          Style.Animations>
        Style>
      TextBlock.Styles>
    TextBlock>
    View Code

     

    值得注意的是在axaml中使用自定义动画器时,需要指定动画器的类型(),否则会报错。 改进的字符串动画 在官方示例的基础上,我们可以略微做一些改进:支持在字符串之间进行插值动画。动画过程中会根据进度逐渐显示新字符串的内容,同时保留旧字符串的公共前缀部分。
    复制代码
    public class StringAnimator : InterpolatingAnimator<string>
    {
        /// 
        /// 计算插值结果。
        /// 
        /// 进度:0到1之间的值,指在两个关键帧之间过渡的时间位置。
        /// 旧值:动画开始时,第一个关键帧的值。
        /// 新值:动画结束时,最后一个关键帧的值。
        /// 返回插值结果。
        public override string Interpolate(double progress, string oldValue, string newValue)
        {
            if (progress <= 0)
                return oldValue;
            if (progress >= 1)
                return newValue;
            int oldLength = string.IsNullOrEmpty(oldValue) ? 0 : oldValue.Length;
            int newLength = string.IsNullOrEmpty(newValue) ? 0 : newValue.Length;
            if (oldLength == 0 && newLength == 0)
                return string.Empty;
            if (oldLength == newLength && oldValue == newValue)
                return newValue;
            if (oldLength == 0 || newValue.StartsWith(oldValue))
            {
                // 如果旧值为空或新值以旧值开头,则从旧值开始插入新值的剩余部分
                return newValue.Substring(0, (int)(oldLength + (newLength - oldLength) * progress));
            }
            else if (newLength == 0 || oldValue.StartsWith(newValue))
            {
                // 如果新值为空或旧值以新值开头,则从新值开始删除旧值的剩余部分
                return oldValue.Substring(0, (int)(newLength + (oldLength - newLength) * (1 - progress)));
            }
            else
            {
                //如果没有包含关系,忽略旧值,直接从新值开始插入
                oldLength = 0;
                return newValue.Substring(0, (int)(oldLength + (newLength - oldLength) * progress));
            }
        }
    
        static StringAnimator()
        {
            // 注册动画器
            Animation.RegisterCustomAnimator<string, StringAnimator>();
        }
    }
    复制代码

     

    使用方法跟官方示例一样。 自定义离散动画 如果要实现通用的离散动画,该怎么实现呢?跟上面类似,我们可以从InterpolatingAnimator继承一个DiscreteAnimator类,代码很简单,放弃中间的过渡部分,直接在进度达到100%时切换到新值,代码如下:
    复制代码
    public class DiscreteAnimator : InterpolatingAnimator
    {
        public override T Interpolate(double progress, T oldValue, T newValue)
        {
            return progress < 1 ? oldValue : newValue;
        }
    
        static DiscreteAnimator()
        {
            // 注册动画器
            Animation.RegisterCustomAnimator>();
        }
    }
    复制代码

     

    使用方法跟上面一样:
    复制代码
    <TextBlock Text="离散动画器文本,切换几种不同的样式。">
        <TextBlock.Styles>
            <Style Selector="TextBlock">
              <Style.Animations>
                <Animation Duration="0:0:3" IterationCount="Infinite" PlaybackDirection="Alternate">
                  <KeyFrame Cue="0%">
                    <Setter Property="FontStyle" Value="Normal">
                        <Animation.Animator>
                            <app:DiscreteAnimator x:TypeArguments="FontStyle" />
                        Animation.Animator>
                    Setter>
                  KeyFrame>
                  <KeyFrame Cue="30%">
                    <Setter Property="FontStyle" Value="Italic">
                        <Animation.Animator>
                            <app:DiscreteAnimator x:TypeArguments="FontStyle" />
                        Animation.Animator>
                    Setter>
                  KeyFrame>
                  <KeyFrame Cue="60%">
                    <Setter Property="FontStyle" Value="Oblique">
                        <Animation.Animator>
                            <app:DiscreteAnimator x:TypeArguments="FontStyle" />
                        Animation.Animator>
                    Setter>
                  KeyFrame>
                Animation>
              Style.Animations>
            Style>
          TextBlock.Styles>
    TextBlock>
    复制代码

     

    因为我们使用了泛型,在指定动画器时需要使用x:TypeArguments来指定类型参数(),否则会报错。 另一种实现自定义动画的变通方法 如果不想实现通用的动画器,我们可以注册AvaloniaProperty,并在属性的PropertyChanged回调中直接修改属性值来实现动画效果。以下是一个简单的示例,演示如何通过注册一个名为DiscreteValue的AvaloniaProperty来实现几何路径变换的动画:
    复制代码
    //注册一个AvaloniaProperty,类型为int,默认值为0
    public const string DISCRETE_VALUE = "DiscreteValue";
    public static readonly StyledProperty<int> DiscreteValueProperty =
        AvaloniaProperty.Registerint>(DISCRETE_VALUE, 0);
    //定义一个数组,存储不同的几何路径,在动画过程中根据DiscreteValue的值来切换不同的路径
    private PathGeometry[] bells = [];      //这里省略了路径的定义
    //创建一个动画并运行它
    private void PlayAnimation_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
    {
        var animation = new Animation()
        {
            Duration = TimeSpan.FromSeconds(8),
            IterationCount = new IterationCount(2),
            PlaybackDirection = PlaybackDirection.Alternate,
            Children =
            {
                new KeyFrame()
                {
                    Setters = { new Setter(DiscreteValueProperty, 0), },
                    KeyTime = TimeSpan.FromSeconds(0)
                },
                new KeyFrame()
                {
                    Setters = { new Setter(DiscreteValueProperty, 1), },
                    KeyTime = TimeSpan.FromSeconds(2)
                },
                new KeyFrame()
                {
                    Setters = { new Setter(DiscreteValueProperty, 2), },
                    KeyTime = TimeSpan.FromSeconds(4)
                },
                new KeyFrame()
                {
                    Setters = { new Setter(DiscreteValueProperty, 3), },
                    KeyTime = TimeSpan.FromSeconds(6)
                },
                new KeyFrame()
                {
                    Setters = { new Setter(DiscreteValueProperty, 3), },
                    KeyTime = TimeSpan.FromSeconds(8)
                }
            }
        };
        _ = animation.RunAsync(Path1);
    }
    //在属性的PropertyChanged事件中,根据DiscreteValue的值来切换不同的路径
    private void Path1_PropertyChanged(object? sender, Avalonia.AvaloniaPropertyChangedEventArgs e)
    {
        if (e.Property.Name == DiscreteValueProperty.Name)
        {
            Debug.WriteLine($"time: {DateTime.Now}, Path1 property changed: {e.Property.Name}, old value: {e.OldValue}, new value: {e.NewValue}");
            int value = (int)e.NewValue;
            Path1.Data = bells[value % bells.Length];
        }
    }
    复制代码

     

    这种方法只能在代码中调用,无法在axaml中使用,但它不需要实现通用的动画器,适合一些特定的动画效果。 本文相关代码可以在我的Gitee仓库中找到: https://gitee.com/xrwang2/avalonia-test/tree/master/AvaloniaMvvpDesktopApp
  • 相关阅读:
    Python 中的 Raincloud 图绘制
    postgresql源码学习(50)—— 小白学习Dtrace追踪源码函数调用
    kubernetes集群编排(12)
    脉冲神经网络原理及应用,脉冲神经网络的优缺点
    FPGA - 7系列 FPGA内部结构之Clocking -02- 时钟布线资源
    NSS [HNCTF 2022 WEEK2]ohmywordpress(CVE-2022-0760)
    Chrome、Edge 合力“围剿“,Safari 夹缝求生?
    cuda在windows10安装教程
    Springboot整合Flowable6.x导出bpmn20
    ESP8266—01S基于透传云的TCP网络通信
  • 原文地址:https://www.cnblogs.com/xrwang/p/20122560/AnimationInAvalonia