Usando C#, Xamarin e SkiaSharp para encantar e surpreender (em todas as plataformas)
O desenvolvimento multiplataforma pode ser complicado, especialmente com plataformas móveis envolvidas. Você pode evitá-lo totalmente apenas criando um aplicativo separado por plataforma, mas isso não é econômico nem especialmente divertido.
What is SkiaSharp?

Ferramentas como Xamarin ajudá-lo, pelo menos, a usar uma linguagem de programação comum em todas as plataformas importantes, mas, embora isso seja ótimo para ajudá-lo a compartilhar a lógica de negócios do seu aplicativo, não facilita automaticamente o compartilhamento da lógica da interface do usuário.
Xamarin /Microsoft fornecem várias ferramentas para ajudar a compartilhar sua implementação de interface do usuário em diferentes plataformas. Eles criaram Xamarin. Formulários para ajudá-lo a definir abstratamente as exibições da interface do usuário para seu aplicativo uma vez e, em seguida, reutilizá-las em plataformas compatíveis. Xamarin. O Forms é muito legal, pelo menos achamos que sim, e é por isso que temos um novo produto disponível para garantir que você possa fazer coisas ainda mais incríveis com Xamarin. Forms. Mas e se ainda houver algo que o Forms não pode fazer, mas você ainda precisa? Não seria ótimo renderizar gráficos personalizados entre plataformas em C#?
Bem, eu queria fazer exatamente isso, alguns anos atrás, mas o desafio era que não havia uma API C# 2D multiplataforma disponível em todas as plataformas importantes na época. Então, criei uma abstração em torno das várias APIs nativas de renderização 2D para me permitir escrever alguma lógica de renderização uma vez e fazer com que algumas camadas de abstração a convertessem na sequência correta de chamadas para as várias APIs de renderização nativas. O que descobri, porém, é que acaba havendo muitas sobrecargas interessantes usando esse caminho. Minha lógica criaria alguns primitivos gráficos 2D em C# que precisariam ser representados, no Android, como objetos Java e, em seguida, esses objetos Java precisariam ser representados como classes nativas na camada de renderização nativa do Android, e assim por diante para cada plataforma. Ter tantas camadas de abstração estava causando uma sobrecarga não insignificante quando combinada com a tagarelice que você tem usando uma API de renderização 2D e gráficos 2D complexos.
Bem, Xamarin eles mesmos devem sentir essa mesma dor, porque isso os levou a criar o SkiaSharp. SkiaSharp é uma API de renderização de gráficos 2D multiplataforma que você pode usar em uma série de plataformas diferentes, incluindo plataformas Android / iOS via Xamarin. SkiaSharp são algumas associações C# diretamente em torno da API nativa C para a biblioteca de renderização Skia. Skia é uma biblioteca de renderização rápida e de código aberto que é muito usada no Android e no navegador Chrome e em vários outros projetos de alto perfil. Com o SkiaSharp em mãos, você pode fazer renderização rápida de gráficos 2D em várias plataformas com comparativamente pouca sobrecarga em sua interação com a API, já que a API C# é capaz de se comunicar diretamente com a biblioteca nativa do Skia. Neste artigo, mostrarei como você pode começar a usar a API.
Começando
Para começar, abra o Visual Studio e crie um novo projeto multiplataforma. Esse é um dos modelos que Xamarin instala no Visual Studio.

Isso pode ser feito para ser um aplicativo Xamarin.Forms ou um aplicativo nativo, dependendo de sua preferência. Acabei de criar um aplicativo nativo para os propósitos desta demonstração, compartilhando código por meio de uma PCL (biblioteca de classes portátil), em vez de um projeto compartilhado. Isso criará uma série de plataformas padrão associadas ao modelo e você poderá adicionar projetos adicionais, desde que eles também suportem o SkiaSharp, conforme desejado.
Quando selecionei um aplicativo multiplataforma nativo, ele criou um projeto PCL para compartilhar código entre plataformas, um projeto Xamarin.Android e um projeto Xamarin.iOS, mas também adicionei um projeto WPF e um projeto UWP à mistura, já que ambas as plataformas também dão suporte ao SkiaSharp para renderização. Em seguida, adicionei referências ao projeto PCL aos dois novos projetos que adicionei à solução.

Em seguida, você precisa adicionar alguns pacotes NuGet à solução. Se você clicar com o botão direito do mouse no nó da solução e selecionar "Gerenciar pacotes Nuget para a solução", poderá pesquisar online e instalar o SkiaSharp e o SkiaSharp.Views em todos os projetos da solução. SkiaSharp.Views tem algumas classes auxiliares que ajudam você a inicializar a renderização do SkiaSharp em uma visualização de interface do usuário nativa em sua plataforma de escolha, economizando alguma lógica clichê. SkiaSharp.Views deve ser instalado para todos os projetos, exceto para o PCL, para o qual ele não fornece nenhum utilitário (já que não está vinculado a uma plataforma de interface do usuário específica).
Nosso objetivo
Começaremos com algo simples, renderizando um círculo simples, mas depois passaremos para algo consideravelmente mais complicado. Um de nossos navegadores de amostras mais antigos tinha um efeito de íris animado elegante:

O que foi realizado, eu suponho, sobrepondo um monte de imagens e depois girando-as em direções diferentes. Nesse caso, as imagens eram estáticas, mas eu estava curioso para saber se você poderia, alternativamente, simplesmente colocar toda a lógica em uma visualização e renderizar tudo dinamicamente. Vamos avançar em direção a esse objetivo.
Começaremos aproveitando o SkiaSharp.Views para criar um componente chamado IrisView para Android. Posteriormente, estenderemos isso ainda mais e, em seguida, preencheremos os detalhes das outras 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); }; } } }
A maior parte desse código é lógica clichê para estender uma das visualizações que o SkiaSharp.Views fornece para poder renderizar conteúdo usando o SkiaSharp em uma visualização normal do Android. Se nos concentrarmos na lógica que faz a 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); };
Aqui nós:
- Obtenha a tela Skia para pintar.
- Limpe a cor inicial exibida na tela (limpando qualquer renderização anterior).
- Determine o centro da visão e o raio de um círculo que podemos desenhar nele.
- Crie um objeto Skia Paint que será preenchido com vermelho, usando anti-aliasing.
- Desenhe o círculo na tela usando o objeto de pintura configurado.
Então, de volta à nossa atividade principal, se você adicionar esse IrisView ao layout, deverá ver algo assim:

Ok, isso é legal, mas, obviamente, se a lógica de renderização estiver na visualização nativa do Android, não podemos compartilhá-la entre plataformas, certo? Então, vamos refatorar isso um pouco. Na PCL, criamos uma classe chamada IrisRenderer.cs com este conteúdo:
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); }; } } }
E alteramos o IrisView para Android para ficar assim:
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); } } }
Dessa forma, fatoramos toda a lógica de renderização em uma classe compartilhada que fica na PCL, que pode ser compartilhada entre todas as plataformas que queremos direcionar. Só precisamos codificá-lo uma vez e pronto. Além disso, adicionamos um sistema de animação primitivo que continuará invalidando a visualização e repintando em um intervalo para que nosso renderizador possa analisar o tempo decorrido e animar as alterações usando interpolação linear (facilitada com uma função de atenuação cúbica). Muito legal, hein? Veja como o círculo se parece durante uma animação entre azul e vermelho:

Ok, neste ponto, podemos preencher o restante das implementações para o IrisView. Essas precisam ser classes separadas porque cada plataforma tem requisitos diferentes em termos do que constitui uma exibição de interface do usuário e mecanismos diferentes que podemos usar para conduzir o loop de animação, mas a ideia é minimizar o conteúdo dessas classes para conter apenas comportamentos específicos da plataforma. Também temos a opção de criar abstrações adicionais (por exemplo, uma em torno da animação) que reduziria ainda mais a lógica nessas classes. Aqui está a versão da visualização 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); } } }
E o 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); } } }
Agora, podemos executar cada um deles e observar exatamente o mesmo comportamento de renderização! Se você ainda não entende por que isso é tão incrível, vamos tornar as coisas consideravelmente mais complicadas, certo? Atualize seu IrisRenderer com este conteúdo:
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); }; } } }
Não vou entrar em detalhes sobre o que está acontecendo nessa lógica para este artigo, mas se houver interesse, posso dividi-lo em um artigo subsequente. Ainda assim, apresento isso para mostrar como podemos reutilizar muita lógica complexa entre plataformas. Se executarmos novamente nossos aplicativos agora, veremos um visual complexo de engrenagens entrelaçadas contra-girando umas contra as outras:

E aqui está um vídeo dele em movimento:

Agora você acredita em mim que isso é incrível? Como resultado, você pode estar pensando: "Graham, se o SkiaSharp torna tão fácil fazer renderização de alto desempenho em todas as plataformas, não seria legal se alguém construísse algumas coisas de interface do usuário realmente incríveis que pudéssemos usar em aplicativos multiplataforma?". Bem, sim, na verdade, e é por isso que fizemos exatamente isso:

Conclusão
Se você acompanha o Infragistics + Xamarin há algum tempo, deve saber que temos um produto baseado em Xamarin há algum tempo (e que temos uma nova versão lançada agora!). O que pode não ser óbvio, porém, é que a nova versão do produto foi significativamente reprojetada para ter uma história de API, desempenho e comportamento totalmente consistente entre todas as plataformas para a versão 17.0. As versões anteriores do nosso produto Xamarin eram um verniz fino sobre nossos produtos nativos Android e iOS. Isso só foi viável devido ao fato de que nossas APIs móveis nativas eram semelhantes o suficiente entre si. No entanto, em termos de busca de consistência máxima, as APIs não eram consistentes o suficiente entre si (e para alguns componentes eram totalmente divergentes), o que tornava essa estratégia mais cara e limitante do que o desejado. Isso, e enquanto trabalhávamos com magia negra incalculável nos bastidores para que você pudesse vincular seus dados baseados em .NET diretamente aos nossos componentes nativos (decididamente non-.NET) de forma eficiente, essas coisas eram monstruosamente complicadas nos bastidores.
Quando o SkiaSharp surgiu, sabíamos que tínhamos a oportunidade de repensar o produto como um produto C# "até o fim" até a camada de renderização, e reorientar a API (e a lógica subjacente) para ser o mais idêntico possível entre Xamarin. Formulários, Xamarin. Android, Xamarin.iOS e, além disso, ser extremamente semelhante às nossas plataformas XAML de desktop. Exceto pelo fato de que o desktop tem alguns recursos exclusivos do WPF, universalmente tornamos as coisas extremamente próximas, de modo que, em muitos casos, você pode simplesmente colar a lógica entre as plataformas com apenas pequenos ou nenhum ajuste. Para completar, quando você usa nossos novos componentes Xamarin, você está, na maioria dos casos, executando exatamente a mesma lógica que em nossos populares produtos WPF para desktop. Estamos muito orgulhosos do trabalho que fizemos e esperamos que ele encante você! Deixe-nos saber!
-Graham