Technology

Vector Image View in Xamarin Forms

Rob Waggott
Rob Waggott
26 Jun 2015
blog post featured image

We've just completed our first <a href="http://xamarin.com/forms" target="_blank">Xamarin Forms</a> project here at <a href="http://www.thetechnologystudio.co.uk/" target="_blank">The Technology Studio</a> 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 <a href="http://www.paintcodeapp.com/" target="_blank">Paintcode</a> (a good tutorial on how to use it, and generate code for Xamarin, can be found <a href="http://blog.xamarin.com/meet-our-new-favorite-design-tool-paintcode/" target="_blank">here</a>).

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 <a href="https://github.com/garuma/xamsvg" target="_blank">XamSvg library</a> 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):

Close chatbot
Open chatbot
Open chatbot