Making Xamarin Forms Richer with Custom Visual Elements

The BoxView class is handy for drawing rectangles in a Xamarin Forms UI. On Windows Phone, a BoxView renders a Rectangle element. On iOS and Android, it paints a rectangle onto a graphics context and a canvas, respectively. But what about other graphics primitives such as ellipses and paths? Windows Phone, Android, and iOS all support them, but Xamarin Forms doesn’t surface any of them as visual elements.

Good news: You can implement new visual elements in Xamarin Forms with custom renderers. Most renderers in Xamarin Forms wrap controls such as TextBlocks and UILabels. But renderers aren’t required to emit controls; some, such as the BoxRenderer classes found in iOS and Android, don’t output controls at all. Instead, they paint pixels depicting BoxViews onto a drawing surface. You can do the same to extend Xamarin Forms with custom visual elements.

The custom element presented in this article is EllipseView. You can declare instances of EllipseView in a Xamarin Forms UI the same way you declare BoxViews. EllipseView exposes the exact same properties as BoxView, including the Color property that controls the color of the element’s interior. Here’s a bubbly UI built with an AbsoluteLayout and a few EllipseViews:

Ellipses

And here’s the XAML that produced that UI:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:EllipseViewDemo;assembly=EllipseViewDemo"
             x:Class="EllipseViewDemo.MainPage"
             BackgroundColor="#FFBCA9F5">
             
    <AbsoluteLayout>
        <local:EllipseView Color="White" Opacity="0.4"
            AbsoluteLayout.LayoutBounds="-60, -60, 400, 400" />
        <local:EllipseView Color="#FFF5A9F2" Opacity="0.4"
            AbsoluteLayout.LayoutBounds="-40, -40, 360, 360" />

        <local:EllipseView Color="White" Opacity="0.4"
            AbsoluteLayout.LayoutBounds="60, 380, 200, 200" />
        <local:EllipseView Color="#FFF2F5A9" Opacity="0.4"
            AbsoluteLayout.LayoutBounds="80, 400, 160, 160" />

        <local:EllipseView Color="White" Opacity="0.4"
            AbsoluteLayout.LayoutBounds="200, 220, 300, 300" />
        <local:EllipseView Color="#FFBCA9F5" Opacity="0.4"
            AbsoluteLayout.LayoutBounds="220, 240, 260, 260" />

        <local:EllipseView Color="White" Opacity="0.4"
            AbsoluteLayout.LayoutBounds="200, 360, 400, 400" />
        <local:EllipseView Color="#FFF781F3" Opacity="0.4"
            AbsoluteLayout.LayoutBounds="220, 380, 360, 360" />

        <local:EllipseView Color="White" Opacity="0.4"
            AbsoluteLayout.LayoutBounds="-100, 600, 600, 600" />
        <local:EllipseView Color="#FF81F79F" Opacity="0.4"
            AbsoluteLayout.LayoutBounds="-80, 620, 560, 560" />

        <local:EllipseView Color="White" Opacity="0.4"
            AbsoluteLayout.LayoutBounds="360, -40, 200, 200" />
        <local:EllipseView Color="#FFF2F5A9" Opacity="0.4"
            AbsoluteLayout.LayoutBounds="380, -20, 160, 160" />
    </AbsoluteLayout>

</ContentPage>

You could use the same techniques I used to build EllipseView to extend Xamarin Forms to support PolygonView, PathView, and other visual elements. Soon you’d be able to build some pretty sophisticated UIs using nothing more than XAML. You can start with my EllipseViewDemo solution, which is available for download.

The EllipseView Class

To define an EllipseView element, you merely derive from Xamarin Forms’ View class. That’s what BoxView does, and BoxView adds one property to the many properties it inherits from View: a property named Color. That property is a bindable property (as opposed to an ordinary C# property), which is analogous to a dependency property for those of you familiar with such properties in WPF, Silverlight, and other Microsoft XAML run-times. Among other things, being a bindable property means that the property can receive a value through data binding. Ordinary C# properties, by contrast, cannot.

Here’s the source code for EllipseView, complete with the bindable Color property:

public class EllipseView : View
{
    public static readonly BindableProperty ColorProperty =
        BindableProperty.Create<EllipseView, Color>(p => p.Color, Color.Accent);

    public Color Color
    {
        get { return (Color)GetValue(ColorProperty); }
        set { SetValue(ColorProperty, value); }
    }
}

The first parameter passed to BindableProperty.Create is a lambda that maps the bindable property to the C# property named Color. The second is the property’s default value. I chose Color.Accent so that if someone declares an EllipseView and fails to assign it a color, the EllipseView will assume the accent color of the platform it’s running on.

Deriving EllipseView is easy. But before an EllipseView draws a single pixel to the screen, you’ll need per-platform renderers for it, too.

The EllipseView Renderer (Windows Phone)

Here’s the EllipseView renderer for Windows Phone:

[assembly: ExportRenderer(typeof(EllipseView), typeof(EllipseViewRenderer))]
namespace CustomRendererDemo.WinPhone
{
    public class EllipseViewRenderer : ViewRenderer<EllipseView, Ellipse>
    {
        protected override void OnElementChanged(ElementChangedEventArgs<EllipseView> e)
        {
            base.OnElementChanged(e);

            var ellipse = new Ellipse();
            ellipse.DataContext = this.Element;
            ellipse.SetBinding(Ellipse.FillProperty,
                new System.Windows.Data.Binding("Color") { Converter = new ColorConverter() });

            this.SetNativeControl(ellipse);
        }
    }
}

EllipseViewRenderer derives from ViewRenderer and overrides one virtual method. OnElementChanged is called each time an EllipseView element is created. The override creates an instance of System.Windows.Shapes.Ellipse and attaches it to the renderer by assigning the Ellipse reference to the renderer’s Control property via a call to SetNativeControl. It also employs a neat little trick used by BoxViewRenderer. Rather than override OnElementPropertyChanged to update the Ellipse’s Fill property when the EllipseView’s Color property changes, it data-binds the two properties together. This ensures that the EllipseView changes color if its Color property is modified at run-time.

The EllipseView Renderer (Android)

In the Android version of Xamarin Forms, the BoxView renderer, which is named BoxRenderer rather than BoxViewRenderer, doesn’t derive from ViewRenderer; instead, it derives from VisualElementRenderer. Why? Because there is no view (control) associated with a BoxView on Android.

The Android EllipseView renderer, which I called EllipseRenderer for consistency with Xamarin Forms, likewise derives from VisualElementRenderer:

[assembly: ExportRenderer(typeof(EllipseView), typeof(EllipseRenderer))]
namespace CustomRendererDemo.Droid
{
    public class EllipseRenderer : VisualElementRenderer<EllipseView>
    {
        public EllipseViewRenderer()
        {
            this.SetWillNotDraw(false);
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == EllipseView.ColorProperty.PropertyName)
                this.Invalidate(); // Force a call to OnDraw
        }

        protected override void OnDraw(Canvas canvas)
        {
            var element = this.Element;
            var rect = new Rect();
            this.GetDrawingRect(rect);

            var paint = new Paint()
            {
                Color = element.Color.ToAndroid(),
                AntiAlias = true
            };

            canvas.DrawOval(new RectF(rect), paint);
        }
    }
}

There’s no need to override OnElementChanged since there’s no control to be created. Instead, EllipseRenderer overrides OnDraw, the virtual method that’s called by the Android graphics subsystem when the renderer needs to render an ellipse. It does so by creating a Paint object describing how the shape that it’s about to paint should be filled, and then calling DrawOval on the Canvas object passed in the parameter list. It also overrides OnElementPropertyChanged to force a repaint if the EllipseView’s Color property changes.

For the OnDraw method to be called, you have to let Android know that OnDraw needs to be called when the element is invalidated. That’s why the renderer calls SetWillNotDraw(false) in the class constructor. The default will-not-draw value is true, which is appropriate for renderers that wrap controls that draw themselves, but not for renderers that do the drawing.

The EllipseView Renderer (iOS)

The iOS EllipseView renderer is similar to the Android version. It derives from VisualElementRenderer and handles the drawing of ellipses itself:

[assembly: ExportRenderer(typeof(EllipseView), typeof(EllipseRenderer))]
namespace CustomRendererDemo.iOS
{
    public class EllipseRenderer : VisualElementRenderer<EllipseView>
    {
        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == EllipseView.ColorProperty.PropertyName)
                this.SetNeedsDisplay(); // Force a call to Draw
        }

        public override void Draw(CGRect rect)
        {
            using (var context = UIGraphics.GetCurrentContext())
            {
                var path = CGPath.EllipseFromRect(rect);
                context.AddPath(path);
                context.SetFillColor(this.Element.Color.ToCGColor());
                context.DrawPath(CGPathDrawingMode.Fill);
            }
        }
    }
}

The renderer overrides the Draw method inherited from VisualElementRenderer (in iOS, Draw is analogous to OnDraw in Android) and uses it to paint an ellipse on the screen. And like its Android counterpart, it overrides OnElementPropertyChanged and invalidates the portion of the screen occupied by the ellipse if the EllipseView’s Color property changes.

And Now for the Bad News

EllipseViews are great for dressing up an interface, but they’re not perfect when it comes to hit-testing. Specifically, if you attach a TapGestureListener to an EllipseView on Windows Phone, Tapped events will only fire when the user taps inside the ellipse. But on iOS and Android, Tapped events will fire when a touch occurs anywhere inside the ellipse’s bounding box. There’s not an easy fix for this – which is perhaps one reason why the Xamarin Forms team didn’t provide a richer assortment of visual elements out of the box.

Summary

The UI shown at the beginning of this article doesn’t look like a typical Xamarin Forms UI, but the fact that we can extend Xamarin Forms with new visual elements means that we should discard preconceived notions about what a Xamarin Forms UI looks like. As a programmer who cut his teeth on XAML using Microsoft run-times such as WPF and Silverlight, I’m accustomed to having a diverse assortment of visual elements at my disposal. EllipseView partially fills the gap, and it demonstrates that with a little perserverance, Xamarin Forms can be every bit as rich as its Microsoft counterparts.

Xamarin Partner

Build your next mobile application with Wintellect and Xamarin!

Need Xamarin Help?

Xamarin Consulting  Xamarin Training