определять анимации и триггеры как повторно используемый ресурс?
-
11-09-2019 - |
Вопрос
Есть ли способ определить анимацию где-нибудь в xaml (например.как ресурс) один раз, а затем повторно использовать его несколько раз?У меня есть много независимых кистей для разных datatemplates, которые независимо друг от друга должны запускать один и тот же вид анимации на основе datatrigger.Теперь, поскольку кажется, что анимация должна определять раскадровку.TargetName и Storyboard.TargetProperty.Это в значительной степени сводит на нет цель повторного использования.Я бы как-то хотел объявить "используйте эту анимацию из ресурса, но на этот раз примените ее к другому элементу".
Мне это кажется довольно простой, важной и насущной просьбой, и я удивлен, что ее не так просто выполнить.Я что-то здесь упускаю?
То же самое относится и к триггерам.Предположим, у меня есть много разных визуальных элементов, которые все представляют один и тот же тип состояния с использованием цветовой анимации.Например.становится зеленым, когда "активно", становится "красным", когда "ошибка" и т.д.Единственное различие между визуальными элементами заключается в их форме / визуальном дереве желаемое поведение анимации одинаково, у всех них где-то в их визуальном дереве есть элемент, который имеет свойство типа color .Я думаю, нетрудно представить, насколько утомительно переопределять одни и те же анимации и наборы данных снова и снова.Каждый разработчик ненавидит это.Я отчаянно ищу более простое решение, которое не требует никакого (или, по крайней мере, очень небольшого) кода на 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>
Вы можете легко представить, сколько раздутого XAML мне приходится многократно копировать и вставлять для всех этих миллионов DataTriggers.
Было бы здорово определить все эти триггеры один раз и применить их к различным визуальным эффектам состояния.Как что-то подобное решается в 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>
Остальное (создание анимации и т.д.) Выполняется в codebehind.
Другие советы
Не могли бы вы попробовать что-то подобное?
- Оберните все ваши текущие шаблоны элементов управления невидимым корневым элементом, напримерграница или StackPanel, ограничивающая рамка которой будет охватывать весь элемент управления.
- Создайте стиль или шаблон элемента управления для этого невидимого поля, который содержит все ваши триггеры и анимации.
- Попросите анимацию анимировать произвольное свойство цвета в невидимом поле.
- В визуальных деревьях для всех ваших различных элементов управления привяжите все свойства, которые вы хотите анимировать, к свойству Color невидимого корневого элемента.
Я понимаю, что на момент публикации эта проблема немного устарела, но я нашел решение, которое требует очень мало кода.
Вы можете сделать Пользовательский элемент управления с пользовательскими свойствами(Прокрутите вниз примерно до рисунка 8), который содержит ваш прямоугольник, а также анимации и триггеры состояния.Этот пользовательский элемент управления определял бы общедоступное свойство, такое как status, которое вызывало бы изменение цвета при изменении.
Единственное, что требуется для разработки кода, - это создать ваши переменные в коде.
Пользовательский элемент управления позволяет использовать их повторно снова и снова, не переписывая раскадровки 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
реализация, но все еще возможна.