Tutorial: Usando el Visual State Manager

image

Tras varios meses trabajando con Silverlight uno empieza a tener buenas práctivas y ver como es más productivo. Por la forma de programar con Silverlight nos obliga a manipular el layout de nuestra aplicación constantemente. Para ello tenemos tres opciones: programáticamente desde C#, con StoryBoards o usando el Visual State Manager (VSM a partir de ahora).

Para nuestro tutorial vamos a crear un control llamado AnimatedImage con un UserControl normal con un Image dentro.

<UserControl
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	x:Class="EugenioEstrada.AnimatedImage"
	Width="Auto" Height="Auto">
	<Grid x:Name="LayoutRoot">
		<Image x:Name="InternalImage">
		
		</Image>
	</Grid>
</UserControl>

Lo que queremos hacer con este control es sencillo, que al pasar el Mouse por encima de la imagen ésta se escale un 1,5. Y luego que al hacer Click (controlando el MouseDown y MouseUp del ratón) haga una animación. Para hacer las animaciones debemos definir los RenderTransform tanto del Grid como del Image:

<Grid x:Name="LayoutRoot" RenderTransformOrigin="0.5,0.5">
	<Grid.RenderTransform>
		<TransformGroup>
			<ScaleTransform/>
        		<RotateTransform/>
    			<SkewTransform/>
</TransformGroup> </Grid.RenderTransform> <Image x:Name="InternalImage" RenderTransformOrigin="0.5,0.5"> <Image.RenderTransform> <TransformGroup> <ScaleTransform/> </TransformGroup> </Image.RenderTransform> </Image> </Grid>

Sólo hemos definido el Scale, Rotate y Skew porque son los únicos que vamos a modificar luego. Y el RenderTransformOrigin define que el origen de las transformaciones será el punto central del elemento. Dentro del Grid vamos a definir el VSM y también debemos importar el namespace del VSM. Quedando así al final:

<UserControl
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:w="clr-namespace:System.Windows;assembly=System.Windows"
	x:Class="EugenioEstrada.AnimatedImage"
	Width="Auto" Height="Auto">
	<Grid x:Name="LayoutRoot" RenderTransformOrigin="0.5,0.5">
		<Grid.RenderTransform>
			<TransformGroup>
				<ScaleTransform/>
        			<RotateTransform/>
<SkewTransform/>
       </TransformGroup>
</Grid.RenderTransform>
        <w:VisualStateManager.VisualStateGroups>
<!-- A partir de ahora aquí -->
</w:VisualStateManager.VisualStateGroups>
        <Image x:Name="InternalImage" RenderTransformOrigin="0.5,0.5">
     <Image.RenderTransform>
        <TransformGroup>
        <ScaleTransform/>

        </TransformGroup>
        </Image.RenderTransform>
     </Image>
    </Grid>
</UserControl>

Ahora seguiremos dentro del VisualStateGroups, para hacernos una idea de como funciona el VSM  este define ciertos grupos de estados, por ejemplo para poder agrupar por estados que responden al mouse, estados comunes, etc. Sirve para agrupar estados de forma lógica reduciendo la combinatoria de éstos. Luego cada estado lo define un StoryBoard, que es el que establecerá el estado de la UI y luego se podrían definir transiciones entre estados para que no haya inconsistencias en el diseño. En nuestro caso solamente tendremos tresestados: Normal, MouseOver, MouseUp y todos los agruparemos en el VisualStateGroup que llamaremos CommonStates.

<w:VisualStateGroup x:Name="CommonStates">
	<w:VisualState x:Name="Normal"/>
	<w:VisualState x:Name="MouseOver"/>
	<w:VisualState x:Name="MouseUp"/>
</w:VisualStateGroup>

Para hacer esto recomiendo el uso de Microsoft Expression Blend (http://expression.microsoft.com) ya que es mucho más sencillo:

image

Empezaremos definiendo el estado Normal para verlo de ejemplo lo que hará es que desde un escalado del 150% pasará al 100% y esto se haría así:

<w:VisualState x:Name="Normal">
    <Storyboard>
        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
            Storyboard.TargetName="InternalImage"
            Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
            <EasingDoubleKeyFrame KeyTime="00:00:00" Value="1.5"/>
            <EasingDoubleKeyFrame KeyTime="00:00:00.2000000" Value="1"/>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
            Storyboard.TargetName="InternalImage"
            Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)">
            <EasingDoubleKeyFrame KeyTime="00:00:00" Value="1.5"/>
            <EasingDoubleKeyFrame KeyTime="00:00:00.2000000" Value="1"/>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>
</w:VisualState>

Lo más complicado es el entender el TargetProperty que es el siguiente:

Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"

No es un acceso intuitivo (del estilo InternalImage.ScaleTransform.ScaleY) esto se debe a como funcionan las DependencyProperties de WPF/Silverlight que nos dará tema para otro artículo. Pero para hacer una vista muy rápida se debe a que las DependencyProperties son métodos estáticos que se encargan de hacer el get y el set mediante eventos. Esto nos permite luego usar todas capacidades de Data Binding que tiene tanto WPF como Silverlight. De una forma muy parecida vamos a crear los otros dos estados:

<w:VisualState x:Name="MouseOver">
	<Storyboard>
		<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="InternalImage"
			Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
			<EasingDoubleKeyFrame KeyTime="00:00:00.2000000" Value="1.5"/>
		</DoubleAnimationUsingKeyFrames>
		<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="InternalImage"
			Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)">
			<EasingDoubleKeyFrame KeyTime="00:00:00.2000000" Value="1.5"/>
		</DoubleAnimationUsingKeyFrames>
	</Storyboard>
</w:VisualState>
<w:VisualState x:Name="MouseUp">
	<Storyboard>
	    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="LayoutRoot"
			Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)">
			<EasingDoubleKeyFrame KeyTime="00:00:00.1000000" Value="30"/>
			<EasingDoubleKeyFrame KeyTime="00:00:00.2000000" Value="-30"/>
			<EasingDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0"/>
		</DoubleAnimationUsingKeyFrames>
	</Storyboard>
</w:VisualState>

Ahora llega el momento de escribir un poco de C#. Debemos manejar los eventos MouseMove, MouseLeave y MouseLeftButtonUp del InternalImage:

<Image x:Name="InternalImage"
	RenderTransformOrigin="0.5,0.5"
MouseMove="InternalImage_MouseMove"
MouseLeftButtonUp="InternalImage_MouseLeftButtonUp"
MouseLeave="InternalImage_MouseLeave">

Y la clase AnimatedImage queda de la siguiente forma con los manejadores de eventos:

using System.Windows;
using System.Windows.Controls;

namespace EugenioEstrada
{
	public partial class AnimatedImage : UserControl
	{
		public AnimatedImage()
		{
			InitializeComponent();
			VisualStateManager.GoToState(this, "Normal", false);
		}

		private void InternalImage_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
		{
			VisualStateManager.GoToState(this, "MouseOver", false);
		}

		private void InternalImage_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
		{
			VisualStateManager.GoToState(this, "MouseUp", false);
		}

		private void InternalImage_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
		{
			VisualStateManager.GoToState(this, "Normal", false);
		}
	}
}

VisualStateManager es una clase estática y la firma de su método GoToState es la siguiente:

public static bool GoToState(Control control, string stateName, bool useTransitions);

El parámetro control es aquel objeto que queremos cambiarle el estado, el stateName es el nombre del estado al que queremos cambiar y el último parámetro, useTransitions, se usa para establecer si se usarán las transiciones de estado o no, en nuestro caso como no hemos definido ninguna lo establecemos en false.

Y finalmente el resultado es el siguiente:

Tras este artículo se nos abren varias opciones de publicación como Dependency Properties, Data Binding, etc.

 http://twitter.com/eugenioestrada (tweet me!)

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *