自己实现一个AnimationPlayer类,已完善了大部分功能。

AnimationPlayer类

    public partial class AnimationPlayer : ObservableObject
    {
        // --------------------- 内部类 ---------------------
        public class TimedAction
        {
            public double StartTime { get; set; }
            public double? EndTime { get; set; }
            public bool OneTime { get; set; }
            private bool executed = false;
            private double fps;
            private Easing easing = new LinearEasing();

            internal Action<double>? localAction;
            internal Action<double>? globalAction;
            internal Action<double, double>? dualAction;

            public TimedAction(double startTime, double? endTime = null)
            {
                StartTime = startTime;
                EndTime = endTime;
            }

            public TimedAction Once()
            {
                OneTime = true;
                return this;
            }

            public TimedAction Fps(double fps) 
            {
                this.fps = fps;
                return this;
            }
            
            public TimedAction Ease(Easing easing)
            {
                this.easing = easing;
                return this;
            }
            public TimedAction Ease(string easingName)
            {
                try
                {
                    // "ElasticEaseOut" 等
                    easing = Easing.Parse(easingName);
                }
                catch
                {
                    easing = new LinearEasing();
                }
                return this;
            }
            public TimedAction PlayLocal(Action<double> action)
            {
                localAction = action;
                return this;
            }

            public TimedAction PlayGlobal(Action<double> action)
            {
                globalAction = action;
                return this;
            }

            public TimedAction PlayDual(Action<double, double> action)
            {
                dualAction = action;
                return this;
            }

            public bool IsActive(double elapsedSeconds)
            {
                return elapsedSeconds >= StartTime &&
                       (EndTime == null || elapsedSeconds <= EndTime.Value + 1 / fps);
            }

            public void TryExecute(double elapsedSeconds, double globalProgress, double totalDuration)
            {
                if (OneTime && elapsedSeconds < StartTime)
                {
                    executed = false;
                }

                if (!IsActive(elapsedSeconds)) return;

                double actionEnd = EndTime ?? totalDuration;
                double denom = actionEnd - StartTime;
                double localProgress = Math.Clamp(denom <= double.Epsilon ? 1.0 : (elapsedSeconds - StartTime) / denom, 0, 1); 

                if (OneTime && executed) return;

                double easedLocal = easing.Ease(localProgress);
                localAction?.Invoke(easedLocal);
                globalAction?.Invoke(globalProgress);
                dualAction?.Invoke(easedLocal, globalProgress);

                if (OneTime)
                    executed = true;
            }

            public void Reset() => executed = false;
        }

        // --------------------- 字段 ---------------------
        private readonly DispatcherTimer timer = new DispatcherTimer();
        private readonly Stopwatch stopwatch = new Stopwatch();
        private TimeSpan elapsedOffset = TimeSpan.Zero; // 累计暂停/Seek时间
        private bool isRunning;

        // --------------------- 属性 ---------------------
        [ObservableProperty] private double _speed = 1.0;
        [ObservableProperty] private double _duration = 10.0;
        [ObservableProperty] private double _progress;
        [ObservableProperty] private string _timeText = "[[ stopped ]]";
        [ObservableProperty] private bool _canPause = false;
        [ObservableProperty] private bool _canResume = false;
        [ObservableProperty] private bool _canStop = false;
        [ObservableProperty] private bool _canSeek = false;
        [ObservableProperty] private double _fps = 0;
        [ObservableProperty] private bool _loop = false;

        private List<TimedAction> Actions { get; } = new();

        public event Action? AnimationCompleted;

        public AnimationPlayer()
        {
            Fps = 60;
            timer.Tick += (_, __) => UpdateProgress();
        }
        partial void OnFpsChanged(double value)
        {
            timer.Interval = TimeSpan.FromMilliseconds(1000 / value);
            foreach (var action in Actions)
                action.Fps(value);
        }
        // --------------------- 链式添加动作 ---------------------
        public TimedAction At(double startTime, double? endTime = null)
        {
            var action = new TimedAction(startTime, endTime).Fps(Fps);
            Actions.Add(action);
            return action;
        }

        // --------------------- 控制方法 ---------------------
        public void Start()
        {
            stopwatch.Restart();
            elapsedOffset = TimeSpan.Zero;
            isRunning = true;

            foreach (var action in Actions)
                action.Reset();

            timer.Start();
            UpdateStates();
        }

        public void Pause()
        {
            if (!CanPause) return;
            stopwatch.Stop();
            elapsedOffset += stopwatch.Elapsed;
            isRunning = false;
            timer.Stop();
            UpdateStates();
        }

        public void Resume()
        {
            if (!CanResume) return;
            stopwatch.Restart();
            isRunning = true;
            timer.Start();
            UpdateStates();
        }

        public void Stop()
        {
            if (!CanStop) return;

            isRunning = false;
            timer.Stop();
            stopwatch.Reset();
            elapsedOffset = TimeSpan.Zero;
            Progress = 0;
            TimeText = "[[ stopped ]]";

            foreach (var action in Actions)
                action.Reset();

            UpdateStates();
        }

        public void Seek(double seconds)
        {
            if (!CanSeek) return;

            seconds = Math.Clamp(seconds, 0, Duration);
            elapsedOffset = TimeSpan.FromSeconds(seconds / Speed);       
            stopwatch.Restart();
            UpdateProgress();
        }

        // --------------------- 更新方法 ---------------------
        private void UpdateProgress()
        {
            double elapsedSeconds = (stopwatch.Elapsed + elapsedOffset).TotalSeconds * Speed;

            if (elapsedSeconds >= Duration)
            {
                if (Loop)
                {
                    // 循环:重置时间
                    elapsedSeconds = 0;
                    elapsedOffset = TimeSpan.Zero;
                    stopwatch.Restart();

                    Progress = 0;

                    foreach (var action in Actions)
                        action.Reset();
                }
                else
                {
                    // 正常结束
                    elapsedSeconds = Duration;
                    isRunning = false;
                    timer.Stop();
                    AnimationCompleted?.Invoke();
                }
            }

            Progress = Math.Clamp(elapsedSeconds / Duration, 0, 1);
            TimeText = TimeSpan.FromSeconds(elapsedSeconds).ToString(@"hh\:mm\:ss\.fff");

            foreach (var timedAction in Actions)
                timedAction.TryExecute(elapsedSeconds, Progress, Duration);

            UpdateStates();
        }

        // --------------------- 状态更新 ---------------------
        private void UpdateStates()
        {
            CanPause = isRunning;
            CanResume = !isRunning && Progress > 0 && Progress < 1;
            CanStop = isRunning || (Progress > 0 && Progress < 1);
            CanSeek = Progress > 0 && Progress < 1; 
        }
    }

AnimationPlayerTest.axaml代码

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Height="396" Width="360.8"
        x:Class="AvaloniaUI.AnimationPlayerTest"
         xmlns:local="using:Shares.Avalonia"
         x:DataType="local:AnimationPlayer"
        Title="AnimationPlayerTest">
    <Grid RowDefinitions="auto,auto,auto,auto,auto">
        <Grid>
            <Image Source="avares://AvaloniaUI/Resources/Images/night.jpg"/>
            <Image Source="avares://AvaloniaUI/Resources/Images/day.jpg" Name="imgDay"/>
        </Grid>
        
        <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="5" VerticalAlignment="Top">
            <Button Name="cmdStart" Content="Start"/>
            <Button Name="cmdPause" Content="Pause" IsEnabled="{Binding CanPause}"/>
            <Button Name="cmdResume" Content="Resume" IsEnabled="{Binding CanResume}"/>
            <Button Name="cmdStop" Content="Stop" IsEnabled="{Binding CanStop}"/>
            <Button Name="cmdMiddle" Content="Move To Middle"/>
        </StackPanel>

        <TextBlock Grid.Row="2" Name="lblTime" HorizontalAlignment="Center"></TextBlock>

        <Grid Grid.Row="3" Margin="5" ColumnDefinitions="auto,*">    
            <TextBlock Margin="0,15,5,0">Speed:</TextBlock>
            <Slider Grid.Column="1" Name="sldSpeed" Minimum="0.1" Maximum="3" Value="1" TickPlacement="BottomRight" TickFrequency="0.1"/>
        </Grid>

        <ProgressBar Grid.Row="4" Margin="0,5,0,0" Height="10" Name="progressBar" Minimum="0" Maximum="1"/>
    </Grid>
</Window>

AnimationPlayerTest.axaml.cs代码

using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Shares.Avalonia;
using System;

namespace AvaloniaUI;

public partial class AnimationPlayerTest : Window
{
    private readonly AnimationPlayer? player;

    public AnimationPlayerTest()
    {
        InitializeComponent();
        player = new AnimationPlayer
        {
            Duration = TimeSpan.FromSeconds(10),
            Speed = 1.0,
            ApplyAction = progress =>
            {
                // 控制 imgDay 的透明度[1,0]
                imgDay.Opacity = 1 - progress;

                // 更新进度条
                progressBar.Value = progress;

                // 更新时间文本
                lblTime.Text = player?.TimeText;
            }
        };

        // 绑定按钮
        cmdStart.Click += (_, _) => player.Start();
        cmdPause.Click += (_, _) => player.Pause();
        cmdResume.Click += (_, _) => player.Resume();
        cmdStop.Click += (_, _) => player.Stop();
        cmdMiddle.Click += (_, _) => player.Seek(TimeSpan.FromSeconds(5));

        // 绑定速度
        sldSpeed.ValueChanged += (_, _) =>
        {
            player.Speed = sldSpeed.Value;
        };
        this.DataContext = player;
    }
}

运行效果

image

 

posted on 2025-09-20 11:31  dalgleish  阅读(9)  评论(0)    收藏  举报