Partilhar via


Efeitos de caminho no SkiaSharp

Descubra os vários efeitos de caminho que permitem que os caminhos sejam usados para acariciar e preencher

Um efeito de caminho é uma instância da SKPathEffect classe que é criada com um dos oito métodos de criação estáticos definidos pela classe. O SKPathEffect objeto é então definido como a PathEffect propriedade de um SKPaint objeto para uma variedade de efeitos interessantes, por exemplo, acariciando uma linha com um pequeno caminho replicado:

O exemplo de Cadeia Vinculada

Os efeitos de caminho permitem:

  • Traçar uma linha com pontos e traços
  • Traçar uma linha com qualquer caminho preenchido
  • Preencher uma área com linhas de hachura
  • Preencher uma área com um caminho lado a lado
  • Faça cantos afiados arredondados
  • Adicionar "jitter" aleatório a linhas e curvas

Além disso, você pode combinar dois ou mais efeitos de caminho.

Este artigo também demonstra como usar o GetFillPath método de SKPaint converter um caminho em outro caminho aplicando propriedades de SKPaint, incluindo StrokeWidth e PathEffect. Isso resulta em algumas técnicas interessantes, como a obtenção de um caminho que é um esboço de outro caminho. GetFillPath também é útil em conexão com efeitos de caminho.

Pontos e traços

O uso do PathEffect.CreateDash método foi descrito no artigo Pontos e Traços. O primeiro argumento do método é uma matriz que contém um número par de dois ou mais valores, alternando entre comprimentos de traços e comprimentos de intervalos entre os traços:

public static SKPathEffect CreateDash (Single[] intervals, Single phase)

Esses valores não são relativos à largura do traçado. Por exemplo, se a largura do traçado for 10 e você quiser uma linha composta de traços quadrados e intervalos quadrados, defina a intervals matriz como { 10, 10 }. O phase argumento indica onde dentro do padrão de traço a linha começa. Neste exemplo, se você quiser que a linha comece com a lacuna quadrada, defina phase como 10.

As extremidades dos traços são afetadas pela StrokeCap propriedade de SKPaint. Para larguras de traçado largas, é muito comum definir essa propriedade como SKStrokeCap.Round arredondar as extremidades dos traços. Nesse caso, os intervals valores na matriz não incluem o comprimento extra resultante do arredondamento. Este fato significa que um ponto circular requer a especificação de uma largura de zero. Para uma largura de traçado de 10, para criar uma linha com pontos circulares e lacunas entre os pontos do mesmo diâmetro, use uma intervals matriz de { 0, 20 }.

A página Texto pontilhado animado é semelhante à página Texto Destacado descrita no artigo Integrando texto e elementos gráficos, pois exibe caracteres de texto destacados definindo a StyleSKPaint propriedade do objeto como SKPaintStyle.Stroke. Além disso, o Texto Pontilhado Animado usa SKPathEffect.CreateDash para dar a esse contorno uma aparência pontilhada, e o programa também anima o phaseSKPathEffect.CreateDash argumento do método para fazer com que os pontos pareçam viajar ao redor dos caracteres de texto. Aqui está a página no modo paisagem:

Captura de tela tripla da página Texto pontilhado animado

A AnimatedDottedTextPage classe começa definindo algumas constantes e também substitui os OnAppearing métodos e OnDisappearing para a animação:

public class AnimatedDottedTextPage : ContentPage
{
    const string text = "DOTTED";
    const float strokeWidth = 10;
    static readonly float[] dashArray = { 0, 2 * strokeWidth };

    SKCanvasView canvasView;
    bool pageIsActive;

    public AnimatedDottedTextPage()
    {
        Title = "Animated Dotted Text";

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

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

        Device.StartTimer(TimeSpan.FromSeconds(1f / 60), () =>
        {
            canvasView.InvalidateSurface();
            return pageIsActive;
        });
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        pageIsActive = false;
    }
    ...
}

O PaintSurface manipulador começa criando um SKPaint objeto para exibir o texto. A TextSize propriedade é ajustada com base na largura da tela:

public class AnimatedDottedTextPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Create an SKPaint object to display the text
        using (SKPaint textPaint = new SKPaint
            {
                Style = SKPaintStyle.Stroke,
                StrokeWidth = strokeWidth,
                StrokeCap = SKStrokeCap.Round,
                Color = SKColors.Blue,
            })
        {
            // Adjust TextSize property so text is 95% of screen width
            float textWidth = textPaint.MeasureText(text);
            textPaint.TextSize *= 0.95f * info.Width / textWidth;

            // Find the text bounds
            SKRect textBounds = new SKRect();
            textPaint.MeasureText(text, ref textBounds);

            // Calculate offsets to center the text on the screen
            float xText = info.Width / 2 - textBounds.MidX;
            float yText = info.Height / 2 - textBounds.MidY;

            // Animate the phase; t is 0 to 1 every second
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 1 / 1);
            float phase = -t * 2 * strokeWidth;

            // Create dotted line effect based on dash array and phase
            using (SKPathEffect dashEffect = SKPathEffect.CreateDash(dashArray, phase))
            {
                // Set it to the paint object
                textPaint.PathEffect = dashEffect;

                // And draw the text
                canvas.DrawText(text, xText, yText, textPaint);
            }
        }
    }
}

No final do método, o SKPathEffect.CreateDash método é chamado usando o dashArray que é definido como um campo e o valor animado phase . A SKPathEffect instância é definida como a PathEffect propriedade do SKPaint objeto para exibir o texto.

Como alternativa, você pode definir o SKPathEffect objeto para o SKPaint objeto antes de medir o texto e centralizá-lo na página. Nesse caso, no entanto, os pontos e traços animados causam alguma variação no tamanho do texto renderizado, e o texto tende a vibrar um pouco. (Experimente!)

Você também notará que, à medida que os pontos animados circulam em torno dos caracteres de texto, há um certo ponto em cada curva fechada onde os pontos parecem aparecer e sair da existência. É aqui que o caminho que define o contorno do caractere começa e termina. Se o comprimento do caminho não for um múltiplo integral do comprimento do padrão de traço (neste caso, 20 pixels), apenas parte desse padrão poderá caber no final do caminho.

É possível ajustar o comprimento do padrão de traço para ajustar o comprimento do caminho, mas isso requer determinar o comprimento do caminho, uma técnica abordada no artigo Informações e enumeração do caminho.

O programa Dot / Dash Morph anima o próprio padrão de traço para que os traços pareçam se dividir em pontos, que se combinam para formar traços novamente:

Captura de tela tripla da página Dot Dash Morph

A DotDashMorphPage classe substitui os OnAppearing métodos e OnDisappearing exatamente como o programa anterior, mas a classe define o SKPaint objeto como um campo:

public class DotDashMorphPage : ContentPage
{
    const float strokeWidth = 30;
    static readonly float[] dashArray = new float[4];

    SKCanvasView canvasView;
    bool pageIsActive = false;

    SKPaint ellipsePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = strokeWidth,
        StrokeCap = SKStrokeCap.Round,
        Color = SKColors.Blue
    };
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Create elliptical path
        using (SKPath ellipsePath = new SKPath())
        {
            ellipsePath.AddOval(new SKRect(50, 50, info.Width - 50, info.Height - 50));

            // Create animated path effect
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 3 / 3);
            float phase = 0;

            if (t < 0.25f)  // 1, 0, 1, 2 --> 0, 2, 0, 2
            {
                float tsub = 4 * t;
                dashArray[0] = strokeWidth * (1 - tsub);
                dashArray[1] = strokeWidth * 2 * tsub;
                dashArray[2] = strokeWidth * (1 - tsub);
                dashArray[3] = strokeWidth * 2;
            }
            else if (t < 0.5f)  // 0, 2, 0, 2 --> 1, 2, 1, 0
            {
                float tsub = 4 * (t - 0.25f);
                dashArray[0] = strokeWidth * tsub;
                dashArray[1] = strokeWidth * 2;
                dashArray[2] = strokeWidth * tsub;
                dashArray[3] = strokeWidth * 2 * (1 - tsub);
                phase = strokeWidth * tsub;
            }
            else if (t < 0.75f) // 1, 2, 1, 0 --> 0, 2, 0, 2
            {
                float tsub = 4 * (t - 0.5f);
                dashArray[0] = strokeWidth * (1 - tsub);
                dashArray[1] = strokeWidth * 2;
                dashArray[2] = strokeWidth * (1 - tsub);
                dashArray[3] = strokeWidth * 2 * tsub;
                phase = strokeWidth * (1 - tsub);
            }
            else               // 0, 2, 0, 2 --> 1, 0, 1, 2
            {
                float tsub = 4 * (t - 0.75f);
                dashArray[0] = strokeWidth * tsub;
                dashArray[1] = strokeWidth * 2 * (1 - tsub);
                dashArray[2] = strokeWidth * tsub;
                dashArray[3] = strokeWidth * 2;
            }

            using (SKPathEffect pathEffect = SKPathEffect.CreateDash(dashArray, phase))
            {
                ellipsePaint.PathEffect = pathEffect;
                canvas.DrawPath(ellipsePath, ellipsePaint);
            }
        }
    }
}

O PaintSurface manipulador cria um caminho elíptico com base no tamanho da página e executa uma seção longa de código que define as dashArray variáveis e phase . Como a variável t animada varia de 0 a 1, os if blocos se dividem nesse tempo em quatro trimestres e, em cada um desses trimestres, tsub também varia de 0 a 1. No final, o programa cria o SKPathEffect e o define como objeto SKPaint para desenho.

De Caminho para Caminho

O GetFillPath método de SKPaint transforma um caminho em outro com base nas configurações no SKPaint objeto. Para ver como isso funciona, substitua a canvas.DrawPath chamada no programa anterior com o seguinte código:

SKPath newPath = new SKPath();
bool fill = ellipsePaint.GetFillPath(ellipsePath, newPath);
SKPaint newPaint = new SKPaint
{
    Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);

Nesse novo código, a chamada converte GetFillPath o ellipsePath (que é apenas um oval) em newPath, que é exibido com newPaint. O newPaint objeto é criado com todas as configurações de propriedade padrão, exceto que a Style propriedade é definida com base no valor de retorno booleano de GetFillPath.

Os visuais são idênticos, exceto pela cor, que é definida ellipsePaint , mas não newPaint. Em vez da elipse simples definida no ellipsePath, newPath contém numerosos contornos de caminho que definem a série de pontos e traços. Isso é o resultado da aplicação de várias propriedades de ellipsePaint (especificamente, StrokeWidth, StrokeCape PathEffect) para ellipsePath e colocando o caminho resultante em newPath. O GetFillPath método retorna um valor booleano indicando se o caminho de destino deve ou não ser preenchido, neste exemplo, o valor de retorno é true para preencher o caminho.

Tente alterar a Style configuração para newPaintSKPaintStyle.Stroke e você verá os contornos de caminho individuais delineados com uma linha de largura de um pixel.

Acariciando com um caminho

O SKPathEffect.Create1DPath método é conceitualmente semelhante, SKPathEffect.CreateDash exceto que você especifica um caminho em vez de um padrão de traços e lacunas. Esse caminho é replicado várias vezes para traçar a linha ou a curva.

A sintaxe do é:

public static SKPathEffect Create1DPath (SKPath path, Single advance,
                                         Single phase, SKPath1DPathEffectStyle style)

Em geral, o caminho para Create1DPath o qual você passa será pequeno e centrado em torno do ponto (0, 0). O advance parâmetro indica a distância entre os centros do caminho à medida que o caminho é replicado na linha. Normalmente, você define esse argumento como a largura aproximada do caminho. O phase argumento desempenha aqui o mesmo papel que desempenha no CreateDash método.

O SKPath1DPathEffectStyle tem três membros:

  • Translate
  • Rotate
  • Morph

O Translate membro faz com que o caminho permaneça na mesma orientação que é replicado ao longo de uma linha ou curva. Para Rotate, o caminho é girado com base em uma tangente à curva. O caminho tem sua orientação normal para linhas horizontais. Morph é semelhante a Rotate exceto que o caminho em si também é curvo para corresponder à curvatura da linha que está sendo traçada.

A página Efeito de Caminho 1D demonstra essas três opções. O arquivo OneDimensionalPathEffectPage.xaml define um seletor que contém três itens correspondentes aos três membros da enumeração:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Curves.OneDimensionalPathEffectPage"
             Title="1D Path Effect">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Picker x:Name="effectStylePicker"
                Title="Effect Style"
                Grid.Row="0"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type x:String}">
                    <x:String>Translate</x:String>
                    <x:String>Rotate</x:String>
                    <x:String>Morph</x:String>
                </x:Array>
            </Picker.ItemsSource>
            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>

        <skia:SKCanvasView x:Name="canvasView"
                           PaintSurface="OnCanvasViewPaintSurface"
                           Grid.Row="1" />
    </Grid>
</ContentPage>

O arquivo code-behind OneDimensionalPathEffectPage.xaml.cs define três SKPathEffect objetos como campos. Todos eles são criados usando SKPathEffect.Create1DPath objetos SKPath criados usando SKPath.ParseSvgPathDatao . O primeiro é uma caixa simples, o segundo é uma forma de diamante e o terceiro é um retângulo. Eles são usados para demonstrar os três estilos de efeito:

public partial class OneDimensionalPathEffectPage : ContentPage
{
    SKPathEffect translatePathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 -10 L 10 -10, 10 10, -10 10 Z"),
                                  24, 0, SKPath1DPathEffectStyle.Translate);

    SKPathEffect rotatePathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -10 0 L 0 -10, 10 0, 0 10 Z"),
                                  20, 0, SKPath1DPathEffectStyle.Rotate);

    SKPathEffect morphPathEffect =
        SKPathEffect.Create1DPath(SKPath.ParseSvgPathData("M -25 -10 L 25 -10, 25 10, -25 10 Z"),
                                  55, 0, SKPath1DPathEffectStyle.Morph);

    SKPaint pathPaint = new SKPaint
    {
        Color = SKColors.Blue
    };

    public OneDimensionalPathEffectPage()
    {
        InitializeComponent();
    }

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

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPath path = new SKPath())
        {
            path.MoveTo(new SKPoint(0, 0));
            path.CubicTo(new SKPoint(2 * info.Width, info.Height),
                         new SKPoint(-info.Width, info.Height),
                         new SKPoint(info.Width, 0));

            switch ((string)effectStylePicker.SelectedItem))
            {
                case "Translate":
                    pathPaint.PathEffect = translatePathEffect;
                    break;

                case "Rotate":
                    pathPaint.PathEffect = rotatePathEffect;
                    break;

                case "Morph":
                    pathPaint.PathEffect = morphPathEffect;
                    break;
            }

            canvas.DrawPath(path, pathPaint);
        }
    }
}

O PaintSurface manipulador cria uma curva de Bézier que gira em torno de si mesma e acessa o seletor para determinar qual PathEffect deve ser usado para traçar o traçado. As três opções — Translate, Rotatee Morph — são mostradas da esquerda para a direita:

Captura de tela tripla da página Efeito de Caminho 1D

O caminho especificado no SKPathEffect.Create1DPath método é sempre preenchido. O caminho especificado no DrawPath método será sempre traçado se o SKPaint objeto tiver sua PathEffect propriedade definida como um efeito de caminho 1D. Observe que o pathPaint objeto não tem nenhuma Style configuração, que normalmente tem Fillcomo padrão , mas o caminho é traçado independentemente disso.

A caixa usada no exemplo é quadrada Translate de 20 pixels e o advance argumento é definido como 24. Essa diferença causa uma lacuna entre as caixas quando a linha é aproximadamente horizontal ou vertical, mas as caixas se sobrepõem um pouco quando a linha é diagonal porque a diagonal da caixa é de 28,3 pixels.

A forma de diamante no exemplo também tem 20 pixels de Rotate largura. O advance é definido como 20 para que os pontos continuem a tocar à medida que o diamante é girado junto com a curvatura da linha.

A forma do retângulo no Morph exemplo tem 50 pixels de largura com uma advance configuração de 55 para fazer um pequeno espaço entre os retângulos à medida que eles são dobrados ao redor da curva de Bézier.

Se o advance argumento for menor que o tamanho do caminho, os caminhos replicados poderão se sobrepor. Isso pode resultar em alguns efeitos interessantes. A página Cadeia Ligada exibe uma série de círculos sobrepostos que parecem se assemelhar a uma cadeia ligada, que fica pendurada na forma distinta de uma catenária:

Captura de tela tripla da página Cadeia Vinculada

Olhe muito de perto e verá que esses não são realmente círculos. Cada elo da corrente tem dois arcos, dimensionados e posicionados de modo que pareçam se conectar com elos adjacentes.

Uma corrente ou cabo de distribuição uniforme de peso pende na forma de uma catenária. Um arco construído na forma de uma catenária invertida se beneficia de uma distribuição igual de pressão do peso de um arco. A catenária tem uma descrição matemática aparentemente simples:

y = a · cosh(x / a)

O cosh é a função cosseno hiperbólica. Para x igual a 0, cosh é zero e y é igual a a. Esse é o centro da catenária. Como a função cosseno, cosh é dito ser par, o que significa que cosh(–x) é igual a cosh(x), e os valores aumentam para aumentar argumentos positivos ou negativos. Esses valores descrevem as curvas que formam as laterais da catenária.

Encontrar o valor adequado de a para ajustar a catenária às dimensões da página do telefone não é um cálculo direto. Se w e h são a largura e a altura de um retângulo, o valor ótimo de a satisfaz a seguinte equação:

cosh(w / 2 / a) = 1 + h / a

O método a seguir na classe incorpora LinkedChainPage essa igualdade referindo-se às duas expressões à esquerda e à direita do sinal de igual como left e right. Para valores pequenos de a, left é maior que right; para valores grandes de a, left é menor que right. O while loop se estreita em um valor ótimo de um:

float FindOptimumA(float width, float height)
{
    Func<float, float> left = (float a) => (float)Math.Cosh(width / 2 / a);
    Func<float, float> right = (float a) => 1 + height / a;

    float gtA = 1;         // starting value for left > right
    float ltA = 10000;     // starting value for left < right

    while (Math.Abs(gtA - ltA) > 0.1f)
    {
        float avgA = (gtA + ltA) / 2;

        if (left(avgA) < right(avgA))
        {
            ltA = avgA;
        }
        else
        {
            gtA = avgA;
        }
    }

    return (gtA + ltA) / 2;
}

O SKPath objeto para os links é criado no construtor da classe e o objeto resultante SKPathEffect é definido como a PathEffect propriedade do SKPaint objeto armazenado como um campo:

public class LinkedChainPage : ContentPage
{
    const float linkRadius = 30;
    const float linkThickness = 5;

    Func<float, float, float> catenary = (float a, float x) => (float)(a * Math.Cosh(x / a));

    SKPaint linksPaint = new SKPaint
    {
        Color = SKColors.Silver
    };

    public LinkedChainPage()
    {
        Title = "Linked Chain";

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

        // Create the path for the individual links
        SKRect outer = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
        SKRect inner = outer;
        inner.Inflate(-linkThickness, -linkThickness);

        using (SKPath linkPath = new SKPath())
        {
            linkPath.AddArc(outer, 55, 160);
            linkPath.ArcTo(inner, 215, -160, false);
            linkPath.Close();

            linkPath.AddArc(outer, 235, 160);
            linkPath.ArcTo(inner, 395, -160, false);
            linkPath.Close();

            // Set that path as the 1D path effect for linksPaint
            linksPaint.PathEffect =
                SKPathEffect.Create1DPath(linkPath, 1.3f * linkRadius, 0,
                                          SKPath1DPathEffectStyle.Rotate);
        }
    }
    ...
}

O principal trabalho do PaintSurface manipulador é criar um caminho para a própria catenária. Depois de determinar o a ótimo e armazená-lo na optA variável, ele também precisa calcular um deslocamento a partir do topo da janela. Em seguida, ele pode acumular uma coleção de SKPoint valores para a catenária, transformá-la em um caminho e desenhar o caminho com o objeto criado SKPaint anteriormente:

public class LinkedChainPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear(SKColors.Black);

        // Width and height of catenary
        int width = info.Width;
        float height = info.Height - linkRadius;

        // Find the optimum 'a' for this width and height
        float optA = FindOptimumA(width, height);

        // Calculate the vertical offset for that value of 'a'
        float yOffset = catenary(optA, -width / 2);

        // Create a path for the catenary
        SKPoint[] points = new SKPoint[width];

        for (int x = 0; x < width; x++)
        {
            points[x] = new SKPoint(x, yOffset - catenary(optA, x - width / 2));
        }

        using (SKPath path = new SKPath())
        {
            path.AddPoly(points, false);

            // And render that path with the linksPaint object
            canvas.DrawPath(path, linksPaint);
        }
    }
    ...
}

Este programa define o caminho usado em Create1DPath para ter seu ponto (0, 0) no centro. Isso parece razoável porque o ponto (0, 0) do caminho está alinhado com a linha ou curva que ele está adornando. No entanto, você pode usar um ponto não centralizado (0, 0) para alguns efeitos especiais.

A página Correia transportadora cria um caminho semelhante a uma correia transportadora oblonga com uma parte superior e inferior curvas que são dimensionadas de acordo com as dimensões da janela. Esse caminho é traçado com um objeto simples SKPaint de 20 pixels de largura e cor cinza e, em seguida, traçado novamente com outro SKPaint objeto com um SKPathEffect objeto fazendo referência a um caminho semelhante a um pequeno balde:

Captura de tela tripla da página Correia transportadora

O ponto (0, 0) do caminho da caçamba é a alça, então quando o phase argumento é animado, as caçambas parecem girar em torno da correia transportadora, talvez pegando água na parte inferior e despejando-a na parte superior.

A ConveyorBeltPage classe implementa animação com substituições dos OnAppearing métodos e OnDisappearing . O caminho para o bucket é definido no construtor da página:

public class ConveyorBeltPage : ContentPage
{
    SKCanvasView canvasView;
    bool pageIsActive = false;

    SKPaint conveyerPaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 20,
        Color = SKColors.DarkGray
    };

    SKPath bucketPath = new SKPath();

    SKPaint bucketsPaint = new SKPaint
    {
        Color = SKColors.BurlyWood,
    };

    public ConveyorBeltPage()
    {
        Title = "Conveyor Belt";

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

        // Create the path for the bucket starting with the handle
        bucketPath.AddRect(new SKRect(-5, -3, 25, 3));

        // Sides
        bucketPath.AddRoundedRect(new SKRect(25, -19, 27, 18), 10, 10,
                                  SKPathDirection.CounterClockwise);
        bucketPath.AddRoundedRect(new SKRect(63, -19, 65, 18), 10, 10,
                                  SKPathDirection.CounterClockwise);

        // Five slats
        for (int i = 0; i < 5; i++)
        {
            bucketPath.MoveTo(25, -19 + 8 * i);
            bucketPath.LineTo(25, -13 + 8 * i);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.CounterClockwise, 65, -13 + 8 * i);
            bucketPath.LineTo(65, -19 + 8 * i);
            bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                             SKPathDirection.Clockwise, 25, -19 + 8 * i);
            bucketPath.Close();
        }

        // Arc to suggest the hidden side
        bucketPath.MoveTo(25, -17);
        bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                         SKPathDirection.Clockwise, 65, -17);
        bucketPath.LineTo(65, -19);
        bucketPath.ArcTo(50, 50, 0, SKPathArcSize.Small,
                         SKPathDirection.CounterClockwise, 25, -19);
        bucketPath.Close();

        // Make it a little bigger and correct the orientation
        bucketPath.Transform(SKMatrix.MakeScale(-2, 2));
        bucketPath.Transform(SKMatrix.MakeRotationDegrees(90));
    }
    ...

O código de criação do bucket é concluído com duas transformações que tornam o bucket um pouco maior e o tornam lateral. Aplicar essas transformações foi mais fácil do que ajustar todas as coordenadas no código anterior.

O PaintSurface manipulador começa definindo um caminho para a própria correia transportadora. Este é simplesmente um par de linhas e um par de semicírculos que são desenhados com uma linha cinza-escura de 20 pixels de largura:

public class ConveyorBeltPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        float width = info.Width / 3;
        float verticalMargin = width / 2 + 150;

        using (SKPath conveyerPath = new SKPath())
        {
            // Straight verticals capped by semicircles on top and bottom
            conveyerPath.MoveTo(width, verticalMargin);
            conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                               SKPathDirection.Clockwise, 2 * width, verticalMargin);
            conveyerPath.LineTo(2 * width, info.Height - verticalMargin);
            conveyerPath.ArcTo(width / 2, width / 2, 0, SKPathArcSize.Large,
                               SKPathDirection.Clockwise, width, info.Height - verticalMargin);
            conveyerPath.Close();

            // Draw the conveyor belt itself
            canvas.DrawPath(conveyerPath, conveyerPaint);

            // Calculate spacing based on length of conveyer path
            float length = 2 * (info.Height - 2 * verticalMargin) +
                           2 * ((float)Math.PI * width / 2);

            // Value will be somewhere around 200
            float spacing = length / (float)Math.Round(length / 200);

            // Now animate the phase; t is 0 to 1 every 2 seconds
            TimeSpan timeSpan = new TimeSpan(DateTime.Now.Ticks);
            float t = (float)(timeSpan.TotalSeconds % 2 / 2);
            float phase = -t * spacing;

            // Create the buckets PathEffect
            using (SKPathEffect bucketsPathEffect =
                        SKPathEffect.Create1DPath(bucketPath, spacing, phase,
                                                  SKPath1DPathEffectStyle.Rotate))
            {
                // Set it to the Paint object and draw the path again
                bucketsPaint.PathEffect = bucketsPathEffect;
                canvas.DrawPath(conveyerPath, bucketsPaint);
            }
        }
    }
}

A lógica para desenhar a correia transportadora não funciona no modo paisagem.

As caçambas devem ser espaçadas cerca de 200 pixels na correia transportadora. No entanto, a correia transportadora provavelmente não tem um múltiplo de 200 pixels de comprimento, o que significa que, à medida que o phase argumento de SKPathEffect.Create1DPath é animado, os baldes aparecerão e sairão da existência.

Por esta razão, o programa primeiro calcula um valor chamado length que é o comprimento da correia transportadora. Como a correia transportadora é composta por linhas retas e semicírculos, esse é um cálculo simples. Em seguida, o número de baldes é calculado dividindo length por 200. Isso é arredondado para o número inteiro mais próximo, e esse número é então dividido em length. O resultado é um espaçamento para um número integral de caçambas. O phase argumento é simplesmente uma fração disso.

De caminho em caminho novamente

Na parte inferior do DrawSurface manipulador em Correia transportadora, comente a canvas.DrawPath chamada e substitua-a pelo seguinte código:

SKPath newPath = new SKPath();
bool fill = bucketsPaint.GetFillPath(conveyerPath, newPath);
SKPaint newPaint = new SKPaint
{
    Style = fill ? SKPaintStyle.Fill : SKPaintStyle.Stroke
};
canvas.DrawPath(newPath, newPaint);

Como no exemplo anterior do GetFillPath, você verá que os resultados são os mesmos, exceto pela cor. Após a execução GetFillPathdo , o newPath objeto contém várias cópias do caminho do bucket, cada uma posicionada no mesmo local em que a animação as posicionou no momento da chamada.

Eclodindo uma área

O SKPathEffect.Create2DLines método preenche uma área com linhas paralelas, muitas vezes chamadas de linhas de hachura. O método tem a seguinte sintaxe:

public static SKPathEffect Create2DLine (Single width, SKMatrix matrix)

O width argumento especifica a largura do traçado das linhas de hachura. O matrix parâmetro é uma combinação de dimensionamento e rotação opcional. O fator de dimensionamento indica o incremento de pixels que o Skia usa para espaçar as linhas de hachura. A separação entre as linhas é o fator de escala menos o width argumento. Se o fator de escala for menor ou igual ao width valor, não haverá espaço entre as linhas de hachura e a área parecerá estar preenchida. Especifique o mesmo valor para dimensionamento horizontal e vertical.

Por padrão, as linhas de hachura são horizontais. Se o matrix parâmetro contiver rotação, as linhas de hachura serão giradas no sentido horário.

A página Preenchimento de hachura demonstra esse efeito de caminho. A HatchFillPage classe define três efeitos de caminho como campos, o primeiro para linhas de hachura horizontais com uma largura de 3 pixels com um fator de dimensionamento indicando que eles estão espaçados 6 pixels de distância. A separação entre as linhas é, portanto, de três pixels. O segundo efeito de caminho é para linhas de hachura verticais com uma largura de seis pixels espaçadas 24 pixels de distância (portanto, a separação é de 18 pixels), e o terceiro é para linhas de hachura diagonais de 12 pixels de largura espaçadas 36 pixels de distância.

public class HatchFillPage : ContentPage
{
    SKPaint fillPaint = new SKPaint();

    SKPathEffect horzLinesPath = SKPathEffect.Create2DLine(3, SKMatrix.MakeScale(6, 6));

    SKPathEffect vertLinesPath = SKPathEffect.Create2DLine(6,
        Multiply(SKMatrix.MakeRotationDegrees(90), SKMatrix.MakeScale(24, 24)));

    SKPathEffect diagLinesPath = SKPathEffect.Create2DLine(12,
        Multiply(SKMatrix.MakeScale(36, 36), SKMatrix.MakeRotationDegrees(45)));

    SKPaint strokePaint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 3,
        Color = SKColors.Black
    };
    ...
    static SKMatrix Multiply(SKMatrix first, SKMatrix second)
    {
        SKMatrix target = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref target, first, second);
        return target;
    }
}

Observe o método matricial Multiply . Como os fatores de dimensionamento horizontal e vertical são os mesmos, a ordem em que as matrizes de escala e rotação são multiplicadas não importa.

O PaintSurface manipulador usa esses três efeitos de caminho com três cores diferentes em combinação com fillPaint para preencher um retângulo arredondado dimensionado para caber na página. A Style propriedade definida em é ignorada, quando o SKPaint objeto inclui um efeito de caminho criado a fillPaint partir do SKPathEffect.Create2DLine, a área é preenchida independentemente de:

public class HatchFillPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPath roundRectPath = new SKPath())
        {
            // Create a path
            roundRectPath.AddRoundedRect(
                new SKRect(50, 50, info.Width - 50, info.Height - 50), 100, 100);

            // Horizontal hatch marks
            fillPaint.PathEffect = horzLinesPath;
            fillPaint.Color = SKColors.Red;
            canvas.DrawPath(roundRectPath, fillPaint);

            // Vertical hatch marks
            fillPaint.PathEffect = vertLinesPath;
            fillPaint.Color = SKColors.Blue;
            canvas.DrawPath(roundRectPath, fillPaint);

            // Diagonal hatch marks -- use clipping
            fillPaint.PathEffect = diagLinesPath;
            fillPaint.Color = SKColors.Green;

            canvas.Save();
            canvas.ClipPath(roundRectPath);
            canvas.DrawRect(new SKRect(0, 0, info.Width, info.Height), fillPaint);
            canvas.Restore();

            // Outline the path
            canvas.DrawPath(roundRectPath, strokePaint);
        }
    }
    ...
}

Se você olhar com atenção para os resultados, verá que as linhas de escotilha vermelha e azul não estão confinadas precisamente ao retângulo arredondado. (Esta é aparentemente uma característica do código Skia subjacente.) Se isso não for satisfatório, uma abordagem alternativa é mostrada para as linhas de hachura diagonais em verde: o retângulo arredondado é usado como um caminho de recorte e as linhas de hachura são desenhadas em toda a página.

O PaintSurface manipulador conclui com uma chamada para simplesmente traçar o retângulo arredondado, para que você possa ver a discrepância com as linhas de hachura vermelha e azul:

Captura de tela tripla da página Preenchimento de hachura

A tela do Android realmente não se parece com isso: o dimensionamento da captura de tela fez com que as linhas vermelhas finas e os espaços finos se consolidassem em linhas vermelhas aparentemente mais largas e espaços mais amplos.

Preenchendo com um caminho

O SKPathEffect.Create2DPath permite que você preencha uma área com um caminho que é replicado horizontal e verticalmente, na verdade lado a lado a área:

public static SKPathEffect Create2DPath (SKMatrix matrix, SKPath path)

Os SKMatrix fatores de escala indicam o espaçamento horizontal e vertical do caminho replicado. Mas você não pode girar o caminho usando esse matrix argumento, se quiser que o caminho seja girado, gire o próprio caminho usando o Transform método definido por SKPath.

O caminho replicado normalmente é alinhado com as bordas esquerda e superior da tela, em vez da área que está sendo preenchida. Você pode substituir esse comportamento fornecendo fatores de conversão entre 0 e os fatores de dimensionamento para especificar deslocamentos horizontais e verticais dos lados esquerdo e superior.

A página Preenchimento de Bloco de Caminho demonstra esse efeito de caminho. O caminho usado para colocar lado a lado a área é definido como um campo na PathTileFillPage classe. As coordenadas horizontais e verticais variam de –40 a 40, o que significa que esse caminho tem 80 pixels quadrados:

public class PathTileFillPage : ContentPage
{
    SKPath tilePath = SKPath.ParseSvgPathData(
        "M -20 -20 L 2 -20, 2 -40, 18 -40, 18 -20, 40 -20, " +
        "40 -12, 20 -12, 20 12, 40 12, 40 40, 22 40, 22 20, " +
        "-2 20, -2 40, -20 40, -20 8, -40 8, -40 -8, -20 -8 Z");
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            paint.Color = SKColors.Red;

            using (SKPathEffect pathEffect =
                   SKPathEffect.Create2DPath(SKMatrix.MakeScale(64, 64), tilePath))
            {
                paint.PathEffect = pathEffect;

                canvas.DrawRoundRect(
                    new SKRect(50, 50, info.Width - 50, info.Height - 50),
                    100, 100, paint);
            }
        }
    }
}

No manipulador, as PaintSurfaceSKPathEffect.Create2DPath chamadas definem o espaçamento horizontal e vertical como 64 para fazer com que os blocos quadrados de 80 pixels se sobreponham. Felizmente, o caminho se assemelha a uma peça de quebra-cabeça, mesclando bem com azulejos adjacentes:

Captura de tela tripla da página Preenchimento de Bloco de Caminho

O dimensionamento da captura de tela original causa alguma distorção, particularmente na tela do Android.

Observe que esses blocos sempre aparecem inteiros e nunca são truncados. Nas duas primeiras capturas de tela, nem é evidente que a área que está sendo preenchida seja um retângulo arredondado. Se você quiser truncar esses blocos para uma área específica, use um caminho de recorte.

Tente definir a StyleSKPaint propriedade do objeto como Stroke, e você verá os blocos individuais delineados em vez de preenchidos.

Também é possível preencher uma área com um bitmap lado a lado, como mostrado no artigo SkiaSharp bitmap tiling.

Arredondamento de cantos afiados

O programa Rounded Heptagon apresentado no artigo Three Ways to Draw an Arc usou um arco tangente para curvar os pontos de uma figura de sete lados. A página Outro Heptagon Arredondado mostra uma abordagem muito mais fácil que usa um efeito de caminho criado a partir do SKPathEffect.CreateCorner método:

public static SKPathEffect CreateCorner (Single radius)

Embora o único argumento seja nomeado radius, você deve defini-lo para metade do raio de canto desejado. (Esta é uma característica do código Skia subjacente.)

Aqui está o PaintSurface manipulador na AnotherRoundedHeptagonPage classe:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    int numVertices = 7;
    float radius = 0.45f * Math.Min(info.Width, info.Height);
    SKPoint[] vertices = new SKPoint[numVertices];
    double vertexAngle = -0.5f * Math.PI;       // straight up

    // Coordinates of the vertices of the polygon
    for (int vertex = 0; vertex < numVertices; vertex++)
    {
        vertices[vertex] = new SKPoint(radius * (float)Math.Cos(vertexAngle),
                                       radius * (float)Math.Sin(vertexAngle));
        vertexAngle += 2 * Math.PI / numVertices;
    }

    float cornerRadius = 100;

    // Create the path
    using (SKPath path = new SKPath())
    {
        path.AddPoly(vertices, true);

        // Render the path in the center of the screen
        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Blue;
            paint.StrokeWidth = 10;

            // Set argument to half the desired corner radius!
            paint.PathEffect = SKPathEffect.CreateCorner(cornerRadius / 2);

            canvas.Translate(info.Width / 2, info.Height / 2);
            canvas.DrawPath(path, paint);

            // Uncomment DrawCircle call to verify corner radius
            float offset = cornerRadius / (float)Math.Sin(Math.PI * (numVertices - 2) / numVertices / 2);
            paint.Color = SKColors.Green;
            // canvas.DrawCircle(vertices[0].X, vertices[0].Y + offset, cornerRadius, paint);
        }
    }
}

Você pode usar esse efeito com acariciamento ou preenchimento com base na Style propriedade do SKPaint objeto. Aqui está em execução:

Captura de tela tripla da página Outro Heptagon Arredondado

Você verá que esse heptagon arredondado é idêntico ao programa anterior. Se você precisar de mais convencimento de que o raio de canto é realmente 100 em vez dos 50 especificados na SKPathEffect.CreateCorner chamada, você pode descomentar a instrução final no programa e ver um círculo de 100 raios sobreposto no canto.

Jitter aleatório

Às vezes, as linhas retas impecáveis da computação gráfica não são exatamente o que você quer, e um pouco de aleatoriedade é desejada. Nesse caso, você vai querer tentar o SKPathEffect.CreateDiscrete método:

public static SKPathEffect CreateDiscrete (Single segLength, Single deviation, UInt32 seedAssist)

Você pode usar esse efeito de caminho para acariciar ou preencher. As linhas são separadas em segmentos conectados — cujo comprimento aproximado é especificado por segLength — e se estendem em direções diferentes. A extensão do desvio em relação à linha original é especificada por deviation.

O argumento final é uma semente usada para gerar a sequência pseudoaleatória usada para o efeito. O efeito jitter será um pouco diferente para sementes diferentes. O argumento tem um valor padrão de zero, o que significa que o efeito é o mesmo sempre que você executa o programa. Se você quiser um jitter diferente sempre que a tela for pintada novamente, poderá definir a semente para a Millisecond propriedade de um DataTime.Now valor (por exemplo).

A página Jitter Experiment permite que você experimente valores diferentes ao acariciar um retângulo:

Captura de tela tripla da página JitterExperiment

O programa é simples. O arquivo JitterExperimentPage.xaml instancia dois Slider elementos e um SKCanvasView:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Curves.JitterExperimentPage"
             Title="Jitter Experiment">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid.Resources>
            <ResourceDictionary>
                <Style TargetType="Label">
                    <Setter Property="HorizontalTextAlignment" Value="Center" />
                </Style>

                <Style TargetType="Slider">
                    <Setter Property="Margin" Value="20, 0" />
                    <Setter Property="Minimum" Value="0" />
                    <Setter Property="Maximum" Value="100" />
                </Style>
            </ResourceDictionary>
        </Grid.Resources>

        <Slider x:Name="segLengthSlider"
                Grid.Row="0"
                ValueChanged="sliderValueChanged" />

        <Label Text="{Binding Source={x:Reference segLengthSlider},
                              Path=Value,
                              StringFormat='Segment Length = {0:F0}'}"
               Grid.Row="1" />

        <Slider x:Name="deviationSlider"
                Grid.Row="2"
                ValueChanged="sliderValueChanged" />

        <Label Text="{Binding Source={x:Reference deviationSlider},
                              Path=Value,
                              StringFormat='Deviation = {0:F0}'}"
               Grid.Row="3" />

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="4"
                           PaintSurface="OnCanvasViewPaintSurface" />
    </Grid>
</ContentPage>

O PaintSurface manipulador no arquivo code-behind JitterExperimentPage.xaml.cs é chamado sempre que um Slider valor é alterado. Ele chama SKPathEffect.CreateDiscrete usando os dois Slider valores e usa isso para traçar um retângulo:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    float segLength = (float)segLengthSlider.Value;
    float deviation = (float)deviationSlider.Value;

    using (SKPaint paint = new SKPaint())
    {
        paint.Style = SKPaintStyle.Stroke;
        paint.StrokeWidth = 5;
        paint.Color = SKColors.Blue;

        using (SKPathEffect pathEffect = SKPathEffect.CreateDiscrete(segLength, deviation))
        {
            paint.PathEffect = pathEffect;

            SKRect rect = new SKRect(100, 100, info.Width - 100, info.Height - 100);
            canvas.DrawRect(rect, paint);
        }
    }
}

Você também pode usar esse efeito para preenchimento, caso em que o contorno da área preenchida está sujeito a esses desvios aleatórios. A página Jitter Text demonstra o uso desse efeito de caminho para exibir texto. A maior parte do código no PaintSurface manipulador da JitterTextPage classe é dedicada ao dimensionamento e centralização do texto:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    string text = "FUZZY";

    using (SKPaint textPaint = new SKPaint())
    {
        textPaint.Color = SKColors.Purple;
        textPaint.PathEffect = SKPathEffect.CreateDiscrete(3f, 10f);

        // Adjust TextSize property so text is 95% of screen width
        float textWidth = textPaint.MeasureText(text);
        textPaint.TextSize *= 0.95f * info.Width / textWidth;

        // Find the text bounds
        SKRect textBounds = new SKRect();
        textPaint.MeasureText(text, ref textBounds);

        // Calculate offsets to center the text on the screen
        float xText = info.Width / 2 - textBounds.MidX;
        float yText = info.Height / 2 - textBounds.MidY;

        canvas.DrawText(text, xText, yText, textPaint);
    }
}

Aqui ele está sendo executado no modo paisagem:

Captura de tela tripla da página JitterText

Estrutura de tópicos do caminho

Você já viu dois pequenos exemplos do GetFillPath método do SKPaint, que existe em duas versões:

public Boolean GetFillPath (SKPath src, SKPath dst, Single resScale = 1)

public Boolean GetFillPath (SKPath src, SKPath dst, SKRect cullRect, Single resScale = 1)

Apenas os dois primeiros argumentos são necessários. O método acessa o caminho referenciado src pelo argumento, modifica os dados do caminho com base nas propriedades de traçado no objeto (incluindo a PathEffect propriedade) e, em SKPaint seguida, grava os resultados no dst caminho. O resScale parâmetro permite reduzir a precisão para criar um caminho de destino menor, e o argumento pode eliminar contornos cullRect fora de um retângulo.

Um uso básico desse método não envolve efeitos de caminho: se o SKPaint objeto tiver sua Style propriedade definida como SKPaintStyle.Stroke, e não tiver seu PathEffect conjunto, criará GetFillPath um caminho que represente um contorno do caminho de origem como se ele tivesse sido traçado pelas propriedades de pintura.

Por exemplo, se o src caminho for um círculo simples de raio 500 e o SKPaint objeto especificar uma largura de traçado de 100, o dst caminho se tornará dois círculos concêntricos, um com um raio de 450 e outro com um raio de 550. O método é chamado GetFillPath porque preencher esse dst caminho é o mesmo que acariciar o src caminho. Mas você também pode traçar o dst caminho para ver os contornos do caminho.

O Toque para Esboçar o Caminho demonstra isso. Os SKCanvasView e TapGestureRecognizer são instanciados no arquivo TapToOutlineThePathPage.xaml . O arquivo code-behind TapToOutlineThePathPage.xaml.cs define três SKPaint objetos como campos, dois para acariciar com larguras de traçado de 100 e 20 e o terceiro para preenchimento:

public partial class TapToOutlineThePathPage : ContentPage
{
    bool outlineThePath = false;

    SKPaint redThickStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        StrokeWidth = 100
    };

    SKPaint redThinStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Red,
        StrokeWidth = 20
    };

    SKPaint blueFill = new SKPaint
    {
        Style = SKPaintStyle.Fill,
        Color = SKColors.Blue
    };

    public TapToOutlineThePathPage()
    {
        InitializeComponent();
    }

    void OnCanvasViewTapped(object sender, EventArgs args)
    {
        outlineThePath ^= true;
        (sender as SKCanvasView).InvalidateSurface();
    }
    ...
}

Se a tela não tiver sido tocada, o PaintSurface manipulador usará os blueFill objetos e redThickStroke paint para renderizar um caminho circular:

public partial class TapToOutlineThePathPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPath circlePath = new SKPath())
        {
            circlePath.AddCircle(info.Width / 2, info.Height / 2,
                                 Math.Min(info.Width / 2, info.Height / 2) -
                                 redThickStroke.StrokeWidth);

            if (!outlineThePath)
            {
                canvas.DrawPath(circlePath, blueFill);
                canvas.DrawPath(circlePath, redThickStroke);
            }
            else
            {
                using (SKPath outlinePath = new SKPath())
                {
                    redThickStroke.GetFillPath(circlePath, outlinePath);

                    canvas.DrawPath(outlinePath, blueFill);
                    canvas.DrawPath(outlinePath, redThinStroke);
                }
            }
        }
    }
}

O círculo é preenchido e traçado como seria de esperar:

Captura de tela tripla da página normal Toque para destacar o caminho

Quando você toca na tela, outlineThePath é definido como true, e o PaintSurface manipulador cria um objeto novo SKPath e o usa como o caminho de destino em uma chamada para GetFillPath no redThickStroke objeto paint. Esse caminho de destino é então preenchido e traçado com redThinStroke, resultando no seguinte:

Captura de tela tripla da página Toque para destacar o caminho destacado

Os dois círculos vermelhos indicam claramente que o caminho circular original foi convertido em dois contornos circulares.

Esse método pode ser muito útil no desenvolvimento de caminhos a serem usados para o SKPathEffect.Create1DPath método. Os caminhos especificados nesses métodos são sempre preenchidos quando os caminhos são replicados. Se você não quiser que todo o caminho seja preenchido, você deve definir cuidadosamente os contornos.

Por exemplo, na amostra de Cadeia Encadeada , os elos foram definidos com uma série de quatro arcos, cada par dos quais foram baseados em dois raios para delinear a área do caminho a ser preenchido. É possível substituir o código na LinkedChainPage classe para fazê-lo um pouco diferente.

Primeiro, convém redefinir a linkRadius constante:

const float linkRadius = 27.5f;
const float linkThickness = 5;

Agora linkPath são apenas dois arcos baseados nesse único raio, com os ângulos de início e ângulos de varredura desejados:

using (SKPath linkPath = new SKPath())
{
    SKRect rect = new SKRect(-linkRadius, -linkRadius, linkRadius, linkRadius);
    linkPath.AddArc(rect, 55, 160);
    linkPath.AddArc(rect, 235, 160);

    using (SKPaint strokePaint = new SKPaint())
    {
        strokePaint.Style = SKPaintStyle.Stroke;
        strokePaint.StrokeWidth = linkThickness;

        using (SKPath outlinePath = new SKPath())
        {
            strokePaint.GetFillPath(linkPath, outlinePath);

            // Set that path as the 1D path effect for linksPaint
            linksPaint.PathEffect =
                SKPathEffect.Create1DPath(outlinePath, 1.3f * linkRadius, 0,
                                          SKPath1DPathEffectStyle.Rotate);

        }

    }
}

O outlinePath objeto é, então, o destinatário da estrutura de tópicos de quando ele é traçado linkPath com as propriedades especificadas em strokePaint.

Outro exemplo usando essa técnica está chegando em seguida para o caminho usado em um método.

Combinando efeitos de caminho

Os dois métodos finais de criação estática de SKPathEffect são SKPathEffect.CreateSum e SKPathEffect.CreateCompose:

public static SKPathEffect CreateSum (SKPathEffect first, SKPathEffect second)

public static SKPathEffect CreateCompose (SKPathEffect outer, SKPathEffect inner)

Ambos os métodos combinam dois efeitos de caminho para criar um efeito de caminho composto. O CreateSum método cria um efeito de caminho que é semelhante aos dois efeitos de caminho aplicados separadamente, enquanto CreateCompose aplica um efeito de caminho (o inner) e, em seguida, aplica o outer a isso.

Você já viu como o GetFillPath método de pode converter um caminho em outro caminho com base em SKPaint propriedades (incluindo PathEffect), portanto, não deve ser muito misterioso como um SKPaint objeto pode executar essa operação duas vezes com os dois efeitos de SKPaint caminho especificados nos CreateSum métodos orCreateCompose.

Um uso óbvio de é definir um SKPaint objeto que preenche um caminho com um efeito de caminho e traça o caminho com outro efeito de CreateSum caminho. Isso é demonstrado no exemplo Cats in Frame , que exibe uma matriz de gatos dentro de um quadro com bordas recortadas:

Captura de tela tripla da página Cats In Frame

A CatsInFramePage classe começa definindo vários campos. Você pode reconhecer o PathDataCatPage primeiro campo da classe do artigo Dados do caminho SVG. O segundo caminho é baseado em uma linha e arco para o padrão de vieira do quadro:

public class CatsInFramePage : ContentPage
{
    // From PathDataCatPage.cs
    SKPath catPath = SKPath.ParseSvgPathData(
        "M 160 140 L 150 50 220 103" +              // Left ear
        "M 320 140 L 330 50 260 103" +              // Right ear
        "M 215 230 L 40 200" +                      // Left whiskers
        "M 215 240 L 40 240" +
        "M 215 250 L 40 280" +
        "M 265 230 L 440 200" +                     // Right whiskers
        "M 265 240 L 440 240" +
        "M 265 250 L 440 280" +
        "M 240 100" +                               // Head
        "A 100 100 0 0 1 240 300" +
        "A 100 100 0 0 1 240 100 Z" +
        "M 180 170" +                               // Left eye
        "A 40 40 0 0 1 220 170" +
        "A 40 40 0 0 1 180 170 Z" +
        "M 300 170" +                               // Right eye
        "A 40 40 0 0 1 260 170" +
        "A 40 40 0 0 1 300 170 Z");

    SKPaint catStroke = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        StrokeWidth = 5
    };

    SKPath scallopPath =
        SKPath.ParseSvgPathData("M 0 0 L 50 0 A 60 60 0 0 1 -50 0 Z");

    SKPaint framePaint = new SKPaint
    {
        Color = SKColors.Black
    };
    ...
}

O catPath pode ser usado no SKPathEffect.Create2DPath método se a propriedade object StyleSKPaint estiver definida como Stroke. No entanto, se o catPath for usado diretamente neste programa, toda a cabeça do gato será preenchida, e os bigodes nem serão visíveis. (Experimente!) É necessário obter o esboço desse caminho e usar esse esboço no SKPathEffect.Create2DPath método.

O construtor faz esse trabalho. Primeiro, ele aplica duas transformações para catPath mover o ponto (0, 0) para o centro e reduzi-lo em tamanho. GetFillPath obtém todos os contornos dos contornos em outlinedCatPath, e esse objeto é usado na SKPathEffect.Create2DPath chamada. Os fatores de escala no SKMatrix valor são ligeiramente maiores do que o tamanho horizontal e vertical do gato para fornecer um pequeno buffer entre os blocos, enquanto os fatores de tradução foram derivados um pouco empiricamente para que um gato completo seja visível no canto superior esquerdo do quadro:

public class CatsInFramePage : ContentPage
{
    ...
    public CatsInFramePage()
    {
        Title = "Cats in Frame";

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

        // Move (0, 0) point to center of cat path
        catPath.Transform(SKMatrix.MakeTranslation(-240, -175));

        // Now catPath is 400 by 250
        // Scale it down to 160 by 100
        catPath.Transform(SKMatrix.MakeScale(0.40f, 0.40f));

        // Get the outlines of the contours of the cat path
        SKPath outlinedCatPath = new SKPath();
        catStroke.GetFillPath(catPath, outlinedCatPath);

        // Create a 2D path effect from those outlines
        SKPathEffect fillEffect = SKPathEffect.Create2DPath(
            new SKMatrix { ScaleX = 170, ScaleY = 110,
                           TransX = 75, TransY = 80,
                           Persp2 = 1 },
            outlinedCatPath);

        // Create a 1D path effect from the scallop path
        SKPathEffect strokeEffect =
            SKPathEffect.Create1DPath(scallopPath, 75, 0, SKPath1DPathEffectStyle.Rotate);

        // Set the sum the effects to frame paint
        framePaint.PathEffect = SKPathEffect.CreateSum(fillEffect, strokeEffect);
    }
    ...
}

O construtor então chama SKPathEffect.Create1DPath o quadro escalopado. Observe que a largura do caminho é de 100 pixels, mas o avanço é de 75 pixels para que o caminho replicado seja sobreposto ao redor do quadro. A instrução final do construtor chama SKPathEffect.CreateSum para combinar os dois efeitos de caminho e definir o resultado para o SKPaint objeto.

Todo esse trabalho permite que o PaintSurface manipulador seja bastante simples. Ele só precisa definir um retângulo e desenhá-lo usando framePaint:

public class CatsInFramePage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        SKRect rect = new SKRect(50, 50, info.Width - 50, info.Height - 50);
        canvas.ClipRect(rect);
        canvas.DrawRect(rect, framePaint);
    }
}

Os algoritmos por trás dos efeitos de caminho sempre fazem com que todo o caminho usado para acariciar ou preencher seja exibido, o que pode fazer com que alguns visuais apareçam fora do retângulo. A ClipRect chamada antes da DrawRect chamada permite que o visual seja consideravelmente mais limpo. (Experimente sem recorte!)

É comum usar SKPathEffect.CreateCompose para adicionar algum jitter a outro efeito de caminho. Você certamente pode experimentar por conta própria, mas aqui está um exemplo um pouco diferente:

As linhas de escotilha tracejadas preenchem uma elipse com linhas de escotilha tracejadas. A maior parte do trabalho na DashedHatchLinesPage aula é realizada diretamente nas definições de campo. Esses campos definem um efeito de traço e um efeito de hachura. Eles são definidos como static porque são então referenciados em uma SKPathEffect.CreateCompose chamada na SKPaint definição:

public class DashedHatchLinesPage : ContentPage
{
    static SKPathEffect dashEffect =
        SKPathEffect.CreateDash(new float[] { 30, 30 }, 0);

    static SKPathEffect hatchEffect = SKPathEffect.Create2DLine(20,
        Multiply(SKMatrix.MakeScale(60, 60),
                 SKMatrix.MakeRotationDegrees(45)));

    SKPaint paint = new SKPaint()
    {
        PathEffect = SKPathEffect.CreateCompose(dashEffect, hatchEffect),
        StrokeCap = SKStrokeCap.Round,
        Color = SKColors.Blue
    };
    ...
    static SKMatrix Multiply(SKMatrix first, SKMatrix second)
    {
        SKMatrix target = SKMatrix.MakeIdentity();
        SKMatrix.Concat(ref target, first, second);
        return target;
    }
}

O PaintSurface manipulador precisa conter apenas a sobrecarga padrão mais uma chamada para DrawOval:

public class DashedHatchLinesPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        canvas.DrawOval(info.Width / 2, info.Height / 2,
                        0.45f * info.Width, 0.45f * info.Height,
                        paint);
    }
    ...
}

Como você já descobriu, as linhas de escotilha não se restringem exatamente ao interior da área e, neste exemplo, elas sempre começam à esquerda com um traço inteiro:

Captura de tela tripla da página Linhas de Escotilha Tracejadas

Agora que você já viu efeitos de caminho que variam de simples pontos e traços a combinações estranhas, use sua imaginação e veja o que você pode criar.