Lines and Stroke Caps
Learn how to use SkiaSharp to draw lines with different stroke caps
In SkiaSharp, rendering a single line is very different from rendering a series of connected straight lines. Even when drawing single lines, however, it's often necessary to give the lines a particular stroke width. As these lines become wider, the appearance of the ends of the lines also becomes important. The appearance of the end of the line is called the stroke cap:
For drawing single lines, SKCanvas
defines a simple DrawLine
method whose arguments indicate the starting and ending coordinates of the line with an SKPaint
object:
canvas.DrawLine (x0, y0, x1, y1, paint);
By default, the StrokeWidth
property of a newly instantiated SKPaint
object is 0, which has the same effect as a value of 1 in rendering a line of one pixel in thickness. This appears very thin on high-resolution devices such as phones, so you'll probably want to set the StrokeWidth
to a larger value. But once you start drawing lines of a sizable thickness, that raises another issue: How should the starts and ends of these thick lines be rendered?
The appearance of the starts and ends of lines is called a line cap or, in Skia, a stroke cap. The word "cap" in this context refers to a kind of hat — something that sits on the end of the line. You set the StrokeCap
property of the SKPaint
object to one of the following members of the SKStrokeCap
enumeration:
Butt
(the default)Square
Round
These are best illustrated with a sample program. The SkiaSharp Lines and Paths section of the sample program begins with a page titled Stroke Caps based on the StrokeCapsPage
class. This page defines a PaintSurface
event handler that loops through the three members of the SKStrokeCap
enumeration, displaying both the name of the enumeration member and drawing a line using that stroke cap:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKPaint textPaint = new SKPaint
{
Color = SKColors.Black,
TextSize = 75,
TextAlign = SKTextAlign.Center
};
SKPaint thickLinePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Orange,
StrokeWidth = 50
};
SKPaint thinLinePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Black,
StrokeWidth = 2
};
float xText = info.Width / 2;
float xLine1 = 100;
float xLine2 = info.Width - xLine1;
float y = textPaint.FontSpacing;
foreach (SKStrokeCap strokeCap in Enum.GetValues(typeof(SKStrokeCap)))
{
// Display text
canvas.DrawText(strokeCap.ToString(), xText, y, textPaint);
y += textPaint.FontSpacing;
// Display thick line
thickLinePaint.StrokeCap = strokeCap;
canvas.DrawLine(xLine1, y, xLine2, y, thickLinePaint);
// Display thin line
canvas.DrawLine(xLine1, y, xLine2, y, thinLinePaint);
y += 2 * textPaint.FontSpacing;
}
}
For each member of the SKStrokeCap
enumeration, the handler draws two lines, one with a stroke thickness of 50 pixels and another line positioned on top with a stroke thickness of two pixels. This second line is intended to illustrate the geometric start and end of the line independent of the line thickness and a stroke cap:
As you can see, the Square
and Round
stroke caps effectively extend the length of the line by half the stroke width at the beginning of the line and again at the end. This extension becomes important when it's necessary to determine the dimensions of a rendered graphics object.
The SKCanvas
class also includes another method for drawing multiple lines that is somewhat peculiar:
DrawPoints (SKPointMode mode, points, paint)
The points
parameter is an array of SKPoint
values and mode
is a member of the SKPointMode
enumeration, which has three members:
Points
to render the individual pointsLines
to connect each pair of pointsPolygon
to connect all consecutive points
The Multiple Lines page demonstrates this method. The MultipleLinesPage.xaml file instantiates two Picker
views that let you select a member of the SKPointMode
enumeration and a member of the SKStrokeCap
enumeration:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp;assembly=SkiaSharp"
xmlns:skiaforms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharpFormsDemos.Paths.MultipleLinesPage"
Title="Multiple Lines">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Picker x:Name="pointModePicker"
Title="Point Mode"
Grid.Row="0"
Grid.Column="0"
SelectedIndexChanged="OnPickerSelectedIndexChanged">
<Picker.ItemsSource>
<x:Array Type="{x:Type skia:SKPointMode}">
<x:Static Member="skia:SKPointMode.Points" />
<x:Static Member="skia:SKPointMode.Lines" />
<x:Static Member="skia:SKPointMode.Polygon" />
</x:Array>
</Picker.ItemsSource>
<Picker.SelectedIndex>
0
</Picker.SelectedIndex>
</Picker>
<Picker x:Name="strokeCapPicker"
Title="Stroke Cap"
Grid.Row="0"
Grid.Column="1"
SelectedIndexChanged="OnPickerSelectedIndexChanged">
<Picker.ItemsSource>
<x:Array Type="{x:Type skia:SKStrokeCap}">
<x:Static Member="skia:SKStrokeCap.Butt" />
<x:Static Member="skia:SKStrokeCap.Round" />
<x:Static Member="skia:SKStrokeCap.Square" />
</x:Array>
</Picker.ItemsSource>
<Picker.SelectedIndex>
0
</Picker.SelectedIndex>
</Picker>
<skiaforms:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2" />
</Grid>
</ContentPage>
Notice that the SkiaSharp namespace declarations are a little different because the SkiaSharp
namespace is needed to reference the members of the SKPointMode
and SKStrokeCap
enumerations. The SelectedIndexChanged
handler for both Picker
views simply invalidates the SKCanvasView
object:
void OnPickerSelectedIndexChanged(object sender, EventArgs args)
{
if (canvasView != null)
{
canvasView.InvalidateSurface();
}
}
This handler needs to check for the existence of the SKCanvasView
object because the event handler is first called when the SelectedIndex
property of the Picker
is set to 0 in the XAML file, and that occurs before the SKCanvasView
has been instantiated.
The PaintSurface
handler obtains the two enumeration values from the Picker
views:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Create an array of points scattered through the page
SKPoint[] points = new SKPoint[10];
for (int i = 0; i < 2; i++)
{
float x = (0.1f + 0.8f * i) * info.Width;
for (int j = 0; j < 5; j++)
{
float y = (0.1f + 0.2f * j) * info.Height;
points[2 * j + i] = new SKPoint(x, y);
}
}
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.DarkOrchid,
StrokeWidth = 50,
StrokeCap = (SKStrokeCap)strokeCapPicker.SelectedItem
};
// Render the points by calling DrawPoints
SKPointMode pointMode = (SKPointMode)pointModePicker.SelectedItem;
canvas.DrawPoints(pointMode, points, paint);
}
The screenshots show a variety of Picker
selections:
The iPhone at the left shows how the SKPointMode.Points
enumeration member causes DrawPoints
to render each of the points in the SKPoint
array as a square if the line cap is Butt
or Square
. Circles are rendered if the line cap is Round
.
The Android screenshot shows the result of the SKPointMode.Lines
. The DrawPoints
method draws a line between each pair of SKPoint
values, using the specified line cap, in this case Round
.
When you instead use SKPointMode.Polygon
, a line is drawn between the successive points in the array, but if you look very closely, you'll see that these lines are not connected. Each of these separate lines starts and ends with the specified line cap. If you select the Round
caps, the lines might appear to be connected, but they're really not connected.
Whether lines are connected or not connected is a crucial aspect of working with graphics paths.