像素和与设备无关的单位

探索 SkiaSharp 坐标和 Xamarin.Forms 坐标之间的差异

本文探讨 SkiaSharp 和 Xamarin.Forms 中使用的坐标系的区别。 可获取信息以在两个坐标系之间转换,还可绘制填充特定区域的图形:

填满屏幕的椭圆形

如果你使用 Xamarin.Forms 编程有一段时间了,你可能对 Xamarin.Forms 坐标和大小有所了解。 前两篇文章中画的圆圈对你来说可能有点小。

与 Xamarin.Forms 大小相比,这些圆圈很小。 默认情况下,SkiaSharp 以像素为单位绘制,而 Xamarin.Forms 基于底层平台建立的设备无关单位确定坐标和大小。 (有关 Xamarin.Forms 坐标系的详细信息,可参阅第 5 章.处理大小(参见《使用 Xamarin.Forms 创建移动应用》一书。)

示例程序中名为“Surface Size”的页使用 SkiaSharp 文本输出来显示来自三个不同源的显示图面的大小

  • SKCanvasView 对象的普通 Xamarin.FormsWidthHeight 属性。
  • CanvasSize 对象的 SKCanvasView 属性。
  • SKImageInfo 值的 Size 属性,它与前两个页面中使用的 WidthHeight 属性一致。

SurfaceSizePage 类演示如何显示这些值。 构造函数将 SKCanvasView 对象保存为字段,这样可在 PaintSurface 事件处理程序中访问它:

SKCanvasView canvasView;

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

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

SKCanvas 包括 6 种不同的 DrawText 方法,但此 DrawText 方法是最简单的:

public void DrawText (String text, Single x, Single y, SKPaint paint)

指定文本字符串、文本开始位置的 X 和 Y 坐标以及 SKPaint 对象。 X 坐标指定文本左侧的位置,但请注意:Y 坐标指定文本基线的位置。 如果你曾经用手在横格纸上书写,基线是字符所在的行,这条线下面是下降部(如字母 g、p、q 和 y 的下降部)。

通过 SKPaint 对象可以指定文本的颜色、字体系列和文本大小。 默认情况下,TextSize 属性的值为 12,这会导致在手机等高分辨率设备上文本很小。 在除了最简单的应用程序之外的任何应用程序中,还需要一些有关要显示的文本大小的信息。 SKPaint 类定义了一个 FontMetrics 属性和几种 MeasureText 方法,但对于不太花哨的需求,FontSpacing 属性为间隔连续的文本行提供了推荐值。

以下 PaintSurface 处理程序为 40 像素的 TextSize 创建一个 SKPaint 对象,这是文本从上升部顶部到下降部底部的期望垂直高度。 SKPaint 对象返回的 FontSpacing 值略大于该值,约 47 像素。

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);
}

该方法以 X 坐标 20(左侧有一点空白)和 Y 坐标 fontSpacing 开始第一行文本,这比在显示图面顶部显示第一行文本的完整高度所需的值要大一点。 每次调用 DrawText 后,Y 坐标将增加 fontSpacing 的一或两个增量。

下面是正在运行的程序:

屏幕截图显示在两台移动设备上运行的 Surface Size 应用。

可以看到,在报告像素尺寸时,SKCanvasViewCanvasSize 属性和 SKImageInfo 值的 Size 属性是一致的。 SKCanvasViewHeightWidth 属性是 Xamarin.Forms 属性,并用平台定义的设备无关单位报告视图的大小。

在左侧的 iOS 7 模拟器中,每个设备无关单位有两个像素,中间的 Android Nexus 5 是每个单元有三个像素。 这就是为什么前面显示的简单圆圈在不同平台上有不同的大小。

如果希望完全用设备无关单位来工作,可以通过将 SKCanvasViewIgnorePixelScaling 属性设置为 true 来执行此操作。 但是,你可能不喜欢这样的结果。 SkiaSharp 在较小的设备图面上呈现图形,像素大小等于用设备无关单位显示的视图大小。 (例如,SkiaSharp 会在 Nexus 5 上使用 360 x 512 像素的显示图面。)然后,它会放大该图像的大小,产生明显的位图锯齿。

为了保持相同的图像分辨率,更好的解决方案是编写自己的简单函数,在两个坐标系之间进行转换。

除了 DrawCircle 方法,SKCanvas 还定义了两 DrawOval 个方法来绘制椭圆。 椭圆是由两个半径而不是一个半径来定义的。 它们称为主半径和次半径。 DrawOval 方法绘制一个椭圆,其两个半径与 X 轴和 Y 轴平行。 (如果需要绘制轴与 X 轴和 Y 轴不平行的椭圆,可以使用旋转变换一文中所述的旋转变换或绘制弧线的三种方法一文中所述的图形路径)。 DrawOval 方法的此重载命名了两个半径参数 rxry,来指示它们与 X 轴和 Y 轴平行:

public void DrawOval (Single cx, Single cy, Single rx, Single ry, SKPaint paint)

是否可以绘制填充显示图面的椭圆? “椭圆填充”页面演示了操作方法。 EllipseFillPage.xaml.cs 类中的 PaintSurface 事件处理程序从 xRadiusyRadius 值中减去笔画宽度的一半,以适应显示图面内的整个椭圆及其轮廓:

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);
}

下面是它的运行:

屏幕截图显示在两台移动设备上运行的 Ellipse Fill 应用。

另一种 DrawOval 方法有一个 SKRect 自变量,它是一个矩形,根据其左上角和右下角的 X 和 Y 坐标来定义。 椭圆填充该矩形,这表明可能会在“椭圆填充”页面中使用它,如下所示:

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

但是,这会截断椭圆轮廓在四个边上的所有边。 需要根据 strokeWidth 调整所有 SKRect 构造函数自变量才能使其正确工作:

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