[Xamarin.Forms] Custom Renders

Introducción

Xamarin.Forms es un toolkit que crea una abstracción sobre la interfaz de usuario de Android, iOS y Windows Phone permitiendo desarrollarla una única vez con código C# o Extensible Application Markup Language (XAML).
Permite crear facilmente y con rapidez interfaces de usuario nativas
compartidas donde  cada elemento visual en Xamarin.Forms son mapeados a
elementos nativos y comportamientos propios de cada plataforma.

Sin embargo, esta posibilidad a veces supone grandes dudas, ¿podemos crear o modificar elementos visuales de Xamarin.Forms?

Xamarin.Forms utiliza abstracciones
para definir los elementos. Posteriormente se transforma cada
abstracción ofreciendo una implementación y mecanismos en cada
plataforma.

¿Qué podemos hacer?
¿Qué podemos hacer?

En
este artículo vamos a ver que podemos crear nuestras propias
abstracciones ya sea modificando alguna de las existentes o partiendo
desde prácticamente cero.

¿Cuándo son necesarios?

En toda
aplicación móvil la apariencia visual es vital. Cada vez es mayor el
esfuerzo depositado a la hora de crear aplicaciones atractivas a la par
que intuitivas y en muchos casos conseguir una imagen única que
diferencia a la Aplicación del resto es prioritario. Por este motivo,
debemos de contar con opciones sencillas de poder personalizar los
distintos elementos que componen la interfaz. Con la llegade de la
versión 1.3 de Xamarin.Forms llegaron los estilos. Los estilos permitir definir múltiples propiedades visuales de elementos de la interfaz de forma reutilizable.

Por lo tanto, para modificaciones visuales simples no es necesario realizar ningun tipo de Custom Render.

¿Cúando serán necesario?

¿Cuándo crear Custom Renders?
¿Cuándo crear Custom Renders?

Tenemos varias opciones:

  • Modificar la apariencia de elementos a niveles no posibles por estilos.
  • Modificar el comportamiento de elementos existentes.
  • Crear nuevos controles personales.

Extender
Xamarin.Forms nos permitirá añadir funcionalidad, controles y páginas
específicas para cada plataforma logrando que nuestras Apps se adapten a
la perfección a las guías de estilo de cada plataforma.

Custom Renders

Los
elementos de Xamarin.Forms, páginas, layouts y controles, utilizan una
API común que permite crear interfaces visuales con el mismo código para
todas las plataformas. Cada página, layout o control se renderiza de
forma diferente en cada plataforma.  El esquema de que ocurre es el
siguiente:

Element y Renderer
Element y Renderer

Todos los elementos de Xamarin.Forms se componen de dos partes diferenciadas, Element y Renderer.

  • Element:
    Es una clase que define al control. Conjunto de propiedades y eventos
    que permitirán gestionar tanto la apariencia, contenido y comportamiento
    del mismo. En el esquema superior nos centramos en el elemento Button. La clase Element define el conjunto de propiedades de contenido (Text), como las de apariencia (TextColor) y eventos.
  • Renderer:
    El elemento definido se renderiza (transforma) en cada plataforma a un
    elemento 100% nativo. En el esquema anterior, la clase Renderer en cada
    plataforma creará un control nativo, UIButton en iOS y Button en Android y Windows Phone, asignando las definiciones que vienen establecidas desde el Element.

Podemos crear nuestra propia clase Renderer para modificar la apariencia o comportamiento del elemento.

Custom Render
Custom Render

Podemos añadir un render personalizado por plataforma o sobreescribir el comportamiento en cada una de las plataformas.

Creando Custom Renders

Comenzamos creando la clase Element de definición de nuestro control. En este caso vamos a crear un control tipo hipervínculo.

public class HyperLinkControl : Label
{
     public static readonly BindableProperty NavigateUriProperty;
 
     static HyperLinkControl()
     {
         NavigateUriProperty = BindableProperty.Create("NavigateUri", typeof (string), typeof (HyperLinkControl),
             string.Empty);
     }
 
     public string NavigateUri
     {
         get { return (string) base.GetValue(NavigateUriProperty); }
         set { base.SetValue(NavigateUriProperty, value); }
     }
}

En nuestra PCL dentro de la carpeta CustomControls creamos
una nueva clase. Esta clase heredará de algun otro elemento que contenga
ya el comportamiento o aspecto visual similar al objetivo buscado. En
nuestro caso, al querer crear un enlace que contendrá texto visible que
podremos personalizar, heredamos del control Label.

NOTA: Si es control es totalmente nuevo sin similitudes con elementos existentes, la herencia será del elemento View.

La
clase nos permitirá definir nuestro control. Deseamos contar con las
mismas propiedades que el Label, de ahi la herencia pero además
necesitamos poder especificar el enlace al que se navegará pulsando
sobre el mismo.

El enlace lo especificamos creando una nueva BindableProperty.
Esta clase nos permite crear propiedades que describen a la misma, tipo
y contenido, además de especificar como lanzar el cambio de la
propiedad. Nuestra propiedad NavigateUri es de tipo string.

Llegados a este punto, tenemos definida nuestra clase Element. Ahora es el turno de crear en cada proyecto de cada plataforma la clase Renderer correspondiente.

En el proyecto Windows Phone, dentro de la carpeta Controls:

public class HyperLinkControlRenderer : ViewRenderer<HyperLinkControl, HyperlinkButton>
{
     protected override void OnElementChanged(ElementChangedEventArgs<HyperLinkControl> e)
     {
         base.OnElementChanged(e);
 
         if (e.OldElement != null || Element == null)
             return;
 
         var element = new HyperlinkButton
         {
             TargetName = Element.Text,
             Content = Element.Text
         };
 
         element.Click += (sender, args) =>
         {
             if (Element.NavigateUri.Contains("@"))
             {
                 var emailComposeTask = new EmailComposeTask { Subject = string.Empty, To = "mailto:" + Element.NavigateUri };
                 emailComposeTask.Show();
             }
             else if (Element.NavigateUri.Contains("www.") ||
                 Element.NavigateUri.Contains("http:"))
             {
                 var uri = Element.NavigateUri.StartsWith("http:")
                               ? new Uri(Element.NavigateUri)
                               : new Uri(@"http://" + Element.NavigateUri);
 
                 var webBrowserTask = new WebBrowserTask { Uri = uri };
                 webBrowserTask.Show();
             }
         };
 
         SetNativeControl(element);
     }
 
     protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
     {
         base.OnElementPropertyChanged(sender, e);
 
         if ((e.PropertyName == Label.TextProperty.PropertyName))
             Control.Content = Element.Text;
     }
}

La clase hereada de ViewRenderer lo que significa
que crearemos una vista personalizada, Xamarin.Forms se encargará de la
gestión de cálculos de tamaño y propiedades comunes de una vista. Le
indicamos que clase estamos renderizando (clase Element), en nuestro ejemplo HyperLinkControl, y que elemento nativo se renderizará. En Windows Phone, el control HyperLinkControl renderizará un control de tipo HyperLinkButton.

Una vez especificada la clase y su herencia, lo único que tenemos que
hacer es crear nuestro control nativo, establecerle las propiedades
correspondientes y renderizarlo. Hacemos esto utilizando el método OnElementChanged. En este método:

  • Tenemos acceso a la propiedad Element que es nuestro HyperLinkControl creado previamente con las propiedades que definimos.
  • Tenemos acceso al control pudiéndole establecer lo que necesitemos utilizando el método SetNativeControl.

NOTA: Al especificar en la herencia del renderer
el tipo del Element y del control nativo a renderizar, Element y
Control cuentan ya con los tipos correctos establecidos lo que nos
facilita la tarea sin necesidades de realizar castings.

En Windows Phone al pulsar el elemento utilizamos el evento Click del control nativo y verificamos si es una URL o un correo. Si es URL utilizamos WebBrowserTask para abrirla y en el caso de ser correo, utilizamos EmailComposeTask. Como vemos, son APIs específicas y nativas de Windows Phone.

Para indicar la clase que renderizará en la plataforma a la clase Element definida, lo haremos utilizando el atributo [assembly] en la parte superior de la clase, incluido la definición del namespace.

[assembly: ExportRenderer(typeof(HyperLinkControl), typeof(HyperLinkControlRenderer))]

Utilizando ExportRenderer le indicamos a Xamarin.Forms que en el proyecto Windows Phone se establece el renderizado de nuestro control HyperLinControl.

Ahora llega el turno de Android:

public class HyperLinkControlRenderer : LabelRenderer
{
     protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
     {
         base.OnElementChanged(e);
 
         if (e.OldElement == null)
         {
             var nativeEditText = Control;
 
             Linkify.AddLinks(nativeEditText, MatchOptions.All);
         }
     }
}

En este caso, la clase hereda del renderer específico del Label, LabelRenderer. Volvemos a utilizar el método OnElementChanged para acceder y establecer las propiedades. En este caso, utilizamos Linkify que tomará un texto y una expresión regular para crear enlaces.

Y por último iOS:

public class HyperLinkControlRenderer : LabelRenderer
{
     protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
     {
         base.OnElementChanged(e);
 
         if (e.OldElement == null)
         {
             var label = Control;
             label.TextColor = UIColor.Red;
             label.BackgroundColor = UIColor.Clear;
             label.UserInteractionEnabled = true;
             var tap = new UITapGestureRecognizer();
 
             tap.AddTarget(() =>
             {
                 var hyperLinkLabel = Element as HyperLinkControl;
 
                 if (hyperLinkLabel != null)
                 {
                     var uri = hyperLinkLabel.NavigateUri;
 
                     if (uri.Contains("@") && !uri.StartsWith("mailto:"))
                         uri = string.Format("{0}{1}", "mailto:", uri);
                     else if (uri.StartsWith("www."))
                         uri = string.Format("{0}{1}", @"http://", uri);
 
                     UIApplication.SharedApplication.OpenUrl(new NSUrl(uri));
                 }
             });
 
             tap.NumberOfTapsRequired = 1;
             tap.DelaysTouchesBegan = true;
             label.AddGestureRecognizer(tap);
         }
     }
}

Al igual que en Android, heredamos de LabelRenderer.
En el método OnElementChanged accedemos a las propiedades, verificamos
el tipo de enlace, le añadimos el schema adecuado segun el tipo, y se
utiliza UIApplication.SharedApplication.OpenUrl para abrir el enlace.

Todo listo!. Bueno… casi, nos falta utilizar el control. Desde
nuestra PCL en una vista definiremos el namespace donde tenemos
declarado nuestro control:

xmlns:custom="clr-namespace:CustomRenders.CustomControls;assembly=CustomRenders"

Ahora podemos definir el nuevo control (tanto desde XAML como desde C#)
utilizando nuestra porpiedad NavigateUri para indicar enlaces a páginas
webs o correos electrónicos:

<StackLayout Padding="20" Spacing="20">
   <Label Text="HyperLinkControl" />
   <custom:HyperLinkControl Text="Email"
                            NavigateUri="user@mail.com" />
   <custom:HyperLinkControl Text="Url"
                            NavigateUri="https://javiersuarezruiz.wordpress.com" />
</StackLayout>

El resultado:

El resultado

El resultado

Pulsando en el segundo enlace:

Funciona!

Funciona!

Podéis descargar el ejemplo completo realizado a continuación:

También tenéis el código fuente disponible e GitHub:

Ver GitHub

¿Puedo utilizar controles nativos de cada plataforma?

Utilizamos un mundo de abstracciones que nos permiten definir vistas
para cada plataforma con un código común. Ya hemos visto como poder
modificar el aspecto o el comportamiento de elementos existentes además
de crear nuevos pero… ¿podemos utilizar controles nativos existentes para alguna plataforma concreta?

La respuesta es sencilla y posiblemente ya sido deducida, si. Utilizaremos Custom Renders para ello.

Vamos a crear un nuevo ejemplo donde crearemos un nuevo control para Xamarin.Forms que en Windows Phone renderizará un Tile dinámico. En el proyecto, dentro de la carpeta CustomControls, creamos una nueva clase:

public class CustomHubTileView : View
{
     public static readonly BindableProperty TitleProperty = BindableProperty.Create<CustomHubTileView, string>(p => p.Title, string.Empty);
 
     public string Title
     {
         get { return (string)GetValue(TitleProperty); }
         set { SetValue(TitleProperty, value); }
     }
 
     public static readonly BindableProperty MessageProperty = BindableProperty.Create<CustomHubTileView, string>(p => p.Message, string.Empty);
 
     public string Message
     {
         get { return (string)GetValue(MessageProperty); }
         set { SetValue(MessageProperty, value); }
     }
 
     public static readonly BindableProperty SourceProperty = BindableProperty.Create<CustomHubTileView, ImageSource>(p => p.Source, string.Empty);
 
     public ImageSource Source
     {
         get { return (ImageSource)GetValue(SourceProperty); }
         set { SetValue(SourceProperty, value); }
     }
 
     public static readonly BindableProperty ColorProperty = BindableProperty.Create<CustomHubTileView, Color>(p => p.Color, Color.Default);
 
     public Color Color
     {
         get { return (Color)GetValue(ColorProperty); }
         set { SetValue(ColorProperty, value); }
     }
}

Será un control nuevo, heredamos de View. Dentro de la clase creamos múltiples BindableProperty:

  • Title: Propiedad de tipo string que mostrará el texto mostrado en la vista principal del Tile.
  • Message: Propiedad ed tipo string que mostrará el texto secundario del Tile.
  • Source: Imagen del Tile.
  • Color: Color de fondo del Tile.

Tras crear el Element con la definición del control, en el proyecto de la plataforma, Windows Phone, crearemos el Renderer:

public class CustomHubTileViewRenderer : ViewRenderer<CustomHubTileView, HubTile>
{
     private HubTile HubTile;
 
     public CustomHubTileViewRenderer()
     {
         HubTile = new HubTile
         {
             Margin = new System.Windows.Thickness(5)
         };
     }
 
     protected override void OnElementChanged(ElementChangedEventArgs<CustomHubTileView> e)
     {
         base.OnElementChanged(e);
 
         if (e.OldElement != null || Element == null)
             return;
 
         HubTile.Title = Element.Title;
         HubTile.Message = Element.Message;
         var fileImageSource = Element.Source as FileImageSource;
         if(fileImageSource != null)
             HubTile.Source = new BitmapImage(new Uri(fileImageSource.File, UriKind.RelativeOrAbsolute));
 
         System.Windows.Media.Color color = System.Windows.Media.Color.FromArgb(
                (byte)(Element.Color.A * 255),
                (byte)(Element.Color.R * 255),
                (byte)(Element.Color.G * 255),
                (byte)(Element.Color.B * 255));
 
         HubTile.Background = new SolidColorBrush(color);
 
         SetNativeControl(HubTile);
     }
 
     protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
     {
         base.OnElementPropertyChanged(sender, e);
 
         if (Control == null || Element == null)
             return;
 
         if (e.PropertyName == CustomHubTileView.TitleProperty.PropertyName)
             HubTile.Title = Element.Title;
 
         if (e.PropertyName == CustomHubTileView.MessageProperty.PropertyName)
             HubTile.Message = Element.Message;
     }
}

En el proyecto Windows Phone 8.0, utilizaremos el control nativo HubTile incluido dentro del paquete Windows Phone Toolkit. Utilizamos el método OnElementChanged para establecer todas las propiedades asignadas y renderizar el control.

Para indicar la clase que renderizará en la plataforma a la clase Element definida, lo haremos utilizando el atributo [assembly] en la parte superior de la clase, incluido la definición del namespace.

[assembly: ExportRenderer((typeof(CustomHubTileView)), typeof(CustomHubTileViewRenderer))]

A la hora de utilizarlo, definimos el namespace donde tenemos al control declarado:

xmlns:custom="clr-namespace:HubTileXamarinForms.CustomControls;assembly=HubTileXamarinForms"

Desde una vista:

<custom:CustomHubTileView
        Title="Arroces"
        Message="25 recetas"
        Source="../Assets/arroces.jpg"
        Color="Red" />

El resultado:

El resultado

El resultado

Podéis descargar el ejemplo completo realizado a continuación:

También tenéis el código fuente disponible e GitHub:

Ver GitHub

Recordar que podéis dejar en los comentarios cualquier tipo de sugerencia o pregunta.

Más información

Deja un comentario

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