线和笔划大写字母

了解如何使用 SkiaSharp 绘制具有不同笔触端点的线条

在 SkiaSharp 中,呈现单个线条与呈现一系列连接的直线很不一样。 但是,即使在绘制单个线条时,也通常需要为线条提供特定的笔触宽度。 随着这些线条变宽,线条末尾的外观也变得很重要。 线条末尾的外观称为“笔触端点”:

三笔划端点选项

绘制单个线条时,SKCanvas 会定义一个简单的 DrawLine 方法,其参数指示线条的起始坐标和结束坐标,以及 SKPaint 对象:

canvas.DrawLine (x0, y0, x1, y1, paint);

默认情况下, 新实例化的 SKPaint 对象的 StrokeWidth 属性为 0,在呈现一个一像素粗的线条时,其效果与值 1 相同。 这在手机等高分辨率设备上显得非常薄,因此你可能想要将 StrokeWidth 设置为更大的值。 但是,一旦你开始绘制相当粗的线条,这就会引发另一个问题:应该如何呈现这些粗线条的开头和结尾?

线条的开头和结尾的外观称为“线条端点”,在 Skia 中,它称为“笔触端点”。 此上下文中的“端点”一词指的是一种帽子,即位于线条末尾的东西。 将 SKPaint 对象的 StrokeCap 属性设置为 SKStrokeCap 枚举的下列成员之一:

  • Butt(默认值)
  • Square
  • Round

最好用示例程序阐明它们。 示例程序的“SkiaSharp 线条和路径”部分的开头是一个标题为“笔划端点”的页面,它基于 StrokeCapsPage 类。 此页定义了一个 PaintSurface 事件处理程序,它会遍历 SKStrokeCap 枚举的三个成员,同时显示枚举成员的名称,并使用该笔触端点绘制线条:

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

对于 SKStrokeCap 枚举的每个成员,该处理程序都会绘制两个线条,一个线条粗细为 50 像素,另一个线条位于顶部,笔触粗细为 2 像素。 第二个线条是为了阐明线条的几何起点和终点,与线条粗细和笔触端点无关:

“笔划端点”页的三屏幕截图

如你所看到的,SquareRound 笔触端点在线条的开头和末尾有效地将线条的长度延长了笔触宽度的一半。 当需要确定呈现的图形对象的尺寸时,这样的延长就变得很重要。

SKCanvas 类还包含用于绘制多个线条的另一种方法,它有点奇特:

DrawPoints (SKPointMode mode, points, paint)

points 参数是一个 SKPoint 值的数组,modeSKPointMode 枚举的成员,其中包含三个成员:

  • Points,用于呈现各个点
  • Lines,用于连接每对点
  • Polygon,用于连接所有连续点

“多线条”页演示了此方法。 MultipleLinesPage.xaml 文件实例化了两个 Picker 视图,使你能够选择 SKPointMode 枚举的一个成员和 SKStrokeCap 枚举的一个成员:

<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>

请注意,SkiaSharp 命名空间声明稍有不同,因为需要 SkiaSharp 命名空间来引用 SKPointModeSKStrokeCap 枚举的成员。 这两个 Picker 视图的 SelectedIndexChanged 处理程序只会使 SKCanvasView 对象失效:

void OnPickerSelectedIndexChanged(object sender, EventArgs args)
{
    if (canvasView != null)
    {
        canvasView.InvalidateSurface();
    }
}

此处理程序需要检查 SKCanvasView 对象是否存在,因为在 XAML 文件中将 PickerSelectedIndex 属性设置为 0 时,会首先调用事件处理程序,这发生在实例化 SKCanvasView 之前。

PaintSurface 处理程序会从 Picker 视图中获取两个枚举值:

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

屏幕截图显示了各种 Picker 选择:

“多线条”页的三屏幕截图

左侧的 iPhone 显示 SKPointMode.Points 枚举成员如何使 DrawPointsSKPoint 数组中的每个点呈现为正方形(如果线条端点为 ButtSquare)。 如果线条端点为 Round,则呈现圆圈。

Android 屏幕截图显示了 SKPointMode.Lines 的结果。 DrawPoints 方法使用指定的线条端点(在本例中为 Round)在每对 SKPoint 值之间绘制一条线。

当你改用 SKPointMode.Polygon 时,会在数组中的连续点之间绘制一条线,但如果仔细观察,你会发现这些线条未连接。 每个单独的线条以指定的线条端点开头和结尾。 如果选择 Round 端点,则线条可能显示为已连接,但它们实际上未连接。

线条连接与否是处理图形路径的关键方面。