Pixels and Device-Independent Units
Explore the differences between SkiaSharp coordinates and Xamarin.Forms coordinates
This article explores the differences in the coordinate system used in SkiaSharp and Xamarin.Forms. You can obtain information to convert between the two coordinate systems and also draw graphics that fill a particular area:
If you've been programming in Xamarin.Forms for a while, you might have a feel for Xamarin.Forms coordinates and sizes. The circles drawn in the two previous articles might seem a little small to you.
Those circles are 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 on the Xamarin.Forms coordinate system 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
andHeight
properties of theSKCanvasView
object. - The
CanvasSize
property of theSKCanvasView
object. - The
Size
property of theSKImageInfo
value, which is consistent with theWidth
andHeight
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:
public void DrawText (String text, Single x, Single y, SKPaint paint)
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 tiny 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:
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 seven simulator on the left has two pixels per device-independent unit, and the Android Nexus 5 in the center has three pixels per unit. That's why the simple circle shown earlier has different sizes on different platforms.
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 up that image 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.
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. (If you need to draw an ellipse with axes that are not parallel to the X and Y axes, you can use a rotation transform as discussed in the article The Rotate Transform or a graphics path as discussed in the article Three Ways to Draw an Arc). This overload of the DrawOval
method names the two radii parameters rx
and ry
to indicate that they are parallel to the X and Y axes:
public void DrawOval (Single cx, Single cy, Single rx, Single ry, SKPaint paint)
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:
The other DrawOval
method has an SKRect
argument, which is a rectangle defined in terms of the X and Y coordinates of its upper-left corner and lower-right corner. 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);