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.
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):