Vector Image View in Xamarin Forms

We've just completed our first Xamarin Forms project here at The Technology Studio and I wanted to share some of the cool things we learnt/created along the way (so stay tuned!).

One of the controls we needed for the project was a Vector Image view. The advantage of using Vector images over raster images is the fact we don't need to create different sized images for each platform and/or idiom, a situation that is worsened when developing for iOS and Android.

Mobile sizes

So lets dig into the control and show you how and where to use it:

Forms

The control is pretty straightforward, it inherits from Image (so we can use the WidthRequest and HeightRequest properties later on) and exposes two properties:

  • Vector - This is an enum of the different Vector images. I've handily called the enum VectorImage in our code but you can call it whatever you like.
  • Colour - a Xamarin.Forms.Color. I'll show how this is used so you don't need to create multiple colour variations of the same image in the renderers.

The control is below:

public class VectorImageView : Image
{
    public static readonly BindableProperty VectorProperty =
        BindableProperty.Create<VectorImageView, VectorImage>(
                p => p.Vector, 
                default(VectorImage));

    public static readonly BindableProperty ColourProperty =
        BindableProperty.Create<VectorImageView, Color?>(p => p.Colour, default(Color?));

    public VectorImage Vector
    {
        get 
        { 
            var value = GetValue(VectorProperty);

            if (value == null)
            {
                return default(VectorImage);
            }

            return (VectorImage)value; 
        }
        set { SetValue(VectorProperty, value); }
    }

    public Color? Colour
    {
        get { return (Color?)GetValue(ColourProperty); }
        set { SetValue(ColourProperty, value); }
    }
}

And it can be used in Xaml like so:

<images:VectorImageView Vector="InfoIcon" Colour="{x:Static controls:ColourPalette.Red}" HeightRequest="20"  WidthRequest="20">

The real work is in the two different renderers for iOS and Android so lets take a look:

iOS

The iOS renderer takes in the WidthRequest and HeightRequest and draws a Stylekit canvas that we created using Paintcode (a good tutorial on how to use it, and generate code for Xamarin, can be found here).

The renderer looks like this:

public class VectorImageViewRenderer : ImageRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
    {
        base.OnElementChanged(e);

        if (e.OldElement == null)
        {
            this.DrawImage();
        }
    }

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

        this.DrawImage();
    }

    private void DrawImage()
    {
        var viw = (VectorImageView)this.Element;
        var image = this.GetVectorImage(
            (nfloat)this.Element.WidthRequest, 
            (nfloat)this.Element.HeightRequest,
            viw.Vector);

        if (viw.Colour.HasValue)
        {                
            this.Control.Image = image.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate);
            this.Control.TintColor = viw.Colour.Value.ToUIColor();
        }
        else
        {
            this.Control.Image = image;
        }
    }

     private UIImage GetVectorImage(nfloat width, nfloat height, VectorImage image)
     {
         UIImage output;

         UIGraphics.BeginImageContextWithOptions(new CGSize(width, height), false, 0f);

        if (image == VectorImage.InfoIcon)
        {
            RocheDASAppStyleKit.DrawInfoIconCanvas((float)width, (float)height);
        }

        output = UIGraphics.GetImageFromCurrentImageContext();

        UIGraphics.EndImageContext();

        return output;
    }
}

Android

The Android renderer uses the XamSvg library to take svgs (which should be saved in Resources.Raw) and turn them into Bitmap drawables. We use WidthRequest and HeightRequest and convert them into dpi with the following calculation:

double pixelsPerOneDp = (double)((int)Forms.Context.Resources.DisplayMetrics.DensityDpi / 160f);
int widthDp = (int)Math.Round(width * pixelsPerOneDp);
int heightDp = (int)Math.Round(height * pixelsPerOneDp);

The renderer looks like:

public class VectorImageViewRenderer : ImageRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
    {
        base.OnElementChanged(e);

        if (e.OldElement == null)
        {
            this.DrawImage();
        }
    }

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

        this.DrawImage();
    }

    private void DrawImage()
    {
        var viw = (VectorImageView)this.Element;
        int width = (int)this.Element.WidthRequest;
        int height = (int)this.Element.HeightRequest;

        if (width == 0 || height == 0)
        {
            return;
        }

        var image = this.GetVectorImage(width, height, viw.Vector, viw.Colour);

        this.Control.SetImageDrawable(image);
     }

    private BitmapDrawable GetVectorImage(int width, int height, VectorImage vector, Xamarin.Forms.Color? colour = null)
    {
        int resource = -1;

        if (vector == VectorImage.InfoIcon)
        {
            resource = Resource.Raw.info_icon;
        }

        if (resource == -1)
        {
            return new BitmapDrawable();
        }

        double pixelsPerOneDp = (double)((int)Forms.Context.Resources.DisplayMetrics.DensityDpi / 160f);
        int widthDp = (int)Math.Round(width * pixelsPerOneDp);
        int heightDp = (int)Math.Round(height * pixelsPerOneDp);

        var bm = SvgFactory.GetBitmap(
            Forms.Context.Resources, 
            resource, 
            widthDp, 
            heightDp, 
            SvgColorMapperFactory.FromFunc((fromColour) => 
            {
                if (colour.HasValue)
                {
                    return colour.Value.ToAndroid();
                }

                return fromColour;
            }));
                    
        return new BitmapDrawable(bm);
    }
}

And it looks like this (taken on an iPhone 5S):