实时更新Storyboard绑定:振幅变更后动画对象路径未同步更新
问题解决:WPF正弦圆周动画路径实时更新问题
问题描述
修改ViewModel的Amplitude属性时,界面上的绿色Path图形会实时更新为新的正弦圆周,但蓝色Ellipse的运动路径仍停留在初始路径,无法同步更新。原因是DoubleAnimationUsingPath通过EventTrigger启动后,不会自动响应绑定的PathGeometry变化,必须手动更新动画实例。
解决方案
通过后台代码手动管理Storyboard动画实例,监听ViewModel的属性变化事件,在路径更新时重启动画,确保动画使用最新的PathGeometry。
修改步骤
- 移除XAML中Ellipse的EventTrigger,改为后台初始化并控制动画。
- 在后台创建Storyboard和DoubleAnimationUsingPath实例,持有引用以便后续更新。
- 监听ViewModel的
PropertyChanged事件,当Amplitude或Points变化时,停止当前动画、更新动画的PathGeometry、重新启动动画。
修改后的代码
XAML代码
<Grid> <Path HorizontalAlignment="Center" VerticalAlignment="Center" Stretch="Uniform" x:Name="MainPath" SizeChanged="MainPath_SizeChanged" Fill="Green" Stroke="Black"> <Path.Data> <PathGeometry x:Name="ScaledAnimationPath"> <PathGeometry.Figures> <PathFigure StartPoint="{Binding StartPoint}"> <PolyLineSegment Points="{Binding Points}"/> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path> <Ellipse x:Name="AnimatedEllipse" Width="30" Height="30" Fill="Blue"> <Ellipse.RenderTransform> <TranslateTransform x:Name="AnimatedTranslateTransform" /> </Ellipse.RenderTransform> </Ellipse> </Grid>
后台代码(MainWindow.xaml.cs)
public partial class MainWindow : Window { private ParticlePathVM _viewModel; private Storyboard _animationStoryboard; private DoubleAnimationUsingPath _xAnimation; private DoubleAnimationUsingPath _yAnimation; public MainWindow() { InitializeComponent(); _viewModel = new ParticlePathVM(); DataContext = _viewModel; _viewModel.PropertyChanged += ViewModel_PropertyChanged; InitializeAnimation(); } private void InitializeAnimation() { _xAnimation = new DoubleAnimationUsingPath { StoryboardTargetName = nameof(AnimatedTranslateTransform), StoryboardTargetProperty = "X", PathGeometry = ScaledAnimationPath, Source = PathAnimationSource.X, Duration = TimeSpan.FromSeconds(3) }; _yAnimation = new DoubleAnimationUsingPath { StoryboardTargetName = nameof(AnimatedTranslateTransform), StoryboardTargetProperty = "Y", PathGeometry = ScaledAnimationPath, Source = PathAnimationSource.Y, Duration = TimeSpan.FromSeconds(3) }; _animationStoryboard = new Storyboard { RepeatBehavior = RepeatBehavior.Forever }; _animationStoryboard.Children.Add(_xAnimation); _animationStoryboard.Children.Add(_yAnimation); } private void RestartAnimation() { _animationStoryboard.Stop(AnimatedEllipse); _xAnimation.PathGeometry = ScaledAnimationPath; _yAnimation.PathGeometry = ScaledAnimationPath; _animationStoryboard.Begin(AnimatedEllipse, true); } private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(ParticlePathVM.Amplitude) || e.PropertyName == nameof(ParticlePathVM.Points)) { RestartAnimation(); } } private void MainPath_SizeChanged(object sender, SizeChangedEventArgs e) { _viewModel.UpdateBounds(e.NewSize); } protected override void OnLoaded(EventArgs e) { base.OnLoaded(e); _animationStoryboard.Begin(AnimatedEllipse, true); } }
ViewModel代码(保持不变)
public class ParticlePathVM : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } PointCollection points; public PointCollection Points { get => points; set { if (points != value) { points = value; RaisePropertyChanged(nameof(Points)); } } } Point startPoint; public Point StartPoint { get => startPoint; set { if (startPoint != value) { startPoint = value; RaisePropertyChanged(nameof(StartPoint)); } } } double amplitude; public double Amplitude { get => amplitude; set { if (amplitude != value) { amplitude = value; RaisePropertyChanged(nameof(Amplitude)); UpdatePoints(); } } } public double AmplitudeMax { get; } = 10; public double Radius; public double Frequency; System.Threading.SynchronizationContext _context; public ParticlePathVM() { _context = System.Threading.SynchronizationContext.Current; Radius = 266 - AmplitudeMax; StartPoint = new Point(Radius, 0); Amplitude = 5; Frequency = 10; UpdatePoints(); } public void UpdateBounds(Size s) { if (s.Width > 1) { Radius = (s.Width / 2) - AmplitudeMax; Debug.WriteLine(string.Format("{0} -> {1}", s, Radius)); StartPoint = new Point(Radius, 0); UpdatePoints(); } } void UpdatePoints() { var points = new List<Point>(); for (float i = 0; i <= 360; i += 0.5F) { points.Add(GetCartesian(i)); } _context.Post(o => { Points = new PointCollection(points); }, null); } public Point GetCartesian(double angle) { var rad = angle * (Math.PI / 180.0); var r = Radius + Amplitude * Math.Sin(2 * Frequency * rad); return new Point(r * Math.Cos(rad), r * Math.Sin(rad)); } }
效果说明
修改Amplitude属性后,ViewModel会重新计算路径点并触发PropertyChanged,后台监听到后会重启动画,此时动画会使用最新的ScaledAnimationPath,蓝色Ellipse的运动路径会与绿色Path图形同步更新。
内容的提问来源于stack exchange,提问作者Eternal Ambiguity
相关产品推荐
相关产品推荐

