【问题标题】:define animations and triggers as reusable resource?将动画和触发器定义为可重用资源?
【发布时间】:2010-12-11 21:06:58
【问题描述】:

有没有办法在 xaml 中的某个地方(例如,作为资源)定义一次动画,然后多次重复使用它?我有很多跨不同数据模板的独立画笔,它们独立需要基于数据触发器启动相同类型的动画。现在看来,动画必须定义 Storyboard.TargetName 和 Storyboard.TargetProperty。这几乎违背了可重用性的目的。我想以某种方式声明“从资源中使用此动画,但这次将其应用于另一个元素”。

对我来说,这似乎是一个相当基本、重要和必要的要求,我很惊讶它并没有那么直接地完成。我在这里遗漏了什么吗?

同样的事情也适用于触发器。假设我有很多不同的视觉元素,它们都使用彩色动画表示相同类型的状态。例如。当“活动”时淡出为绿色 当“错误”等时淡出为“红色”等。视觉效果之间的唯一区别是它们的形状/视觉树所需的动画行为是相同的,它们都有一个元素在其视觉树中的某处具有颜色类型的属性。我认为不难想象一遍又一遍地重新定义相同的动画和数据触发器集是多么乏味。每个开发人员都讨厌这个。我拼命寻找一种更简单的解决方案,它不需要(或至少很少)背后的 c# 代码。

到目前为止,我想出的是:

在类似这样的资源中定义动画(对所有基本状态重复此操作,如激活、活动、非活动、错误):

<ColorAnimationUsingKeyFrames x:Key="deactivatingColorAnimation" 
                    Storyboard.TargetProperty="Material.(MaterialGroup.Children)[0].Brush.(SolidColorBrush.Color)"                    
                    FillBehavior="HoldEnd" RepeatBehavior="Forever" AutoReverse="True">
      <ColorAnimationUsingKeyFrames.KeyFrames>
        <LinearColorKeyFrame KeyTime="00:00:00" Value="Gray"/>
        <LinearColorKeyFrame KeyTime="00:00:0.25" Value="Gray"/>
        <LinearColorKeyFrame KeyTime="00:00:0.5" Value="Gray" />
        <LinearColorKeyFrame KeyTime="00:00:0.75" Value="Gray" />
     </ColorAnimationUsingKeyFrames.KeyFrames>
</ColorAnimationUsingKeyFrames>

在触发器的情节提要中使用它(对于每个状态 X 每个不同的 stateviusal 重复无数次,总是为情节提要想出一个新名称):

<DataTrigger Binding="{Binding SubstrateHolder.State}" Value="Deactivating">
        <DataTrigger.EnterActions>
            <BeginStoryboard x:Name="someStateVisualDeactivatingStoryboard">
                <Storyboard Storyboard.TargetName="someStateVisual">
                    <StaticResource ResourceKey="deactivatingColorAnimation" />
                </Storyboard>
            </BeginStoryboard>
        </DataTrigger.EnterActions>
        <DataTrigger.ExitActions>
            <RemoveStoryboard BeginStoryboardName="someStateVisualDeactivatingStoryboard" />
        </DataTrigger.ExitActions>
</DataTrigger>

您可以轻松想象我必须为所有这些数不胜数的 DataTrigger 反复复制和粘贴多少臃肿的 XAML。

一次定义所有这些触发器并将其应用于不同的状态视觉效果会很酷。在 WPF 中如何解决这样的问题?有什么建议吗?

【问题讨论】:

    标签: wpf animation resources datatrigger reusability


    【解决方案1】:

    对于这个一般性问题,似乎没有任何好的仅 XAML 解决方案。我最终编写了自己的附加属性,这些属性定义了给定元素的所有动画行为。像这样的:

    <DataTemplate>
       <!-- ...  -->
       <Rectangle Fill="Gray">
         <v:AnimationHelper.Animations>
            <v:StandardColorStateAnimation TargetColorProperty="(Rectangle.Fill).(SolidColorBrush.Color)" TargetStateProperty={Binding State} />
         </v:AnimationHelper.Animations>
       </Rectangle>
    <DataTemplate>
    

    其余的(创建动画等)在代码隐藏中完成。

    【讨论】:

    • 你知道我尝试了很多不同的方法来为此提出建议,但如果不做一些代码是不可能的。这是一个很好的解决方案,我认为你会得到。 WPF 可扩展性万岁,对吧? :)
    • 为此使用 silverlight 行为 API 可能是个好主意。你同意吗?它与标准的 .net 3.5 框架兼容吗?
    • 不,它不兼容,尽管它们是应该的。老实说,我认为您已经找到了一个很好的解决方案,它以一种非常“自然”的方式利用了 WPF 体系结构,并且完全不应该在这里猜测自己。
    • 抱歉,太晚了,我意识到那里有点不对劲。首先,并不是它们不兼容,只是它们本身不是在 .NET 3.5 中。它们现在通过 System.Windows.Interactivity 与 Blend 一起提供。您在这里所做的是实现了被称为 WPF 的“附加行为”模式,这就是我们如何完成在 System.Windows.Interactivity 之前提供的那种东西行为。 :)
    • 我不太喜欢我的方法,因为缺乏可设计性。如果设计师想要完全重新设计动画怎么办?我想我能做的最好的事情就是更多地为附加属性设置参数,而设计师必须手动编辑 xaml。你同意吗?
    【解决方案2】:

    我能想到的实现这一目标的最“XAML 方式”是创建专用的MarkupExtension,它将从资源字典中提取动画并设置必要的属性——我假设这些是有限的到Storyboard.TargetStoryboard.TargetNameStoryboard.TargetProperty 的子集。尽管它需要一些代码隐藏,但它是一次性的,此外,MarkupExtensions 旨在与 XAML 一起使用。这是最简单的版本:

    [MarkupExtensionReturnType(typeof(Timeline))]
    public class AnimationResourceExtension : StaticResourceExtension
    {
        //This property is for convienience so that we
        //don't have to remember to set x:Shared="False"
        //on all animation resources, but have an option
        //to avoid redundant cloning if it is
        public bool IsShared { get; set; } = true;
    
        public DependencyObject Target { get; set; }
    
        public string TargetName { get; set; }
    
        public PropertyPath TargetProperty { get; set; }
    
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            if (base.ProvideValue(serviceProvider) is Timeline animation)
            {
                //If the animation is shared we shall clone it
                //Checking if it is frozen is optional and we can
                //either clone it or throw an exception
                //(or simply proceed knowing one will be thrown anyway)
                if (IsShared || animation.IsFrozen)
                    animation = animation.Clone();
                Storyboard.SetTarget(animation, Target);
                Storyboard.SetTargetName(animation, TargetName);
                Storyboard.SetTargetProperty(animation, TargetProperty);
                return animation;
            }
            else
                throw new XamlException("The referenced resource is not an animation");
        }
    }
    

    用法非常简单:

    <FrameworkElement.Resources>
        <DoubleAnimation x:Key="MyAnimation" From="0" To="1" Duration="0:0:1" />
    </FrameworkElement.Resources>
    (...)
    <Storyboard>
        <utils:AnimationResource ResourceKey="MyAnimation" TargetName="SomeElement" TargetProperty="Opacity" />
    </Storyboard>
    

    这个解决方案尽可能简单有其局限性——它既不支持上述属性的Binding 也不支持DynamicResource 扩展。然而,这是可以实现的,但需要一些额外的努力。 Binding 支持应该很简单 - 正确使用 XamlSetMarkupExtensionAttribute 的问题 (加上一些样板代码)。 DynamicResource 支持会有点棘手,除了使用 XamlSetMarkupExtensionAttribute 之外,还需要包装 IServiceProvider 以返回足够的 IProvideValueTarget 实现,但仍然是可能的。

    【讨论】:

    • 您应该在设置动画的每个部分之前测试默认值,否则您最终会覆盖您拥有动画的情况,例如,克隆动画的 TargetName 动态更改但 TargetProperty 保持不变一样的。
    • 除此之外,我不明白为什么这个问题+答案得到的回应如此之少。这似乎是一个非常惯用的答案,对于 XAML 中的动画重用来说是完全必要的。所以我附和原始问题的观点。我们是否错过了一些直截了当的东西?因为,如果不是,其他人如何处理 XAML + 重用?
    • 还有一个小问题:条件应该测试 !IsShared。
    • 关于条件,我认为现在是正确的。默认情况下,如果您从ResourceDictionary 拉取资源,它每次都会创建一个新实例。但是,如果您使用x:Shared="True" 属性标记资源,则字典将只创建一个实例并在每次请求资源时返回它。现在我们要确保每次都使用新的动画,所以如果我们检测到资源是共享的(因此是当前条件),我们就克隆它。关于第一条评论,我不确定我理解你的意思。你能澄清一下吗?
    • 我在测试默认值时的意思是想象你有一个动画,它本身就以某些属性为目标——比如说不透明度。然后,您可以使用此扩展程序使用以下语法设置资源的目标: 您最终会覆盖目标属性,并且它将来自 Opacity为空。因此,扩展首先测试是否提供了一个值,然后再清空该值是有意义的。
    【解决方案3】:

    我意识到这个问题在发布这篇文章时有点死,但我确实找到了一个只需要很少代码的解决方案。

    您可以创建一个UserControl with custom properties(向下滚动到图 8 左右),其中包含您的矩形以及动画和状态触发器。此用户控件将定义一个公共属性,例如在更改时会触发颜色更改的状态。

    唯一需要的代码隐藏是在代码中创建变量。

    用户控件可以一遍又一遍地重复使用,而无需重写 XAML 故事板或数据触发器。

    【讨论】:

      【解决方案4】:

      你能试试这样的吗?

      • 用不可见的根元素包装所有当前控件模板,例如Border 或 StackPanel,其边界框将覆盖整个控件。
      • 为这个包含所有触发器和动画的不可见框创建样式或控件模板。
      • 让动画为不可见框上的任意 Color 属性设置动画。
      • 在所有不同控件的可视化树中,将要设置动画的任何属性绑定到不可见根元素的 Color 属性。

      【讨论】:

        猜你喜欢
        • 2020-10-09
        • 1970-01-01
        • 2017-02-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-01-18
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多