自己实现一个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;
}
}
运行效果

浙公网安备 33010602011771号