WPF使用DataTrigger实现RenderTransform动画

3
我在使用数据触发器时,遇到了一个动画Canvas对象的渲染变换的困难。作为一个测试用例,我在Canvas上放置了一个椭圆和两个按钮。当我按下每个按钮时,我希望椭圆的位置动画到该按钮的X坐标: enter image description here 在这个特定的示例中,我使用按钮并不重要,关键是我试图通过绑定到视图模型中的属性的DataTrigger来触发这些动画。
// this is the property my DataTrigger will bind to
private string _Anim;
public string Anim
{
    get { return this._Anim; }
    set
    {
        this._Anim = value;
        RaisePropertyChanged(() => this.Anim);
    }
}

// a command handler for the buttons that sets my animation property
public ICommand AnimCommand { get { return new RelayCommand<string>(OnAnim); } }
private void OnAnim(string value)
{
    this.Anim = String.Empty;   // cancel any existing animation, probably not needed...
    this.Anim = value;
}

以下是我用来实现此功能的Xaml。它基本上只是一个包含椭圆和两个按钮的画布,椭圆有两个故事板,会在响应后端属性设置为不同字符串(即“左”和“右”)时触发:

<Viewbox xmlns:sys="clr-namespace:System;assembly=mscorlib">
    <Canvas Width="350" Height="150" Background="CornflowerBlue">
        <Ellipse x:Name="MyEllipse" Width="20" Height="20" Fill="Red">
            <Ellipse.RenderTransform>
                <TranslateTransform x:Name="myTransform" X="50" Y="50" />
            </Ellipse.RenderTransform>
            <Ellipse.Style>
                <Style TargetType="Ellipse">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding Anim}" Value="Left">
                            <DataTrigger.EnterActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetProperty="(Ellipse.RenderTransform).(TranslateTransform.X)" To="100" Duration="0:0:1" />
                                    </Storyboard>
                                </BeginStoryboard>
                            </DataTrigger.EnterActions>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Anim}" Value="Right">
                            <DataTrigger.EnterActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetProperty="(Ellipse.RenderTransform).(TranslateTransform.X)" To="200" Duration="0:0:1" />
                                    </Storyboard>
                                </BeginStoryboard>
                            </DataTrigger.EnterActions>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Ellipse.Style>
        </Ellipse>

        <Button x:Name="btnLeft" Content="Left" Command="{Binding AnimCommand}" CommandParameter="Left" Canvas.Left="100" Canvas.Top="100" Width="32" Height="24" />
        <Button x:Name="btnRight" Content="Right" Command="{Binding AnimCommand}" CommandParameter="Right" Canvas.Left="200" Canvas.Top="100" Width="32" Height="24" />

    </Canvas>
</Viewbox>

当我运行这个程序时,我可以点击第一个动画然后再点击第二个动画,这两次的操作都是正确的。但是反过来点击它们就不起作用了……“右”按钮会正确地进行动画,而“左”按钮则不会有任何反应。似乎“右”动画仍在运行或锁定属性等,换句话说,如果我在Xaml中交换Storyboard声明的顺序,情况就会反转。

使用EventTriggers没有问题,只有DataTriggers出现了这种情况。我也尝试了调整FillBehaviours和HandoffBehaviors,但没有效果。请问有人能解释为什么会发生这种情况以及如何解决吗?


尝试将 x:Name 分配给 BeginStoryboard 元素,并通过 ExitAction 中的 RemoveStoryboard 移除它。 - Clemens
1个回答

4
一旦动画完成,您应该删除Storyboard。您可以使用RemoveStoryboard类来完成此操作。只需给BeginStoryboard元素一个Name,然后将RemoveStoryboard元素添加到DataTriggerExitActions中即可:
<Style TargetType="Ellipse">
    <Style.Triggers>
        <DataTrigger Binding="{Binding Anim}" Value="Left">
            <DataTrigger.EnterActions>
                <BeginStoryboard Name="sb">
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetProperty="(Ellipse.RenderTransform).(TranslateTransform.X)" To="100" Duration="0:0:1" />
                    </Storyboard>
                </BeginStoryboard>
            </DataTrigger.EnterActions>
            <DataTrigger.ExitActions>
                <RemoveStoryboard BeginStoryboardName="sb" />
            </DataTrigger.ExitActions>
        </DataTrigger>
        <DataTrigger Binding="{Binding Anim}" Value="Right">
            <DataTrigger.EnterActions>
                <BeginStoryboard Name="sb2">
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetProperty="(Ellipse.RenderTransform).(TranslateTransform.X)" To="200" Duration="0:0:1" />
                    </Storyboard>
                </BeginStoryboard>
            </DataTrigger.EnterActions>
            <DataTrigger.ExitActions>
                <RemoveStoryboard BeginStoryboardName="sb2" />
            </DataTrigger.ExitActions>
        </DataTrigger>
    </Style.Triggers>
</Style>

另一个选择是将Storyboard的FillBehavior设置为Stop,但是动画完成后椭圆的位置将被恢复。

恐怕不起作用。先点击左边然后再点击右边可以工作,但是再次点击左边会导致动画从位置0重新开始,即使我设置了HandoffBehaviors和FillBehaviours。

在XAML中去掉DataTriggers并编写一些自定义代码。这样就可以解决问题。
private void MyEllipse_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    INotifyPropertyChanged inpc = MyEllipse.DataContext as INotifyPropertyChanged;
    if (inpc != null)
        WeakEventManager<INotifyPropertyChanged, PropertyChangedEventArgs>.AddHandler(inpc, nameof(INotifyPropertyChanged.PropertyChanged), OnChanged);
}

private PropertyInfo _pi;
private void OnChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "Anim")
    {
        _pi = _pi ?? sender.GetType().GetProperty(e.PropertyName);
        string anim = _pi?.GetValue(sender) as string;
        switch (anim)
        {
            case "Left":
                myTransform.BeginAnimation(TranslateTransform.XProperty, new DoubleAnimation() { To = 100.0, Duration = TimeSpan.FromSeconds(1) });
                break;
            case "Right":
                myTransform.BeginAnimation(TranslateTransform.XProperty, new DoubleAnimation() { To = 200.0, Duration = TimeSpan.FromSeconds(1) });
                break;
        }
    }
}

XAML:

<Viewbox xmlns:sys="clr-namespace:System;assembly=mscorlib">
    <Canvas Width="350" Height="150" Background="CornflowerBlue">
        <Ellipse x:Name="MyEllipse" Width="20" Height="20" Fill="Red"
                         DataContextChanged="MyEllipse_DataContextChanged">
            <Ellipse.RenderTransform>
                <TranslateTransform x:Name="myTransform" X="50" Y="50" />
            </Ellipse.RenderTransform>
        </Ellipse>

        <Button x:Name="btnLeft" Content="Left" Command="{Binding AnimCommand}" CommandParameter="Left" Canvas.Left="100" Canvas.Top="100" Width="32" Height="24" />
        <Button x:Name="btnRight" Content="Right" Command="{Binding AnimCommand}" CommandParameter="Right" Canvas.Left="200" Canvas.Top="100" Width="32" Height="24" />
    </Canvas>
</Viewbox>

很抱歉,它不起作用。一开始点击左边然后再点击右边是可以的,但是再次点击左边会导致动画从位置0重新开始,即使我设置了HandoffBehaviors和FillBehaviours。 - Mark Feldman
谢谢!从技术上讲,它并没有完全回答我的问题,因为我真的需要使用DataTriggers来做这个,但是我可以轻松地添加代码来触发动画,就像您展示的那样,使用任何东西,包括DataTriggers。最重要的是,它具有可扩展性...我真正需要的是一些无需设置“From”属性即可工作的内容,而您的解决方案可以在多个位置上实现这一点。尽管仍然有些复杂...但我不禁想知道我的原始解决方案是否存在错误? - Mark Feldman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接