Uso de C#, Xamarin y SkiaSharp para deleitar y sorprender (en todas las plataformas)
El desarrollo multiplataforma puede ser complicado, especialmente con plataformas móviles involucradas. Podría evitarlo por completo simplemente creando una aplicación separada por plataforma, pero eso no es rentable ni especialmente divertido.
What is SkiaSharp?

Herramientas como Xamarin te ayudan, al menos, a usar un lenguaje de programación común en todas las plataformas importantes, pero aunque eso es excelente para ayudarte a compartir la lógica de negocios de tu aplicación, no facilita automáticamente el uso compartido de la lógica de la interfaz de usuario.
Xamarin /Microsoft proporcionan varias herramientas para ayudar a compartir la implementación de la interfaz de usuario en diferentes plataformas. Crearon Xamarin. Formularios para ayudarle a definir de forma abstracta las vistas de la interfaz de usuario de la aplicación una vez y, a continuación, reutilizarlas en las plataformas compatibles. Xamarin. Forms es muy chulo, al menos eso creemos, por eso tenemos un nuevo producto disponible para asegurarnos de que puedas hacer cosas aún más increíbles con Xamarin. Formas. Pero, ¿qué pasa si todavía hay algo que Forms no puede hacer, pero que aún necesitas? ¿No sería genial renderizar gráficos personalizados en todas las plataformas en C#?
Bueno, quería hacer precisamente esto, hace unos años, pero el desafío era que no había una API 2D de C# multiplataforma disponible en todas las plataformas importantes en ese momento. Por lo tanto, creé una abstracción en torno a las diversas API de renderizado 2D nativas para permitirme escribir alguna lógica de renderizado una vez y hacer que algunas capas de abstracción la conviertan en la secuencia correcta de llamadas a las diversas API de renderizado nativas. Lo que descubrí, sin embargo, es que termina habiendo una gran cantidad de gastos generales interesantes usando este camino. Mi lógica crearía algunas primitivas de gráficos 2D en C# que luego tendrían que representarse, en Android, como objetos Java, y luego esos objetos Java tendrían que representarse como clases nativas en la capa de representación nativa de Android, y así sucesivamente para cada plataforma. Tener tantas capas de abstracción causaba una sobrecarga nada despreciable cuando se combinaba con la charla que se tiene al usar una API de renderizado 2D y gráficos 2D complejos.
Pues bien, Xamarin mismos debieron sentir este mismo dolor, porque les llevó a crear SkiaSharp. SkiaSharp es una API de renderizado de gráficos 2D multiplataforma que puede utilizar en una gran cantidad de plataformas diferentes, incluidas las plataformas Android / iOS a través de Xamarin. SkiaSharp son algunos enlaces de C# directamente alrededor de la API nativa de C para la biblioteca de representación de Skia. Skia es una biblioteca de renderizado rápida y de código abierto que se usa mucho en Android y el navegador Chrome y muchos otros proyectos de alto perfil. Con SkiaSharp en la mano, puede realizar una representación rápida de gráficos 2D en muchas plataformas con comparativamente poca sobrecarga en su interacción con la API, ya que la API de C# puede comunicarse directamente con la biblioteca nativa de Skia. En este artículo, te explicaré cómo puedes empezar a utilizar la API.
Empezando
Para empezar, abra Visual Studio y cree un nuevo proyecto multiplataforma. Esta es una de las plantillas que Xamarin instala en Visual Studio.

Se puede hacer que sea una aplicación de Xamarin.Forms o una aplicación nativa, según sus preferencias. Acabo de crear una aplicación nativa para los fines de esta demostración, compartiendo código a través de una PCL (biblioteca de clases portátil), en lugar de un proyecto compartido. Esto creará una gran cantidad de plataformas predeterminadas asociadas con la plantilla, y puede agregar proyectos adicionales, siempre que también sean compatibles con SkiaSharp, según lo desee.
Cuando seleccioné una aplicación multiplataforma nativa, creó un proyecto PCL para compartir código entre plataformas, un proyecto de Xamarin.Android y un proyecto de Xamarin.iOS, pero luego procedí a agregar también un proyecto de WPF y un proyecto de UWP a la mezcla, ya que ambas plataformas también admiten SkiaSharp para la representación. A continuación, agregué referencias al proyecto PCL a los dos nuevos proyectos que agregué a la solución.

A continuación, debe agregar algunos paquetes NuGet a la solución. Si hace clic con el botón secundario en el nodo de la solución y selecciona "Administrar paquetes Nuget para la solución", debería poder buscar e instalar en línea SkiaSharp y SkiaSharp.Views en todos los proyectos de la solución. SkiaSharp.Views tiene algunas clases auxiliares que le ayudan a iniciar la representación de SkiaSharp en una vista de interfaz de usuario nativa en la plataforma que elija, lo que le ahorra algo de lógica repetitiva. SkiaSharp.Views debe instalarse para todos los proyectos excepto para la PCL, a la que no proporciona ninguna utilidad (ya que no está enlazada a una plataforma de interfaz de usuario determinada).
Nuestro objetivo
Comenzaremos con algo simple, renderizando un círculo simple, pero luego pasaremos a algo considerablemente más complicado. Uno de nuestros navegadores de muestra más antiguos tenía un efecto de iris animado:

Lo cual se realizó, deduzco, superponiendo un montón de imágenes y luego rotándolas en diferentes direcciones. En ese caso, las imágenes eran estáticas, pero tenía curiosidad por saber si se podría, alternativamente, simplemente poner toda la lógica en una vista y renderizarla toda dinámicamente. Avancemos hacia ese objetivo.
Comenzaremos aprovechando SkiaSharp.Views para crear un componente llamado IrisView para Android. Más adelante ampliaremos esto más y luego completaremos los detalles para las otras plataformas.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using SkiaSharp;
using Android.Util;
namespace SkiaSharpDemo.Droid
{
public class IrisView
: SkiaSharp.Views.Android.SKCanvasView
{
private Handler _handler;
private void Initialize()
{
_handler = new Handler(Context.MainLooper);
}
public IrisView(Context context)
: base(context)
{
Initialize();
}
public IrisView(Context context, IAttributeSet attrs)
: base(context, attrs)
{
Initialize();
}
public IrisView(Context context, IAttributeSet attrs, int defStyleAttr)
: base(context, attrs)
{
Initialize();
}
protected IrisView(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
Initialize();
}
protected override void OnDraw(SKSurface surface, SKImageInfo info)
{
base.OnDraw(surface, info);
//Get the canvas from the skia surface.
var context = surface.Canvas;
//Clear out the current content for the canvas.
context.DrawColor(SKColors.Transparent, SKBlendMode.Clear);
//Determine the center for the circle.
var centerX = info.Width / 2.0f;
var centerY = info.Height / 2.0f;
//Determine the radius for the circle.
var rad = Math.Min(info.Width, info.Height) / 2.0f;
//Create the paint object to fill the circle.
using (SKPaint p = new SKPaint())
{
p.IsStroke = false;
p.IsAntialias = true;
p.Color = new SKColor(255, 0, 0);
//Fill the circle.
context.DrawCircle(centerX, centerY, rad, p);
};
}
}
}
La mayor parte de este código es lógica repetitiva para ampliar una de las vistas que proporciona SkiaSharp.Views para poder representar contenido con SkiaSharp dentro de una vista normal de Android. Si nos centramos en la lógica que hace la pintura:
//Get the canvas from the skia surface.
var context = surface.Canvas;
//Clear out the current content for the canvas.
context.DrawColor(SKColors.Transparent, SKBlendMode.Clear);
//Determine the center for the circle.
var centerX = info.Width / 2.0f;
var centerY = info.Height / 2.0f;
//Determine the radius for the circle.
var rad = Math.Min(info.Width, info.Height) / 2.0f;
//Create the paint object to fill the circle.
using (SKPaint p = new SKPaint())
{
p.IsStroke = false;
p.IsAntialias = true;
p.Color = new SKColor(255, 0, 0);
//Fill the circle.
context.DrawCircle(centerX, centerY, rad, p);
};
Aquí:
- Consigue el lienzo de Skia para pintar.
- Borre el color inicial que se muestra en el lienzo (borrando cualquier renderizado anterior).
- Determina el centro de la vista y el radio de un círculo que podemos dibujar en él.
- Crea un objeto de Skia Paint que se llenará de rojo, usando suavizado.
- Dibuje el círculo en el lienzo utilizando el objeto de pintura configurado.
Luego, de vuelta en nuestra actividad principal, si agregas este IrisView al diseño, deberías ver algo como esto:

Ok, esto está bien, pero, obviamente, si la lógica de renderizado está en la vista nativa de Android, no podemos compartirla entre plataformas, ¿verdad? Así que refactoricemos esto un poco. En la PCL, creamos una clase llamada IrisRenderer.cs con este contenido:
using SkiaSharp;
using System;
using System.Collections.Generic;
namespace SkiaSharpDemo
{
public class IrisRenderer
{
public IrisRenderer()
{
}
private DateTime _lastRender = DateTime.Now;
private bool _forward = true;
private double _progress = 0;
private double _duration = 5000;
private Random _rand = new Random();
private static double Cubic(double p)
{
return p * p * p;
}
public static double CubicEase(double t)
{
if (t < .5)
{
var fastTime = t * 2.0;
return .5 * Cubic(fastTime);
}
var outFastTime = (1.0 - t) * 2.0;
var y = 1.0 - Cubic(outFastTime);
return .5 * y + .5;
}
private bool _first = true;
public void RenderIris(SKSurface surface, SKImageInfo info)
{
if (_first)
{
_first = false;
_lastRender = DateTime.Now;
}
var currTime = DateTime.Now;
var elapsed = (currTime - _lastRender).TotalMilliseconds;
_lastRender = currTime;
if (_forward)
{
_progress += elapsed / _duration;
}
else
{
_progress -= elapsed / _duration;
}
if (_progress > 1.0)
{
_progress = 1.0;
_forward = false;
_duration = 1000 + 4000 * _rand.NextDouble();
}
if (_progress < 0)
{
_progress = 0;
_forward = true;
_duration = 1000 + 4000 * _rand.NextDouble();
}
var context = surface.Canvas;
context.DrawColor(SKColors.Transparent, SKBlendMode.Clear);
//Determine the center for the circle.
var centerX = info.Width / 2.0f;
var centerY = info.Height / 2.0f;
//Determine the radius for the circle.
var rad = Math.Min(info.Width, info.Height) / 2.0f;
var fromR = 255;
var fromG = 0;
var fromB = 0;
var toR = 0;
var toG = 0;
var toB = 255;
var actualProgress = CubicEase(_progress);
var actualR = (byte)Math.Round(fromR + (double)(toR - fromR) * actualProgress);
var actualG = (byte)Math.Round(fromG + (double)(toG - fromG) * actualProgress);
var actualB = (byte)Math.Round(fromB + (double)(toB - fromB) * actualProgress);
//Create the paint object to fill the circle.
using (SKPaint p = new SKPaint())
{
p.IsStroke = false;
p.IsAntialias = true;
p.Color = new SKColor(actualR, actualG, actualB);
//Fill the circle.
context.DrawCircle(centerX, centerY, rad, p);
};
}
}
}
Y modificamos el IrisView para Android para que se vea así:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using SkiaSharp;
using Android.Util;
namespace SkiaSharpDemo.Droid
{
public class IrisView
: SkiaSharp.Views.Android.SKCanvasView
{
private IrisRenderer _irisRenderer;
private Handler _handler;
private void Initialize()
{
//The IrisRenderer will perform the actual rendering logic for this view.
_irisRenderer = new IrisRenderer();
_handler = new Handler(Context.MainLooper);
//This starts a tick loop that we will use later for animation.
_handler.Post(Tick);
}
private DateTime _lastTime = DateTime.Now;
private void Tick()
{
DateTime currTime = DateTime.Now;
//Don't render new frames too often.
if (currTime - _lastTime < TimeSpan.FromMilliseconds(16))
{
_handler.Post(Tick);
return;
}
_lastTime = currTime;
Invalidate();
_handler.Post(Tick);
}
public IrisView(Context context)
: base(context)
{
Initialize();
}
public IrisView(Context context, IAttributeSet attrs)
: base(context, attrs)
{
Initialize();
}
public IrisView(Context context, IAttributeSet attrs, int defStyleAttr)
: base(context, attrs)
{
Initialize();
}
protected IrisView(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
Initialize();
}
protected override void OnDraw(SKSurface surface, SKImageInfo info)
{
base.OnDraw(surface, info);
_irisRenderer.RenderIris(surface, info);
}
}
}
De esta manera, hemos factorizado toda la lógica de representación en una clase compartida que se encuentra en la PCL, que se puede compartir entre todas las plataformas a las que queremos dirigirnos. Solo necesitamos codificarlo una vez y listo. Además, hemos agregado un sistema de animación primitivo que seguirá invalidando la vista y repintando en un intervalo para que nuestro renderizador pueda analizar el tiempo transcurrido y animar los cambios usando interpolación lineal (facilitada con una función de aceleración cúbica). Genial, ¿eh? Así es como se ve el círculo durante una animación entre azul y rojo:

Bien, en este punto, podemos completar el resto de las implementaciones para IrisView. Estas deben ser clases separadas porque cada plataforma tiene diferentes requisitos en términos de lo que constituye una vista de interfaz de usuario y diferentes mecanismos que podemos usar para controlar el bucle de animación, pero la idea es minimizar el contenido de estas clases para que contengan solo comportamientos específicos de la plataforma. También tenemos la opción de crear abstracciones adicionales (por ejemplo, una alrededor de la animación) que reducirían aún más la lógica en estas clases. Esta es la versión de la vista para iOS:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Foundation;
using SkiaSharp;
using UIKit;
using CoreGraphics;
using CoreFoundation;
namespace SkiaSharpDemo.iOS
{
public class IrisView
: SkiaSharp.Views.iOS.SKCanvasView
{
private IrisRenderer _irisRenderer;
public IrisView()
: base()
{
Initialize();
}
public IrisView(CGRect frame)
: base(frame)
{
Initialize();
}
public IrisView(IntPtr p)
: base(p)
{
Initialize();
}
private void Initialize()
{
BackgroundColor = UIColor.Clear;
_irisRenderer = new IrisRenderer();
DispatchQueue.MainQueue.DispatchAsync(Tick);
}
private DateTime _lastTime = DateTime.Now;
private void Tick()
{
DateTime currTime = DateTime.Now;
if (currTime - _lastTime < TimeSpan.FromMilliseconds(16))
{
DispatchQueue.MainQueue.DispatchAsync(Tick);
return;
}
_lastTime = currTime;
SetNeedsDisplay();
DispatchQueue.MainQueue.DispatchAsync(Tick);
}
public override void DrawInSurface(SKSurface surface, SKImageInfo info)
{
base.DrawInSurface(surface, info);
var ctx = UIGraphics.GetCurrentContext();
ctx.ClearRect(Bounds);
_irisRenderer.RenderIris(surface, info);
}
}
}
Y WPF:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using SkiaSharp.Views.Desktop;
namespace SkiaSharpDemo.WPF
{
public class IrisView
: SkiaSharp.Views.WPF.SKElement
{
private IrisRenderer _irisRenderer;
public IrisView()
{
Initialize();
}
private void Initialize()
{
_irisRenderer = new IrisRenderer();
Task.Delay(8).ContinueWith((t) => Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)Tick));
}
private DateTime _lastTime = DateTime.Now;
private void Tick()
{
DateTime currTime = DateTime.Now;
if (currTime - _lastTime < TimeSpan.FromMilliseconds(16))
{
Task.Delay(8).ContinueWith((t) => Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)Tick));
return;
}
_lastTime = currTime;
InvalidateVisual();
Task.Delay(8).ContinueWith((t) => Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)Tick));
}
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
base.OnPaintSurface(e);
_irisRenderer.RenderIris(e.Surface, e.Info);
}
}
}
And UWP:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using Windows.UI.Core;
using SkiaSharp.Views.UWP;
namespace SkiaSharpDemo.UWP
{
public class IrisView
: SkiaSharp.Views.UWP.SKXamlCanvas
{
private IrisRenderer _irisRenderer;
public IrisView()
{
Initialize();
}
private void Initialize()
{
_irisRenderer = new IrisRenderer();
Task.Delay(8).ContinueWith((t) => Dispatcher.RunAsync(CoreDispatcherPriority.Normal, Tick));
}
private DateTime _lastTime = DateTime.Now;
private void Tick()
{
DateTime currTime = DateTime.Now;
if (currTime - _lastTime < TimeSpan.FromMilliseconds(16))
{
Task.Delay(8).ContinueWith((t) => Dispatcher.RunAsync(CoreDispatcherPriority.Normal, Tick));
return;
}
_lastTime = currTime;
Invalidate();
Task.Delay(8).ContinueWith((t) => Dispatcher.RunAsync(CoreDispatcherPriority.Normal, Tick));
}
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
base.OnPaintSurface(e);
_irisRenderer.RenderIris(e.Surface, e.Info);
}
}
}
Ahora, podemos ejecutar cada uno de estos y observar exactamente el mismo comportamiento de renderizado. Si aún no entiendes por qué esto es tan increíble, vamos a complicar las cosas considerablemente, ¿de acuerdo? Actualiza tu IrisRenderer con este contenido:
using SkiaSharp;
using System;
using System.Collections.Generic;
namespace SkiaSharpDemo
{
public class IrisArc
{
public float CenterX { get; set; }
public float CenterY { get; set; }
public bool AreCogsOutward { get; set; }
public int NumLevels { get; set; }
public float BaseHue { get; set; }
public float BaseLightness { get; set; }
public float BaseSaturation { get; set; }
public float Radius { get; set; }
public float Span { get; set; }
public List<Tuple<float, int>> Shape { get; set; }
public float MinTransitionLength { get; set; }
public float MaxTransitionLength { get; set; }
public float RotationAngle { get; set; }
public float Opacity { get; set; }
public bool IsClockwise { get; internal set; }
public IrisArc()
{
CenterX = .5f;
CenterY = .5f;
AreCogsOutward = true;
NumLevels = 3;
BaseHue = 220;
BaseLightness = 50;
BaseSaturation = 50;
Radius = .75f;
Span = .2f;
Shape = new List<Tuple<float, int>>();
MinTransitionLength = 6;
MaxTransitionLength = 10;
RotationAngle = 0;
Opacity = .8f;
GenerateShape();
}
private static Random _rand = new Random();
private void GenerateShape()
{
float currentAngle = 0.0f;
int currentLevel = 1 + (int)Math.Round(_rand.NextDouble() * this.NumLevels);
float degreeChange = 0.0f;
while (currentAngle <= 360)
{
AddToShape(currentAngle, currentLevel);
if (currentAngle >= 360)
{
break;
}
degreeChange = (float)Math.Round(MinTransitionLength + _rand.NextDouble() *
MaxTransitionLength);
if (currentAngle + degreeChange > 360)
{
degreeChange = 360 - currentAngle;
}
currentAngle = currentAngle + degreeChange;
}
}
private void AddToShape(float currentAngle, int currentLevel)
{
bool isUp = true;
int changeAmount;
int maxLevels = NumLevels + 1;
if (currentLevel == maxLevels)
{
isUp = false;
}
else
{
if (_rand.NextDouble() > .5)
{
isUp = false;
}
}
if (isUp)
{
changeAmount = (int)Math.Round(1.0 + _rand.NextDouble() * (maxLevels - currentLevel));
currentLevel = currentLevel + changeAmount;
if (currentLevel > this.NumLevels)
{
currentLevel = this.NumLevels;
}
}
else
{
changeAmount = (int)Math.Round(1.0 + _rand.NextDouble() * (currentLevel - 1));
currentLevel = currentLevel - changeAmount;
if (currentLevel < 1)
{
currentLevel = 1;
}
}
this.Shape.Add(new Tuple<float, int>(currentAngle * (float)Math.PI / 180.0f, currentLevel));
}
public void Render(SKSurface surface, SKImageInfo info)
{
float centerX = CenterX;
float centerY = CenterY;
float minRadius = Radius - Span / 2.0f;
float maxRadius = Radius + Span / 2.0f;
var context = surface.Canvas;
centerX = info.Width * centerX;
centerY = info.Height * centerY;
float rad = (float)Math.Min(info.Width, info.Height) / 2.0f;
minRadius = minRadius * rad;
maxRadius = maxRadius * rad;
List<float> radii = new List<float>();
List<float> oldRadii;
Tuple<float, int> currentItem;
float lastAngle;
float angleDelta;
int currentRadius;
float currentAngle;
for (var i = 0; i < NumLevels + 1; i++)
{
radii.Add(minRadius + (maxRadius - minRadius) * i / (NumLevels));
}
if (!AreCogsOutward)
{
oldRadii = radii;
radii = new List<float>();
for (var j = oldRadii.Count - 1; j >= 0; j--)
{
radii.Add(oldRadii[j]);
}
}
context.Save();
context.Translate(centerX, centerY);
context.RotateDegrees(RotationAngle);
context.Translate(-centerX, -centerY);
SKPath path = new SKPath();
SKColor c = SKColor.FromHsl(
BaseHue,
BaseSaturation,
BaseLightness,
(byte)Math.Round(Opacity * 255.0));
SKPaint p = new SKPaint();
p.IsAntialias = true;
p.IsStroke = false;
p.Color = c;
if (!AreCogsOutward)
{
path.MoveTo(radii[0] + centerX, 0 + centerY);
SKRect r = new SKRect(centerX - radii[0], centerY - radii[0], centerX + radii[0], centerY + radii[0]);
path.ArcTo(r, 360, -180, false);
path.ArcTo(r, 180, -180, false);
path.Close();
}
currentRadius = this.Shape[0].Item2;
lastAngle = 0;
path.MoveTo(radii[currentRadius] + centerX, 0 + centerY);
for (var i = 1; i < this.Shape.Count; i++)
{
currentItem = this.Shape[i];
currentAngle = currentItem.Item1;
currentRadius = currentItem.Item2;
angleDelta = currentAngle - lastAngle;
path.LineTo(
(float)(centerX + radii[currentRadius] * Math.Cos(lastAngle)),
(float)(centerY + radii[currentRadius] * Math.Sin(lastAngle)));
SKRect r = new SKRect(
centerX - radii[currentRadius],
centerY - radii[currentRadius],
centerX + radii[currentRadius],
centerY + radii[currentRadius]);
path.ArcTo(r,
(float)(lastAngle * 180.0 / Math.PI),
(float)((currentAngle - lastAngle) * 180.0 / Math.PI), false);
lastAngle = currentAngle;
}
if (AreCogsOutward)
{
path.Close();
path.MoveTo(radii[0] + centerX, 0 + centerY);
SKRect r = new SKRect(centerX - radii[0], centerY - radii[0], centerX + radii[0], centerY + radii[0]);
path.ArcTo(r, 360, -180, false);
path.ArcTo(r, 180, -180, false);
}
path.Close();
context.DrawPath(path, p);
path.Dispose();
p.Dispose();
context.Restore();
}
}
public class IrisRenderer
{
public IrisRenderer()
{
}
private DateTime _lastRender = DateTime.Now;
private bool _forward = true;
private double _progress = 0;
private double _duration = 5000;
private Random _rand = new Random();
private static double Cubic(double p)
{
return p * p * p;
}
public static double CubicEase(double t)
{
if (t < .5)
{
var fastTime = t * 2.0;
return .5 * Cubic(fastTime);
}
var outFastTime = (1.0 - t) * 2.0;
var y = 1.0 - Cubic(outFastTime);
return .5 * y + .5;
}
private bool _first = true;
public void RenderIris(SKSurface surface, SKImageInfo info)
{
if (_first)
{
_first = false;
_lastRender = DateTime.Now;
}
var currTime = DateTime.Now;
var elapsed = (currTime - _lastRender).TotalMilliseconds;
_lastRender = currTime;
if (_forward)
{
_progress += elapsed / _duration;
}
else
{
_progress -= elapsed / _duration;
}
if (_progress > 1.0)
{
_progress = 1.0;
_forward = false;
_duration = 1000 + 4000 * _rand.NextDouble();
}
if (_progress < 0)
{
_progress = 0;
_forward = true;
_duration = 1000 + 4000 * _rand.NextDouble();
}
var context = surface.Canvas;
context.DrawColor(SKColors.Transparent, SKBlendMode.Clear);
//Determine the center for the circle.
var centerX = info.Width / 2.0f;
var centerY = info.Height / 2.0f;
//Determine the radius for the circle.
var rad = Math.Min(info.Width, info.Height) / 2.0f;
var fromR = 255;
var fromG = 0;
var fromB = 0;
var toR = 0;
var toG = 0;
var toB = 255;
var actualProgress = CubicEase(_progress);
var actualR = (byte)Math.Round(fromR + (double)(toR - fromR) * actualProgress);
var actualG = (byte)Math.Round(fromG + (double)(toG - fromG) * actualProgress);
var actualB = (byte)Math.Round(fromB + (double)(toB - fromB) * actualProgress);
//Create the paint object to fill the circle.
using (SKPaint p = new SKPaint())
{
p.IsStroke = false;
p.IsAntialias = true;
p.Color = new SKColor(actualR, actualG, actualB);
//Fill the circle.
context.DrawCircle(centerX, centerY, rad, p);
};
}
}
}
No entraré en detalles sobre lo que está sucediendo en esta lógica para este artículo, pero si hay interés, puedo desglosarlo en un artículo posterior. Aun así, presento esto a modo de mostrar cómo podemos reutilizar una gran cantidad de lógica compleja en todas las plataformas. Si volvemos a ejecutar nuestras aplicaciones ahora, deberíamos ver una imagen compleja de engranes entrelazados que giran en sentido contrario entre sí:

Y aquí hay un video de él en movimiento:

Ahora, ¿me crees que esto es increíble? Como resultado, podrías estar pensando: "Graham, si SkiaSharp hace que sea tan fácil hacer renderizado de alto rendimiento en todas las plataformas, ¿no sería genial si alguien construyera algunas cosas de interfaz de usuario realmente increíbles que pudiéramos usar en aplicaciones multiplataforma?". Bueno, sí, en realidad, por eso hicimos exactamente eso:

Terminando
Si ha estado siguiendo Infragistics + Xamarin por un tiempo, es posible que sepa que hemos tenido un producto basado en Xamarin durante algún tiempo (¡y que ahora tenemos una nueva versión!). Lo que puede no ser obvio, sin embargo, es que la nueva versión del producto ha sido significativamente rediseñada para tener una API, rendimiento y historia de comportamiento totalmente consistente entre todas las plataformas para la versión 17.0. Las versiones anteriores de nuestro producto Xamarin eran una fina capa sobre nuestros productos nativos de Android e iOS. Esto solo fue posible debido al hecho de que nuestras API móviles nativas eran lo suficientemente similares entre sí. Sin embargo, en términos de búsqueda de la máxima consistencia, las API no eran lo suficientemente consistentes entre sí (y para algunos componentes eran completamente divergentes), lo que hizo que esta estrategia fuera más costosa y limitante de lo deseado. Eso, y mientras hacíamos magia negra incalculable bajo las sábanas para que pudiera vincular sus datos basados en .NET directamente con nuestros componentes nativos (decididamente non-.NET) de manera eficiente, estas cosas eran monstruosamente complicadas detrás de escena.
Cuando apareció SkiaSharp, sabíamos que teníamos la oportunidad de volver a concebir el producto como un producto de C# "hasta el final", incluso hasta la capa de representación, y de reenfocar la API (y la lógica subyacente) para que fuera lo más idéntica posible entre Xamarin. Formularios, Xamarin. Android, Xamarin.iOS y, además, ser extremadamente similar a nuestras plataformas XAML de escritorio. Salvo el hecho de que el escritorio tiene algunas características únicas de WPF, hemos hecho universalmente las cosas extremadamente cercanas, de modo que en muchos casos puede simplemente pegar lógica entre plataformas con solo ajustes menores o sin ajustes. Para colmo, cuando utiliza nuestros nuevos componentes de Xamarin, en la mayoría de los casos, ejecuta exactamente la misma lógica que en nuestros populares productos WPF de escritorio. Estamos muy orgullosos del trabajo que hemos hecho y esperamos que os deleite. ¡Cuéntanos!
-Graham