SkiaSharp 中的点和短划线

掌握在 SkiaSharp 中绘制点划线的复杂技巧

利用 SkiaSharp,可以绘制由点和短划线组成的非实线:

虚线

可以使用路径效果来完成此操作,该效果是 SKPathEffect 类的一个实例,将其设置为 SKPaintPathEffect 属性。 可以使用 SKPathEffect 定义的静态创建方法之一创建路径效果(或组合路径效果)。 (SKPathEffect 是 SkiaSharp 支持的六种效果之一;其他效果在 SkiaSharp 效果部分进行了介绍。)

若要绘制点状或短划线式虚线,请使用 SKPathEffect.CreateDash 静态方法。 有两个参数:第一个是 float 值的数组,表示点和短划线的长度以及它们之间的空隙的长度。 此数组必须有偶数个元素,并且至少含两个元素。 (数组中可以有零个元素,但这会导致生成实线。)如果有两个元素,则第一个元素是点或短划线的长度,第二个元素是下一个点或短划线之前的间隙长度。 如果有两个以上的元素,则它们按以下顺序排列:短划线长度、间隙长度、短划线长度、间隙长度,依次类推。

通常,短划线和间隙长度需要为笔划宽度的倍数。 例如,如果笔划宽度为 10 像素,则数组 { 10, 10 } 将绘制一条点状虚线,其中点和间隙长度与笔划粗细的长度相同。

但是,SKPaint 对象的 StrokeCap 设置也会影响这些点和短划线。 你很快就会看到,这对该数组的元素有影响。

点和短划线页面上演示了点划线。 DotsAndDashesPage.xaml 文件实例化两个 Picker 视图,一个用于让你选择笔划末端,第二个用于选择短划线数组

<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.DotsAndDashesPage"
             Title="Dots and Dashes">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Picker x:Name="strokeCapPicker"
                Title="Stroke Cap"
                Grid.Row="0"
                Grid.Column="0"
                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>

        <Picker x:Name="dashArrayPicker"
                Title="Dash Array"
                Grid.Row="0"
                Grid.Column="1"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type x:String}">
                    <x:String>10, 10</x:String>
                    <x:String>30, 10</x:String>
                    <x:String>10, 10, 30, 10</x:String>
                    <x:String>0, 20</x:String>
                    <x:String>20, 20</x:String>
                    <x:String>0, 20, 20, 20</x:String>
                </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>

dashArrayPicker 中的前三项假设笔画宽度为 10 像素。 { 10, 10 } 数组表示点状虚线,{ 30, 10 } 表示短划线式虚线,{ 10, 10, 30, 10 } 表示点划线。 (其他三个将在稍后讨论。)

DotsAndDashesPage 代码隐藏文件包含 PaintSurface 事件处理程序和一对用于访问 Picker 视图的帮助程序例程:

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 = SKColors.Blue,
        StrokeWidth = 10,
        StrokeCap = (SKStrokeCap)strokeCapPicker.SelectedItem,
        PathEffect = SKPathEffect.CreateDash(GetPickerArray(dashArrayPicker), 20)
    };

    SKPath path = new SKPath();
    path.MoveTo(0.2f * info.Width, 0.2f * info.Height);
    path.LineTo(0.8f * info.Width, 0.8f * info.Height);
    path.LineTo(0.2f * info.Width, 0.8f * info.Height);
    path.LineTo(0.8f * info.Width, 0.2f * info.Height);

    canvas.DrawPath(path, paint);
}

float[] GetPickerArray(Picker picker)
{
    if (picker.SelectedIndex == -1)
    {
        return new float[0];
    }

    string str = (string)picker.SelectedItem;
    string[] strs = str.Split(new char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
    float[] array = new float[strs.Length];

    for (int i = 0; i < strs.Length; i++)
    {
        array[i] = Convert.ToSingle(strs[i]);
    }
    return array;
}

在以下屏幕截图中,最左侧的 iOS 屏幕显示了一条点状虚线:

“点和短划线”页的三重屏幕截图

但是,使用数组 { 10, 10 } 时,Android 屏幕本来也应该显示点状虚线,但实际上这条线是实线。 发生了什么情况? 问题是 Android 屏幕也有一个 Square 笔画末端设置。 这会使所有短划线都延长笔画宽度的一半,导致空隙被填满。

当使用 SquareRound 笔划末端时,若要解决这个问题,必须将数组中的短划线长度减小到笔划长度(有时会导致短划线长度为 0),并将间隙长度增加到笔划长度。 下面是 XAML 文件 Picker 中最后三个短划线数组的计算方法:

  • { 10, 10 } 变为 { 0, 20 } 得到点状虚线
  • { 30, 10 } 变为 { 20, 20 } 得到短划线式虚线
  • { 10, 10, 30, 10 } 变为 { 0, 20, 20, 20} 得到点划线

UWP 屏幕显示了笔画末端为 Round 时的点划线。 Round 笔画末端通常可在粗线条中提供最佳的点和短划线外观。

到目前为止,还没有提到 SKPathEffect.CreateDash 方法的第二个参数。 此参数名为 phase,它指代线条开头的点划模式中的偏移量。 例如,如果短划线数组为 { 10, 10 } 且 phase 为 10,则线条以间隙而不是点开头。

phase 参数的一个有趣的应用是在动画中。 Animated Spiral 页面与 Archimedean Spiral 页面类似,除了 AnimatedSpiralPage 类使用 Xamarin.FormsDevice.Timer 方法使 phase 参数显示动画效果:

public class AnimatedSpiralPage : ContentPage
{
    const double cycleTime = 250;       // in milliseconds

    SKCanvasView canvasView;
    Stopwatch stopwatch = new Stopwatch();
    bool pageIsActive;
    float dashPhase;

    public AnimatedSpiralPage()
    {
        Title = "Animated Spiral";

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

    protected override void OnAppearing()
    {
        base.OnAppearing();
        pageIsActive = true;
        stopwatch.Start();

        Device.StartTimer(TimeSpan.FromMilliseconds(33), () =>
        {
            double t = stopwatch.Elapsed.TotalMilliseconds % cycleTime / cycleTime;
            dashPhase = (float)(10 * t);
            canvasView.InvalidateSurface();

            if (!pageIsActive)
            {
                stopwatch.Stop();
            }

            return pageIsActive;
        });
    }
    ···  
}

当然,你必须实际运行程序才能看到动画:

“动画螺旋”页的三重屏幕截图