[Xamarin.Forms UI Challenge] Art News, transiciones entre páginas

Introducción

Volvemos a por un reto de interfaz de usuario con Xamarin.Forms. En este artículo, vamos a tomar como referencia un diseño de Dribbble (por Shirley Yao), que intentaremos replicar con paso a paso.

Art News

Vamos a intentar replicar la UI del diseño paso a paso en Xamarin.Forms.

Los retos del ejemplo

Vamos a replicar dos pantallas con algunos retos, pero la clave del ejemplo es la transición de elementos compartidos entre las dos páginas.

  • Listado horizontal: La llegada de CollectionView es no solo una mejora en el rendimiento a la hora de trabajar con colecciones, también con diferentes Layouts (listados horizontales, GridViews, etc.). En este ejemplo, el número de elementos en el listado horizontal es bajo, por lo que podemos también hacer uso de un sencillo StackLayout y Bindable Layout.
  • Grid de fotos en los detalles: En caso de mostrar un número limitado o concreto de fotos en la galería mostrada en la página de detalles, podría usar un Layout. Probablemente un Grid o FlexLayout. Sin embargo, en caso de no ser limitado, CollectionView nos permite mostrar un número indeterminado de fotos en dos columnas de forma sencilla.
  • Transición entre páginas: Por defecto, no tenemos soporte a transiciones de páginas en Xamarin.Forms. Contamos con dos tipos de transiciones diferentes. Por un lado, las transiciones tradiciones que implican una animación de toda la página al entrar o salir. Por otro lado, tenemos las conocidas como transcisiones de elementos compartidos. En muchas ocasiones, tenemos un elemento visual compartido entre dos páginas (por ejemplo, una imagen) para trasmitir una sensación de fluidez y continuidad. En este ejemplo, veremos ambas opciones. Es necesario crear un Custom Renderer de la NavigationPage para conseguir el objetivo. Por suerte, no partimos de cero, utilizaremos Xamarin.Plugin.SharedTransitions.
  • Animaciones: Necesitamos una sencilla animación de Fade In y translación desde la parte inferior hacia la superior al navegar a la página de detalles (además de la transición). Xamarin.Forms cuenta con una completa Api de animaciones. En el ejemplo usaremos Xamanimation que nos ofrece animaciones prepadas, Storyboard y la posibilidad de usarlo todo desde XAML.

Listado horizontal

Para crear el listado horizontal utilizaremos Bindable Layouts.

<ScrollView 
     Orientation="Horizontal"
     HorizontalScrollBarVisibility="Default"
     VerticalScrollBarVisibility="Never">
     <StackLayout 
          x:Name="Highlights"
          Padding="20, 0, 0, 36"
          Orientation="Horizontal"
          BindableLayout.ItemsSource="{Binding Author.Highlights}">
          <BindableLayout.ItemTemplate>
               <DataTemplate>
                     <Grid
                          x:Name="HighlightTemplate"
                          RowSpacing="0"
                          Style="{StaticResource HighlightStyle}">
                          ...
                    </Grid>
               </DataTemplate>
          </BindableLayout.ItemTemplate>
     </StackLayout>
</ScrollView>

Usamos un scroll horizontal (sin mostrar la barra de scroll vertical) con un StackLayout apilando los elementos horizontalmente. La clave es el uso de las propiedades BindableLayout.ItemsSource y BindableLayout.ItemTemplate.

El resultado:

Listado horizontal

NOTA: En caso de contar con un número de elementos elevado, recuerda que no tenemos virtualización, etc. al usar Bindable Layouts. En dicho caso, es más recomendable utilizar CollectionView.

Grid de fotos en los detalles

El Layout de fotos se puede conseguir de diferentes formas. Probablemente, y ante un número determinado de elementos (un número bajo de elementos) se podría usar un Grid o FlexLayout con Bindable Layouts. Sin embargo, en este caso se ha utilizado CollectionView.

<CollectionView
     Grid.Row="1"
     ItemsSource="{Binding ArtItem.Related}"
     SelectionMode="None"
     InputTransparent="True">
     <CollectionView.ItemsLayout>
           <GridItemsLayout 
                Orientation="Vertical" 
                Span="2"/>
     </CollectionView.ItemsLayout>
     <CollectionView.ItemTemplate>
          <DataTemplate>
               <templates:RelatedContentTemplate />
          </DataTemplate>
     </CollectionView.ItemTemplate>
</CollectionView>

Para mostrar las dos columnas, utilizamos la propiedad ItemsLayout con GridItemsLayout utilizando la propiedad Span para indicar el número de columnas deseadas.

El resultado:

Grid de contenido relacionado

Sencillo, ¿verdad?. La llegada de BindableLayouts y de CollectionView nos permite conseguir resultados que hasta ahora requerían la creación de Custom Controls o Custom Renderers.

Transiciones entre páginas

Llegamos al “plato fuerte” del ejemplo, las transiciones entre páginas. Vamos a utilizar una versión modificada (al momento de escribir este artículo quedan algunas Pull Request pendientea por mergear, aunque los cambios de este ejemplo acabarán estando probablemente en la librería) de Xamarin.Plugin.SharedTransitions.

La idea de la librería es:

  • Custom Renderer de NavigationPage donde permitir las transiciones tradicionales (animación de entrada y salida de una página).
  • Effects que poder aplicar a elementos de la UI para permitir aplicar transiciones de elementos compartidos.

En cualquier página (ContentPage), podemos indicar la transición a utilizar de forma sencilla utilizando el método SetBackgroundAnimation:

SharedTransitionNavigationPage.SetBackgroundAnimation(this, BackgroundAnimation.SlideFromLeft);
SharedTransitionNavigationPage.SetSharedTransitionDuration(this, 500);

Entre las opciones disponibles:

  • Fade
  • Flip
  • SlideFromLeft
  • SlideFromRight
  • SlideFromTop
  • SlideFromBottom

¿Y las transiciones de elementos conectados?

Vamos a ver como utilizarlas. Comenzando añadiendo el namespace XAML necesario:

xmlns:sharedTransitions="clr-namespace:Plugin.SharedTransitions;assembly=Plugin.SharedTransitions"

En casos básicos, por ejemplo, conectar dos elementos individuales, por ejemplo un botón en dos páginas diferentes, bastará con utilizar la propiedad Tag disponible en la clase Transition.

En caso de querer hacer transiciones entre elementos de una colección como es nuestro caso (imagen correspondiente a una plantilla usada en un Bindable Layout), necesitamos utilizar además de Tag, la propiedad TagGroup.

sharedTransitions:Transition.TagGroup="1"
sharedTransitions:Transition.Tag="{Binding Number}"

NOTA: Cada elemento debe tener un Tag único.

En la página de destino, volvemos a aplicar el mismo Tag utilizado en la página anterior.

sharedTransitions:Transition.Tag="{Binding ArtItem.Number}"

NOTA: No es necesario utilizar TagGroup en la página de destino.

¿Limitaciones?

  • De momento, entre las transiciones tradicionales soportadas se incluyen las opciones vistas previamente. Proximamente espero añadir alguna opción más (Scale, etc.).
  • Las transiciones de elementos compartidos funcionan con: Label, Image, Button. Próximamente se añadirá soporte a más elementos.
  • Funciona utilizando una NavigationPage. No tiene integración con Shell por ahora.

Animaciones

Llegamos al detalle final del ejemplo. Aunque al navegar a la página de detalles la transición realizada con la imagen que se situará como cabecera conseguirá ya un efecto de fluidez y continuidad elevado, también contamos con una animación del resto del contenido.

Xamarin.Forms cuenta con una API de animaciones completa y sencilla de utilizar. Sin embargo, vamos a conseguir el efecto buscado de forma aún más sencilla directamente desde XAML utilizando Xamanimation.

Para utilizar la librería, comenzamos añadiendo el namespace en la página de detalles:

xmlns:xamanimation="clr-namespace:Xamanimation;assembly=Xamanimation"

A continuación, vamos a crear un Storyboard:

<xamanimation:StoryBoard
     x:Key="ArtItemContentAnimation"
     Target="{x:Reference ArtItemContent}">
     <xamanimation:TranslateToAnimation TranslateY="0" Duration="300"/>
     <xamanimation:FadeInAnimation />
</xamanimation:StoryBoard>

El Storyboard nos permite realizar una animación más compleja, compuesta por otras animaciones. Vamos a hacer una animación de translación hacia arriba además de hacer animar la opacidad del contenido (de 0 a 1).

¿Y cómo se lanza la animación?

Utilizamos un Trigger, para lanzar la animación en el evento Appearing de la página:

<ContentPage.Triggers>
     <EventTrigger Event="Appearing">
          <xamanimation:BeginAnimation 
               Animation="{StaticResource ArtItemContentAnimation}" />
     </EventTrigger>
</ContentPage.Triggers>

El resultado final:

El resultado

¿Qué te parece?

En cuanto al ejemplo, esta disponible en GitHub:

Ver GitHub

Llegamos hasta aquí. Estamos ante un UI Challenge donde el mayor punto de interés recae en las transiciones. Espero que te haya resultado interesante. Pronto más y mejor. Recuerda, cualquier comentario es bienvenida en el artículo!.

Más información

[Xamarin.Forms] Transiciones entre páginas

Introducción

En todas las plataformas, las aplicaciones móviles incluyen animaciones que otorgan movimiento, fluidez y focalizan la atención del usuario en las zonas deseadas. Actualmente no son un extra o añadido en las aplicaciones, sino una parte importante en la experiencia y usabilidad de las mismas.

Como desarrolladores, debemos no solo cuidar por supuesto el correcto funcionamiento de la aplicación, sino que debemos preocuparnos también por la usabilidad y la experiencia otorgada, donde entran en juego las animaciones.

Entre el conjunto de posibilidades a la hora de animar elementos, las transiciones entre páginas son un punto destacado para conseguir trasmitir fluidez y sensación de continuidad.

¿Cómo aplicamos transiciones entre páginas en Xamarin.Forms?.

En este artículo, vamos a ver como aplicar diferentes transiciones entre páginas en aplicaciones Xamarin.Forms.

Navegar entre páginas y animaciones

Entre los diferentes patrones habituales utilizados en el desarrollo móvil, el más utilizado es la navegación en pila. En Xamarin.Forms la clase NavigationPage ofrece una experiencia de navegación jerárquica donde el usuario puede navegar a través de las páginas tanto hacia delante como hacia atrás.

Para navegar de una página a otra, la aplicación añadirá (push) una nueva página en el navigation stack o pila de navegación.

await Navigation.PushAsync (new Page());

Para navegar atrás, a la página anterior, la aplicación eliminará (pop) la página actual de la pila de navegación, y a partir de ese momento la última página disponible en la pila pasará a ser la página activa.

await Navigation.PopAsync ();

Por defecto, tanto al navegar hacia delante como hacia atrás, se aplica una transición entre páginas. Podemos desactivar la animación tanto al navegar hacia delante:

await Navigation.PushAsync (new Page(), false);

Como hacia atrás:

await Navigation.PopAsync (false);

Transiciones personalizadas

Pero…¿y si necesitamos/queremos aplicar una transición personalizada en nuestra aplicación?. Las transiciones entre páginas son un aspecto bien cubierto en cada plataforma. Accediendo a cada una de ellas, con código específico por plataforma, podremos crear experiencias personalizadas. Si, lo habrás imaginado, vamos a utilizar un Custom Renderer.

En nuestra librería portable o net standard, comenzamos creando un nuevo control que hereda de NavigationPage:

public class TransitionNavigationPage : NavigationPage
{
     public TransitionNavigationPage() : base()
     {
 
     }

     public TransitionNavigationPage(Page root) : base(root)
     {

     }
}

Necesitamos determinar que animación entre un conjunto deseamos aplicar. Para conseguir este objetivo, primero creamos una enumeración con todos los tipos diferentes de transiciones que podremos utilizar:

public enum TransitionType
{
     Fade,
     Flip,
     Scale,
     SlideFromLeft,
     SlideFromRight,
     SlideFromTop,
     SlideFromBottom
}

Creamos una BindableProperty en nuestro control:

public static readonly BindableProperty TransitionTypeProperty =
     BindableProperty.Create("TransitionType", typeof(TransitionType), typeof(TransitionNavigationPage), TransitionType.SlideFromLeft);

public TransitionType TransitionType
{
     get { return (TransitionType)GetValue(TransitionTypeProperty); }
     set { SetValue(TransitionTypeProperty, value); }
}

Todo listo en nuestro control!.

Nuestra interfaz de usuario será sumamente simple, un listado de botones.

Nuestra interfaz

En cada caso, vamos a navegar a una página de detalles aplicando el tipo de animación correspondiente a cada botón.

Utilizaremos nuestro control TransitionNavigationPage en lugar de NavigationPage:

MainPage = new TransitionNavigationPage(new MainView());

Utilizar una transición u otra es tan sencillo como establecer la propiedad TransitionType (desde XAML o C#):

transitionNavigationPage.TransitionType = TransitionType.Fade;

Transiciones en Android

En Android, desde Lollipop, se dedicó un gran esfuerzo en mejorar el sistema de transiciones disponibles. Con la llegada de Material Design, llegó una nueva oleada de opciones. Tenemos la posibilidad de aplicar transiciones entre Activities y entre Fragments.

Utilizaremos la clase FragmentTransaction que nos permite realizar diferentes operaciones entre las que se encuentra, establecer la animación a aplicar en la transición utilizando el método SetCustomAnimations.

Transiciones en iOS

En el caso de iOS, aunque dependerá del tipo de animación a aplicar, utilizaremos principalmente la clase CATransition. Esta clase permite trabajar con la funcionalidad de animaciones del Core pudiendo aplicar una animación a un Layer completo.

Código específico de plataforma

En cada plataforma, vamos a crear una nueva clase TransitionNavigationPageRenderer donde realizaremos la implementación de nuestro Custom Renderer.

[assembly: ExportRenderer(typeof(TransitionNavigationPage.Controls.TransitionNavigationPage), typeof(TransitionNavigationPageRenderer))]
namespace TransitionNavigationPage.iOS.Renderers
{
     public class TransitionNavigationPageRenderer : NavigationRenderer
     {
 
     } 
}

En Android, utilizaremos el método SetupPageTransition para modificar la animación a utilizar.

protected override void SetupPageTransition(FragmentTransaction transaction, bool isPush)
{
     switch (_transitionType)
     {
          case TransitionType.Fade:
          break;
          case TransitionType.Flip:
          break;
          case TransitionType.Scale:
          break;
          case TransitionType.SlideFromLeft:
          break;
          case TransitionType.SlideFromRight:
          break;
          case TransitionType.SlideFromTop:
          break;
          case TransitionType.SlideFromBottom:
          break;
          default:
          break;
     }
}

En el caso de iOS, utilizaremos los métodos PushViewController y PopViewController para detectar cuando navegamos hacia delante y hacia atrás para aplicar animaciones personalizadas.

Fade

Comenzamos aplicando una de las animaciones más sencillas y posiblemente utilizadas en muchas ocasiones, Fade.

En Android, creamos un archivo XML con la definición de la animación en la carpeta anim dentro de Resources.

<?xml version="1.0" encoding="utf-8" ?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
 android:fillAfter="true">
 <alpha
      android:duration="1000"
      android:fromAlpha="0.0"
      android:toAlpha="1.0" />
</set>

Utilizamos el recurso en el método SetupPageTransition en nuestro Custom Renderer:

transaction.SetCustomAnimations(Resource.Animation.fade_in, Resource.Animation.fade_out);

En el caso de iOS:

  • Establecemos la propiedad Alpha a cero.
  • Utilizando el método Animate, animamos Alpha para establecerlo a valor 1.0.
View.Alpha = 0.0f;
View.Transform = CGAffineTransform.MakeIdentity();

UIView.Animate(0.5f, 0, UIViewAnimationOptions.CurveEaseInOut,
     () =>
     {
          View.Alpha = 1.0f;
     },
     null
);

Sencillo, ¿no?.

Flip

En Android, volvemos a crear la animación:

<?xml version="1.0" encoding="utf-8" ?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
 <scale
      android:interpolator="@android:anim/linear_interpolator"
      android:fromXScale="0.0"
      android:toXScale="1.0"
      android:fromYScale="0.7"
      android:toYScale="1.0"
      android:fillAfter="false"
      android:startOffset="200"
      android:duration="200" />
 <translate
      android:fromXDelta="50%"
      android:toXDelta="0"
      android:startOffset="200"
      android:duration="200"/>
</set>

Jugamos con la escala y translación de la vista para conseguir el “efecto óptico” buscado. Y la aplicamos de nuevo, utilizando SetCustomAnimations:

transaction.SetCustomAnimations(Resource.Animation.fade_in, Resource.Animation.fade_out);

En iOS, aplicamos una animación donde aplicamos una transformación a la View.

var m34 = (nfloat)(-1 * 0.001);
var initialTransform = CATransform3D.Identity;
initialTransform.m34 = m34;
initialTransform = initialTransform.Rotate((nfloat)(1 * Math.PI * 0.5), 0.0f, 1.0f, 0.0f);

View.Alpha = 0.0f;
View.Layer.Transform = initialTransform;
UIView.Animate(0.5f, 0, UIViewAnimationOptions.CurveEaseInOut,
     () =>
     {
          View.Layer.AnchorPoint = new CGPoint((nfloat)0.5, 0.5f);
          var newTransform = CATransform3D.Identity;
          newTransform.m34 = m34;
          View.Layer.Transform = newTransform;
          View.Alpha = 1.0f;
     },
     null
);

NOTA: Utilizando ObjectAnimator podemos aplciar transformaciones mucho más efectivas para realizar esta animación en Android. Fue introducido con API Level 11 (Android 3.0).

Scale

Animación en Android:

<?xml version="1.0" encoding="utf-8" ?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
 android:fillAfter="true">
 <alpha
      android:duration="100"
      android:fromAlpha="0.0"
      android:toAlpha="1.0" />
 <scale
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:duration="1000"
      android:fromXScale="0.5"
      android:fromYScale="0.5"
      android:pivotX="50%"
      android:pivotY="50%"
      android:toXScale="1.0"
      android:toYScale="1.0" />
</set>

Y en iOS:

View.Alpha = 0.0f;
View.Transform = CGAffineTransform.MakeScale((nfloat)0.5, (nfloat)0.5);

UIView.Animate(duration, 0, UIViewAnimationOptions.CurveEaseInOut,
     () =>
     {
          View.Alpha = 1.0f;
          View.Transform = CGAffineTransform.MakeScale((nfloat)1.0, (nfloat)1.0);
     },
     null
);

En ambos casos, jugamos con la opacidad y la escala de la vista.

 

SlideFromLeft

En Android:

<?xml version="1.0" encoding="utf-8" ?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
 android:shareInterpolator="false">
     <translate
          android:fromXDelta="-100%" android:toXDelta="0%"
          android:fromYDelta="0%" android:toYDelta="0%"
          android:duration="300"/>
</set>

De las animaciones más sencillas, una simple translación.

En iOS:

var transition = CATransition.CreateAnimation();
transition.Duration = 0.5f;
transition.Type = CAAnimation.TransitionPush;
transition.Subtype = CAAnimation.TransitionFromLeft;
View.Layer.AddAnimation(transition, null);

Aprovechamos CATransition para aplicar animaciones de transición desde izquierda, derecha

SlideFromRight

En Android:

<?xml version="1.0" encoding="utf-8" ?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
 android:shareInterpolator="false">
      <translate
           android:fromXDelta="100%" android:toXDelta="0%"
           android:fromYDelta="0%" android:toYDelta="0%"
           android:duration="300" />
</set>

En iOS:

transition.Subtype = CAAnimation.TransitionFromLeft;

SlideFromTop

En Android:

<?xml version="1.0" encoding="utf-8" ?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
 android:shareInterpolator="false">
     <translate
          android:fromXDelta="0%" android:toXDelta="0%"
          android:fromYDelta="-100%" android:toYDelta="0%"
          android:duration="300" />
</set>

En iOS:

transition.Subtype = CAAnimation.TransitionFromTop;

SlideFromBottom

En Android:

<?xml version="1.0" encoding="utf-8" ?>
<set xmlns:android="http://schemas.android.com/apk/res/android" 
 android:shareInterpolator="false">
     <translate
          android:fromXDelta="0%" android:toXDelta="0%"
          android:fromYDelta="100%" android:toYDelta="0%"
          android:duration="300" />
</set>

En iOS:

transition.Subtype = CAAnimation.TransitionFromBottom;

Puedes descargar el ejemplo realizado desde GitHub:

Ver GitHub

¿Más opciones?

Realizando una combinación de las opciones realizadas podemos abordar muchas otras transiciones habituales. Existen otro tipo de transiciones que han ido ganando peso en Material Design, etc.

Por ejemplo, en Android hablamos de opciones como CircularReveal.

Circular Reveal

Podemos conseguir este efecto utilizando el método CreateCircularReveal disponible en ViewAnimationUtils.

En próximos artículos, podemos abordar de nuevo este punto viendo otras opciones como la anterior. ¿Qué transiciones sueles utilizar?, ¿cuál te gustaría ver?. Recuerda, cualquier duda o comentario es bienvenida en los comentarios de la entrada.

Más información