In this excersize I will teach you how to create a radial menu in a Xamarin.Forms application. The concept you will learn from this tutorial will give you the skills to create new 2D pickers of any design! To make this happen we need a 2D graphics system to help us draw this shape. SkiaSharp works wonderfully for this. Head over to nuget and install SkiaSharp and SkiaSharp.Views.Forms into your Xamarin.Forms project. Once it is installed you can get started creating a 2D control for your UI.
In this section we will go over:
- Adding a SkiaSharp view as a component
- Painting the canvas
- Responding to touch events of a canvas
Preparing the control
Lets get started by creating a SKCanvasView that can be accessed from XAML. Create a RadialMenu class that inherits from SKCanvasView. This is blank canvas we can draw onto using coordinates.
public sealed class RadialMenu : SKCanvasView {}
The canvas can now be accessed anywhere in XAML because SKCanvisView inherits from View. Add a reference to the XML namespace of your radial menu and add it to your UI.
<?xml version="1.0" encoding="utf-8"?> <ContentPage ios:Page.UseSafeArea="True" x:Class="RadialMenuApp.MainPage" x:DataType="dd:MainPageViewModel" xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:pickers="clr-namespace:RadialMenuApp;assembly=RadialMenuApp" xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"> <pickers:RadialMenu /> </ContentPage>
Override the OnPaintSurface method so you can have control over how the canvas is drawn. We’re not quite ready to start drawing yet but can get some materials such as info and canvas. Add a canvas.Clear() to whipe the canvas clean everytime we paint to prevent overlapping shapes.
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e) { base.OnPaintSurface(e); var info = e.Info; var canvas = e.Surface.Canvas; canvas.Clear(); }
We need to create arcs around the circumfrence of a circle to draw our menu items. The number of arcs is determined by the menu items count so to create this item source we need a MenuItem class.
public class MenuItem { public string Name { get; } public string Icon { get;} public MenuItem(string name, string icon) { Name = name; Icon = icon; } }
Create a viewmodel to generate a static list of menu items.
public class MainPageViewModel { public MainPageViewModel() { ItemSource = new List<MenuItem> { new MenuItem("Apple", "fa-apple"), new MenuItem("Android", "fa-android"), new MenuItem("Windows", "fa-windows"), new MenuItem("Chip", "fa-microchip"), new MenuItem("Code", "fa-code"), new MenuItem("Terminal", "fa-terminal"), new MenuItem("Fire", "fa-fire-extinguisher"), new MenuItem("Bug", "fa-bug"), new MenuItem("Coffee", "fa-coffee"), new MenuItem("Developer", "fa-user-secret"), }; } public IReadOnlyList<MenuItem> ItemSource { get; } }
To access the ItemSource in our viewmodel you must bind the viewmodel to your UI. To do this I will assign the views binding context to the new viewmodel in Xaml if you dont already have an MVVM framework in place that handles this for you.
<ContentPage ios:Page.UseSafeArea="True" x:Class="SkiaTestApp.MainPage" x:DataType="dd:MainPageViewModel" xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:pickers="clr-namespace:SkiaTestApp;assembly=SkiaTestApp" xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"> <ContentPage.BindingContext> <pickers:MainPageViewModel /> </ContentPage.BindingContext> <pickers:RadialMenu ItemSource="{Binding ItemSource}"/> </ContentPage>
Learn more about data bindings.
Create a BindableProperty in RadialMenu to access the list of menu items.
public static readonly BindableProperty ItemSourceProperty = BindableProperty.Create(nameof(ItemSource), typeof(IReadOnlyList<MenuItem>), typeof(RadialMenu), new List<MenuItem>()); public IReadOnlyList<MenuItem> ItemSource { get => (IReadOnlyList<MenuItem>)GetValue(ItemSourceProperty); set => SetValue(ItemSourceProperty, value); }
Learn more about BindableProperties.
Head over to your UI and assign the ItemSource and your canvas should now be bound to a list of items!
<pickers:RadialMenu ItemSource="{Binding ItemSource}"/>
Draw an arc for each menu item
Now that you’ve added the item source binding we can determine the number of arcs to draw. In the OnPaintSurface method add a null check to the item source and iterate through it.
if (ItemSource.Count == 0) { throw new Exception("Missing menu items for radial wheel"); } for (var i = 0; i < ItemSource.Count; i++) { }
For each menu item we need to draw a arc around the circle. We will use methods of SKPath to draw the menu item borders.
- MoveTo – Starting coordinates of your path, creates a new path
- LineTo – Draw line to next coordinate of path
- ArcTo – Draw an arc to next coordinate of path
- Close – Completes the path
So how can we get our path coordinates? We want the use the points along the circumfrance of a circle to draw our path. Use this equation to determine coordinates based on degrees.
x = cx + r * cos(d - o) * (π / 180)
y = cy + r * sin(d - o) * (π / 180)
- cx/cy – Centre x and centre y position of the canvas.
- r – radius from centre of the canvas to edge of arc
- d- degree around the circle
- 0 -offset degrees
To get the sweeping angle we divide 360 by ItemSource count. The sweeping angle tells us the angle of each arc. The offset will be set to 90° which forces the degrees to rotate to their correct position.
Add this code before your loop.
const int offset = 90; var sweepingAngle = 360 / ItemSource.Count;
Now that you have the angle of the arcs you can calculate the starting degrees of arc. So if you have 3 items then degrees should be set to 120, 240, then 360 as you itterate through the loop. Since you now have the degrees you have everything you need to plug into the formula so you can get the starting coordinates of your arc.
Inside your loop add this code
var degrees = i * sweepingAngle; var startX = info.Rect.MidX + _arcLength * Math.Cos((degrees - offset) * (Math.PI / 180)); var startY = info.Rect.MidY + _arcLength * Math.Sin((degrees - offset) * (Math.PI / 180)); ... } private const int _arcLength = 200;
Okay now you have the starting coordinates so lets start drawing the SKPath. Use MoveTo to add the starting position of to our path. You will also want to close to path to indicate the paths’ end which allows the polygon to properly fill.
var path = new SKPath(); path.MoveTo(new SKPoint((float)startX, (float)startY)); path.Close();
Our brush needs some paint so create a border paint to use on our path, do this before of the loop and add a private variable to define your border width constant.
var borderPaint = new SKPaint { StrokeWidth = _borderWidth, Style = SKPaintStyle.Stroke, StrokeCap = SKStrokeCap.Round, Color = Color.Red.ToSKColor() }; ... } private int _borderWidth = 6;
Great, now you have your paint and path so lets put code to canvas!
Inside the loop
canvas.DrawPath(path, borderPaint);

Now use ArcTo to draw the arc from start to end. The first parameter is the rectangle which your arcs’ circle would fit inside, then the starting position of the arc, degrees of the arc, then finally force move to which starts a new path.
path.ArcTo(new SKRect( info.Rect.MidX - _arcLength, info.Rect.MidY - _arcLength, info.Rect.MidX + _arcLength, info.Rect.MidY + _arcLength ), degrees - offset, sweepingAngle, true);

As you can see that the arcs are connected but they are actually drawing an additional line back to the starting point. That is because we are closing our path which will connect a polygon.
The final stop of our path is to draw a line back to the centre of the circle.

Congradulations you’ve made a pizza, I hope your hungry! This does not quite look like the finished product yet but you’ve made some good progress.
Painting the canvas
Lets add some gradient color to our slices.. For now I will just add orange as dark orange as my gradient colors as these will become more dynamic later.
this code should go before the loop
var gradient = SKShader.CreateRadialGradient( new SKPoint(info.Rect.MidX, info.Rect.MidY), _arcLength, new SKColor[]{Color.Orange.ToSKColor(), Color.DarkOrange.ToSKColor()}, null, SKShaderTileMode.Clamp); var innerPaint = new SKPaint { Style = SKPaintStyle.Fill, Shader = gradient };
Now draw another path using the innerPaint. Drawing the inner paint should happen before drawing the border paint because the border paint should overlap the inner paint.
canvas.DrawPath(path, innerPaint); canvas.DrawPath(path, borderPaint);

This is really starting to look like a pizza! Add a circle in the centre of the circle so it looks more like the intended design.
Add this code after the loop
_circlePath.Reset(); _circlePath.AddCircle(info.Rect.MidX, info.Rect.MidY, _diameter); _circlePath.Close(); canvas.DrawPath(_circlePath, borderPaint);

Fill in the inner circle using DrawCircle
_circlePath.Reset(); canvas.DrawCircle( new SKPoint(info.Rect.MidX, info.Rect.MidY), _diameter - (_borderWidth / 2), innerPaint);

Position the icons
We need to define coordinates for our FontAwesome icons. At the end of our loop we will add code to create placeholder coordinates for the icons.
using (var textPaint = new SKPaint { IsAntialias = true, TextSize = 60, TextAlign = SKTextAlign.Center }) { const float lengthRatio = 0.75f; var iconX = (float)(info.Rect.MidX + (_arcLength * lengthRatio) * Math.Cos((degrees + (sweepingAngle / 2) - offset) * (Math.PI / 180))); var iconY = (float)(info.Rect.MidY + (_arcLength * lengthRatio) * Math.Sin((degrees + (sweepingAngle / 2) - offset) * (Math.PI / 180))) //vertical text alignment offset + (textPaint.TextSize / 2); canvas.DrawCircle(new SKPoint(iconX, iconY), 10, borderPaint); }

Add the icons
We dont want to draw pepporonis we want to add FontAwesome icons. To do this you need to add the SkiaSharp.Extended.Iconify.FontAwesome library to your Xamarin.Forms project and init FontAwesome capabilities in your app.xaml.cs
public App() { InitializeComponent(); SKTextRunLookup.Instance.AddFontAwesome(); MainPage = new MainPage(); }
Download the FontAwesome OTF files and add them to your Xamarin.Forms project. You will need to change their build action to be EmbeddedResource.
You’re all setup to use FontAwesome in SkiaSharp! Replace the DrawCircle code you added to position the placeholder with a call to DrawIconifiedText.
//canvas.DrawCircle(new SKPoint(iconX, iconY), 10, borderPaint); canvas.DrawIconifiedText($"{{{{{ItemSource[i].Icon} color=FFFFFF}}}}", (float)iconX, (float)iconY, textPaint);

Responding to touch events
You can respond to touch evens using the onTouch override. When the canvas is pressed it will re-draw the it using InvalidateSurface if it is determined a menu item was selected. For OnTouch to be triggered you will need to enable touch events in the constructor. Now when the user touches the canvas it will be redrawn.
public RadialMenu() { EnableTouchEvents = true; } ... protected override void OnTouch(SKTouchEventArgs e) { base.OnTouch(e); switch (e.ActionType) { case SKTouchAction.Pressed: InvalidateSurface(); break; } e.Handled = true; }
When the canvas is touched how can you know which item they’re selecting or if they selected an item at all? Thankfully the SKPath has a Contains method that will tell you if a path contains coordinates. We will use this method to know when an arc was touched because have the coordinates of where a user touched. If the path contains the touch then we set a selected item.
To do this create a List of SKPaths. We will populate it with the arc paths you created earlier. This way we can access the path data on touch.
Because you will be creating new arc segments everytime the canvas is touched you will want to clear the list of arc segments on paint surface.
before the loop.
_arcSegments.Clear(); ... } private List<SKPath> _arcSegments = new List<SKPath>();
Now add each path to the list after the arc path is closed.
_arcSegments.Add(path);
Now that we’re tracking the paths we can check if any arc path contains the location coordinates of a users touch. Back in the OnTouch override we are going to check if a user touched our polygon slice by checking if path in our list contains the touched location coordinate. If a user clicks the inner circle we also dont want anything to be redrawn as there is no selectable item in centre.
When this criteria is met we can then say a user clicked inside one of our menu item arcs. Create a selected index property that will only be set when an arc is selected and set it on selection
case SKTouchAction.Pressed: _selectedArcSegmentIndex = -1; for (var i = 0; i < _arcSegments.Count; i++) { if (_arcSegments[i].Contains(e.Location.X, e.Location.Y) && !_circlePath.Contains(e.Location.X, e.Location.Y)) { _selectedArcSegmentIndex = i; //TODO add toast using Xamarin.CommunityToolkit to XF project //this.DisplayToastAsync(ItemSource[i].Name, 1000); } } InvalidateSurface(); break; ... } private int _selectedArcSegmentIndex = -1;
Pulling out a slice
Add a selected arc padding before the for loop set to 20. Within the loop you will need to change how arc length is used by creating a more dynamic variable for arc length
var length = _selectedArcSegmentIndex == i ? _arcLength + selectedArcPadding : _arcLength; var startX = info.Rect.MidX + length * Math.Cos((degrees - offset) * (Math.PI / 180)); var startY = info.Rect.MidY + length * Math.Sin((degrees - offset) * (Math.PI / 180)); path.MoveTo(new SKPoint((float)startX, (float)startY)); path.ArcTo(new SKRect( info.Rect.MidX - length, info.Rect.MidY - length, info.Rect.MidX + length, info.Rect.MidY + length ), degrees - offset, sweepingAngle, true);
Go ahead and grab your slice. The arc will now extending outward on selection. There are many fun ways to use SkiaSharp and I hope that you found this tutorial useful.

Great work!! ???
I blog quite often аnd I genuinely thank you for youг content.
This grеat artіcle has truly peaked my interest.
I’m going to booқ mark your webѕitе and keep checking for
new informɑtion about once per week. Ι opted іn for your
Feed too.
Awesome ! Can the radial menu rotate ?
Hi Sam, yes it’s possible to rotate, but the code does not do this in its current state. If you want to create a “wheel-of-fortune” style picker you can. Use the existing get points by degrees method to find out how many degrees the users finger has moved around the center of circle, As the user is touching the canvas you are adding/subtracting degrees, depending on which direction the user is circling their finger.