December 19th, 2014

Elegant Circle Images in Xamarin.Forms

James Montemagno
Principal Manager, Tech PM

Circular images in apps are currently a popular trend in mobile, and it is a simple technique that can add a lot of polish to your mobile apps. We made use of them in this year’s Xamarin Evolve mobile apps, and with a little bit of code and a single custom renderer, you too can transform standard images in Xamarin.Forms into these elegant round images.

MonkeyCircleImages

Custom Image

After you create your Xamarin.Forms project, the first thing you need to do is create a custom control that inherits from Image. Let’s call this ImageCircle and then implement the custom renderers on each platform.

public class ImageCircle : Image
{
  //Optional custom properties could go here
}

Custom Renderers

Now you just need to tell Xamarin.Forms how to render this new ImageCircle on each platform by using custom renderers.

For all three projects, you will need to create an ImageCircleRenderer that inherits from ImageRenderer and then implement how to cut out the circle image.

public class ImageCircleRenderer : ImageRenderer
{
  //...Implementation
}

iOS Renderer

For iOS you are able to directly interact with the CALayer of our UIImageView to add a corner radius and set the border color.

private void CreateCircle()
{
  try
  {
    double min = Math.Min(Element.Width, Element.Height);
    Control.Layer.CornerRadius = (float)(min / 2.0);
    Control.Layer.MasksToBounds = false;
    Control.Layer.BorderColor = Color.White.ToCGColor();
    Control.Layer.BorderWidth = 3;
    Control.ClipsToBounds = true;
  }
  catch(Exception ex)
  {
    Debug.WriteLine("Unable to create circle image: " + ex)
  }
}

This method should be called on the initial OnElementChanged or when the Height or Width properties are updated:

protected override void OnElementChanged(ElementChangedEventArgs e)
{
  base.OnElementChanged(e);

  if (e.OldElement != null || Element == null)
    return;

  CreateCircle();
}

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

  if (e.PropertyName == VisualElement.HeightProperty.PropertyName ||
      e.PropertyName == VisualElement.WidthProperty.PropertyName)
  {
    CreateCircle();
  }
}

And of course you must export the renderer with an assembly export:

[assembly: ExportRenderer(typeof(ImageCircle), typeof(ImageCircleRenderer))]
namespace CircleImage.iOS
{
 //...
}

Android Renderer

Android is a unique platform; you don’t have a physical layer to interact with, however, you are able to modify how Xamarin.Forms draws the child and cut out a path from the canvas. This method is a bit of complex math, but calculates the path to clip and then adds a circular border around the child.

protected override bool DrawChild(Canvas canvas, global::Android.Views.View child, long drawingTime)
{
  try
  {
    var radius = Math.Min(Width, Height) / 2;
    var strokeWidth = 10;
    radius -= strokeWidth / 2;

    //Create path to clip
    var path = new Path();
    path.AddCircle(Width / 2, Height / 2, radius, Path.Direction.Ccw);
    canvas.Save();
    canvas.ClipPath(path);

    var result = base.DrawChild(canvas, child, drawingTime);

    canvas.Restore();

    // Create path for circle border
    path = new Path();
    path.AddCircle(Width / 2, Height / 2, radius, Path.Direction.Ccw);

    var paint = new Paint();
    paint.AntiAlias = true;
    paint.StrokeWidth = 5;
    paint.SetStyle(Paint.Style.Stroke);
    paint.Color = global::Android.Graphics.Color.White;

    canvas.DrawPath(path, paint);

    //Properly dispose
    paint.Dispose();
    path.Dispose();
    return result;
  }
  catch (Exception ex)
  {
    Debug.WriteLine("Unable to create circle image: " + ex);
  }

  return base.DrawChild(canvas, child, drawingTime);
}

One intricate part here is that Android handles the clipping of paths differently depending on the version of Android. Hardware acceleration for this method was introduced in API 18, so you must tell Android to use software rendering on older devices. You can set the layer type flag in the OnElementChanged method:

protected override void OnElementChanged(ElementChangedEventArgs e)
{
  base.OnElementChanged(e);

  if (e.OldElement == null)
  {

    if ((int)Android.OS.Build.VERSION.SdkInt < 18)
      SetLayerType(LayerType.Software, null);
  }
}

And of course you must export the renderer with an assembly export:

[assembly: ExportRenderer(typeof(ImageCircle), typeof(ImageCircleRenderer))]
namespace CircleImage.Droid
{
 //...
}

Windows Phone Renderer

Last, but not least, we come to Windows Phone, which has a Clip property that you can use to create EllipseGeometry to cut out the circle from the image:

protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
  base.OnElementPropertyChanged(sender, e);
  if (Control != null && Control.Clip == null)
  {
    var min = Math.Min(Element.Width, Element.Height) / 2.0f;
    if (min <= 0)
      return;
    Control.Clip = new EllipseGeometry
    {
      Center = new System.Windows.Point(min, min),
      RadiusX = min,
      RadiusY = min
    };
  }
}

And of course you must export the renderer with an assembly export:

[assembly: ExportRenderer(typeof(ImageCircle), typeof(ImageCircleRenderer))]
namespace CircleImage.WinPhone
{
 //...
}

Custom Cell

With all of the custom renders in place, you can simply use your new ImageCircle anywhere you would like, including custom ViewCells for your ListViews. The key here is to ensure that you request the same Width & Height for your image and set the Aspect to AspectFill to ensure a perfect square to cut your circle from.

int photoSize = Device.OnPlatform(50, 50, 80);
var photo = new ImageCircle
{
  WidthRequest = photoSize,
  HeightRequest = photoSize,
  Aspect = Aspect.AspectFill,
  HorizontalOptions = LayoutOptions.Center
};
photo.SetBinding(Image.SourceProperty, s => s.Image);

CircleImages

See the Code in Action

You can grab the entire source code from GitHub. Additionally, I sat down with Xamarin’s own Steven Yi at this year’s Xamarin Evolve to explain how I implemented and used Circle Images in the Xamarin Evolve mobile apps:

Author

James Montemagno
Principal Manager, Tech PM

James Montemagno is a Principal Lead Program Manager for Developer Community at Microsoft. He has been a .NET developer since 2005, working in a wide range of industries including game development, printer software, and web services. Prior to becoming a Principal Program Manager, James was a professional mobile developer and has now been crafting apps since 2011 with Xamarin. In his spare time, he is most likely cycling around Seattle or guzzling gallons of coffee at a local coffee shop. He ...

More about author

0 comments

Discussion are closed.