Partilhar via


O gradiente linear SkiaSharp

A SKPaint classe define uma Color propriedade que é usada para traçar linhas ou preencher áreas com uma cor sólida. Como alternativa, você pode traçar linhas ou preencher áreas com gradientes, que são misturas graduais de cores:

Amostra de gradiente linear

O tipo mais básico de gradiente é um gradiente linear . A mistura de cores ocorre em uma linha (chamada de linha de gradiente) de um ponto a outro. As linhas perpendiculares à linha de gradiente têm a mesma cor. Criar um gradiente linear usando um dos dois métodos estáticos SKShader.CreateLinearGradient . A diferença entre as duas sobrecargas é que uma inclui uma transformação matricial e a outra não.

Esses métodos retornam um objeto do tipo SKShader que você definiu como a Shader propriedade de SKPaint. Se a Shader propriedade não for nula, ela substituirá a Color propriedade. Qualquer linha traçada ou qualquer área preenchida usando esse SKPaint objeto é baseada no gradiente e não na cor sólida.

Observação

A Shader propriedade é ignorada quando você inclui um SKPaint objeto em uma DrawBitmap chamada. Você pode usar a Color propriedade de para definir um nível de SKPaint transparência para exibir um bitmap (conforme descrito no artigo Exibindo bitmaps SkiaSharp), mas não pode usar a Shader propriedade para exibir um bitmap com uma transparência de gradiente. Outras técnicas estão disponíveis para exibir bitmaps com transparências de gradiente: Elas são descritas nos artigos SkiaSharp circulares gradientes e SkiaSharp compositing and blend modes.

Gradientes de canto a canto

Muitas vezes, um gradiente linear se estende de um canto de um retângulo para outro. Se o ponto inicial for o canto superior esquerdo do retângulo, o gradiente poderá se estender:

  • verticalmente para o canto inferior esquerdo
  • horizontalmente para o canto superior direito
  • diagonalmente para o canto inferior direito

O gradiente linear diagonal é demonstrado na primeira página na seção SkiaSharp Shaders and Other Effects da amostra. A página Gradiente de canto a canto cria um SKCanvasView em seu construtor. O PaintSurface manipulador cria um SKPaint objeto em uma using instrução e, em seguida, define um retângulo quadrado de 300 pixels centralizado na tela:

public class CornerToCornerGradientPage : ContentPage
{
    ···
    public CornerToCornerGradientPage ()
    {
        Title = "Corner-to-Corner Gradient";

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

    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())
        {
            // Create 300-pixel square centered rectangle
            float x = (info.Width - 300) / 2;
            float y = (info.Height - 300) / 2;
            SKRect rect = new SKRect(x, y, x + 300, y + 300);

            // Create linear gradient from upper-left to lower-right
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(rect.Left, rect.Top),
                                new SKPoint(rect.Right, rect.Bottom),
                                new SKColor[] { SKColors.Red, SKColors.Blue },
                                new float[] { 0, 1 },
                                SKShaderTileMode.Repeat);

            // Draw the gradient on the rectangle
            canvas.DrawRect(rect, paint);
            ···
        }
    }
}

A Shader propriedade de SKPaint é atribuído o SKShader valor de retorno do método estático SKShader.CreateLinearGradient . Os cinco argumentos são os seguintes:

  • O ponto inicial do gradiente, definido aqui no canto superior esquerdo do retângulo
  • O ponto final do gradiente, definido aqui no canto inferior direito do retângulo
  • Uma matriz de duas ou mais cores que contribuem para o gradiente
  • Uma matriz de float valores indicando a posição relativa das cores dentro da linha de gradiente
  • Um membro da SKShaderTileMode enumeração que indica como o gradiente se comporta além das extremidades da linha de gradiente

Depois que o objeto de gradiente é criado, o DrawRect método desenha o retângulo quadrado de 300 pixels usando o SKPaint objeto que inclui o sombreador. Aqui ele está sendo executado no iOS, Android e na Plataforma Universal do Windows (UWP):

Gradiente de canto a canto

A linha de gradiente é definida pelos dois pontos especificados como os dois primeiros argumentos. Observe que esses pontos são relativos à tela e não ao objeto gráfico exibido com o gradiente. Ao longo da linha de gradiente, a cor gradualmente transita de vermelho no canto superior esquerdo para azul no canto inferior direito. Qualquer linha perpendicular à linha de gradiente tem uma cor constante.

A matriz de float valores especificada como o quarto argumento tem uma correspondência um-para-um com a matriz de cores. Os valores indicam a posição relativa ao longo da linha de gradiente onde essas cores ocorrem. Aqui, o 0 significa que Red ocorre no início da linha de gradiente e 1 significa que Blue ocorre no final da linha. Os números devem ser ascendentes, e devem estar no intervalo de 0 a 1. Se eles não estiverem nessa faixa, eles serão ajustados para estar nessa faixa.

Os dois valores na matriz podem ser definidos como algo diferente de 0 e 1. Tente o seguinte:

new float[] { 0.25f, 0.75f }

Agora, todo o primeiro quarto da linha de gradiente é vermelho puro, e o último trimestre é azul puro. A mistura de vermelho e azul é restrita à metade central da linha de gradiente.

Geralmente, convém espaçar esses valores de posição igualmente de 0 a 1. Se esse for o caso, você pode simplesmente fornecer null como quarto argumento para CreateLinearGradient.

Embora esse gradiente seja definido entre dois cantos do retângulo quadrado de 300 pixels, ele não se restringe ao preenchimento desse retângulo. A página Gradiente de canto a canto inclui algum código extra que responde a toques ou cliques do mouse na página. O drawBackground campo é alternado entre true e false com cada toque. Se o valor for true, o PaintSurface manipulador usará o mesmo SKPaint objeto para preencher toda a tela e, em seguida, desenhará um retângulo preto indicando o retângulo menor:

public class CornerToCornerGradientPage : ContentPage
{
    bool drawBackground;

    public CornerToCornerGradientPage ()
    {
        ···
        TapGestureRecognizer tap = new TapGestureRecognizer();
        tap.Tapped += (sender, args) =>
        {
            drawBackground ^= true;
            canvasView.InvalidateSurface();
        };
        canvasView.GestureRecognizers.Add(tap);
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        using (SKPaint paint = new SKPaint())
        {
            ···
            if (drawBackground)
            {
                // Draw the gradient on the whole canvas
                canvas.DrawRect(info.Rect, paint);

                // Outline the smaller rectangle
                paint.Shader = null;
                paint.Style = SKPaintStyle.Stroke;
                paint.Color = SKColors.Black;
                canvas.DrawRect(rect, paint);
            }
        }
    }
}

Veja o que você verá depois de tocar na tela:

Gradiente de canto a canto completo

Observe que o gradiente se repete no mesmo padrão além dos pontos que definem a linha de gradiente. Essa repetição ocorre porque o último argumento a CreateLinearGradient ser é SKShaderTileMode.Repeat. (Você verá as outras opções em breve.)

Observe também que os pontos que você usa para especificar a linha de gradiente não são exclusivos. As linhas perpendiculares à linha de gradiente têm a mesma cor, portanto, há um número infinito de linhas de gradiente que você pode especificar para o mesmo efeito. Por exemplo, ao preencher um retângulo com um gradiente horizontal, você pode especificar os cantos superior esquerdo e superior direito, ou os cantos inferior esquerdo e inferior direito, ou quaisquer dois pontos que estejam pares e paralelos a essas linhas.

Experiência interativa

Você pode experimentar interativamente gradientes lineares com a página Gradiente linear interativo. Esta página usa a classe introduzida InteractivePage no artigo Três maneiras de desenhar um arco. InteractivePage manipula TouchEffect eventos para manter uma coleção de TouchPoint objetos que você pode mover com os dedos ou o mouse.

O arquivo XAML anexa o TouchEffect a um pai do SKCanvasView e também inclui um Picker que permite selecionar um dos três membros da SKShaderTileMode enumeração:

<local:InteractivePage xmlns="http://xamarin.com/schemas/2014/forms"
                       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                       xmlns:local="clr-namespace:SkiaSharpFormsDemos"
                       xmlns:skia="clr-namespace:SkiaSharp;assembly=SkiaSharp"
                       xmlns:skiaforms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
                       xmlns:tt="clr-namespace:TouchTracking"
                       x:Class="SkiaSharpFormsDemos.Effects.InteractiveLinearGradientPage"
                       Title="Interactive Linear Gradient">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid BackgroundColor="White"
              Grid.Row="0">
            <skiaforms:SKCanvasView x:Name="canvasView"
                                    PaintSurface="OnCanvasViewPaintSurface" />
            <Grid.Effects>
                <tt:TouchEffect Capture="True"
                                TouchAction="OnTouchEffectAction" />
            </Grid.Effects>
        </Grid>

        <Picker x:Name="tileModePicker"
                Grid.Row="1"
                Title="Shader Tile Mode"
                Margin="10"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type skia:SKShaderTileMode}">
                    <x:Static Member="skia:SKShaderTileMode.Clamp" />
                    <x:Static Member="skia:SKShaderTileMode.Repeat" />
                    <x:Static Member="skia:SKShaderTileMode.Mirror" />
                </x:Array>
            </Picker.ItemsSource>

            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>
    </Grid>
</local:InteractivePage>

O construtor no arquivo code-behind cria dois TouchPoint objetos para os pontos inicial e final do gradiente linear. O PaintSurface manipulador define uma matriz de três cores (para um gradiente de vermelho para verde para azul) e obtém a corrente SKShaderTileMode do Picker:

public partial class InteractiveLinearGradientPage : InteractivePage
{
    public InteractiveLinearGradientPage ()
    {
        InitializeComponent ();

        touchPoints = new TouchPoint[2];

        for (int i = 0; i < 2; i++)
        {
            touchPoints[i] = new TouchPoint
            {
                Center = new SKPoint(100 + i * 200, 100 + i * 200)
            };
        }

        InitializeComponent();
        baseCanvasView = canvasView;
    }

    void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        canvasView.InvalidateSurface();
    }

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

        canvas.Clear();

        SKColor[] colors = { SKColors.Red, SKColors.Green, SKColors.Blue };
        SKShaderTileMode tileMode =
            (SKShaderTileMode)(tileModePicker.SelectedIndex == -1 ?
                                        0 : tileModePicker.SelectedItem);

        using (SKPaint paint = new SKPaint())
        {
            paint.Shader = SKShader.CreateLinearGradient(touchPoints[0].Center,
                                                         touchPoints[1].Center,
                                                         colors,
                                                         null,
                                                         tileMode);
            canvas.DrawRect(info.Rect, paint);
        }
        ···
    }
}

O PaintSurface manipulador cria o objeto a SKShader partir de todas essas informações e o usa para colorir toda a tela. A matriz de float valores é definida como null. Caso contrário, para espaçar igualmente três cores, você definiria esse parâmetro como uma matriz com os valores 0, 0,5 e 1.

A maior parte do manipulador é dedicada à exibição de vários objetos: os pontos de toque como círculos de contorno, a linha de PaintSurface gradiente e as linhas perpendiculares às linhas de gradiente nos pontos de toque:

public partial class InteractiveLinearGradientPage : InteractivePage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        // Display the touch points here rather than by TouchPoint
        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.Color = SKColors.Black;
            paint.StrokeWidth = 3;

            foreach (TouchPoint touchPoint in touchPoints)
            {
                canvas.DrawCircle(touchPoint.Center, touchPoint.Radius, paint);
            }

            // Draw gradient line connecting touchpoints
            canvas.DrawLine(touchPoints[0].Center, touchPoints[1].Center, paint);

            // Draw lines perpendicular to the gradient line
            SKPoint vector = touchPoints[1].Center - touchPoints[0].Center;
            float length = (float)Math.Sqrt(Math.Pow(vector.X, 2) +
                                            Math.Pow(vector.Y, 2));
            vector.X /= length;
            vector.Y /= length;
            SKPoint rotate90 = new SKPoint(-vector.Y, vector.X);
            rotate90.X *= 200;
            rotate90.Y *= 200;

            canvas.DrawLine(touchPoints[0].Center,
                            touchPoints[0].Center + rotate90,
                            paint);

            canvas.DrawLine(touchPoints[0].Center,
                            touchPoints[0].Center - rotate90,
                            paint);

            canvas.DrawLine(touchPoints[1].Center,
                            touchPoints[1].Center + rotate90,
                            paint);

            canvas.DrawLine(touchPoints[1].Center,
                            touchPoints[1].Center - rotate90,
                            paint);
        }
    }
}

A linha de gradiente que liga os dois pontos de contato é fácil de desenhar, mas as linhas perpendiculares exigem um pouco mais de trabalho. A linha de gradiente é convertida em um vetor, normalizada para ter um comprimento de uma unidade e, em seguida, girada em 90 graus. Esse vetor recebe então um comprimento de 200 pixels. Ele é usado para desenhar quatro linhas que se estendem dos pontos de contato para serem perpendiculares à linha de gradiente.

As linhas perpendiculares coincidem com o início e o fim do gradiente. O que acontece além dessas linhas depende da configuração da SKShaderTileMode enumeração:

Gradiente linear interativo

As três capturas de tela mostram os resultados dos três valores diferentes de SKShaderTileMode. A captura de tela do iOS mostra SKShaderTileMode.Clamp, que apenas estende as cores na borda do gradiente. A SKShaderTileMode.Repeat opção na captura de tela do Android mostra como o padrão de gradiente é repetido. A SKShaderTileMode.Mirror opção na captura de tela UWP também repete o padrão, mas o padrão é invertido a cada vez, resultando em nenhuma descontinuidade de cor.

Gradientes em gradientes

A SKShader classe não define propriedades ou métodos públicos, exceto para Dispose. Os SKShader objetos criados por seus métodos estáticos são, portanto, imutáveis. Mesmo se você usar o mesmo gradiente para dois objetos diferentes, é provável que você queira variar um pouco o gradiente. Para fazer isso, você precisará criar um novo SKShader objeto.

A página Texto de gradiente exibe texto e um brackground que são coloridos com gradientes semelhantes:

Texto gradiente

As únicas diferenças nos gradientes são os pontos inicial e final. O gradiente usado para exibir texto é baseado em dois pontos nos cantos do retângulo delimitador do texto. Para o plano de fundo, os dois pontos são baseados em toda a tela. Este é o código :

public class GradientTextPage : ContentPage
{
    const string TEXT = "GRADIENT";

    public GradientTextPage ()
    {
        Title = "Gradient Text";

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

    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())
        {
            // Create gradient for background
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(0, 0),
                                new SKPoint(info.Width, info.Height),
                                new SKColor[] { new SKColor(0x40, 0x40, 0x40),
                                                new SKColor(0xC0, 0xC0, 0xC0) },
                                null,
                                SKShaderTileMode.Clamp);

            // Draw background
            canvas.DrawRect(info.Rect, paint);

            // Set TextSize to fill 90% of width
            paint.TextSize = 100;
            float width = paint.MeasureText(TEXT);
            float scale = 0.9f * info.Width / width;
            paint.TextSize *= scale;

            // Get text bounds
            SKRect textBounds = new SKRect();
            paint.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;

            // Shift textBounds by that amount
            textBounds.Offset(xText, yText);

            // Create gradient for text
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(textBounds.Left, textBounds.Top),
                                new SKPoint(textBounds.Right, textBounds.Bottom),
                                new SKColor[] { new SKColor(0x40, 0x40, 0x40),
                                                new SKColor(0xC0, 0xC0, 0xC0) },
                                null,
                                SKShaderTileMode.Clamp);

            // Draw text
            canvas.DrawText(TEXT, xText, yText, paint);
        }
    }
}

A Shader propriedade do objeto é definida primeiro para exibir um gradiente para cobrir o plano de SKPaint fundo. Os pontos de gradiente são definidos nos cantos superior esquerdo e inferior direito da tela.

O código define a TextSize SKPaint propriedade do objeto para que o texto seja exibido em 90% da largura da tela. Os limites de texto são usados para calcular xText e yText os valores a serem passados para o DrawText método para centralizar o texto.

No entanto, os pontos de gradiente para a segunda CreateLinearGradient chamada devem se referir ao canto superior esquerdo e inferior direito do texto em relação à tela quando ele é exibido. Isso é feito deslocando o textBounds retângulo pelos mesmos xText e yText valores:

textBounds.Offset(xText, yText);

Agora, os cantos superior esquerdo e inferior direito do retângulo podem ser usados para definir os pontos inicial e final do gradiente.

Animando um gradiente

Há várias maneiras de animar um gradiente. Uma abordagem é animar os pontos de início e fim. A página Animação de gradiente move os dois pontos em um círculo centralizado na tela. O raio desse círculo é metade da largura ou altura da tela, o que for menor. Os pontos inicial e final são opostos um ao outro neste círculo, e o gradiente vai de branco para preto com um Mirror modo de bloco:

Animação de gradiente

O construtor cria o SKCanvasView. Os OnAppearing métodos e OnDisappearing manipulam a lógica de animação:

public class GradientAnimationPage : ContentPage
{
    SKCanvasView canvasView;
    bool isAnimating;
    double angle;
    Stopwatch stopwatch = new Stopwatch();

    public GradientAnimationPage()
    {
        Title = "Gradient Animation";

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

    protected override void OnAppearing()
    {
        base.OnAppearing();

        isAnimating = true;
        stopwatch.Start();
        Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        stopwatch.Stop();
        isAnimating = false;
    }

    bool OnTimerTick()
    {
        const int duration = 3000;
        angle = 2 * Math.PI * (stopwatch.ElapsedMilliseconds % duration) / duration;
        canvasView.InvalidateSurface();

        return isAnimating;
    }
    ···
}

O OnTimerTick método calcula um angle valor que é animado de 0 a 2π a cada 3 segundos.

Aqui está uma maneira de calcular os dois pontos de gradiente. Um SKPoint valor nomeado vector é calculado para se estender do centro da tela até um ponto no raio do círculo. A direção deste vetor é baseada nos valores de seno e cosseno do ângulo. Os dois pontos de gradiente opostos são então calculados: um ponto é calculado subtraindo esse vetor do ponto central, e outro ponto é calculado adicionando o vetor ao ponto central:

public class GradientAnimationPage : ContentPage
{
    ···
    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())
        {
            SKPoint center = new SKPoint(info.Rect.MidX, info.Rect.MidY);
            int radius = Math.Min(info.Width, info.Height) / 2;
            SKPoint vector = new SKPoint((float)(radius * Math.Cos(angle)),
                                         (float)(radius * Math.Sin(angle)));

            paint.Shader = SKShader.CreateLinearGradient(
                                center - vector,
                                center + vector,
                                new SKColor[] { SKColors.White, SKColors.Black },
                                null,
                                SKShaderTileMode.Mirror);

            canvas.DrawRect(info.Rect, paint);
        }
    }
}

Uma abordagem um pouco diferente requer menos código. Essa abordagem faz uso do método de sobrecarga com uma transformação matricial SKShader.CreateLinearGradient como último argumento. Essa abordagem é a versão no exemplo:

public class GradientAnimationPage : ContentPage
{
    ···
    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.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(0, 0),
                                info.Width < info.Height ? new SKPoint(info.Width, 0) :
                                                           new SKPoint(0, info.Height),
                                new SKColor[] { SKColors.White, SKColors.Black },
                                new float[] { 0, 1 },
                                SKShaderTileMode.Mirror,
                                SKMatrix.MakeRotation((float)angle, info.Rect.MidX, info.Rect.MidY));

            canvas.DrawRect(info.Rect, paint);
        }
    }
}

Se a largura da tela for menor que a altura, os dois pontos de gradiente serão definidos como (0, 0) e (info.Width, 0). A transformação de rotação passou como o último argumento para CreateLinearGradient girar efetivamente esses dois pontos ao redor do centro da tela.

Observe que, se o ângulo for 0, não haverá rotação e os dois pontos de gradiente serão os cantos superior esquerdo e superior direito da tela. Esses pontos não são os mesmos pontos de gradiente calculados como mostrado na chamada anterior CreateLinearGradient . Mas esses pontos são paralelos à linha de gradiente horizontal que corta o centro da tela, e resultam em um gradiente idêntico.

Gradiente do arco-íris

A página Gradiente do arco-íris desenha um arco-íris do canto superior esquerdo da tela para o canto inferior direito. Mas esse gradiente de arco-íris não é como um arco-íris de verdade. É reto em vez de curvo, mas é baseado em oito cores HSL (matiz-saturação-luminosidade) que são determinadas pela ciclagem através de valores de matiz de 0 a 360:

SKColor[] colors = new SKColor[8];

for (int i = 0; i < colors.Length; i++)
{
    colors[i] = SKColor.FromHsl(i * 360f / (colors.Length - 1), 100, 50);
}

Esse código faz parte do PaintSurface manipulador mostrado abaixo. O manipulador começa criando um caminho que define um polígono de seis lados que se estende do canto superior esquerdo da tela até o canto inferior direito:

public class RainbowGradientPage : ContentPage
{
    public RainbowGradientPage ()
    {
        Title = "Rainbow Gradient";

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

    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())
        {
            float rainbowWidth = Math.Min(info.Width, info.Height) / 2f;

            // Create path from upper-left to lower-right corner
            path.MoveTo(0, 0);
            path.LineTo(rainbowWidth / 2, 0);
            path.LineTo(info.Width, info.Height - rainbowWidth / 2);
            path.LineTo(info.Width, info.Height);
            path.LineTo(info.Width - rainbowWidth / 2, info.Height);
            path.LineTo(0, rainbowWidth / 2);
            path.Close();

            using (SKPaint paint = new SKPaint())
            {
                SKColor[] colors = new SKColor[8];

                for (int i = 0; i < colors.Length; i++)
                {
                    colors[i] = SKColor.FromHsl(i * 360f / (colors.Length - 1), 100, 50);
                }

                paint.Shader = SKShader.CreateLinearGradient(
                                    new SKPoint(0, rainbowWidth / 2),
                                    new SKPoint(rainbowWidth / 2, 0),
                                    colors,
                                    null,
                                    SKShaderTileMode.Repeat);

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

Os dois pontos de gradiente no método são baseados em CreateLinearGradient dois dos pontos que definem esse caminho: Ambos os pontos estão próximos ao canto superior esquerdo. O primeiro está na borda superior da tela e o segundo está na borda esquerda da tela. Eis o resultado:

Gradiente do arco-íris defeituoso

Esta é uma imagem interessante, mas não é bem a intenção. O problema é que, ao criar um gradiente linear, as linhas de cor constante são perpendiculares à linha do gradiente. A linha de gradiente é baseada nos pontos onde a figura toca os lados superior e esquerdo, e essa linha geralmente não é perpendicular às bordas da figura que se estendem até o canto inferior direito. Essa abordagem só funcionaria se a tela fosse quadrada.

Para criar um gradiente arco-íris adequado, a linha de gradiente deve ser perpendicular à borda do arco-íris. Esse é um cálculo mais envolvido. Deve ser definido um vetor paralelo ao lado longo da figura. O vetor é girado 90 graus para que seja perpendicular a esse lado. Ele é então alongado para ser a largura da figura, multiplicando por rainbowWidth. Os dois pontos de gradiente são calculados com base em um ponto no lado da figura, e esse ponto mais o vetor. Aqui está o código que aparece na página Rainbow Gradient no exemplo:

public class RainbowGradientPage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        ···
        using (SKPath path = new SKPath())
        {
            ···
            using (SKPaint paint = new SKPaint())
            {
                ···
                // Vector on lower-left edge, from top to bottom
                SKPoint edgeVector = new SKPoint(info.Width - rainbowWidth / 2, info.Height) -
                                     new SKPoint(0, rainbowWidth / 2);

                // Rotate 90 degrees counter-clockwise:
                SKPoint gradientVector = new SKPoint(edgeVector.Y, -edgeVector.X);

                // Normalize
                float length = (float)Math.Sqrt(Math.Pow(gradientVector.X, 2) +
                                                Math.Pow(gradientVector.Y, 2));
                gradientVector.X /= length;
                gradientVector.Y /= length;

                // Make it the width of the rainbow
                gradientVector.X *= rainbowWidth;
                gradientVector.Y *= rainbowWidth;

                // Calculate the two points
                SKPoint point1 = new SKPoint(0, rainbowWidth / 2);
                SKPoint point2 = point1 + gradientVector;

                paint.Shader = SKShader.CreateLinearGradient(point1,
                                                             point2,
                                                             colors,
                                                             null,
                                                             SKShaderTileMode.Repeat);

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

Agora as cores do arco-íris estão alinhadas com a figura:

Gradiente do arco-íris

Infinitas Cores

Um gradiente arco-íris também é usado na página Infinity Colors . Esta página desenha um sinal de infinito usando um objeto de caminho descrito no artigo Três tipos de curvas de Bézier. A imagem é então colorida com um gradiente arco-íris animado que varre continuamente a imagem.

O construtor cria o SKPath objeto que descreve o sinal de infinito. Depois que o caminho é criado, o construtor também pode obter os limites retangulares do caminho. Em seguida, calcula um valor chamado gradientCycleLength. Se um gradiente for baseado nos cantos superior esquerdo e inferior direito do pathBounds retângulo, esse gradientCycleLength valor será a largura horizontal total do padrão de gradiente:

public class InfinityColorsPage : ContentPage
{
    ···
    SKCanvasView canvasView;

    // Path information
    SKPath infinityPath;
    SKRect pathBounds;
    float gradientCycleLength;

    // Gradient information
    SKColor[] colors = new SKColor[8];
    ···

    public InfinityColorsPage ()
    {
        Title = "Infinity Colors";

        // Create path for infinity sign
        infinityPath = new SKPath();
        infinityPath.MoveTo(0, 0);                                  // Center
        infinityPath.CubicTo(  50,  -50,   95, -100,  150, -100);   // To top of right loop
        infinityPath.CubicTo( 205, -100,  250,  -55,  250,    0);   // To far right of right loop
        infinityPath.CubicTo( 250,   55,  205,  100,  150,  100);   // To bottom of right loop
        infinityPath.CubicTo(  95,  100,   50,   50,    0,    0);   // Back to center  
        infinityPath.CubicTo( -50,  -50,  -95, -100, -150, -100);   // To top of left loop
        infinityPath.CubicTo(-205, -100, -250,  -55, -250,    0);   // To far left of left loop
        infinityPath.CubicTo(-250,   55, -205,  100, -150,  100);   // To bottom of left loop
        infinityPath.CubicTo( -95,  100, - 50,   50,    0,    0);   // Back to center
        infinityPath.Close();

        // Calculate path information
        pathBounds = infinityPath.Bounds;
        gradientCycleLength = pathBounds.Width +
            pathBounds.Height * pathBounds.Height / pathBounds.Width;

        // Create SKColor array for gradient
        for (int i = 0; i < colors.Length; i++)
        {
            colors[i] = SKColor.FromHsl(i * 360f / (colors.Length - 1), 100, 50);
        }

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

O construtor também cria a colors matriz para o arco-íris e o SKCanvasView objeto.

Substituições dos OnAppearing métodos e OnDisappearing executar a sobrecarga para a animação. O OnTimerTick método anima o offset campo de 0 a gradientCycleLength cada dois segundos:

public class InfinityColorsPage : ContentPage
{
    ···
    // For animation
    bool isAnimating;
    float offset;
    Stopwatch stopwatch = new Stopwatch();
    ···

    protected override void OnAppearing()
    {
        base.OnAppearing();

        isAnimating = true;
        stopwatch.Start();
        Device.StartTimer(TimeSpan.FromMilliseconds(16), OnTimerTick);
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        stopwatch.Stop();
        isAnimating = false;
    }

    bool OnTimerTick()
    {
        const int duration = 2;     // seconds
        double progress = stopwatch.Elapsed.TotalSeconds % duration / duration;
        offset = (float)(gradientCycleLength * progress);
        canvasView.InvalidateSurface();

        return isAnimating;
    }
    ···
}

Finalmente, o PaintSurface manipulador renderiza o sinal de infinito. Como o caminho contém coordenadas negativas e positivas ao redor de um ponto central de (0, 0), uma Translate transformação na tela é usada para deslocá-lo para o centro. A transformação de tradução é seguida por uma Scale transformação que aplica um fator de escala que torna o sinal de infinito o maior possível, permanecendo dentro de 95% da largura e altura da tela.

Observe que a STROKE_WIDTH constante é adicionada à largura e altura do retângulo delimitador de caminho. O caminho será traçado com uma linha dessa largura, de modo que o tamanho do infinito renderizado seja aumentado pela metade dessa largura em todos os quatro lados:

public class InfinityColorsPage : ContentPage
{
    const int STROKE_WIDTH = 50;
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Set transforms to shift path to center and scale to canvas size
        canvas.Translate(info.Width / 2, info.Height / 2);
        canvas.Scale(0.95f *
            Math.Min(info.Width / (pathBounds.Width + STROKE_WIDTH),
                     info.Height / (pathBounds.Height + STROKE_WIDTH)));

        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Stroke;
            paint.StrokeWidth = STROKE_WIDTH;
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(pathBounds.Left, pathBounds.Top),
                                new SKPoint(pathBounds.Right, pathBounds.Bottom),
                                colors,
                                null,
                                SKShaderTileMode.Repeat,
                                SKMatrix.MakeTranslation(offset, 0));

            canvas.DrawPath(infinityPath, paint);
        }
    }
}

Veja os pontos passados como os dois primeiros argumentos do SKShader.CreateLinearGradient. Esses pontos são baseados no retângulo delimitador do caminho original. O primeiro ponto é (-250, -100) e o segundo é (250, 100). Internos ao SkiaSharp, esses pontos são submetidos à transformação de tela atual para que se alinhem corretamente com o sinal de infinito exibido.

Sem o último argumento para CreateLinearGradient, você veria um gradiente arco-íris que se estende da parte superior esquerda do sinal do infinito até a parte inferior direita. (Na verdade, o gradiente se estende do canto superior esquerdo até o canto inferior direito do retângulo delimitador. O sinal de infinito renderizado é maior que o retângulo delimitador pela metade do STROKE_WIDTH valor em todos os lados. Como o gradiente é vermelho no início e no fim, e o gradiente é criado com SKShaderTileMode.Repeat, a diferença não é perceptível.)

Com esse último argumento para CreateLinearGradient, o padrão de gradiente varre continuamente a imagem:

Infinitas Cores

Transparência e gradientes

As cores que contribuem para um gradiente podem incorporar transparência. Em vez de um gradiente que desaparece de uma cor para outra, o gradiente pode desaparecer de uma cor para transparente.

Você pode usar esta técnica para alguns efeitos interessantes. Um dos exemplos clássicos mostra um objeto gráfico com seu reflexo:

Gradiente de reflexão

O texto que está de cabeça para baixo é colorido com um gradiente que é 50% transparente na parte superior para totalmente transparente na parte inferior. Esses níveis de transparência estão associados a valores de alfa de 0x80 e 0.

O PaintSurface manipulador na página Gradiente de Reflexão dimensiona o tamanho do texto para 90% da largura da tela. Em seguida, calcula xText e yText valoriza para posicionar o texto a ser centralizado horizontalmente, mas sentado em uma linha de base correspondente ao centro vertical da página:

public class ReflectionGradientPage : ContentPage
{
    const string TEXT = "Reflection";

    public ReflectionGradientPage ()
    {
        Title = "Reflection Gradient";

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

    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())
        {
            // Set text color to blue
            paint.Color = SKColors.Blue;

            // Set text size to fill 90% of width
            paint.TextSize = 100;
            float width = paint.MeasureText(TEXT);
            float scale = 0.9f * info.Width / width;
            paint.TextSize *= scale;

            // Get text bounds
            SKRect textBounds = new SKRect();
            paint.MeasureText(TEXT, ref textBounds);

            // Calculate offsets to position text above center
            float xText = info.Width / 2 - textBounds.MidX;
            float yText = info.Height / 2;

            // Draw unreflected text
            canvas.DrawText(TEXT, xText, yText, paint);

            // Shift textBounds to match displayed text
            textBounds.Offset(xText, yText);

            // Use those offsets to create a gradient for the reflected text
            paint.Shader = SKShader.CreateLinearGradient(
                                new SKPoint(0, textBounds.Top),
                                new SKPoint(0, textBounds.Bottom),
                                new SKColor[] { paint.Color.WithAlpha(0),
                                                paint.Color.WithAlpha(0x80) },
                                null,
                                SKShaderTileMode.Clamp);

            // Scale the canvas to flip upside-down around the vertical center
            canvas.Scale(1, -1, 0, yText);

            // Draw reflected text
            canvas.DrawText(TEXT, xText, yText, paint);
        }
    }
}

Esses xText e yText valores são os mesmos valores usados para exibir o texto refletido DrawText na chamada na parte inferior do PaintSurface manipulador. Pouco antes desse código, no entanto, você verá uma chamada para o Scale método de SKCanvas. Esse Scale método escala horizontalmente por 1 (que não faz nada), mas verticalmente por –1, o que efetivamente inverte tudo. O centro de rotação é definido para o ponto (0, yText), onde yText está o centro vertical da tela, originalmente calculado como info.Height dividido por 2.

Lembre-se de que o Skia usa o gradiente para colorir objetos gráficos antes das transformações de tela. Depois que o texto não refletido é desenhado, o retângulo é deslocado textBounds para que corresponda ao texto exibido:

textBounds.Offset(xText, yText);

A CreateLinearGradient chamada define um gradiente da parte superior desse retângulo até a parte inferior. O gradiente é de um azul completamente transparente (paint.Color.WithAlpha(0)) para um azul 50% transparente (paint.Color.WithAlpha(0x80)). A transformação de tela inverte o texto de cabeça para baixo, de modo que o azul transparente de 50% começa na linha de base e se torna transparente na parte superior do texto.