[Windows Phone 8.1] Trio de ases: Behaviors, Animations y VisualStates (2)

En el artículo anterior de esta serie hablamos sobre el behavior SDK, una herramienta que nos permitía realizar ciertas acciones directamente en XAML y sin necesidad de escribir código C# en respuesta a cambios de datos o lanzamiento de eventos. En este, vamos a ver otra herramienta que XAML pone a nuestra disposición: las animaciones.

En todo desarrollo existe algo en lo que debemos poner toda nuestra atención: fluidez. Que nuestra aplicación se comporte rápido y de forma fluida es muy importante. Pero muchas veces, podemos dividir la fluidez en dos conceptos: fluidez real y fluidez percibida. Y contrariamente a lo que pudiésemos pensar, es la segunda, la fluidez percibida, la más importante. Por su puesto que es muy importante que nuestra aplicación sea fluida y rápida. Pero siempre existirán puntos, como ciertos procesos de carga, que serán inevitables. Si aderezamos esos momentos con una distracción para el usuario, desviando su atención del tiempo exacto que tardamos en cargar datos, conseguiremos que este perciba la aplicación como más fluida y rápida de lo que realmente es.

Pongamos un ejemplo. Imaginad que tras cargar la página principal de nuestra aplicación y mostrarla en pantalla debemos llamar a un servicio web para obtener ciertos datos necesarios. aquí el truco para dar fluidez a la aplicación es muy sencillo: Primero mostramos la página con todo el contenido estático que podamos, para a continuación en otro hilo cargar los datos del servicio. La página principal se carga rápido, la interface no se bloquea. Podemos decir que nuestra aplicación es fluida. Pero realmente, existe un ligero desfase, en el que el usuario ve la barra de carga de la aplicación, quizás durante 500 milisegundos o un segundo. Aunque se ha cargado todo sin bloquear nada, el usuario ha tenido que esperar casi un segundo para poder empezar a usar nuestra aplicación. Bien, si sabemos que ese tiempo oscila entre 500 y 1000 milisegundos, aprovechémoslo. Por ejemplo podríamos hacer que al cargarse la pantalla, los elementos estáticos en vez de aparecer ya posicionados, vayan apareciendo en una sucesión, tener una imagen como la splash screen sobre la página que animemos para que desaparezca con un fade out, o algo por el estilo. Si ajustamos esta animación a 750 milisegundos por ejemplo, por debajo podemos al mismo tiempo pedir los datos al servicio. El resultado es que: En el mejor de los casos cuando la animación acabe la página estará operativa. En el peor de los casos quizás el usuario tendrá que esperar unos 250 milisegundos a obtener todo. Pero fijaros que hemos pasado de una espera de 500 a 1000 milisegundos a otra de entre 0 y 250 milisegundos. No hemos hecho que nuestra aplicación sea más rápida, pero si hemos mejorado la fluidez percibida. Todo esto gracias además a que las animaciones en XAML son totalmente asíncronas, mientras no bloqueemos el hilo de interface de usuario, podemos ejecutar código al mismo tiempo que lanzamos una animación. ¿Qué os parece el truco?

Además de esta utilidad “práctica”, las animaciones también cumplen un papel claramente estético. usar una pequeña y simple animación para mostrar un elemento o resaltarlo, como veremos hoy, pueden mejorar el aspecto de nuestra aplicación muchísimo. Así que empecemos a meternos en faena, viendo como crear animaciones en Windows Phone 8.1, Windows 8.1 y apps universales.

Storyboard, Animations y KeyFrames.

Toda animación en XAML se encuadra dentro de un elemento Storyboard. El elemento Storyboard define la duración, el auto rebobinado, el comportamiento de repetición y más cosas mediante varias propiedades:

  • Duration. Nos permite expresar el tiempo como un TimeSpan (hh:mm:ss.ms) se trata del tiempo total que va a durar la animación en completarse.
  • BeginTime. Expresa, también como un TimeSpan, el tiempo a esperar, una vez comenzada la ejecución de la animación, antes de ejecutar el primer frame. Es muy útil si queremos lanzar y sincronizar o encadenar distintas animaciones.
  • FillBehavior. Aporta dos opciones: HoldEnd y Stop. Indica el comportamiento a llevar a cabo al terminar la animación. HoldEnd es el valor por defecto y mantiene la animación en el último cuadro de la misma. Stop para la animación y vuelve al estado inicial.
  • AutoReverse: indica si al terminar se debe ejecutar la animación al revés automáticamente.
  • RepeatBehavior: Permite especificar el comportamiento de repetición: Puede contener un número que indica el número de veces que se repetirá la animación, 0 para nunca, o la palabra Forever, que indica que nunca dejará de repetirse a no ser que la paremos explícitamente.
  • SpeedRatio. La velocidad de reproducción. Podemos considerarlo como un avance rápido de la animación.

La clase Storyboard también contiene dos métodos: Begin y Pause, que nos permiten comenzar o parar la reproducción de la animación. Así mismo dispondremos del evento Completed para saber cuando ha terminado. En XAML, Un Storyboard con una duración de 2 segundos, 1 repetición y auto rebobinado tendría este aspecto:

Sample storyboard
<Storyboard x:Key="SampleStoryboard" RepeatBehavior="1" Duration="0:0:2" AutoReverse="True">
</Storyboard>

Al declarar un Storyboard usamos el identificador x:Key. Esto se debe a que una Storyboard es un recurso, ya sea de la aplicación, de la página o de un control.

Lo siguiente que tenemos que hacer es definir la colección de animaciones a usar en el Storyboard. Disponemos de diferentes tipos de animaciones, dependiendo del tipo de la propeidad que deseemos modificar. De cada tipo existen dos variantes: Una variante simple, que nos permite indicar un valor final y una duración y una variante más completa que nos permite definir una colección de cuadros para la animación:

  • DoubleAnimation / DoubleAnimationUsingKeyFrames, para animar propiedades numéricas.
  • ColorAnimation / ColorAnimationUsingKeyFrames, para animar propiedades basadas en colores.
  • PointAnimation / PointAnimationUsingKeyFrames, para animar propiedades basadas en puntos.
  • ObjectAnimationUsingKeyFrames, para animar cualquier otro tipo de propiedad.

Dentro de la animación usaremos las propiedades Storyboard.TargetName y Storyboard.Property para definir el elemento y la propiedad que deseamos animar. En este sentido debemos tener en cuenta que, en un mismo Storyboard no podremos definir dos animaciones distintas para una misma propiedad de un mismo elemento. Esto no es problema puesto que al poder usar KeyFrames, dentro de una animación podemos definir tantos cambios como queramos. Por ejemplo podemos animar la propiedad Opacity de un elemento de la siguiente forma usando una DoubleAnimationUsingKeyFrames:

DoubleAnimationUsingKeyFrames
<Storyboard x:Key="SampleStoryboard" RepeatBehavior="1" Duration="0:0:2" AutoReverse="True">
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScoreCard" Storyboard.TargetProperty="Opacity">
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

A continuación ya solo nos queda definir los frames de la animación. Cada frame es un instante en el tiempo de la animación en el que definimos un valor para la propiedad que estamos animando. Cada tipo de animación soporta cuatro tipos de frames:

  • Linear, que usa una interpolación lineal entre frames para animar la propiedad.
  • Discrete, que usa una interpolación discreta entre frames para animar la propiedad.
  • Easing, que usa una función de easing para modificar la interpolación entre frames.
  • Spline que usa un Spline para definir la interpolación entre frames.

Normalmente utilizaremos Lineal y Easing dependiendo de si queremos una animación uniforme o aplicar efectos de rebote, aceleración o deceleración entre cuadros. El uso es el mismo en ambos casos, debemos indicar el tiempo dentro del Storyboard en el que deseamos ejecutar cada frame y el valor que debe tener la propiedad animada en ese momento. A mayores, en el caso del Easing podremos indicar una función de easing para aplicar el efecto deseado:

 

LinearDoubleKeyFrame
<Storyboard x:Key="SampleStoryboard" RepeatBehavior="1" Duration="0:0:2" AutoReverse="True">
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScoreCard" Storyboard.TargetProperty="Opacity">
        <LinearDoubleKeyFrame KeyTime="0:0:2" Value="1"/>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

 

 

EasingDoubleKeyFrame
<Storyboard x:Key="SampleStoryboard" RepeatBehavior="1" Duration="0:0:2" AutoReverse="True">
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScoreCard" Storyboard.TargetProperty="Opacity">
        <EasingDoubleKeyFrame KeyTime="0:0:2" Value="1">
            <EasingDoubleKeyFrame.EasingFunction>
                <CubicEase EasingMode="EaseOut"/>
            </EasingDoubleKeyFrame.EasingFunction>
        </EasingDoubleKeyFrame>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

 

Y ya tenemos lista nuestra animación. En ambos casos animamos la propiedad Opacity de un elemento llamado ScoreCard hasta un valor de 1. No indicamos el valor inicial de la propiedad, así la animación tomará como valor inicial el que tenga la propiedad en el momento en que se ejecute la animación. En el caso del EasingDoubleKeyFrame además definimos la función de easing, un Cubic ease en modo out. Esto hace que la animación haga un efecto de aceleración.

Como hemos mencionado antes, dentro de un Storyboard podemos definir distintas animaciones para conseguir efectos más complejos. Incluso podemos combinar distintas animaciones (jugando con la propiedad BeginTime). En el ejemplo adjunto a este artículo tenemos una animación que cambia el color y la escala de un TextBlock con un número y además muestra unos rayos girando a toda pantalla:

image

Los rayos no aparecen de golpe, se anima el tamaño y la opacidad para que aparezcan expandiéndose y luego girando. El código de la animación completa es el siguiente:

Rays animation
<Page.Resources>
    <Storyboard x:Key="ShowSuccess">
        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Rays" Storyboard.TargetProperty="Opacity">
            <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value=".5">
                <EasingDoubleKeyFrame.EasingFunction>
                    <CubicEase EasingMode="EaseOut"/>
                </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Rays" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)">
            <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="15">
                <EasingDoubleKeyFrame.EasingFunction>
                    <CubicEase EasingMode="EaseOut"/>
                </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="Rays" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)">
            <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="15">
                <EasingDoubleKeyFrame.EasingFunction>
                    <CubicEase EasingMode="EaseOut"/>
                </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScoreCard" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)">
            <EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="1.2">
                <EasingDoubleKeyFrame.EasingFunction>
                    <CubicEase EasingMode="EaseOut"/>
                </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScoreCard" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)">
            <EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="1.2">
                <EasingDoubleKeyFrame.EasingFunction>
                    <CubicEase EasingMode="EaseOut"/>
                </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ScoreCard" Storyboard.TargetProperty="Opacity">
            <EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="1">
                <EasingDoubleKeyFrame.EasingFunction>
                    <CubicEase EasingMode="EaseOut"/>
                </EasingDoubleKeyFrame.EasingFunction>
            </EasingDoubleKeyFrame>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>
    <Storyboard x:Key="AnimateRays" RepeatBehavior="Forever" BeginTime="0:0:0.1" FillBehavior="HoldEnd" >
        <DoubleAnimationUsingKeyFrames EnableDependentAnimation="True" Storyboard.TargetName="Rays" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.Rotation)">
            <LinearDoubleKeyFrame KeyTime="0:0:12" Value="500"/>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>
</Page.Resources>

Quizás lo más extraño de esta animación es la forma de definir las propiedades. en vez de usar una propiedad del elemento estamos usando una transformación para poder rotarlo o escalarlo, por eso la animación se define como (UIElement.RenderTransform).(CompositeTransform.ScaleY):

  • (UIElement.RenderTransform) Aquí indicamos que de un UIElement (Todos los elementos en XAML heredan de la clase base UIElement), deseamos acceder a su propiedad RenderTransform.
  • (CompositeTransfrom.ScaleY) indicamos que dentro de la propiedad RenderTransform tenemos definido un CompositeTransform y que deseamos animar la propiedad ScaleY del composite transform.

En el TextBlock ScoreCard, definimos el RenderTransform de la siguiente forma:

CompositeTransform
<TextBlock x:Name="ScoreCard" Text="{Binding ScoreValue, Mode=TwoWay}" FontSize="64" Width="130" Height="70"
           TextAlignment="Center" FontWeight="Bold" Grid.Row="1" Opacity=".5" VerticalAlignment="Center"
           HorizontalAlignment="Center">
    <TextBlock.RenderTransform>
        <CompositeTransform CenterX="65" CenterY="35" ScaleX="1" ScaleY="1"/>
    </TextBlock.RenderTransform>
</TextBlock>

Por último, solo nos queda iniciar las animaciones en el momento justo. Aquí es donde entran en juego los behaviors que explicamos en el artículo anterior. En este caso podemos hacer uso de la acción ControlStoryboardAction del namespace Microsoft.Xaml.Interaction.Media. Como disparador, podemos usar el click de un botón:

ControlStoyboardAction (1)
<Button Content="play">
    <i:Interaction.Behaviors>
        <core:EventTriggerBehavior EventName="Tapped">
            <media:ControlStoryboardAction ControlStoryboardOption="Play" Storyboard="{StaticResource ShowSuccess}"/>
            <media:ControlStoryboardAction ControlStoryboardOption="Play" Storyboard="{StaticResource AnimateRays}"/>
        </core:EventTriggerBehavior>
    </i:Interaction.Behaviors>
</Button>

Simplemente indicamos el evento, Tapped, y lo que queremos hacer con cada Storyboard (Play, Pause, Stop…) y por último el Storyboard afectado y listo! Pero pensemos por un momento, que queremos mostrar esta animación cuando el usuario completa el 100% de una tarea o llega a un hito concreto en nuestra aplicación. Para esto, podemos usar el disparador basado en datos (DataTiggerBehavior). Lo podemos definir por ejemplo en el TextBlock ScoreCard:

ControlStoryboardAction(2)
<TextBlock x:Name="ScoreCard" Text="{Binding ScoreValue, Mode=TwoWay}" FontSize="64" Width="130" Height="70"
           TextAlignment="Center" FontWeight="Bold" Grid.Row="1" Opacity=".5" VerticalAlignment="Center"
           HorizontalAlignment="Center">
    <TextBlock.RenderTransform>
        <CompositeTransform CenterX="65" CenterY="35" ScaleX="1" ScaleY="1"/>
    </TextBlock.RenderTransform>
    <i:Interaction.Behaviors>
        <core:DataTriggerBehavior Binding="{Binding ScoreValue}" ComparisonCondition="GreaterThanOrEqual" Value="100">
            <media:ControlStoryboardAction ControlStoryboardOption="Play" Storyboard="{StaticResource ShowSuccess}"/>
            <media:ControlStoryboardAction ControlStoryboardOption="Play" Storyboard="{StaticResource AnimateRays}"/>
        </core:DataTriggerBehavior>
    </i:Interaction.Behaviors>
</TextBlock>

En este caso, indicamos al DataTriggerBehavior la propiedad que nos interesa de nuestra ViewModel (ScoreValue), la condición de comparación que debe cumplir (GreaterThanOrEqual) y el valor contra el que compararlo (100 en este caso).

En nuestra ViewModel, simplemente incrementamos la propiedad y notificamos el cambio:

ScoreValue
public int ScoreValue
{
    get { return this.scoreValue; }
    set
    {
        this.scoreValue = value;
        RaisePropertyChanged();
    }
}

Sin tener que escribir ningún código extra, cuando la propiedad ScoreValue llegue a 100, se ejecutará la animación ShowSuccess y AnimateRays. Como podemos observar, este último método nos ofrece la gran ventaja de hacer que nuestras vistas sean más ricas y reaccionen de forma animada a los cambios de los datos de nuestras ViewModels, abriendo un gran abanico de posibilidades para nuestra UI.

Por supuesto, algo a tener muy en cuenta es que, al igual que pasa con los behaviors, el sistema de animación es totalmente universal y compartido entre Windows phone y Windows store. De echo, todo el código de este artículo está compartido en un proyecto universal: la página, las animaciones y las viewmodels. No hay ni una sola línea de código C# o XAML en los proyectos específicos de la plataforma.

Y hasta aquí llega este segundo artículo. En el próximo colocaremos la última pieza de este trio de ases, los VisualStates, que nos permitirán crear páginas totalmente adaptables al momento, datos, situación… Pero para eso todavía os haré esperar un poco más. Mientras tanto, podéis descargaros de aquí el código completo y funcional de este artículo y ejecutarlo para ver lo bien que quedan las animaciones y lo sencilla que es hacerlas.

Un saludo y Happy Coding!!

Published 9/6/2014 1:01 por Josué Yeray Julián Ferreiro
Comparte este post:

Comentarios

# [Windows Phone 8.1] Trio de ases: Behaviors, Animations y VisualStates (3)

Tuesday, June 17, 2014 3:24 PM por Josue Yeray

Y llegamos al último artículo de esta serie, tras ver como trabajar con behaviors y animaciones