Animación de cambio de propiedad WPF MVVM
animation (4)
Estoy buscando una manera limpia de comenzar una animación que tendrá valores dinámicos. Básicamente, quiero hacer una animación en la que un elemento cambie de ancho en función de los datos de otro elemento. Supongamos que tengo un TextBlock que es Text Property is Binding. Cuando esta propiedad cambia, quiero que un elemento visual diga un Rectángulo para nuestro bien para hacer una Doble Animación cambiando el ancho del valor anterior al nuevo.
Estoy tratando de evitar poner código en mi vista si es posible. He analizado DataTriggers pero parecen requerir que sepas cuál sería el valor, como un Enum. En mi caso, es solo el cambio de valor el que necesita desencadenar un guión gráfico y la animación debería comenzar en el valor actual (anterior) y moverse de forma agradable al nuevo valor.
Algunas ideas. Tal vez me perdí una propiedad.
Puede explorar el uso de Propiedades adjuntas para conectar la lógica necesaria al Guión gráfico / Animación que desee.
Esto no necesariamente le impedirá tener que escribir código, pero lo mantendrá separado de la vista y permitirá que se vuelva a utilizar en varias vistas.
Aquí está la solución con la que terminé. Para hacer la animación basada en los datos de mi ViewModel utilicé un DataTrigger. A continuación está mi estilo para el control.
<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>
Dado que las propiedades modificadas por la animación no se pueden establecer fuera del "contexto" de la animación, se me ocurrió una solución de código ya que no podía hacer lo mismo en XAML de manera efectiva.
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;
}
En realidad, desea vincular DoubleAnimation.ToProperty
a la propiedad ViewModel
y animar el control real. El problema es que la animación debería continuar cuando ToProperty
cambió. Mi solución encapsula toda esta lógica en una MarkupExtenstion
que envuelve un 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;
}
}
Puede hacer lo mismo con otras clases de animación para animar otros tipos.