Точки и дефисы в SkiaSharp

Download Sample Скачайте пример

Мастер тонкостей рисования пунктирных и дефисированных линий в SkiaSharp

SkiaSharp позволяет нарисовать линии, которые не являются твердыми, но вместо этого состоят из точек и дефисов:

Dotted line

Это можно сделать с эффектом пути, который является экземпляром класса, заданного SKPathEffect свойством PathEffectSKPaint. Вы можете создать эффект пути (или объединить эффекты пути) с помощью одного из методов статического создания, определенных SKPathEffect. (SKPathEffect является одним из шести эффектов, поддерживаемых SkiaSharp; другие описаны в разделе SkiaSharp Effect.)

Чтобы нарисовать пунктирные или пунктирные линии, используйте статический SKPathEffect.CreateDash метод. Существует два аргумента: первый — это массив значений float , указывающий длину точек и дефисов и длину пробелов между ними. Этот массив должен иметь четное число элементов, и должно быть не менее двух элементов. (В массиве могут быть нулевые элементы, но это приводит к сплошной линии.) Если есть два элемента, первая — длина точки или тире, а вторая — длина пробела до следующей точки или тире. Если существует более двух элементов, то они находятся в этом порядке: длина тире, длина пробела, длина тире, длина пробела и т. д.

Как правило, вы хотите сделать тире и разрыв длиной кратной ширины штриха. Если ширина штриха равна 10 пикселям, например массив {10, 10 } нарисует пунктирную линию, где точки и пробелы совпадают с толщиной штриха.

StrokeCap Однако параметр SKPaint объекта также влияет на эти точки и дефисы. Как вы увидите вскоре, это влияет на элементы этого массива.

На странице "Точки и дефисы" показаны пунктирные и пунктирные линии. Файл 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 } предназначен для линии dot-and-dash. (Остальные три будут обсуждаться в ближайшее время.)

Файл 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 в левом углу отображает пунктирную линию:

Triple screenshot of the Dots and Dashes page

Однако экран Android также должен показать пунктирную линию с помощью массива { 10, 10 } но вместо этого линия является твердой. Что произошло? Проблема заключается в том, что экран Android также имеет параметр Squareзаголовков штрихов. Это расширяет все дефисы на половину ширины штриха, что приводит к их заполнению пробелов.

Чтобы обойти эту проблему при использовании штриховой крышки Square или Round, необходимо уменьшить длину тире в массиве на длину штриха (иногда в результате тире длиной 0), а также увеличить длину разрывов на длину штриха. Вот как вычислялись последние три массива дефисов в Picker XAML-файле:

  • { 10, 10 } становится { 0, 20 } для пунктирной линии
  • { 30, 10 } становится { 20, 20 } для тире линии
  • { 10, 10, 30, 10 } становится { 0, 20, 20, 20} для пунктирной и пунктирной линии

На экране UWP показано, что точка и пунктирная линия для штриховой крышки Round. Крышка Round штриха часто дает лучший вид точек и тире в толстых линиях.

До сих пор упоминание не было сделано из второго параметра методаSKPathEffect.CreateDash. Этот параметр называется phase и ссылается на смещение в шаблоне dot-and-dash для начала строки. Например, если массив тире равен {10, 10 } и phase равен 10, то строка начинается с пробела, а не точки.

Одним из интересных приложений phase параметра является анимация. Страница анимированной спирали похожа на страницу Archimedean Spiral, за исключением того, что AnimatedSpiralPage класс анимирует phase параметр с помощью Xamarin.FormsDevice.Timer метода:

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

Конечно, вам придется на самом деле запустить программу, чтобы увидеть анимацию:

Triple screenshot of the Animated Spiral page