Sugerencias y trucos para animaciones
Actualización: noviembre 2007
Cuando se trabaja con animaciones en WPF, hay varias sugerencias y trucos que pueden mejorar el rendimiento de las animaciones y evitar muchas frustraciones.
Problemas generales
Al animar la posición de una barra de desplazamiento o de un control deslizante, se inmoviliza
Si anima la posición de una barra de desplazamiento o de un control deslizante utilizando una animación cuyo FillBehavior es HoldEnd (el valor predeterminado), el usuario ya no podrá mover la barra de desplazamiento o el control deslizante. Esto se debe a que, aunque la animación finaliza, continúa invalidando el valor base de la propiedad de destino. Para que la animación deje de invalidar el valor actual de la propiedad, quítelo o asigne a FillBehavior el valor Stop. Para obtener más información y un ejemplo, consulte Cómo: Establecer una propiedad después de animarla con un guión gráfico.
Animar el resultado de una animación no surte ningún efecto
No se puede animar ningún objeto que sea el resultado de otra animación. Por ejemplo, si utiliza un objeto ObjectAnimationUsingKeyFrames para animar la propiedad Fill de un objeto Rectangle desde RadialGradientBrush hasta SolidColorBrush, no podrá animar propiedad del objeto RadialGradientBrush ni de SolidColorBrush.
No se puede cambiar el valor de una propiedad después de animarla
En algunos casos, puede parecer que no es posible cambiar el valor de una propiedad después de animarla, incluso después de que la animación haya finalizado. Esto se debe a que, aunque la animación finaliza, continúa invalidando el valor base de la propiedad. Para que la animación deje de invalidar el valor actual de la propiedad, quítelo o asigne a FillBehavior el valor Stop. Para obtener más información y un ejemplo, consulte Cómo: Establecer una propiedad después de animarla con un guión gráfico.
Cambiar una escala de tiempo no surte ningún efecto
Aunque la mayoría de las propiedades Timeline se pueden animar y enlazar a datos, cambiar los valores de propiedad de un objeto Timeline activo parece no surtir ningún efecto. Esto se debe a que, cuando la Timeline se inicia, el sistema de control de tiempo realiza una copia de Timeline y la utiliza para crear un objeto Clock. Modificar el original no surte ningún efecto en la copia del sistema.
Para que una Timeline refleje los cambios, deberá volver a generarse su reloj y utilizarlo para reemplazar el reloj previamente creado. Los relojes no se regeneran automáticamente. A continuación, se muestran distintas maneras de aplicar cambios a las escalas de tiempo:
Si la escala de tiempo es un objeto Storyboard o pertenece a esta clase, para reflejar los cambios puede volver a aplicar su guión gráfico a través del objeto BeginStoryboard o del método Begin. Esto tiene el efecto secundario de reiniciar también la animación. En el código, puede utilizar el método Seek para hacer avanzar el guión gráfico de nuevo hasta su posición anterior.
Si aplicó una animación directamente a una propiedad utilizando el método BeginAnimation, llame de nuevo al método BeginAnimation y pásele la animación que se ha modificado.
Si está trabajando directamente en el nivel de relojes, cree y aplique un nuevo conjunto de relojes y utilícelos para reemplazar el conjunto anterior de relojes generados.
Para obtener más información sobre escalas de tiempo y relojes, consulte Información general sobre sistemas de temporización y animación.
FillBehavior.Stop no funciona como se espera
En ocasiones, parece que establecer la propiedad FillBehavior en Stop no surte ningún efecto, como cuando una animación "se entrega" a otra porque el valor de su propiedad HandoffBehavior es SnapshotAndReplace.
En el siguiente ejemplo, se crean una conexión Canvas, un objeto Rectangle y un TranslateTransform. TranslateTransform se animará para mover Rectangle alrededor de Canvas.
<Canvas Width="600" Height="200">
<Rectangle
Canvas.Top="50" Canvas.Left="0"
Width="50" Height="50" Fill="Red">
<Rectangle.RenderTransform>
<TranslateTransform
x:Name="MyTranslateTransform"
X="0" Y="0" />
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
En los ejemplos de esta sección se utilizan los objetos anteriores para mostrar varios casos en que la propiedad FillBehavior no se comporta como cabría esperar.
FillBehavior="Stop" y HandoffBehavior con varias animaciones
A veces, parece como si una animación omitiese su propiedad FillBehavior cuando la reemplaza una segunda animación. Tomemos el ejemplo siguiente, en el que se crean dos objetos Storyboard y se utilizan para animar la misma transformación TranslateTransform mostrada en el ejemplo anterior.
El primer Storyboard, B1, anima la propiedad X de TranslateTransform desde 0 hasta 350, lo que mueve el rectángulo 350 píxeles a la derecha. Cuando la animación alcanza el fin de su duración y su reproducción se detiene, la propiedad X revierte a su valor original, 0. Como resultado, el rectángulo se mueve 350 píxeles a la derecha y, a continuación, salta para situarse en su posición original.
<Button Content="Start Storyboard B1">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard x:Name="B1">
<DoubleAnimation
Storyboard.TargetName="MyTranslateTransform"
Storyboard.TargetProperty="X"
From="0" To="350" Duration="0:0:5"
FillBehavior="Stop"
/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
El segundo Storyboard, B2, también anima la propiedad X de la misma TranslateTransform. Dado que se establece únicamente la propiedad To de la animación en este Storyboard, la animación utiliza el valor actual de la propiedad que anima como su valor inicial.
<!-- Animates the same object and property as the preceding
Storyboard. -->
<Button Content="Start Storyboard B2">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard x:Name="B2">
<DoubleAnimation
Storyboard.TargetName="MyTranslateTransform"
Storyboard.TargetProperty="X"
To="500" Duration="0:0:5"
FillBehavior="Stop" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
Si hace clic en el segundo botón mientras el primer Storyboard se está reproduciendo, cabría esperar el comportamiento siguiente:
El primer guión gráfico finaliza y envía el rectángulo a su posición original, porque el valor de la propiedad FillBehavior de la animación es Stop.
El segundo guión gráfico se lleva a efecto y anima el objeto a partir de la posición actual, que ahora es 0, hasta 500.
Pero esto no es lo que sucede. En lugar de ello, el rectángulo no salta a su posición original, sino que continúa moviéndose a la derecha. Esto se debe a que la segunda animación utiliza el valor actual de la primera animación como su valor inicial y anima desde ese valor hasta 500. Cuando la segunda animación reemplaza a la primera porque se utiliza el valor SnapshotAndReplace de HandoffBehavior, el valor de FillBehavior de la primera animación no se tiene en cuenta.
FillBehavior y el evento Completed
En los ejemplos siguientes se muestra otro escenario en el que el valor Stop de la propiedad FillBehavior parece no surtir ningún efecto. De nuevo, en el ejemplo se utiliza un guión gráfico para animar la propiedad X de la transformación TranslateTransform desde 0 hasta 350. Sin embargo, esta vez en el ejemplo se efectúa el registro para el evento Completed.
<Button Content="Start Storyboard C">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard Completed="StoryboardC_Completed">
<DoubleAnimation
Storyboard.TargetName="MyTranslateTransform"
Storyboard.TargetProperty="X"
From="0" To="350" Duration="0:0:5"
FillBehavior="Stop" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
El controlador de eventos Completed inicia otro Storyboard que anima la misma propiedad desde su valor actual hasta 500.
private void StoryboardC_Completed(object sender, EventArgs e)
{
Storyboard translationAnimationStoryboard =
(Storyboard)this.Resources["TranslationAnimationStoryboardResource"];
translationAnimationStoryboard.Begin(this);
}
A continuación, se muestra el marcado que define el segundo Storyboard como recurso.
<Page.Resources>
<Storyboard x:Key="TranslationAnimationStoryboardResource">
<DoubleAnimation
Storyboard.TargetName="MyTranslateTransform"
Storyboard.TargetProperty="X"
To="500" Duration="0:0:5" />
</Storyboard>
</Page.Resources>
Al ejecutar Storyboard, cabría esperar que la propiedad X de TranslateTransform se anime de 0 a 350, y luego revierta a 0 después de completarse (dado que su valor de FillBehavior es Stop), para, por último, animarse desde 0 hasta 500. En cambio, TranslateTransform se anima de 0 a 350 y, a continuación, hasta 500.
Esto se debe al orden en el que WPF provoca los eventos y al hecho de que los valores de la propiedad se almacenan en la memoria caché y no se actualizan a menos que se invalide la propiedad. El evento Completed se procesa en primer lugar porque lo activa la escala de tiempo raíz (el primer Storyboard). En este momento, la propiedad X sigue devolviendo el valor animado, porque todavía no se ha invalidado. El segundo Storyboard utiliza el valor almacenado en memoria caché como su valor inicial y comienza la animación.
Rendimiento
Las animaciones siguen ejecutándose después de salir de una página
Cuando se navega para salir de un control Page que contiene animaciones en ejecución, éstas continuarán reproduciéndose hasta que se efectúe la recolección de elementos no utilizados de Page. Según el sistema de navegación que se utilice, la página de la que ha salido al navegar podría permanecer en memoria por tiempo indefinido, durante el cual seguiría consumiendo recursos con sus animaciones. Esto resulta especialmente patente cuando una página contiene animaciones de ejecución continua ("ambiente").
Por esta razón, es conveniente utilizar el evento Unloaded para quitar las animaciones cuando se sale de una página al navegar.
Hay diferentes maneras de quitar una animación. Las técnicas siguientes se pueden utilizar para quitar animaciones que pertenecen a un objeto Storyboard.
Para quitar un Storyboard iniciado con un desencadenador de eventos, consulte Cómo: Quitar un guión gráfico.
Para quitar un Storyboard mediante código, consulte el método Remove.
La técnica siguiente se puede utilizar sin tener en cuenta cómo se inició la animación.
- Para quitar animaciones de una propiedad concreta, utilice el método BeginAnimation(DependencyProperty, AnimationTimeline). Especifique la propiedad animada como primer parámetro y null, como segundo. De este modo, se quitarán todos los relojes de animación de la propiedad.
Para obtener más información sobre los distintos modos de animar propiedades, consulte Información general sobre técnicas de animación de propiedades.
Utilizar el valor Compose de HandoffBehavior consume recursos del sistema
Cuando se aplica un objeto Storyboard, AnimationTimeline o AnimationClock a una propiedad utilizando el valor Compose de HandoffBehavior, los objetos Clock asociados con anterioridad a esa propiedad siguen utilizando recursos del sistema; el sistema de control de tiempo no quitará estos relojes automáticamente.
Para evitar problemas de rendimiento cuando aplique muchos relojes mediante Compose, quite los relojes de composición de la propiedad animada cuando se hayan completado. Hay varias formas de quitar un reloj.
Para quitar todos los relojes de una propiedad, utilice el método ApplyAnimationClock(DependencyProperty, AnimationClock) o BeginAnimation(DependencyProperty, AnimationTimeline) del objeto animado. Especifique la propiedad animada como primer parámetro y null, como segundo. De este modo, se quitarán todos los relojes de animación de la propiedad.
Para quitar un objeto AnimationClock específico de una lista de relojes, utilice la propiedad Controller de AnimationClock para recuperar un objeto ClockController y, después, llame al método Remove de ClockController. Normalmente se hace en el controlador del evento Completed para un reloj. Observe que ClockController sólo puede controlar los relojes de raíz; la propiedad Controller de un reloj secundario devolverá null. Tenga en cuenta también que no se provocará el evento Completed si la duración real del reloj es para siempre. En ese caso, el usuario deberá determinar cuándo llamar a Remove.
Este problema se produce principalmente en las animaciones de objetos que tienen un período de duración prolongado. Cuando un objeto se recolecta como elemento no utilizado, sus relojes también se desconectan y se recolectan como elementos no utilizados.
Para obtener más información acerca de los objetos de reloj, consulte Información general sobre sistemas de temporización y animación.