WPF MVVM属性更改动画

13
我希望找到一种干净的方法来启动具有动态值的动画。基本上,我想做一个动画,在这个动画中,一个元素的宽度会根据另一个元素的数据而改变。比如我有一个TextBlock,它的Text属性是绑定的。当这个属性改变时,我想要一个可视化元素,比如Rectangle,用DoubleAnimation将宽度从先前的值改变为新值。
如果可能,我想避免在我的视图中放置代码。我已经研究了DataTriggers,但它们似乎需要您知道值是什么,比如枚举类型。在我的情况下,只需要触发故事板的值发生变化,动画就需要从当前(之前)值开始,然后平滑地移动到新值。
有什么想法吗?也许我错过了某个属性。
4个回答

20

这是我最终采用的解决方案。为了根据ViewModel中的数据进行动画,我使用了DataTrigger。以下是控件的样式。

<Style TargetType="Grid" x:Key="DetailRotation" >
    <Style.Triggers>
        <DataTrigger Binding="{Binding Path=AnimationState}" Value="New">
            <DataTrigger.EnterActions>
                <StopStoryboard BeginStoryboardName="EndAnimation" />
                <BeginStoryboard Name="NewAnimation">
                    <Storyboard>
                        <ThicknessAnimation Storyboard.TargetProperty="Margin" From="0,30,0,0" To="0,0,0,0" Duration="0:0:1" />
                        <DoubleAnimation Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:1" />
                    </Storyboard>
                </BeginStoryboard>
            </DataTrigger.EnterActions>
            <DataTrigger.ExitActions>

            </DataTrigger.ExitActions>

        </DataTrigger>
        <DataTrigger Binding="{Binding Path=AnimationState}" Value="End">
            <DataTrigger.EnterActions>
                <StopStoryboard BeginStoryboardName="NewAnimation" />
                <BeginStoryboard Name="EndAnimation">
                    <Storyboard>
                        <ThicknessAnimation Storyboard.TargetProperty="Margin" From="0,0,0,0" To="0,-20,0,0" Duration="0:0:1"/>
                        <DoubleAnimation Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:1" />
                    </Storyboard>
                </BeginStoryboard>
            </DataTrigger.EnterActions>
        </DataTrigger>

    </Style.Triggers>
</Style>

1
谢谢,你的回答帮了我很多。 - Zeus-Adenilton

2
你可以尝试使用附加属性来连接所需的Storyboard/Animation逻辑。这并不一定能完全避免编写代码,但它将代码与视图分离,并允许在多个视图中重复使用。

我真的不确定这是我需要的方向。我已经快到使用 routedevent 命令故事板进行绑定,将 PreviousWidth 和 CurrentWidth 绑定到动画的 to 和 from 属性上了。这可能是唯一的方法。但我仍然不确定。DataTriggers 只能用于状态样式更改而不能用于动态更改。这就是我有点偏离模式的地方。 - cjibo

2

实际上,您希望将DoubleAnimation.ToProperty绑定到ViewModel属性,并对实际控件进行动画处理。问题在于当ToProperty更改时,动画应该继续进行。我的解决方案是将所有这些逻辑封装到一个MarkupExtenstion中,该扩展包装了一个Binding

public class AnimateBindingExtension : MarkupExtension {
    static DependencyPropertyDescriptor dpd =
        DependencyPropertyDescriptor.FromProperty(DoubleAnimation.ToProperty, 
            typeof(DoubleAnimation));

    public AnimateBindingExtension(PropertyPath path) {
        Path = path;
    }

    public bool ValidatesOnExceptions { get; set; }
    public IValueConverter Converter { get; set; }
    public object ConverterParamter { get; set; }
    public string ElementName { get; set; }
    public RelativeSource RelativeSource { get; set; }
    public object Source { get; set; }
    public bool ValidatesOnDataErrors { get; set; }
    [ConstructorArgument("path")]
    public PropertyPath Path { get; set; }
    public object TargetNullValue { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider) {
        var valueProvider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;

        if (valueProvider == null) {
            throw new Exception("could not get IProviderValueTarget service.");
        }

        var bindingTarget = valueProvider.TargetObject as FrameworkElement;
        var bindingProperty = valueProvider.TargetProperty as DependencyProperty;

        if (bindingProperty == null || bindingTarget == null) {
            throw new Exception();
        }
        
        var binding = new Binding {
            Path = Path,
            Converter = Converter,
            ConverterParameter = ConverterParamter,
            ValidatesOnDataErrors = ValidatesOnDataErrors,
            ValidatesOnExceptions = ValidatesOnExceptions,
            TargetNullValue = TargetNullValue
        };

        if (ElementName != null) binding.ElementName = ElementName;
        else if (RelativeSource != null) binding.RelativeSource = RelativeSource;
        else if (Source != null) binding.Source = Source;

        // you can add a Duration property to this class and use it here
        var anim = new DoubleAnimation {
            Duration = new Duration(TimeSpan.FromSeconds(0.1)),
            AccelerationRatio = 0.2,
            DecelerationRatio = 0.8
        };
        // this can be a new subclass of DoubleAnimation that 
        // overrides ToProperty metadata and add a property 
        // change callback
        dpd.AddValueChanged(anim, (s, e) => bindingTarget.BeginAnimation(bindingProperty, anim));

        BindingOperations.SetBinding(anim, DoubleAnimation.ToProperty, binding);
        // this is because we need to catch the DataContext so add animation object 
        // to the visual tree by adding it to target object's resources.
        bindingTarget.Resources[bindingProperty.Name] = anim;
        // animation will set the value
        return DependencyProperty.UnsetValue;
    }
}

您可以使用其他动画类来动画化其他类型。

这会不会遇到将故事板放置在样式/模板中时出现冻结的普遍问题?如果没有,看起来非常有趣... - tobriand
在 Xaml 中使用此绑定扩展的语法是什么? - Aldinei Sampaio

1

由于动画修改的属性不能在动画“上下文”之外设置,因此我想出了一种代码解决方案,因为我无法在XAML中有效地执行相同的操作。

private void UserControl_IsVisibleChanged(object sender, 
    DependencyPropertyChangedEventArgs e)
{
    if (this.Visibility == Visibility.Visible)
    {
        DoubleAnimation fadeIn = new DoubleAnimation();
        fadeIn.From = 1d;
        fadeIn.To = 1d;
        fadeIn.Duration = new Duration(new TimeSpan(0, 0, 0));

        DoubleAnimation fade = new DoubleAnimation();
        fade.From = 1d;
        fade.To = 0d;
        fade.BeginTime = TimeSpan.FromSeconds(5);
        fade.Duration = new Duration(new TimeSpan(0, 0, 1));

        NameScope.SetNameScope(this, new NameScope());
        this.RegisterName(this.Name, this);

        Storyboard.SetTargetName(fadeIn, this.Name);
        Storyboard.SetTargetProperty(fadeIn, new PropertyPath
            (UIElement.OpacityProperty));

        Storyboard.SetTargetName(fade, this.Name);
        Storyboard.SetTargetProperty(fade, new PropertyPath
            (UIElement.OpacityProperty));

        Storyboard sb = new Storyboard();
        sb.Children.Add(fadeIn);
        sb.Children.Add(fade);

        sb.Completed += new EventHandler(sb_Completed);
        sb.Begin(this);
    }
}

void sb_Completed(object sender, EventArgs e)
{
    this.Visibility = Visibility.Hidden;
}

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