SkiaSharp Basics for Xamarin.Forms

PDF for offline use:
Sample Code:
Related APIs:

Let us know how you feel about this.


0/250
Thanks for the feedback!

last updated: 2017-02

Learn the basics of SkiaSharp graphics concepts and coordinates

After you have added the SkiaSharp NuGet package to your Xamarin.Forms application, you can begin using SkiaSharp graphics. The SkiaSharpFormsDemos solution includes several pages that demonstrate SkiaSharp programming techniques in progressively more advanced lessons.

Drawing a Simple Circle

The first page of the SkiaSharpFormsDemos program is entitled Simple Circle and invokes the page class SimpleCirclePage. This code shows how to draw a circle in the center of the page with a radius of 100 pixels. The outline of the circle is red, and the interior of the circle is blue.

The page class derives from ContentPage and contains two using directives for the SkiaSharp namespaces:

using SkiaSharp;
using SkiaSharp.Views.Forms;

The following constructor of the class creates an SKCanvasView object, attaches a handler for the PaintSurface event, and sets the SKCanvasView object as the content of the page:

public SimpleCirclePage()
{
    Title = "Simple Circle";

    SKCanvasView canvasView = new SKCanvasView();
    canvasView.PaintSurface += OnCanvasViewPaintSurface;
    Content = canvasView;
}

The SKCanvasView occupies the entire content area of the page. You can alternatively combine an SKCanvasView with other Xamarin.Forms View derivatives, as you'll see in other examples.

The PaintSurface event handler is where you do all your drawing. This method is generally called multiple times while your program is running, so it should maintain all the information necessary to recreate the graphics display:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
}

The SKPaintSurfaceEventArgs object that accompanies the event has two properties:

The SKImageInfo structure contains information about the drawing surface, most importantly, it's width and height in pixels. The SKSurface object represents the drawing surface itself. In this program, the drawing surface is a video display, but in other programs an SKSurface object can also represent a bitmap that you use SkiaSharp to draw on.

The most important property of SKSurface is Canvas of type SKCanvas. This class is a graphics drawing context that you use to perform the actual drawing. The SKCanvas object encapsulates a graphics state, which includes graphics transforms and clipping.

Here's a typical start of a PaintSurface event handler:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();
    ...
}

The Clear method clears the canvas with a transparent color. An overload lets you specify a background color for the canvas.

The goal here is to draw a red circle filled with blue. Because this particular graphic image contains two different colors, the job needs to be done in two steps. The first step is to draw the outline of the circle. To specify the color and other characteristic of the line, you create and initialize an SKPaint object:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
    SKPaint paint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = Color.Red.ToSKColor(),
        StrokeWidth = 25
    };
    ...
}

The Style property indicates that you want to stroke a line (in this case the outline of the circle) rather than fill the interior. The three members of the SKPaintStyle enumeration are as follows:

Use the third option to stroke the line and fill the interior with the same color.

Set the Color property to a value of type SKColor. One way to get an SKColor value is by converting a Xamarin.Forms Color value to an SKColor value using the extension method ToSKColor. The Extensions class in the SkiaSharp.Views.Forms namespace includes other methods that convert between Xamarin.Forms values and SkiaSharp values.

The StrokeWidth property indicates the thickness of the line. Here it's set to 25 pixels.

You use that SKPaint object to draw the circle:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
    canvas.DrawCircle(info.Width / 2, info.Height / 2, 100, paint);
    ...
}

Coordinates are specified relative to the upper-left corner of the display surface. X coordinates increase to the right and Y coordinates increase going down.

The first two arguments indicate the X and Y coordinates of the center of the circle. These are assigned half the width and height of the display surface to put the center of the circle in the center of the display surface. The third argument specifies the circle's radius, and the last argument is the SKPaint object.

To fill the interior of the circle, you can alter two properties of the SKPaint object and call DrawCircle again. This code also shows an alternative way to get an SKColor value from one of the many fields of the SKColors structure:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
    paint.Style = SKPaintStyle.Fill;
    paint.Color = SKColors.Blue;
    canvas.DrawCircle(args.Info.Width / 2, args.Info.Height / 2, 100, paint);
}

This time, the DrawCircle call fills the circle using the new properties of the SKPaint object.

Here's the program running on iOS, Android, and the Universal Windows Platform:

Triple screenshot of the Simple Circle page

When running the program yourself, you can turn the phone or simulator sideways to see how the graphic is redrawn. Each time the graphic needs to be redrawn, the PaintSurface event handler is called again.

An SKPaint object is little more than a collection of graphics drawing properties. These objects are very lightweight. You can reuse SKPaint objects as this program does, or you can create multiple SKPaint objects for various combinations of drawing properties. You can create and initialize these objects outside of the PaintSurface event handler, and you can save them as fields in your page class.

Although the width of the circle's outline is specified as 25 pixels — or one-quarter of the radius of the circle — it appears to be thinner, and there's a good reason for that: Half the width of the line is obscured by the blue circle. The arguments to the DrawCircle method define the abstract geometric coordinates of a circle. The blue interior is sized to that dimension to the nearest pixel, but the 25-pixel-wide outline straddles the geometric circle — half on the inside and half on the outside.

The following sample demonstrates this visually.

Responding to Touch

The second page in the SkiaSharpFormsDemos program is entitled Tap Toggle Fill. The TapToggleFillPage class shows how you can alter SkiaSharp graphics in response to user input.

For this page, the SKCanvasView class is instantiated in the TapToggleFill.xaml file, which also sets a Xamarin.Forms TapGestureRecognizer on the view:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.TapToggleFillPage"
             Title="Tap Toggle Fill">

    <skia:SKCanvasView PaintSurface="OnCanvasViewPaintSurface">
        <skia:SKCanvasView.GestureRecognizers>
            <TapGestureRecognizer Tapped="OnCanvasViewTapped" />
        </skia:SKCanvasView.GestureRecognizers>
    </skia:SKCanvasView>
</ContentPage>

Notice the skia XML namespace declaration.

The Tapped handler for the TapGestureRecognizer object simply toggles the value of a Boolean field and calls the InvalidateSurface method of SKCanvasView:

bool showFill = true;

...

void OnCanvasViewTapped(object sender, EventArgs args)
{
    showFill ^= true;
    (sender as SKCanvasView).InvalidateSurface();
}

The call to InvalidateSurface effectively generates a call to the PaintSurface handler, which uses the showFill field to fill or not fill the circle:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    SKPaint paint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = Color.Red.ToSKColor(),
        StrokeWidth = 50
    };
    canvas.DrawCircle(info.Width / 2, info.Height / 2, 100, paint);

    if (showFill)
    {
        paint.Style = SKPaintStyle.Fill;
        paint.Color = SKColors.Blue;
        canvas.DrawCircle(info.Width / 2, info.Height / 2, 100, paint);
    }
}

The StrokeWidth property has been set to 50 to accentuate the difference. You can also see the whole line width by drawing the interior first and then the outline. By default, graphics figures drawn later in the PaintSurface event handler obscure those drawn earlier in the handler.

Pixels and Device-Independent Units

If you've been programming in Xamarin.Forms for a while, you might have a feel for Xamarin.Forms coordinates and sizes. The circle might seem a little small to you.

It is small in comparison with Xamarin.Forms sizes. By default, SkiaSharp draws in units of pixels while Xamarin.Forms bases coordinates and sizes on a device-independent unit established by the underlying platform.(More information can be found in Chapter 5. Dealing with Sizes of the book Creating Mobile Apps with Xamarin.Forms.)

The page in the sample program entitled Surface Size uses SkiaSharp text output to show the size of the display surface from three different sources:

  • The normal Xamarin.Forms Width and Height properties of the SKCanvasView object.
  • The CanvasSize property of the SKCanvasView object.
  • The Size property of the SKImageInfo value, which is consistent with the Width and Height properties used in the two previous pages.

The SurfaceSizePage class shows how to display these values. The constructor saves the SKCanvasView object as a field, so it can be accessed in the PaintSurface event handler:

SKCanvasView canvasView;

public SurfaceSizePage()
{
    Title = "Surface Size";

    canvasView = new SKCanvasView();
    canvasView.PaintSurface += OnCanvasViewPaintSurface;
    Content = canvasView;
}

SKCanvas includes six different DrawText methods, but this DrawText method is the simplest. You specify the text string, the X and Y coordinates where the text is to begin, and an SKPaint object. The X coordinate specifies where the left side of the text is positioned, but watch out: The Y coordinate specifies the position of the baseline of the text. If you've ever written by hand on lined paper, the baseline is the line on which characters sit, and below which descenders (such as those on the letters g, p, q, and y) descend.

The SKPaint object allows you to specify the color of the text, the font family, and the text size. By default, the TextSize property has a value of 12, which results in very small text on high-resolution devices such as phones. In anything but the simplest applications, you'll also need some information on the size of the text you're displaying. The SKPaint class defines a FontMetrics property and several MeasureText methods, but for less fancy needs, the FontSpacing property provides a recommended value for spacing successive lines of text.

The following PaintSurface handler creates an SKPaint object for a TextSize of 40 pixels, which is the desired vertical height of the text from the top of ascenders to the bottom of descenders. The FontSpacing value that the SKPaint object returns is a little larger than that, about 47 pixels.

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    SKPaint paint = new SKPaint
    {
        Color = SKColors.Black,
        TextSize = 40
    };

    float fontSpacing = paint.FontSpacing;
    float x = 20;               // left margin
    float y = fontSpacing;      // first baseline
    float indent = 100;

    canvas.DrawText("SKCanvasView Height and Width:", x, y, paint);
    y += fontSpacing;
    canvas.DrawText(String.Format("{0:F2} x {1:F2}",
                                  canvasView.Width, canvasView.Height),
                    x + indent, y, paint);
    y += fontSpacing * 2;
    canvas.DrawText("SKCanvasView CanvasSize:", x, y, paint);
    y += fontSpacing;
    canvas.DrawText(canvasView.CanvasSize.ToString(), x + indent, y, paint);
    y += fontSpacing * 2;
    canvas.DrawText("SKImageInfo Size:", x, y, paint);
    y += fontSpacing;
    canvas.DrawText(info.Size.ToString(), x + indent, y, paint);
}

The method begins the first line of text with an X coordinate of 20 (for a little margin at the left) and a Y coordinate of fontSpacing, which is a little more than what's necessary to display the full height of the first line of text at the top of the display surface. After each call to DrawText, the Y coordinate is increased by one or two increments of fontSpacing.

Here's the program running on all three platforms:

Triple screenshot of the Surface Size  page

As you can see, the CanvasSize property of the SKCanvasView and the Size property of the SKImageInfo value are consistent in reporting the pixel dimensions. The Height and Width properties of the SKCanvasView are Xamarin.Forms properties, and report the size of the view in the device-independent units defined by the platform.

The iOS 7 simulator on the left has 2 pixels per device-independent unit, the Android Nexus 5 in the center has 3 pixels per unit, and the Nokia Lumia 925 on the right has 2.25 pixels per unit. That's why the simple circle shown earlier looks about the same size on the iPhone and Windows phone, but is smaller on the Android phone.

If you'd prefer to work entirely in device-independent units, you can do so by setting the IgnorePixelScaling property of the SKCanvasView to true. However, you might not like the results. SkiaSharp renders the graphics on a smaller device surface, with a pixel size equal to the size of the view in device-independent units. (For example, SkiaSharp would use a display surface of 360 x 512 pixels on the Nexus 5.) It then scales that image up in size, resulting in noticeable bitmap jaggies.

To maintain the same image resolution, a better solution is to write your own simple functions to convert between the two coordinate systems.

Scaling to the Display Surface

In addition to the DrawCircle method, SKCanvas also defines two DrawOval methods that draw an ellipse. An ellipse is defined by two radii rather than a single radius. These are known as the major radius and the minor radius. The DrawOval method draws an ellipse with the two radii parallel to the X and Y axes. That restriction can be overcome with transforms or the use of a graphics path (to be covered later), but this DrawOval method names the two radii argument rx and ry to indicate that they are parallel to the X and Y axes.

Is it possible to draw an ellipse that fills the display surface? The Ellipse Fill page demonstrates how. The PaintSurface event handler in the EllipseFillPage.xaml.cs class subtracts half the stroke width from the xRadius and yRadius values to fit the whole ellipse and its outline within the display surface:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    float strokeWidth = 50;
    float xRadius = (info.Width - strokeWidth) / 2;
    float yRadius = (info.Height - strokeWidth) / 2;

    SKPaint paint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Blue,
        StrokeWidth = strokeWidth
    };
    canvas.DrawOval(info.Width / 2, info.Height / 2, xRadius, yRadius, paint);
}

Here it is running on the three platforms:

Triple screenshot of the Surface Size  page

The other DrawOval method has an SGRect argument, which is a rectangle defined in terms of the X and Y coordinates of its upper-left corner, and a width and height. The oval fills that rectangle, which suggests that it might be possible to use it in the Ellipse Fill page like this:

SKRect rect = new SKRect(0, 0, info.Width, info.Height);
canvas.DrawOval(rect, paint);

However, that truncates all the edges of the outline of the ellipse on the four sides. You need to adjust all the SKRect constructor arguments based on the strokeWidth to make this work right:

SKRect rect = new SKRect(strokeWidth / 2,
                         strokeWidth / 2,
                         info.Width - strokeWidth / 2,
                         info.Height - strokeWidth / 2);
canvas.DrawOval(rect, paint);

Basic Animation

You can animate SkiaSharp graphics by causing the PaintSurface method to be called very frequently, each time drawing the graphics a little differently. This is demonstrated by the Pulsating Ellipse page, which animates the two axes of an ellipse so that it appears to be pulsating, and you can even control the rate of this pulsation.

The PulsatingEllipsePage.xaml file instantiates a Xamarin.Forms Slider and a Label to display the current value of the slider. Here's one way to integrate an SKCanvasView with other Xamarin.Forms views:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.PulsatingEllipsePage"
             Title="Pulsating Ellipse">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Slider x:Name="slider"
                Grid.Row="0"
                Maximum="10"
                Minimum="0.1"
                Value="5"
                Margin="20, 0" />

        <Label Grid.Row="1"
               Text="{Binding Source={x:Reference slider},
                              Path=Value,
                              StringFormat='Cycle time = {0:F1} seconds'}"
               HorizontalTextAlignment="Center" />

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="2"
                           PaintSurface="OnCanvasViewPaintSurface" />
    </Grid>
</ContentPage>

The code-behind file instantiates a Stopwatch object to serve as a high-precision clock. The OnAppearing override sets the pageIsActive field to true and calls a method named AnimationLoop. The OnDisappearing override sets that pageIsActive field to false:

Stopwatch stopwatch = new Stopwatch();
bool pageIsActive;
float scale;            // ranges from 0 to 1 to 0

public PulsatingEllipsePage()
{
    InitializeComponent();
}

protected override void OnAppearing()
{
    base.OnAppearing();
    pageIsActive = true;
    AnimationLoop();
}

protected override void OnDisappearing()
{
    base.OnDisappearing();
    pageIsActive = false;
}

The AnimationLoop method starts the Stopwatch and then loops while pageIsActive is true. This is essentially an "infinite loop" while the page is active, but it doesn't cause the program to hang because the loop concludes with a call to Task.Delay with the await operator, which lets other parts of the program function. The argument to Task.Delay causes it to complete after 1/30th second. This defines the frame rate of the animation.

async Task AnimationLoop()
{
    stopwatch.Start();

    while (pageIsActive)
    {
        double cycleTime = slider.Value;
        double t = stopwatch.Elapsed.TotalSeconds % cycleTime / cycleTime;
        scale = (1 + (float)Math.Sin(2 * Math.PI * t)) / 2;
        canvasView.InvalidateSurface();
        await Task.Delay(TimeSpan.FromSeconds(1.0 / 30));
    }

    stopwatch.Stop();
}

The while loop begins by obtaining a cycle time from the Slider. This is a time in seconds, for example, 5. The second statement calculates a value of t for time. For a cycleTime of 5, t increases from 0 to 1 every 5 seconds. The argument to the Math.Sin function in the second statement ranges from 0 to 2π every 5 seconds. The Math.Sin function returns a value ranging from 0 to 1 back to 0 and then to –1 and 0 every 5 seconds, but with values that change more slowly when the value is near 1 or –1. The value 1 is added so the values are always positive, and then it's divided by 2, so the values ranges from ½ to 1 to ½ to 0 to ½, but slower when the value is around 1 and 0. This is stored in the scale field, and the SKCanvasView is invalidated.

The PaintSurface method uses this scale value to calculate the two axes of the ellipse:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    float maxRadius = 0.75f * Math.Min(info.Width, info.Height) / 2;
    float minRadius = 0.25f * maxRadius;

    float xRadius = minRadius * scale + maxRadius * (1 - scale);
    float yRadius = maxRadius * scale + minRadius * (1 - scale);

    using (SKPaint paint = new SKPaint())
    {
        paint.Style = SKPaintStyle.Stroke;
        paint.Color = SKColors.Blue;
        paint.StrokeWidth = 50;
        canvas.DrawOval(info.Width / 2, info.Height / 2, xRadius, yRadius, paint);

        paint.Style = SKPaintStyle.Fill;
        paint.Color = SKColors.SkyBlue;
        canvas.DrawOval(info.Width / 2, info.Height / 2, xRadius, yRadius, paint);
    }
}

The method calculates a maximum radius based on the size of the display area, and a minimum radius based on the maximum radius. The scale value is animated between 0 and 1 and back to 0, so the method uses that to compute an xRadius and yRadius that ranges between minRadius and maxRadius. These values are used to draw and fill an ellipse:

Triple screenshot of the Pulsating Ellipse page

Notice that the SKPaint object is created in a using block. Like many SkiaShsrp classes SKPaint derives from SKObject, which derives from SKNativeObject, which implements the IDisposable interface. SKPaint overrides the Dispose method to release unmanaged resources.

Putting SKPaint in a using block ensures that Dispose is called at the end of the block to free these unmanaged resources. This happens anyway when memory used by the SKPaint object is freed by the .NET garbage collector, but in animation code, it's best to be somewhat proactive in freeing memory in a more orderly way.

A better solution in this particular case would be to create two SKPaint objects once and save them as fields.

Integrating Text and Graphics

The SkiaSharp Canvas class also includes methods to draw a rectangle (DrawRect) and a rectangle with rounded corners (DrawRoundRect). These methods require the rectangle to be defined as an SKRect value.

The Framed Text page centers a short text string on the page and surrounds it with a frame composed of a pair of rounded rectangles. The FramedTextPage class shows how it's done.

In SkiaSharp you use the SKPaint class to set text and font attributes, but you can also use it to obtain the rendered size of text. The beginning of the following PaintSurface event handler calls two different MeasureText methods. The first MeasureText call has a simple string argument and returns the pixel width of the text based on the current font attributes. The program then calculates a new TextSize property of the SKPaint object based on that rendered width, the current TextSize property, and the width of the display area. This is intended to set TextSize so that the text string to be rendered at 90% of the width of the screen:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    string str = "Hello SkiaSharp!";

    // Create an SKPaint object to display the text
    SKPaint textPaint = new SKPaint
    {
        Color = SKColors.Chocolate
    };

    // Adjust TextSize property so text is 90% of screen width
    float textWidth = textPaint.MeasureText(str);
    textPaint.TextSize = 0.9f * info.Width * textPaint.TextSize / textWidth;

    // Find the text bounds
    SKRect textBounds;
    textPaint.MeasureText(str, ref textBounds);
    ...
}

The second MeasureText call has an SKRect argument, so it obtains both a width and height of the rendered text. The Height property of this SKRect value depends on the presence of capital letters, ascenders, and descenders in the text string. Different Height values are reported for the text strings "mom", "cat", and "dog", for example.

The Left and Top properties of the SKRect structure indicate the coordinates of the upper-left corner of the rendered text if the text is displayed by a DrawText call with X and Y positions of 0. For example, when this program is running on an iPhone 7 simulator, TextSize is assigned the value 90.6254 as a result of the calculation following the first call to MeasureText. The SKRect value obtained from the second call to MeasureText has the following property values:

  • Left = 6
  • Top = –68
  • Width = 664.8214
  • Height = 88;

Keep in mind that the X and Y coordinates you pass to the DrawText method specify the left side of the text at the baseline. The Top value indicates that the text extends 68 pixels above that baseline and (subtracting 68 it from 88) 20 pixels below the baseline. The Left value of 6 indicates that the text begins 6 pixels to the right of the X value in the DrawText call. This allows for normal inter-character spacing. If you want to display the text snugly in the upper-left corner of the display, pass the negatives of these Left and Top values as the X and Y coordinates of DrawText, in this example, –6 and 68.

The SKRect structure defines several handy properties and methods, some of which are used in the remainder of the PaintSurface handler. The MidX and MidY values indicate the coordinates of the center of the rectangle. (In the iPhone 7 example, those values are 338.4107 and –24.) The following code uses these values for the easiest calculation of coordinates to center text on the display:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
    // Calculate offsets to center the text on the screen
    float xText = info.Width / 2 - textBounds.MidX;
    float yText = info.Height / 2 - textBounds.MidY;

    // And draw the text
    canvas.DrawText(str, xText, yText, textPaint);
    ...
}

The PaintSurface handler concludes with two calls to DrawRoundRect, both of which require arguments of SKRect. This SKRect value is certainly similar to the SKRect value obtained from the MeasureText method, but it can't be the same. First, it needs to be a little larger so that the rounded rectangle doesn't draw over edges of the text, and secondly, it needs to be shifted in space so that the Left and Top values correspond to the upper-left corner where the rectangle is to be positioned. These two jobs are accomplished by Offset and Inflate methods defined by SKRect:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
    // Create a new SKRect object for the frame around the text
    SKRect frameRect = textBounds;
    frameRect.Offset(xText, yText);
    frameRect.Inflate(10, 10);

    // Create an SKPaint object to display the frame
    SKPaint framePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 5,
        Color = SKColors.Blue
    };

    // Draw one frame
    canvas.DrawRoundRect(frameRect, 20, 20, framePaint);

    // Inflate the frameRect and draw another
    frameRect.Inflate(10, 10);
    framePaint.Color = SKColors.DarkBlue;
    canvas.DrawRoundRect(frameRect, 30, 30, framePaint);
}

Following that, the remainder of the method is straight-forward. It creates another SKPaint object for the borders and calls DrawRoundRect twice. The second call uses a rectangle inflated by another 10 pixels. The first call specifies a corner radius of 20 pixels; the second has a corner radius of 30 pixels, so they seem to be parallel:

Triple screenshot of the Framed Text page

You can turn your phone or simulator sideways to see the text and frame increase in size.

If you only need to center some text on the screen, you can do it approximately without measuring the text by setting the TextAlign property of SKPaint to SKTextAlign.Center. The X coordinate you specify in the DrawText method then indicates where the horizontal center of the text is positioned. If you pass the midpoint of the screen to the DrawText method, the text will be horizontally centered and nearly vertically centered because the baseline will be vertically centered.

You have now seen how to use SkiaSharp to draw text, circles, ellipses, and rounded rectangles. The next step is SkiaSharp Lines and Paths in which you will learn how to draw connected lines and curves in a graphics path.

Xamarin Workbook

If it's not already installed, install the Xamarin Workbooks app first. The workbook file should download automatically, but if it doesn't, just click to start the workbook download manually.