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.
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);
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:
0 comments