Píxeles y unidades independientes del dispositivo

Download SampleDescargar el ejemplo

Diferencias entre las coordenadas de SkiaSharp y las de Xamarin.Forms

En este artículo se exploran las diferencias en el sistema de coordenadas usado en SkiaSharp y Xamarin.Forms. Puede obtener información para convertir entre los dos sistemas de coordenadas y también dibujar gráficos que rellenen un área determinada:

An oval that fills the screen

Si lleva un tiempo programando en Xamarin.Forms, puede que tenga una idea de las coordenadas y los tamaños de Xamarin.Forms. Los círculos dibujados en los dos artículos anteriores pueden parecerle un poco pequeños.

Esos círculos son pequeños en comparación con los tamaños Xamarin.Forms. De forma predeterminada, SkiaSharp dibuja en unidades de píxeles, mientras que Xamarin.Forms basa las coordenadas y los tamaños en una unidad independiente del dispositivo establecida por la plataforma subyacente. (Puede encontrar más información sobre el sistema de coordenadas de Xamarin.Forms en el Capítulo 5. Lidiando con tamaños del libro Creación de aplicaciones móviles con Xamarin.Forms).

La página del programa SkewSharpFormsDemos titulada Tamaño de superficie usa la salida de texto de SkiaSharp para mostrar el tamaño de la superficie de visualización de tres orígenes diferentes:

  • Las propiedades Xamarin.FormsWidth y Height normales del objeto SKCanvasView.
  • La propiedad CanvasSize del objeto SKCanvasView.
  • La propiedad Size del valor SKImageInfo, que es coherente con las propiedades Width y Height usadas en las dos páginas anteriores.

La clase SurfaceSizePage muestra cómo mostrar estos valores. El constructor guarda el objeto SKCanvasView como un campo, por lo que se puede acceder a él en el controlador de eventos PaintSurface :

SKCanvasView canvasView;

public SurfaceSizePage()
{
    Title = "Surface Size";

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

SKCanvas incluye seis métodos DrawText diferentes, pero este método DrawText es el más sencillo:

public void DrawText (String text, Single x, Single y, SKPaint paint)

Especifique la cadena de texto, las coordenadas X e Y donde se va a comenzar el texto y un objeto SKPaint. La coordenada X especifica dónde se sitúa el lado izquierdo del texto, pero cuidado: la coordenada Y especifica la posición de la línea de base del texto. Si alguna vez ha escrito a mano en papel rayado, la línea de base es la línea sobre la que se asientan los caracteres y por debajo de la cual descienden los descendentes (como los de las letras g, p, q e y).

El objeto SKPaint permite especificar el color del texto, la familia de fuentes y el tamaño del texto. De forma predeterminada, la propiedad TextSize tiene un valor de 12, lo que da como resultado texto diminuto en dispositivos de alta resolución, como teléfonos. Salvo en las aplicaciones más sencillas, también necesitará información sobre el tamaño del texto que va a mostrar. La clase SKPaint define una propiedad FontMetrics y varios métodos MeasureText, pero para necesidades menos sofisticadas, la propiedad FontSpacing proporciona un valor recomendado para espaciar líneas sucesivas de texto.

El siguiente controlador PaintSurface crea un objeto SKPaint para un TextSize de 40 píxeles, que es el alto vertical deseado del texto desde la parte superior de los ascendentes hasta la parte inferior de los descendientes. El valor FontSpacing que devuelve el objeto SKPaint es un poco mayor que ese, unos 47 píxeles.

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

    canvas.Clear();

    SKPaint paint = new SKPaint
    {
        Color = SKColors.Black,
        TextSize = 40
    };

    float fontSpacing = paint.FontSpacing;
    float x = 20;               // left margin
    float y = fontSpacing;      // first baseline
    float indent = 100;

    canvas.DrawText("SKCanvasView Height and Width:", x, y, paint);
    y += fontSpacing;
    canvas.DrawText(String.Format("{0:F2} x {1:F2}",
                                  canvasView.Width, canvasView.Height),
                    x + indent, y, paint);
    y += fontSpacing * 2;
    canvas.DrawText("SKCanvasView CanvasSize:", x, y, paint);
    y += fontSpacing;
    canvas.DrawText(canvasView.CanvasSize.ToString(), x + indent, y, paint);
    y += fontSpacing * 2;
    canvas.DrawText("SKImageInfo Size:", x, y, paint);
    y += fontSpacing;
    canvas.DrawText(info.Size.ToString(), x + indent, y, paint);
}

El método comienza la primera línea de texto con una coordenada X de 20 (para un poco de margen a la izquierda) y una coordenada Y de fontSpacing, que es un poco más de lo necesario para mostrar el alto completo de la primera línea de texto en la parte superior de la superficie de presentación. Después de cada llamada a DrawText, la coordenada Y aumenta en uno o dos incrementos de fontSpacing.

Esta es la ejecución del programa:

Screenshots show the Surface Size app running on two mobile devices.

Como puede ver, la propiedad CanvasSize del SKCanvasView y la propiedad Size del valor SKImageInfo son coherentes en la generación de informes de las dimensiones de píxel. Las propiedades Height y Width del SKCanvasView son propiedades Xamarin.Forms e informan del tamaño de la vista en las unidades independientes del dispositivo definidas por la plataforma.

El simulador de iOS siete de la izquierda tiene dos píxeles por unidad independiente del dispositivo, y el Android Nexus 5 del centro tiene tres píxeles por unidad. Por eso, el círculo simple mostrado anteriormente tiene diferentes tamaños en distintas plataformas.

Si prefiere trabajar completamente en unidades independientes del dispositivo, puede hacerlo estableciendo la propiedad IgnorePixelScaling del SKCanvasView en true. Sin embargo, es posible que no le gusten los resultados. SkiaSharp representa los gráficos en una superficie de dispositivo más pequeña, con un tamaño de píxel igual al tamaño de la vista en unidades independientes del dispositivo. (Por ejemplo, SkiaSharp usaría una superficie de visualización de 360 x 512 píxeles en el Nexus 5). A continuación, aumenta el tamaño de la imagen, lo que produce dentelladas perceptibles en el mapa de bits.

Para mantener la misma resolución de imágenes, una solución mejor es escribir sus propias funciones sencillas para convertir entre los dos sistemas de coordenadas.

Además del método DrawCircle, SKCanvas también define dos métodos DrawOval que dibujan una elipse. Una elipse se define mediante dos radios en lugar de un solo radio. Estos se conocen como el radio principal y el radio menor. El método DrawOval dibuja una elipse con los dos radios paralelos a los ejes X e Y. (Si necesita dibujar una elipse con ejes que no son paralelos a los ejes X e Y, puede usar una transformación de rotación como se describe en el artículo La transformación de rotación o una ruta de acceso gráfica, como se describe en el artículo Tres maneras de dibujar un arco). Esta sobrecarga del método DrawOval asigna nombres a los dos parámetros radios rx y ry para indicar que son paralelos a los ejes X e Y:

public void DrawOval (Single cx, Single cy, Single rx, Single ry, SKPaint paint)

¿Es posible dibujar una elipse que rellene la superficie de visualización? La página relleno de elipse muestra cómo. El controlador de eventos PaintSurface de la clase EllipseFillPage.xaml.cs resta la mitad del ancho del trazo del los valores xRadius y yRadius para ajustarse a toda la elipse y su contorno dentro de la superficie de presentación:

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

    canvas.Clear();

    float strokeWidth = 50;
    float xRadius = (info.Width - strokeWidth) / 2;
    float yRadius = (info.Height - strokeWidth) / 2;

    SKPaint paint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Blue,
        StrokeWidth = strokeWidth
    };
    canvas.DrawOval(info.Width / 2, info.Height / 2, xRadius, yRadius, paint);
}

Aquí se está ejecutando:

Screenshots show the Ellipse Fill app running on two mobile devices.

El otro método DrawOval tiene un argumento SKRect, que es un rectángulo definido en términos de las coordenadas X e Y de su esquina superior izquierda y esquina inferior derecha. El óvalo rellena ese rectángulo, lo que sugiere que podría ser posible usarlo en la página Relleno de elipse de esta manera:

SKRect rect = new SKRect(0, 0, info.Width, info.Height);
canvas.DrawOval(rect, paint);

Sin embargo, esto trunca todos los bordes del contorno de la elipse en los cuatro lados. Debe ajustar todos los argumentos del constructor SKRect en función del strokeWidth para que esto funcione correctamente:

SKRect rect = new SKRect(strokeWidth / 2,
                         strokeWidth / 2,
                         info.Width - strokeWidth / 2,
                         info.Height - strokeWidth / 2);
canvas.DrawOval(rect, paint);