将动画和触发器定义为可重用资源?
-
11-09-2019 - |
题
有没有办法在 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 每个不同的状态重复无数次,总是为故事板想出一个新名称):
<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 中如何解决这样的问题?有什么建议吗?
解决方案 2
有似乎没有任何好处仅XAML的解决这个问题的一般。我最后写的是定义所有给定元素的动画行为我自己的附加属性。是这样的:
<DataTemplate>
<!-- ... -->
<Rectangle Fill="Gray">
<v:AnimationHelper.Animations>
<v:StandardColorStateAnimation TargetColorProperty="(Rectangle.Fill).(SolidColorBrush.Color)" TargetStateProperty={Binding State} />
</v:AnimationHelper.Animations>
</Rectangle>
<DataTemplate>
的剩余部分(创建动画等)的代码隐藏完成。
其他提示
你能尝试这样的事情吗?
- 使用不可见的根元素包装所有当前的控件模板,例如Border 或 StackPanel,其边界框将覆盖整个控件。
- 为这个包含所有触发器和动画的隐形框创建样式或控件模板。
- 让动画为不可见框上的任意 Color 属性设置动画。
- 在所有不同控件的可视化树中,将要设置动画的任何属性绑定到不可见根元素上的 Color 属性。
我意识到这个问题是在此张贴的时间有点死了,但我没有找到需要的背后很少的代码的解决方案。
可以使一个用户控件与自定义属性(向下滚动到包含您的矩形约图8),以及动画和状态触发器。这个用户控件将定义一个公共属性,例如状态改变时,将触发颜色变化。
唯一的代码隐藏需要的是在代码中创建您的变量。
在用户控制可以在它们被重新使用一遍又一遍,不重写的XAML故事板或数据触发器。
最多 ”XAML 实现这个目标我能想到的方法就是创建专门的 MarkupExtension
这将从资源字典中提取动画并设置必要的属性 - 我假设这些属性仅限于 Storyboard.Target
, Storyboard.TargetName
和 Storyboard.TargetProperty
. 。虽然需要一些后台代码,但这是一次性的工作,而且, MarkupExtension
s 设计用于 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
实施,但仍然是可能的。