Compartir vía


Puntos y guiones en SkiaSharp

Domine las complejidades del dibujo de líneas de puntos y discontinuas en SkiaSharp

SkiaSharp le permite dibujar líneas que no son continuas, sino que se componen de puntos y guiones:

Línea de puntos

Esto se hace con un efecto de ruta, que es una instancia de la clase SKPathEffect que se establece en la propiedad PathEffect de SKPaint. Puede crear un efecto de ruta (o combinar efectos de ruta) mediante uno de los métodos de creación estáticos definidos por SKPathEffect. (SKPathEffect es uno de los seis efectos admitidos por SkiaSharp; los demás se describen en la sección Efectos de SkiaSharp).

Para dibujar líneas de puntos o discontinuas, use el método estático SKPathEffect.CreateDash. Hay dos argumentos: el primero es una matriz de valores float que indica las longitudes de los puntos y guiones y la longitud de los espacios entre ellos. Esta matriz debe tener un número par de elementos y contener al menos dos elementos. (Puede haber cero elementos en la matriz, pero esto da como resultado una línea continua). Si hay dos elementos, el primero es la longitud de un punto o guión, y el segundo es la longitud del espacio antes del siguiente punto o guión. Si hay más de dos elementos, entonces se encuentran en este orden: longitud del guión, longitud de espacio, longitud del guión, longitud de espacio, etc.

Por lo general, queremos que el guión y las longitudes de espacio sea un múltiplo del ancho del trazo. Si el ancho del trazo es de 10 píxeles, por ejemplo, la matriz { 10, 10 } dibujará una línea de puntos donde los puntos y los espacios tienen la misma longitud que el grosor del trazo.

Sin embargo, el valor StrokeCap del objeto SKPaint también afecta a estos puntos y guiones. Como verá en breve, esto afecta a los elementos de esta matriz.

Las líneas de puntos y discontinuas se muestran en la página Puntos y guiones. El archivo DotsAndDashesPage.xaml crea una instancia de dos vistas Picker, una para permitirle seleccionar un límite de trazo y otra para seleccionar una matriz de guiones:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp;assembly=SkiaSharp"
             xmlns:skiaforms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Paths.DotsAndDashesPage"
             Title="Dots and Dashes">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

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

        <Picker x:Name="strokeCapPicker"
                Title="Stroke Cap"
                Grid.Row="0"
                Grid.Column="0"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type skia:SKStrokeCap}">
                    <x:Static Member="skia:SKStrokeCap.Butt" />
                    <x:Static Member="skia:SKStrokeCap.Round" />
                    <x:Static Member="skia:SKStrokeCap.Square" />
                </x:Array>
            </Picker.ItemsSource>
            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>

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

        <skiaforms:SKCanvasView x:Name="canvasView"
                                PaintSurface="OnCanvasViewPaintSurface"
                                Grid.Row="1"
                                Grid.Column="0"
                                Grid.ColumnSpan="2" />
    </Grid>
</ContentPage>

Los tres primeros elementos de dashArrayPicker presuponen que el ancho del trazo es de 10 píxeles. La matriz { 10, 10 } es para una línea de puntos, { 30, 10 } es para una línea discontinua y { 10, 10, 30, 10 } es para una línea de puntos y guiones. (Los otros tres se discutirán en breve).

El archivo de código subyacente DotsAndDashesPage contiene el controlador de eventos PaintSurface y un par de rutinas auxiliares para acceder a las vistas Picker:

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

    canvas.Clear();

    SKPaint paint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Blue,
        StrokeWidth = 10,
        StrokeCap = (SKStrokeCap)strokeCapPicker.SelectedItem,
        PathEffect = SKPathEffect.CreateDash(GetPickerArray(dashArrayPicker), 20)
    };

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

    canvas.DrawPath(path, paint);
}

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

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

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

En las capturas de pantalla siguientes, la pantalla de iOS en el extremo izquierdo muestra una línea de puntos:

Captura de pantalla triple de la página Puntos y guiones

En la pantalla de Android también se supondría que mostrara una línea de puntos con la matriz { 10, 10 }, pero en su lugar la línea es continua. ¿Qué ha ocurrido? El problema es que la pantalla de Android también tiene una configuración de límites de trazo de Square. Esto extiende todos los guiones por la mitad del ancho del trazo, lo que hace que rellenen los espacios.

Para solucionar este problema cuando se usa un límite de trazo de Square o Round, debe reducir las longitudes de guion de la matriz en función de la longitud del trazo (a veces resulta en una longitud de guión de 0) y aumentar las longitudes de espacio en función la longitud del trazo. Así se calculan las tres matrices de guiones finales del archivo XAML Picker:

  • { 10, 10 } se convierte en { 0, 20 } para una línea de puntos
  • { 30, 10 } se convierte en { 20, 20 } para una línea discontinua
  • { 10, 10, 30, 10 } se convierte en { 0, 20, 20, 20} para una línea de puntos y discontinua

La pantalla de UWP muestra la línea de puntos y discontinua para un límite de trazo de Round. El límite de trazo Round suele ofrecer el mejor aspecto de puntos y guiones en líneas gruesas.

Hasta ahora no hemos mencionado el segundo parámetro del método SKPathEffect.CreateDash. Este parámetro se denomina phase y hace referencia a un desplazamiento dentro del patrón de puntos y guiones para el principio de la línea. Por ejemplo, si la matriz de guiones es { 10, 10 } y phase es 10, la línea comienza con un espacio en lugar de un punto.

Una aplicación interesante del parámetro phase es la animación. La página Espiral animada es similar a la página Espiral de Arquímedes, salvo que la clase AnimatedSpiralPage anime el parámetro phase mediante el método Xamarin.FormsDevice.Timer:

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

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

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

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

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

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

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

            return pageIsActive;
        });
    }
    ···  
}

Por supuesto, tendrá que ejecutar el programa para ver la animación:

Captura de pantalla triple de la página De espiral animada