Creando la interfaz de Netflix con Xamarin.Forms

Introducción

La evolución de Xamarin.Forms es meritoria. En los últimos tiempos se han recibido novedades interesantes como efectos, vistas nativas, Forms Embedding, etc. Sin embargo, en muchos casos se sigue asociado a desarrollos muy simples o formularios básicos.

Realmente, en el estado actual de Xamarin.Forms se pueden conseguir aplicaciones nativas de gran escala, con interfaces cuidadas y con alta integración con la plataforma. Hay que tener en cuenta el conjunto de Custom Renderers (código específico en cada plataforma) necesario para lograrlo.

NOTA: La elección entre Xamarin Classic o Xamarin.Forms es importante. Es necesario evaluar la aplicación a desarrollar, el conjunto de características específicas de cada plataforma (que pueden requerir un Custom Renderer), etc. 

En este artículo, vamos a tomar como referencia una aplicación bastante conocida, disponible en las listas de destacados de las diferentes tiendas, Netflix. Vamos a desarrollar la interfaz de la aplicación móvil de Netflix con Xamarin.Forms paso a paso.

¿Cuántos Custom Renderers crees que serán necesarios?, ¿qué cantidad de código podremos compartir?, ¿tendremos buen resultado final?.

La pantalla de selección de perfil

Vamos a comenzar partiendo de una pantalla bastante representativa y común en Netflix, la selección de usuario o perfil.

Vamos a analizar características de la pantalla y a determinar que vamos a necesitar:

  • Se puede ver una barra de navegación. Algo que podemos conseguir de forma sencilla en Xamarin.Forms gracias al uso de NavigationPage.
  • En la barra de navegación se muestra una imagen, el logo de la aplicación. Para poder ajustar la imagen, tamaño y posición, vamos a necesitar un Custom Renderer en el caso de iOS. Para Android, podemos utilizar layout AXML con facilidad.
  • En la barra de navegación también tenemos un botón de edición. Nada complejo que podemos abordar utilizando ToolbarItem.
  • Y llegamos al listado de opciones de perfil para elegir. La mayor característica que diferencia al listado es ele uso de columnas. Utilizaremos FlowListView.

Añadir el logo en la barra de navegación

En Android tenemos la definición de la Toolbar en Resources > layout >Toolbar.axml. Para añadir una imagen como logo y poder configurarla a nuestro antojo:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/toolbar"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:background="?attr/colorPrimary"
 android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
 android:popupTheme="@style/ThemeOverlay.AppCompat.Light"
 android:elevation="5dp">
      <ImageView
           android:id="@+id/logoImageLayout"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:adjustViewBounds="true"
           android:src="@drawable/netflix" />
</android.support.v7.widget.Toolbar>

El caso de iOS es ligeramente diferente.

public class LogoPageRenderer : PageRenderer
{
     public override void ViewWillAppear(bool animated)
     {
          base.ViewWillAppear(animated);

          var image = UIImage.FromBundle("netflix.png");
          var imageView = new UIImageView(new CGRect(0, 0, 140, 70));

          imageView.ContentMode = UIViewContentMode.ScaleAspectFit;
          imageView.Image = image.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);

          if (NavigationController != null)
          {
               NavigationController.TopViewController.NavigationItem.TitleView = imageView;
          }
     }
}

Creamos un sencillo Custom Renderer donde vamos a añadir un UIImageView como nuestro logo de la TitleView de UINavitationItem.

Botones en la barra de navegación

Este bloque no tiene gran complejidad ya que Xamarin.Forms cuenta con soporte directo para poder añadir y gestionar botones de la Toolbar.

<ContentPage.ToolbarItems>
     <ToolbarItem
          Icon="edit"/>
</ContentPage.ToolbarItems>

Listado con columnas

¿Cómo podemos gestionar esta necesidad?. Tenemos muchas opciones:

  • Crear un control personalizado de Layout.
  • Crear un control más simple utilizando un ScrollView y un Grid.
  • Utilizar algo existente que nos otorge esta solución.

La comunidad Xamarin y el conjunto de plugins, controles y librerías disponibles es elevada. Vamos a utilizar FlowListView.

FlowListView permite crear listados con columnas además de permitir:

  • Carga infinita.
  • Cualquier contenido como celda.
  • DataTemplateSelector.
  • Grupos.
  • Etc.

Añadimos el paquete NuGet a todos los proyectos de la solución. Debemos realizar la inicialización de FlowListView en nuestra librería compartida, App.xaml.cs:

FlowListView.Init();

A continuación, y tras declarar el espacio de nombres en la página XAML de selección de perfil:

xmlns:flv="clr-namespace:DLToolkit.Forms.Controls;assembly=DLToolkit.Forms.Controls.FlowListView"

Utilizamos el control:

<flv:FlowListView
     FlowItemsSource="{Binding Profiles}"
     FlowItemTappedCommand="{Binding HomeCommand}"
     BackgroundColor="{StaticResource BackgroundColor}"
     FlowColumnCount="2" 
     FlowColumnExpand="First"
     SeparatorVisibility="None"
     HasUnevenRows="True">
     <flv:FlowListView.FlowColumnTemplate>
          <DataTemplate>
               <templates:ProfileItemTemplate />
          </DataTemplate>
     </flv:FlowListView.FlowColumnTemplate>
</flv:FlowListView>

Definimos dos columnas con la propiedad FlowColumnCount.

Vamos a ver el resultado final de lo que llevamos, en Android:

Selección de perfil en Android

Y en iOS:

Selección de perfil en iOS

Vamos bastante bien, ¿continuamos?.

Pantalla principal

Continuamos con la pantalla principal y al igual que hicimos previamente, vamos a realizar un análisis previo de que vamos a necesitar:

  • Tenemos un menú lateral deslizante. Un patrón de navegación muy conocido y utilizado en una gran variedad de aplicaciones. En Xamarin.Forms podemos utilizar este patrón de navegación utilizando una MasterDetailPage.
  • Se continua mostrando el logo en la barra superior. Continuaremos utilizando lo previamente realizado.
  • Contamos con un botón de búsqueda en la parte superior. De nuevo, volveremos a hacer uso de ToolbarItem.
  • El contenido se caracteriza por contar con diferentes grupos o bloques de elementos a los que accedemos realizando scroll horizontal. Para conseguir este objetivo, vamos a realizar un control personal.

Menú lateral deslizante

Al navegar al apartado principal, necesitaremos utilizar una MasterDetailPage.

<MasterDetailPage 
     xmlns="http://xamarin.com/schemas/2014/forms"
     xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
     x:Class="Xamarin.Netflix.Views.MainView" 
     xmlns:views="clr-namespace:Xamarin.Netflix.Views"
     MasterBehavior="Popover">
     <MasterDetailPage.Master>
          <views:MenuView
               BindingContext="{Binding MenuViewModel}"/>
     </MasterDetailPage.Master>
     <MasterDetailPage.Detail>
          <ContentPage 
               BackgroundColor="Transparent" />
     </MasterDetailPage.Detail>
</MasterDetailPage>

MasterDetailPage cuenta con dos propiedades fundamentales:

  • Master: Contenido del menú lateral deslizante.
  • Detail: Vista que visualizamos.

En Master vamos a definir el menú principal de Netflix un poco más adelante. En Detail, vamos a definir en una ContentPage la vista principal.

Listado horizontal

Necesitamos utilizar un listado horizontal. Ante un volumen elevado de elementos debemos de pensar en aspectos como el rendimiento (caché, reutilización del celdas, etc.). En este caso, cada listado horizontal cuenta con un volumen bajo de elementos.

Vamos a realizar un control personal con código totalmente compartido y utilizando elementos de Xamarin.Forms. Vamos a utilizar un ScrollView junto a un StackLayout apilando elementos horizontalmente. Veamos el constructor del control con la definición básica del mismo:

public HorizontalList()
{
      _scrollView = new ScrollView();
      _itemsStackLayout = new StackLayout
     {
          Padding = Padding,
          Spacing = Spacing,
          HorizontalOptions = LayoutOptions.FillAndExpand
     };

     _scrollView.Content = _itemsStackLayout;
     Children.Add(_scrollView);
}

Necesitamos definir la fuente de información, como se verá cada elemento, etc. Para ello, definimos una serie de BindableProperties:

public static readonly BindableProperty SelectedCommandProperty =
     BindableProperty.Create("SelectedCommand", typeof(ICommand), typeof(HorizontalList), null);

public static readonly BindableProperty ItemsSourceProperty =
     BindableProperty.Create("ItemsSource", typeof(IEnumerable), typeof(HorizontalList), default(IEnumerable<object>), BindingMode.TwoWay, propertyChanged: ItemsSourceChanged);

public static readonly BindableProperty SelectedItemProperty =
     BindableProperty.Create("SelectedItem", typeof(object), typeof(HorizontalList), null, BindingMode.TwoWay, propertyChanged: OnSelectedItemChanged);

public static readonly BindableProperty ItemTemplateProperty =
     BindableProperty.Create("ItemTemplate", typeof(DataTemplate), typeof(HorizontalList), default(DataTemplate));

A la hora de utilizar el control:

<Label
     Text="Continue watching"
     Style="{StaticResource TitleStyle}">
</Label>
<controls:HorizontalList 
     ListOrientation="Horizontal" 
     ItemsSource="{Binding Watching}"
     HeightRequest="200">
     <controls:HorizontalList.ItemTemplate>
          <DataTemplate>
               <templates:WatchingItemTemplate />
          </DataTemplate>
     </controls:HorizontalList.ItemTemplate>
</controls:HorizontalList>

Dentro de un StackLayout (apila elementos por defecto verticalmente) repetimos la misma estructura para mostrar diferentes grupos (últimas novedades, películas, series, etc.). El resultado en Android:

Vista principal en Android

Y en iOS:

Vista principal en iOS

El menú principal

¿Recuerdas que anteriormente hablamos ligeramente del menú principal al hablar de la MasterDetailPage?. Es hora de retomarlo. Características:

  • Tenemos una cabecera donde podemos ver el perfil utilizado en la aplicación.
  • A continuación, tenemos un listado con las diferentes secciones de la aplicación.
  • Hay apartados que destacan al contar con separadores entre otros elementos además de icono que refuerza su contenido.

Listado de opciones

Todos los puntos anteriores los podemos conseguir directamente utilizando un ListView junto a una ViewCell personalizada y el Header.

<ListView
     ItemsSource="{Binding MenuItems}" 
     BackgroundColor="{StaticResource MenuBackgroundColor}"
     SeparatorVisibility="None">
     <ListView.ItemTemplate>
          <DataTemplate>
               <ViewCell>
                    <templates:MenuItemTemplate />
               </ViewCell>
          </DataTemplate>
     </ListView.ItemTemplate>
</ListView>

El separador solo aparece con algunos de los elementos. Por este motivo, y aunque el ListView cuenta con la propiedad SeparatorVisibility, creamos el separador en la plantilla que define a cada elemento donde vamos a controlar la visibilidad en base a una propiedad que define cada elemento del menú.

<Grid
     HeightRequest="48">
     <Grid.Triggers>
          <DataTrigger
               TargetType="Grid"
               Binding="{Binding IsEnabled, Mode=TwoWay}"
               Value="False">
               <Setter Property="Opacity" Value="0.6" />
          </DataTrigger>
     </Grid.Triggers>
     <Grid.ColumnDefinitions>
          <ColumnDefinition Width="2" />
          <ColumnDefinition Width="Auto" />
          <ColumnDefinition Width="*" />
     </Grid.ColumnDefinitions>
     <Grid.RowDefinitions>
          <RowDefinition Height="*" />
          <RowDefinition Height="Auto" />
     </Grid.RowDefinitions>
     <Image 
          Grid.Row="0"
          Grid.Column="1"
          Source="{Binding Icon}"
          Style="{StaticResource MenuItemIconStyle}"/>
     <Label 
          Grid.Row="0"
          Grid.Column="2"
          Text="{Binding Title}"
          Style="{StaticResource MenuItemTextStyle}"/>
     <Grid 
          Grid.Row="1"
          Grid.Column="0"
          Grid.ColumnSpan="3"
          HeightRequest="2"
          BackgroundColor="{StaticResource BlackColor}"
          IsVisible="{Binding Separator}"/>
</Grid>

El resultado en Android:

Menú en Android

Y en iOS:

Menú en iOS

La información detallada de un contenido

Llegamos a la vista más compleja de todas las que llevamos.

¿Por qué?.

Analicemos las necesidades:

  • La barra de navegación, donde encontraremos el botón de navegación atrás entre otras opciones, es transparente!. Vamos a necesitar un Custom Renderer para conseguir este objetivo tanto en Android como en iOS.
  • El logo… desaparece. Tenemos que gestionar este cambio. En Android usaremos código específico para acceder a la imagen de logo y jugar con su visibilidad. En iOS, también vamos a utilizar código específico pero en forma de Custom Renderer.
  • La imagen destacada del elemento (película, documental o serie) seleccionada hace efecto Parallax. Gracias a las opciones de transformación de Xamarin.Forms podremos hacer translaciones, jugar con la opacidad o escala. Es decir, podemos conseguir este efecto gestionando el scroll realizado y con código totalmente compartido.
  • Hay un listado de elementos similares mostrado en tres columnas. Aquí volveremos a hacer uso de FlowListView.

Barra transparente

Una de las características principales de la página de detalles es la barra de navegación transparente.

¿Cómo lo conseguimos?.

En Android, vamos a crear un Custom Renderer de NavigationPage.

public class CustomNavigationBarRenderer : NavigationPageRenderer
{

}

Junto al uso de mensajería utilizando FormsToolkit haremos que por defecto el color de la barra de navegación sea de color, menos el la página de detalles, que será transparente.

MessagingService.Current.SendMessage(MessageKeys.ToolbarColor, Color.Transparent);

En el renderer personalizado de la NavigationPage, vamos a verificar si el color es transparente para modificar el tamaño y posición del Layout para conseguir el efecto.

En el caso de iOS, necesitamos de nuevo un Custom Renderer.

public class TransparentNavigationBarPageRenderer : PageRenderer
{
     public override void ViewDidLayoutSubviews()
     {
          base.ViewDidLayoutSubviews();

          if (NavigationController != null)
          {
               NavigationController.NavigationBar.SetBackgroundImage(new UIImage(), UIBarMetrics.Default);
               NavigationController.NavigationBar.ShadowImage = new UIImage();
               NavigationController.NavigationBar.BarTintColor = UIColor.Clear;
               NavigationController.NavigationBar.TintColor = UIColor.White;
          }
     }
}

Modificamos el color de la barra, suprimimos la imagen de sombra para evitar cualquier tipo de separación visual entre la cabecera y el contenido para lograr la sensación de continuidad buscado.

Es momento de quitar el logo

De nuevo, hacemos uso de mensajería utilizando FormsToolkit para conseguir este objetivo en Android. Desde la vista de detalles (PCL):

MessagingService.Current.SendMessage(MessageKeys.ChangeToolbar, true);

Y nos suscribimos para recibir el mensaje desde el proyecto Android, en la actividad principal:

MessagingService.Current.Subscribe<bool>(MessageKeys.ChangeToolbar, (page, showLogo) =>
{
     var logo = FindViewById<ImageView>(Resource.Id.logoImageLayout);

     if (showLogo)
     {
          logo.Visibility = ViewStates.Visible;
     }
     else
     {
          logo.Visibility = ViewStates.Invisible;
     }
});

Accedemos a la imagen Toolbar y cambiamos la visibilidad según en caso (mostrar u ocultar).

En el caso de iOS, gracias al uso de Platform Specifics, hacemos la barra traslúcida.

var navigationPage = Parent as Forms.NavigationPage;

if (navigationPage != null)
     navigationPage.On<iOS>().EnableTranslucentNavigationBar();

Además. recuerda que previamente ya vimos el renderer de la NavigationPage aplicado a la página de detalles.

[assembly: ExportRenderer(typeof(DetailView), typeof(TransparentNavigationBarPageRenderer))]

Parallax

Sin duda, lo habrás experimentado ya sea en web o e aplicaciones móviles. Haces scroll y el fondo (o una imagen) se mueve a una velocidad distinta que el contenido, creando un ligero efecto de profundidad.

¿Cómo conseguimos esto?.

Hemos comentado que el efecto se aplica al hacer scroll. Por lo tanto, comenzamos capturando información cada vez que se realiza scroll gracias al evento Scrolled del ScrollView.

ParallaxScroll.Scrolled += OnParallaxScrollScrolled;

Tras hacer scroll vertical, la dirección puede ser en dos sentidos, hacia arriba o hacia abajo:

double translation = 0;

if (_lastScroll < e.ScrollY)
     translation = 0 - ((e.ScrollY / 2));
else
     translation = 0 + ((e.ScrollY / 2));

HeaderPanel.TranslateTo(HeaderPanel.TranslationX, translation);

Dependiendo de la dirección del scroll, se captura la cantidad de scroll realizado con e.ScrollY, para finalmente aplicar una transformación de transladación de la cabecera.

El resultado:

NOTA: Se puede modificar este efecto para conseguir potenciarlo aún más. Habitualmente también se juega con la escala y con la opacidad. Ambas opciones al igual que la transladación son posibles desde código compartido.

Listado con tres columnas

Nada «diferente» a lo ya visto previamente. Hacemos uso de FlowListView:

<flv:FlowListView
     FlowItemsSource="{Binding SimilarMovies}"
     BackgroundColor="{StaticResource BackgroundColor}"
     FlowColumnCount="3" 
     FlowColumnExpand="First"
     SeparatorVisibility="None"
     HasUnevenRows="True"
     HeightRequest="350">
     <flv:FlowListView.FlowColumnTemplate>
          <DataTemplate>
               <templates:MovieItemTemplate />
          </DataTemplate>
     </flv:FlowListView.FlowColumnTemplate>
</flv:FlowListView>

Aplicando 3 columnas con la propiedad FlowColumnCount.

El resultado en Android:

Detalles de una pelícucla en Android

Y en iOS:

Detalles en iOS

Puedes descargar el código del ejemplo desde GitHub:

Ver GitHub

¿Qué plugins o componentes se han utilizado?

Se ha utilizado:

  • FFImageLoading – Con el objetivo principal de cachear imágenes. Dado el tipo de aplicación y la importancia de las imágenes, es importante. Aunque recuerda, en este ejemplo todas las imágenes son locales.
  • Xamarin Forms Toolkit – Toolkit para Xamarin.Forms con helpers, converters, etc. Se hace uso principalmente del sistema de mensajería.
  • FlowListView – ListView con soporte a columnas (entre otras características).

Conclusiones

Creo que las capturas son una buena conclusión. Logramos replicar la interfaz de usuario de una aplicación sumamente utilizada y destacada en las diferentes tiendas utilizando exclusivamente lo que proporciona Xamarin.Forms junto a plugins o componentes gratuitos por parte de la comunidad.

Tras pasar varias analíticas podemos ver que en este ejemplo, en Android se llega a compartir un 85,87% de código y un 89.53% en iOS. Podríamos seguir compartiendo gran cantidad de lógica como peticiones HTTP, gestión de errores, etc. Sin embargo, probablemente y de cara a ciertas necesidades de la aplicación, también se requerirían más Custom Renders y/o efectos. La cantidad de código compartida seguiría siendo elevada.

Y llegamos a la parte final del artículo. Es un concepto de artículo que tenía en mente y he decidido a lanzarme a ello tras ver una idea similar por varios compañeros en la comunidad. ¿Qué te parece este tipo de artículos?. Es sumamente divertido preparar una aplicación conocida e intetar «desgranar» cada pasos a realizar. Sin duda, espero repetir esta idea con otras aplicaciones aunque me gustaría saber tu opinión. ¿prefieres artículo habitual, videoblog, otro formato?.

Más información

Deja un comentario

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