[Xamarin UI Challenge] Art Plant Mall

El reto

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 JIANGGM), que intentaremos replicar con paso a paso.

Art Plant Mall

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

Los retos del ejemplo

A pesar de ser un diseño atractivo, no hay retos muy complejos en el diseño, si tenemos muchos pequeños detalles:

  • Listado de plantas: 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 caso necesitamos dos columnas, algo sencillo gracias al CollectionView.
  • Menu deslizante desde la parte inferior: El menu desplizante es una de las claves del ejemplo. Podemos conseguir este resultado de forma sencilla con plugins como SlideOverKit. Sin embargo, en este ejemplo vamos a crear algo sencillo desde cero utilizando una ContentView y animaciones.
  • Contenido del menu deslizante: El contenido del menu varia dependiendo de dos estados, si el menu esta abierto o cerrado. De entrada, para gestionar dos estados y el cambio usaremos VisualStateManager. Al tener el menu en el estado cerrado o colapsado mostramos un listado horizontal con las fotos (circulares) de las plantas que tenemos en el carrito. Esto lo podemos conseguir facilmente usando Bindable Layouts o CollectionView. Al expandir el menu deslizante vemos un listado con los detalles del carrito. Podemos usar de nuevo el CollectionView e incluso un ListView con un DataTemplateSelector para diferenciar entre las plantas añadidas al carrito y otros elementos como los gastos de envio. Los botones con bordes redondeados los podemos conseguir con una combinación de uso de Layouts (donde el Frame con su CornerRadius adquiere el peso) o bien, usar algun plugin de comunidad como PancakeView que nos facilite la gestión de Layouts con bordes redondeados, etc.
  • Detalles de una planta: La página de detalles se puede conseguir fácilmente. La complejidad de esta vista recae en los controles necesarios que no tenemos por defecto en Xamarin.Forms como son el NumericUpDown o el ToggleButton.
  • Parallax: A pesar que desde la imagen estática del diseño no podemos saber si la experiencia diseñada usada el efecto Parallax…personalmente al ver el diseño me encajaba perfectamente!. Podemos crear un efecto Parallax de forma sencilla usando un ScrollView, su evento al realizar desplazamiento del scroll y animaciones (princpalmente de translación).
  • NumericUpDown: Tenemos varias opciones para conseguir el control NumericUpDown. Tenemos la opción de usar Custom Renderer o crearlo usando SkiaSharp. Tenemos otra opción que es crear un control personalizado utilizando una composición de elementos de Xamarin.Forms. En este caso, vamos a usar un ContentView combinando un Layout con tres Labels. El control contará con varias BindableProperties para establecer el valor, el valor mínimo, máximo, etc.
  • ToggleButton: Vamos a realizar algo similar al caso anterior, usaremos una ContentView. Contaremos con BindableProperties para establecer el estado, etc. Para conseguir el mismo resultado, la clave será usar imágenes para definir cada estado visual del control (Checked o UnChecked).

Listado de plantas

Comenzamos con el listado principal con las plantas en ventas en la tienda. Por el tipo de Layout requerido (dos columnas) vamos a utilizar el CollectionView.

El CollectionView sigue marcado con estado experimental y solo se puede usar agregando la siguiente línea de código en el AppDelegate en el caso de iOS, así como en la MainActivity en Android, antes de llamar a Forms.Init:

Forms.SetFlags("CollectionView_Experimental");

El uso del CollectionView será sencillo:

<CollectionView
     ItemsSource="{Binding Plants}" />

CollectionView cuenta con  la propiedad ItemsLayout, del tipo IItemsLayout , que permite especificar el Layout que se usará.

<CollectionView.ItemsLayout>
     <GridItemsLayout 
          Orientation="Vertical"
          Span="2" />
</CollectionView.ItemsLayout>

Hemos usado GridItemsLayout especificando la orientación y el número de columnas gracias a la propiedad Span.

El resultado:

El listado de plantas

Menu dezlizante desde la parte inferior

Nuestro menu deslizante va a ser algo muy sencillo…una ContentView. La situaremos en la página principal aunque, ¿cómo la posicionamos?.

Hay que tener en cuenta que el efecto de expandir y contraer lo vamos a conseguir utilizando animaciones de translación en el eje Y.

La posición inicial del menu deslizante (cerrado o colapsado) debe ser en la parte inferior solo mostrando la «cabecera» del menu.

CartPopup.TranslationY = pageHeight - CartPopup.HeaderHeight;

Sencillo. Transladamos el menu en el eje Y el alto de la página menos el alto de la cabecera!.

Una vez establecida la posición inicial necesitamos controlar dos acciones:

  • Expandir: Haremos una translación en el eje Y desde la posición inicial, al borde superior de la pantalla menos el alto del menu.
  • Contraer: Volveremos a la posición inicial. Es decir, alto de la página menos alto de la cabecera.

Veamos la lógica:

private void OnExpand()
{
     CartPopupFade.IsVisible = true;
     CartPopupFade.FadeTo(1, ExpandAnimationSpeed, Easing.SinInOut);

     var height = pageHeight - CartPopup.HeaderHeight;
     CartPopup.TranslateTo(0, Height - height, ExpandAnimationSpeed, Easing.SinInOut);
}

private void OnCollapse()
{
     CartPopupFade.FadeTo(0, CollapseAnimationSpeed, Easing.SinInOut);
     CartPopupFade.IsVisible = false;
     CartPopup.TranslateTo(0, pageHeight - CartPopup.HeaderHeight, CollapseAnimationSpeed, Easing.SinInOut);
}

Además de la translación del menu deslizante, hemos añadido un Grid con el fondo oscuro translúcido para potenciar el efecto de apertura y cierre del menu.

Pero…¿y qué ocurre con los estados del menu?. Cierto. Hay elementos visibles al estar el menu colapsado y otros visibles al expandir el mismo.

¿Cómo gestionamos esto?.

Hablamos de gestionar diferentes estados visuales. No hay caso donde encaje mejor el uso de VisualStateManager.

Vamos a definir estados visuales en ciertos elementos del menu deslizante (botones de abrir y cerrar el menu, listados, etc.):

<VisualStateManager.VisualStateGroups>
     <VisualStateGroup x:Name="CommonStates">
          <VisualState x:Name="Expanded">
               <VisualState.Setters>
                    <Setter Property="IsVisible" Value="False" />
               </VisualState.Setters>
          </VisualState>
          <VisualState x:Name="Collapsed">
               <VisualState.Setters>
                    <Setter Property="IsVisible" Value="True" />
               </VisualState.Setters>
          </VisualState> 
     </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Hemos creado los dos estados visuales que necesitamos. La gestión del cambio de estado la realizaremos utilizando el método GoToState. Por ejemplo:

VisualStateManager.GoToState(CollapseButton, "Expanded");

Veamos el resultado:

El menu deslizante

Contenido del menu deslizante

De todo el contenido del menu deslizante, la parte más interesante es el listado detallado de elementos incluidos en el carrito. Si nos fijamos en el diseño, en el listado tenemos elementos diferentes. Podemos diferenciar claramente entre plantas y otros gastos como el envio.

Para conseguir este resultado, vamos a utilizar un ListView:

<ListView 
     ItemsSource="{Binding Basket}"
     ItemTemplate="{StaticResource BasketItemDataTemplateSelector}" />

Junto a un DataTemplateSelector:

public class BasketItemDataTemplateSelector : DataTemplateSelector
{
     public DataTemplate PlantTemplate { get; set; }
     public DataTemplate DeliveryTemplate { get; set; }

     protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
     {
          return ((BasketItem)item).BasketItemType == BasketItemType.Plant ? PlantTemplate : DeliveryTemplate;
     }
}

Un selector de plantillas se implementa mediante la creación de una clase que hereda de DataTemplateSelector. Después, utilizamos el método OnSelectTemplate para devolver un elemento de tipo DataTemplate.

El resultado:

El contenido del menu

Detalles de una planta

La vista de detalles (a falta de crear los controles NumericUpDown y ToggleButton) se basa en una composición de Layouts en combinación con una Image y varios Labels.

El resultado:

Detalles de una planta

Parallax

A pesar de ser una tendencia (en realidad lleva ya usandose hace años en web y también en movilidad) es bastante sencillo de conseguir un efecto de tipo Parallax. El efecto se comporta con un complemento «narrativo». Jugamos con el movimiento y la profundidad para resaltar algo. En nuestro caso, segun se haga scroll hacia la parte inferior, vamos a resaltar la información de la planta quitando peso a la imagen de la misma.

Vamos a simplificarlo todo creando un control personalizado. Se trata de una clase que hereda de ScrollView:

public class ParallaxControl : ScrollView
{
     const float ParallaxSpeed = 2.25f;

     double _height;

     public ParallaxControl()
     {
          Scrolled += (sender, e) => Parallax();
     }

     public static readonly BindableProperty ParallaxViewProperty =
          BindableProperty.Create(nameof(ParallaxControl), typeof(CachedImage), typeof(ParallaxControl), null);

     public View ParallaxView
     {
          get { return (View)GetValue(ParallaxViewProperty); }
          set { SetValue(ParallaxViewProperty, value); }
     }

     public void Parallax()
     {
          if (ParallaxView == null)
               return;

          if (_height <= 0)
               _height = ParallaxView.Height;

          var y = -(int)((float)ScrollY / ParallaxSpeed);

          if (y < 0)
               ParallaxView.TranslationY = y;
          else
               ParallaxView.TranslationY = 0;
     }
}

Vamos a desplazar la imagen de la cabecera a una velocidad inferior al resto del contenido. Para ello, creamos una BindableProperty para especificar que elemento visual es la cabecera. En el evento Scrolled del ScrollView, vamos a transladar (TranslationY) la cabecera.

El resultado:

Parallax effect

NOTA: Podemos mejorar el resultado!. Además de transladar la cabecera (la imagen de la cabecera), podemos aplicar otras animaciones como aumentar la escala o aplicar efecto tipo Blur a la imagen por ejemplo.

NumericUpDown

Y solo nos queda por completar los dos controles personalizados. Vamos a comenzar por el NumericUpDown. Podemos crear controles compuestos utilizando otros elementos de Xamarin.Forms y apoyándonos en Bindable Properties para conseguir resultados bastante sorprendentes.

En el caso del NumericUpDown, vamos a crear el control con un Layout y tres Labels:

<pancakeview:PancakeView
     HeightRequest="24"
     CornerRadius="24"
     BorderThickness="1"
     BackgroundColor="White"
     BorderColor="Gray">
     <Grid>
          <Grid.ColumnDefinitions>
          <ColumnDefinition Width="Auto" />
          <ColumnDefinition Width="*" />
          <ColumnDefinition Width="Auto" />
          </Grid.ColumnDefinitions>
          <Label 
               x:Name="MinusButton"
               Grid.Column="0"
               Text="-"
               Margin="12, 0, 0, 0">
               <Label.GestureRecognizers>
                    <TapGestureRecognizer 
                         Tapped="MinusTapped" />
               </Label.GestureRecognizers>
          </Label>
          <Label 
               x:Name="ValueText"
               Grid.Column="1" />
          <Label 
               x:Name="PlusButton"
               Grid.Column="2"
               Text="+"
               Margin="0, 0, 12, 0">
               <Label.GestureRecognizers>
                    <TapGestureRecognizer 
                         Tapped="PlusTapped" />
               </Label.GestureRecognizers>
          </Label>
     </Grid>
</pancakeview:PancakeView>

En el code-behind del control, tendremos Bindable Properties para definir:

  • Minimum
  • Maximum
  • Value
  • Step

Por ejemplo:

public static readonly BindableProperty ValueProperty =
     BindableProperty.Create(nameof(Value), typeof(double), typeof(NumericUpDown), 1.0,
     propertyChanged: (bindable, oldValue, newValue) =>
     ((NumericUpDown)bindable).Value = (double)newValue,
     defaultBindingMode: BindingMode.TwoWay
);

public double Value
{
     get { return (double)GetValue(ValueProperty); }
     set { SetValue(ValueProperty, value); }
}

...

Su uso sería muy sencillo:

<controls:NumericUpDown
     Minimum="1"
     Maximum="10"/>

NOTA: Para tener un control reutilizable deberíamos llegar más lejos. Nos faltarían propiedades para personalizar el control (Animate, NumericBackgroundColor, NumericBorderColor, NumericTextColor, NumericBorderThickness, NumericCornerRadius) así como enventos (ValueChanged) y/o comandos. En este caso tan solo hemos añadido las propiedades básicas necesarias para replicar la UI.

¿Quieres ver el resultado?.

NumericUpDown

ToggleButton

El caso del ToggleButton va a ser sumamente similar al anterior control. Usaremos una ContentView con un conjunto de BindableProperties:

public static readonly BindableProperty CheckedImageProperty =
     BindableProperty.Create("CheckedImage", typeof(ImageSource), typeof(ToggleButton), null);

public ImageSource CheckedImage
{
     get { return (ImageSource)GetValue(CheckedImageProperty); }
     set { SetValue(CheckedImageProperty, value); }
}

public static readonly BindableProperty UnCheckedImageProperty =
     BindableProperty.Create("UnCheckedImage", typeof(ImageSource), typeof(ToggleButton), null);

public ImageSource UnCheckedImage
{
     get { return (ImageSource)GetValue(UnCheckedImageProperty); }
     set { SetValue(UnCheckedImageProperty, value); }
}

En este caso, no hacemos uso de XAML ya que la apariencia visual será lo que se defina en las imágenes de las propiedades CheckedImage y UnCheckedImage.

La gestión de estado, cambio de imagen y otras acciones las realizamos al cambiar el valor de la propiedad Checked:

private static async void OnCheckedChanged(BindableObject bindable, object oldValue, object newValue)
{

}

De modo que, el uso del control es simple:

<controls:ToggleButton
     Checked="False"
     Animate="True"
     CheckedImage="fav.png"
     UnCheckedImage="nofav.png"/>

El resultado:

El resultado final

¿Qué te parece?.

En cuanto al ejemplo realizado, esta disponible en GitHub:

Ver GitHub

Llegamos hasta aquí. Estamos ante un UI Challenge no excesivamente complejo pero con una gran variedad de detalles donde hemos utilizado:

  • Animaciones
  • Creación de controles personalizados
  • Navegación
  • Estilos
  • Parallax
  • VisualStateManager
  • Etc.

Este artículo ha sido parte de la iniciativa XamarinUIJuly:

#XamarinUIJuly

Un verdadero placer poder contribuir en una iniciativa tan interesante. Ah, si te ha resultado interesante este artículo, estamos a sólo 17 de Julio no te pierdas el resto de increíbles artículos que saldrán en el resto del mes!.

Más información

Anuncio publicitario

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s