{"id":545,"date":"2017-04-10T17:09:00","date_gmt":"2017-04-10T17:09:00","guid":{"rendered":"https:\/\/staging.infragistics.com\/blogs\/?p=545"},"modified":"2025-02-25T13:28:43","modified_gmt":"2025-02-25T13:28:43","slug":"using-c-and-skiasharp","status":"publish","type":"post","link":"https:\/\/www.infragistics.com\/blogs\/using-c-and-skiasharp","title":{"rendered":"Using C#, Xamarin and SkiaSharp to Delight and Amaze (Across Platforms)"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\" class=\"wp-block-heading\" id=\"what-is-skiasharp\">What is SkiaSharp?<\/h2>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/users.infragistics.com\/gmurray\/Blogs\/Images\/SkiaSharp\/iris.png\" alt=\"SkiaSharp\" title=\"SkiaSharp\"\/><\/figure>\n\n\n\n<p>Tools like <a href=\"https:\/\/www.xamarin.com\/\" target=\"_blank\" rel=\"noopener noreferrer\">Xamarin<\/a> help you, at least, use a common programming language across all important platforms, but while that\u2019s great for helping you share the business logic for your application, it doesn\u2019t automatically make it easy to share UI logic.<\/p>\n\n\n\n<p>Xamarin\/Microsoft provide multiple tools to help share your UI implementation across different platforms. They created <a href=\"https:\/\/www.xamarin.com\/forms\" target=\"_blank\" rel=\"noopener noreferrer\">Xamarin.Forms<\/a> to help you abstractly define UI views for your application once, and then reuse them across supported platforms. Xamarin.Forms is <em>very cool<\/em>, at least we think so, which is why we have a <a href=\"\/products\/xamarin\">new product<\/a> available to make sure you can do <em>even more awesome things<\/em> with Xamarin.Forms. But what if there\u2019s still something that Forms can\u2019t do, but you still need? Wouldn\u2019t it be great to render custom graphics across platforms in C#?<\/p>\n\n\n\n<p>Well, I wanted to do just this, a few years ago, but the challenge was that there was no cross platform C# 2D API available across all important platforms at the time. So, I created an abstraction around the various native 2D rendering APIs to let me write some rendering logic once, and have some abstraction layers convert it to the correct sequence of calls to the various native rendering APIs. What I found, though, is that there winds up being lots of interesting overhead using this path. My logic would build some 2D graphics primitives in C# that would then need to be represented, on Android, as Java objects, and then those Java objects would need to be represented as native classes down in the native rendering layer of Android, and so on and so forth for each platform. Having this many layers of abstraction was causing some not insignificant overhead when combined with the chattiness that you have using a 2D rendering API and complex 2D graphics.<\/p>\n\n\n\n<p>Well, Xamarin themselves must felt this same pain, because it lead them to create <a href=\"https:\/\/github.com\/mono\/SkiaSharp\" target=\"_blank\" rel=\"noopener noreferrer\">SkiaSharp<\/a>. SkiaSharp is a cross platform 2D graphics rendering API which you can use across a host of different platforms, including Android\/iOS platforms via Xamarin. SkiaSharp is some C# bindings directly around the C native API for the <a href=\"https:\/\/skia.org\/\" target=\"_blank\" rel=\"noopener noreferrer\">Skia<\/a> rendering library. Skia is a fast, open source rendering library that is heavily used in Android and the Chrome browser and numerous other high profile projects. With SkiaSharp in hand, you can do fast 2D graphics rendering across <em>lots<\/em> of platforms with comparatively little overhead in your interaction with the API since the C# API is able to talk directly to the Skia native library. In this article, I\u2019ll lead you through how you can get started with the API.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" class=\"wp-block-heading\" id=\"getting-started\">Getting Started<\/h2>\n\n\n\n<p>To start with, open Visual Studio and create a new cross platform project. This is one of the templates that Xamarin installs into Visual Studio.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/users.infragistics.com\/gmurray\/Blogs\/Images\/SkiaSharp\/CrossPlatformApp.png\" alt=\" This is one of the templates that Xamarin installs into VS\" title=\" This is one of the templates that Xamarin installs into VS\"\/><\/figure>\n\n\n\n<p>This can be made to be a Xamarin.Forms app, or a native app, depending on your preference. I just created a native app for the purposes of this demo, sharing code via a PCL (portable class library), rather than a shared project. This will create a host of default platforms associated with the template, and you can add additional projects, provided they also support SkiaSharp, as desired.<\/p>\n\n\n\n<p>When I selected a native cross platform app, it created a PCL project to share code between platforms, a Xamarin.Android project, and a Xamarin.iOS project, but I then proceeded to also add a WPF project and a UWP project into the mix, since both of these platforms also support SkiaSharp for rendering. I then added references to the PCL project to the two new projects I added to the solution.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/users.infragistics.com\/gmurray\/Blogs\/Images\/SkiaSharp\/AddReference.png\" alt=\" then added references to the PCL project to the two new projects I added to the solution\" title=\"then added references to the PCL project to the two new projects I added to the solution\"\/><\/figure>\n\n\n\n<p>Next, you need to add some NuGet packages to the solution. If you right click on the solution node and select \u201cManage Nuget Packages for the Solution\u201d you should then be able to search online for and install SkiaSharp and SkiaSharp.Views into all projects in the solution. SkiaSharp.Views has some helper classes that help you bootstrap the rendering of SkiaSharp into a native UI view on your platform of choice, saving you some boilerplate logic. SkiaSharp.Views should be installed for all projects except for the PCL, which it provides no utility to (as it isn\u2019t bound to a particular UI platform).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" class=\"wp-block-heading\" id=\"our-goal\">Our Goal<\/h2>\n\n\n\n<p>We\u2019ll start with something simple, by rendering a simple circle, but then we\u2019ll move onto something considerably more complicated. One of our older samples browsers had a neat animated iris effect:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/users.infragistics.com\/gmurray\/Blogs\/Images\/SkiaSharp\/nucliosIris.png\" alt=\" One of our older samples browsers had a neat animated iris effect\" title=\"One of our older samples browsers had a neat animated iris effect\"\/><\/figure>\n\n\n\n<p>Which was performed, I gather, by overlaying a bunch of images and then rotating them in different directions. In that case the images were static, but I was curious if you could, alternatively, simply put all the logic into a view and render it all dynamically. Let\u2019s move toward that goal.<\/p>\n\n\n\n<p>We\u2019ll start by leveraging SkiaSharp.Views to create a component called <strong>IrisView<\/strong> for Android. We\u2019ll later extend this further and then fill out the details for the other platforms.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\n \nusing Android.App;\nusing Android.Content;\nusing Android.OS;\nusing Android.Runtime;\nusing Android.Views;\nusing Android.Widget;\nusing SkiaSharp;\nusing Android.Util;\n \nnamespace SkiaSharpDemo.Droid\n{\n    public class IrisView\n        : SkiaSharp.Views.Android.SKCanvasView\n    {\n        private Handler _handler;\n \n        private void Initialize()\n        {\n            _handler = new Handler(Context.MainLooper);\n        }\n \n        public IrisView(Context context)\n            : base(context)\n        {\n            Initialize();\n        }\n \n        public IrisView(Context context, IAttributeSet attrs)\n            : base(context, attrs)\n        {\n            Initialize();\n        }\n        public IrisView(Context context, IAttributeSet attrs, int defStyleAttr)\n            : base(context, attrs)\n        {\n            Initialize();\n        }\n        protected IrisView(IntPtr javaReference, JniHandleOwnership transfer)\n            : base(javaReference, transfer)\n        {\n            Initialize();\n        }\n \n        protected override void OnDraw(SKSurface surface, SKImageInfo info)\n        {\n            base.OnDraw(surface, info);\n \n            \/\/Get the canvas from the skia surface.\n            var context = surface.Canvas;\n \n            \/\/Clear out the current content for the canvas.\n            context.DrawColor(SKColors.Transparent, SKBlendMode.Clear);\n \n            \/\/Determine the center for the circle.\n            var centerX = info.Width \/ 2.0f;\n            var centerY = info.Height \/ 2.0f;\n \n            \/\/Determine the radius for the circle.\n            var rad = Math.Min(info.Width, info.Height) \/ 2.0f;\n \n            \/\/Create the paint object to fill the circle.\n            using (SKPaint p = new SKPaint())\n            {\n                p.IsStroke = false;\n                p.IsAntialias = true;\n                p.Color = new SKColor(255, 0, 0);\n                \/\/Fill the circle.\n                context.DrawCircle(centerX, centerY, rad, p);\n            };\n        }\n    }\n}<\/pre>\n\n\n\n<p>Most of this code is boilerplate logic to extend one of the views that SkiaSharp.Views provides to be able to render content using SkiaSharp within a normal Android view. If we focus on the logic that does the painting:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/Get the canvas from the skia surface.\nvar context = surface.Canvas;\n \n\/\/Clear out the current content for the canvas.\ncontext.DrawColor(SKColors.Transparent, SKBlendMode.Clear);\n \n\/\/Determine the center for the circle.\nvar centerX = info.Width \/ 2.0f;\nvar centerY = info.Height \/ 2.0f;\n \n\/\/Determine the radius for the circle.\nvar rad = Math.Min(info.Width, info.Height) \/ 2.0f;\n \n\/\/Create the paint object to fill the circle.\nusing (SKPaint p = new SKPaint())\n{\n    p.IsStroke = false;\n    p.IsAntialias = true;\n    p.Color = new SKColor(255, 0, 0);\n    \/\/Fill the circle.\n    context.DrawCircle(centerX, centerY, rad, p);\n};<\/pre>\n\n\n\n<p>Here we:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Obtain the Skia Canvas to paint into.<\/li>\n\n\n\n<li>Clear the initial color displayed in the canvas (wiping away any prior rendering).<\/li>\n\n\n\n<li>Determine the center of the view and the radius of a circle we can draw into it.<\/li>\n\n\n\n<li>Create a Skia Paint object that will fill with red, using anti-aliasing.<\/li>\n\n\n\n<li>Draw the circle into the canvas using the configured paint object.<\/li>\n<\/ul>\n\n\n\n<p>Then, back in our main activity, if you add this IrisView to the layout, you should see something like this:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/users.infragistics.com\/gmurray\/Blogs\/Images\/SkiaSharp\/RedCircle.png\" alt=\" back in our main activity, if you add this IrisView to the layout, you should see something like this\" title=\"back in our main activity, if you add this IrisView to the layout, you should see something like this\"\/><\/figure>\n\n\n\n<p>Ok, this is neat, but, obviously, if the rendering logic is in the native Android view, we can\u2019t share it between platforms, right? So let\u2019s refactor this a bit. Up in the PCL, we create a class called <strong>IrisRenderer.cs<\/strong> with this content:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">using SkiaSharp;\nusing System;\nusing System.Collections.Generic;\n \nnamespace SkiaSharpDemo\n{\n    public class IrisRenderer\n    {\n        public IrisRenderer()\n        {\n \n        }\n \n        private DateTime _lastRender = DateTime.Now;\n        private bool _forward = true;\n        private double _progress = 0;\n        private double _duration = 5000;\n        private Random _rand = new Random();\n \n        private static double Cubic(double p)\n        {\n            return p * p * p;\n        }\n \n        public static double CubicEase(double t)\n        {\n            if (t &lt; .5)\n            {\n                var fastTime = t * 2.0;\n                return .5 * Cubic(fastTime);\n            }\n \n            var outFastTime = (1.0 - t) * 2.0;\n            var y = 1.0 - Cubic(outFastTime);\n            return .5 * y + .5;\n        }\n \n        private bool _first = true;\n        public void RenderIris(SKSurface surface, SKImageInfo info)\n        {\n            if (_first)\n            {\n                _first = false;\n                _lastRender = DateTime.Now;\n            }\n            var currTime = DateTime.Now;\n            var elapsed = (currTime - _lastRender).TotalMilliseconds;\n \n            _lastRender = currTime;\n \n            if (_forward)\n            {\n                _progress += elapsed \/ _duration;\n            }\n            else\n            {\n                _progress -= elapsed \/ _duration;\n            }\n            if (_progress > 1.0)\n            {\n                _progress = 1.0;\n                _forward = false;\n                _duration = 1000 + 4000 * _rand.NextDouble();\n            }\n            if (_progress &lt; 0)\n            {\n                _progress = 0;\n                _forward = true;\n                _duration = 1000 + 4000 * _rand.NextDouble();\n            }\n \n            var context = surface.Canvas;\n            context.DrawColor(SKColors.Transparent, SKBlendMode.Clear);\n \n            \/\/Determine the center for the circle.\n            var centerX = info.Width \/ 2.0f;\n            var centerY = info.Height \/ 2.0f;\n \n            \/\/Determine the radius for the circle.\n            var rad = Math.Min(info.Width, info.Height) \/ 2.0f;\n \n            var fromR = 255;\n            var fromG = 0;\n            var fromB = 0;\n \n            var toR = 0;\n            var toG = 0;\n            var toB = 255;\n \n            var actualProgress = CubicEase(_progress);\n            var actualR = (byte)Math.Round(fromR + (double)(toR - fromR) * actualProgress);\n            var actualG = (byte)Math.Round(fromG + (double)(toG - fromG) * actualProgress);\n            var actualB = (byte)Math.Round(fromB + (double)(toB - fromB) * actualProgress);\n \n            \/\/Create the paint object to fill the circle.\n            using (SKPaint p = new SKPaint())\n            {\n                p.IsStroke = false;\n                p.IsAntialias = true;\n                p.Color = new SKColor(actualR, actualG, actualB);\n                \/\/Fill the circle.\n                context.DrawCircle(centerX, centerY, rad, p);\n            };\n        }\n    }\n}<\/pre>\n\n\n\n<p>And we amend the <strong>IrisView<\/strong> for Android to look like this:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\n \nusing Android.App;\nusing Android.Content;\nusing Android.OS;\nusing Android.Runtime;\nusing Android.Views;\nusing Android.Widget;\nusing SkiaSharp;\nusing Android.Util;\n \nnamespace SkiaSharpDemo.Droid\n{\n    public class IrisView\n        : SkiaSharp.Views.Android.SKCanvasView\n    {\n        private IrisRenderer _irisRenderer;\n        private Handler _handler;\n \n        private void Initialize()\n        {\n            \/\/The IrisRenderer will perform the actual rendering logic for this view.\n            _irisRenderer = new IrisRenderer();\n            _handler = new Handler(Context.MainLooper);\n            \/\/This starts a tick loop that we will use later for animation.\n            _handler.Post(Tick);\n        }\n \n        private DateTime _lastTime = DateTime.Now;\n        private void Tick()\n        {\n            DateTime currTime = DateTime.Now;\n            \/\/Don't render new frames too often.\n            if (currTime - _lastTime &lt; TimeSpan.FromMilliseconds(16))\n            {\n                _handler.Post(Tick);\n                return;\n            }\n            _lastTime = currTime;\n            Invalidate();\n            _handler.Post(Tick);\n        }\n \n        public IrisView(Context context)\n            : base(context)\n        {\n            Initialize();\n        }\n \n        public IrisView(Context context, IAttributeSet attrs)\n            : base(context, attrs)\n        {\n            Initialize();\n        }\n        public IrisView(Context context, IAttributeSet attrs, int defStyleAttr)\n            : base(context, attrs)\n        {\n            Initialize();\n        }\n        protected IrisView(IntPtr javaReference, JniHandleOwnership transfer)\n            : base(javaReference, transfer)\n        {\n            Initialize();\n        }\n \n        protected override void OnDraw(SKSurface surface, SKImageInfo info)\n        {\n            base.OnDraw(surface, info);\n \n            _irisRenderer.RenderIris(surface, info);\n        }\n    }\n}<\/pre>\n\n\n\n<p>In this way, we\u2019ve factored all of the rendering logic into a shared class that sits in the PCL, which can be shared between all the platforms we want to target. We only need to code it once and it\u2019s done. Additionally, we\u2019ve added a primitive animation system which will keep invalidating the view and repainting on an interval so that our renderer can analyze the elapsed time and animate changes using linear interpolation (eased with a cubic easing function). Pretty cool, huh? Here\u2019s what the circle looks like during an animation between blue and red:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/users.infragistics.com\/gmurray\/Blogs\/Images\/SkiaSharp\/PurpleCircle.png\" alt=\" \"\/><\/figure>\n\n\n\n<p>Ok, at this point, we can fill out the rest of the implementations for <strong>IrisView<\/strong>. These need to be separate classes because each platform has different requirements in terms as to what constitutes a UI view, and different mechanisms that we can use to drive the animation loop, but the idea is to minimize the content of these classes to contain only platform specific behaviors. We also have the option of building additional abstractions (for example, one around animation) that would further reduce the logic in these classes. Here\u2019s the version of the view for iOS:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\n \nusing Foundation;\nusing SkiaSharp;\nusing UIKit;\nusing CoreGraphics;\nusing CoreFoundation;\n \nnamespace SkiaSharpDemo.iOS\n{\n    public class IrisView\n        : SkiaSharp.Views.iOS.SKCanvasView\n    {\n        private IrisRenderer _irisRenderer;\n \n        public IrisView()\n            : base()\n        {\n            Initialize();\n        }\n        public IrisView(CGRect frame)\n            : base(frame)\n        {\n            Initialize();\n        }\n        public IrisView(IntPtr p)\n            : base(p)\n        {\n            Initialize();\n        }\n \n        private void Initialize()\n        {\n            \n            BackgroundColor = UIColor.Clear;\n            _irisRenderer = new IrisRenderer();\n            DispatchQueue.MainQueue.DispatchAsync(Tick);\n        }\n \n        private DateTime _lastTime = DateTime.Now;\n        private void Tick()\n        {\n            DateTime currTime = DateTime.Now;\n            if (currTime - _lastTime &lt; TimeSpan.FromMilliseconds(16))\n            {\n                DispatchQueue.MainQueue.DispatchAsync(Tick);\n                return;\n            }\n            _lastTime = currTime;\n            SetNeedsDisplay();\n            DispatchQueue.MainQueue.DispatchAsync(Tick);\n        }\n \n        public override void DrawInSurface(SKSurface surface, SKImageInfo info)\n        {\n            base.DrawInSurface(surface, info);\n \n            var ctx = UIGraphics.GetCurrentContext();\n            ctx.ClearRect(Bounds);\n \n            _irisRenderer.RenderIris(surface, info);\n        }\n    }\n}<\/pre>\n\n\n\n<p>And WPF:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\nusing System.Windows;\nusing SkiaSharp.Views.Desktop;\n \nnamespace SkiaSharpDemo.WPF\n{\n    public class IrisView\n        : SkiaSharp.Views.WPF.SKElement\n    {\n        private IrisRenderer _irisRenderer;\n \n        public IrisView()\n        {\n            Initialize();\n        }\n \n        private void Initialize()\n        {\n            _irisRenderer = new IrisRenderer();\n            Task.Delay(8).ContinueWith((t) => Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)Tick));\n        }\n \n \n        private DateTime _lastTime = DateTime.Now;\n        private void Tick()\n        {\n            DateTime currTime = DateTime.Now;\n            if (currTime - _lastTime &lt; TimeSpan.FromMilliseconds(16))\n            {\n                Task.Delay(8).ContinueWith((t) => Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)Tick));\n                return;\n            }\n            _lastTime = currTime;\n            InvalidateVisual();\n            Task.Delay(8).ContinueWith((t) => Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)Tick));\n        }\n \n        protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)\n        {\n            base.OnPaintSurface(e);\n \n            _irisRenderer.RenderIris(e.Surface, e.Info);\n        }\n    }\n}<\/pre>\n\n\n\n<p>And UWP:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\nusing System.Windows;\nusing Windows.UI.Core;\nusing SkiaSharp.Views.UWP;\n \nnamespace SkiaSharpDemo.UWP\n{\n    public class IrisView\n        : SkiaSharp.Views.UWP.SKXamlCanvas\n    {\n        private IrisRenderer _irisRenderer;\n \n        public IrisView()\n        {\n            Initialize();\n        }\n \n        private void Initialize()\n        {\n            _irisRenderer = new IrisRenderer();\n            Task.Delay(8).ContinueWith((t) => Dispatcher.RunAsync(CoreDispatcherPriority.Normal, Tick));\n        }\n \n \n        private DateTime _lastTime = DateTime.Now;\n        private void Tick()\n        {\n            DateTime currTime = DateTime.Now;\n            if (currTime - _lastTime &lt; TimeSpan.FromMilliseconds(16))\n            {\n                Task.Delay(8).ContinueWith((t) => Dispatcher.RunAsync(CoreDispatcherPriority.Normal, Tick));\n                return;\n            }\n            _lastTime = currTime;\n            Invalidate();\n            Task.Delay(8).ContinueWith((t) => Dispatcher.RunAsync(CoreDispatcherPriority.Normal, Tick));\n        }\n \n        protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)\n        {\n            base.OnPaintSurface(e);\n \n            _irisRenderer.RenderIris(e.Surface, e.Info);\n        }\n    }\n}<\/pre>\n\n\n\n<p>Now, we can run each of these and observe the <strong>exact same<\/strong> rendering behavior! If you don\u2019t yet grasp why this is so awesome, let\u2019s make things considerably more complicated, shall we? Update your<strong> IrisRenderer<\/strong> with this content:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">using SkiaSharp;\nusing System;\nusing System.Collections.Generic;\n \nnamespace SkiaSharpDemo\n{\n    public class IrisArc\n    {\n        public float CenterX { get; set; }\n        public float CenterY { get; set; }\n        public bool AreCogsOutward { get; set; }\n        public int NumLevels { get; set; }\n        public float BaseHue { get; set; }\n        public float BaseLightness { get; set; }\n        public float BaseSaturation { get; set; }\n        public float Radius { get; set; }\n        public float Span { get; set; }\n        public List&lt;Tuple&lt;float, int>> Shape { get; set; }\n        public float MinTransitionLength { get; set; }\n        public float MaxTransitionLength { get; set; }\n        public float RotationAngle { get; set; }\n        public float Opacity { get; set; }\n        public bool IsClockwise { get; internal set; }\n \n        public IrisArc()\n        {\n            CenterX = .5f;\n            CenterY = .5f;\n            AreCogsOutward = true;\n            NumLevels = 3;\n            BaseHue = 220;\n            BaseLightness = 50;\n            BaseSaturation = 50;\n            Radius = .75f;\n            Span = .2f;\n            Shape = new List&lt;Tuple&lt;float, int>>();\n            MinTransitionLength = 6;\n            MaxTransitionLength = 10;\n            RotationAngle = 0;\n            Opacity = .8f;\n            GenerateShape();\n        }\n \n        private static Random _rand = new Random();\n \n        private void GenerateShape()\n        {\n            float currentAngle = 0.0f;\n            int currentLevel = 1 + (int)Math.Round(_rand.NextDouble() * this.NumLevels);\n            float degreeChange = 0.0f;\n \n            while (currentAngle &lt;= 360)\n            {\n                AddToShape(currentAngle, currentLevel);\n                if (currentAngle >= 360)\n                {\n                    break;\n                }\n                degreeChange = (float)Math.Round(MinTransitionLength + _rand.NextDouble() *\n                    MaxTransitionLength);\n \n                if (currentAngle + degreeChange > 360)\n                {\n                    degreeChange = 360 - currentAngle;\n                }\n \n                currentAngle = currentAngle + degreeChange;\n            }\n        }\n        private void AddToShape(float currentAngle, int currentLevel)\n        {\n            bool isUp = true;\n            int changeAmount;\n            int maxLevels = NumLevels + 1;\n \n            if (currentLevel == maxLevels)\n            {\n                isUp = false;\n            }\n            else\n            {\n                if (_rand.NextDouble() > .5)\n                {\n                    isUp = false;\n                }\n            }\n \n            if (isUp)\n            {\n                changeAmount = (int)Math.Round(1.0 + _rand.NextDouble() * (maxLevels - currentLevel));\n                currentLevel = currentLevel + changeAmount;\n \n                if (currentLevel > this.NumLevels)\n                {\n                    currentLevel = this.NumLevels;\n                }\n            }\n            else\n            {\n                changeAmount = (int)Math.Round(1.0 + _rand.NextDouble() * (currentLevel - 1));\n                currentLevel = currentLevel - changeAmount;\n \n                if (currentLevel &lt; 1)\n                {\n                    currentLevel = 1;\n                }\n            }\n \n            this.Shape.Add(new Tuple&lt;float, int>(currentAngle * (float)Math.PI \/ 180.0f, currentLevel));\n        }\n \n        public void Render(SKSurface surface, SKImageInfo info)\n        {            \n            float centerX = CenterX;\n            float centerY = CenterY;\n            float minRadius = Radius - Span \/ 2.0f;\n            float maxRadius = Radius + Span \/ 2.0f;\n \n            var context = surface.Canvas;\n            centerX = info.Width * centerX;\n            centerY = info.Height * centerY;\n \n            float rad = (float)Math.Min(info.Width, info.Height) \/ 2.0f;\n \n            minRadius = minRadius * rad;\n            maxRadius = maxRadius * rad;\n \n            List&lt;float> radii = new List&lt;float>();\n            List&lt;float> oldRadii;\n            Tuple&lt;float, int> currentItem;\n            float lastAngle;\n            float angleDelta;\n            int currentRadius;\n            float currentAngle;\n \n            for (var i = 0; i &lt; NumLevels + 1; i++)\n            {\n                radii.Add(minRadius + (maxRadius - minRadius) * i \/ (NumLevels));\n            }\n            if (!AreCogsOutward)\n            {\n                oldRadii = radii;\n                radii = new List&lt;float>();\n                for (var j = oldRadii.Count - 1; j >= 0; j--)\n                {\n                    radii.Add(oldRadii[j]);\n                }\n            }\n \n \n            context.Save();\n            context.Translate(centerX, centerY);\n            context.RotateDegrees(RotationAngle);\n            context.Translate(-centerX, -centerY);\n \n            SKPath path = new SKPath();\n            SKColor c = SKColor.FromHsl(\n                BaseHue,\n                BaseSaturation,\n                BaseLightness,\n                (byte)Math.Round(Opacity * 255.0));\n            SKPaint p = new SKPaint();\n            p.IsAntialias = true;\n            p.IsStroke = false;\n            p.Color = c;\n \n \n            if (!AreCogsOutward)\n            {\n                path.MoveTo(radii[0] + centerX, 0 + centerY);\n \n                SKRect r = new SKRect(centerX - radii[0], centerY - radii[0], centerX + radii[0], centerY + radii[0]);\n \n                path.ArcTo(r, 360, -180, false);\n                path.ArcTo(r, 180, -180, false);\n                path.Close();\n            }\n \n            currentRadius = this.Shape[0].Item2;\n            lastAngle = 0;\n            path.MoveTo(radii[currentRadius] + centerX, 0 + centerY);\n            for (var i = 1; i &lt; this.Shape.Count; i++)\n            {\n                currentItem = this.Shape[i];\n                currentAngle = currentItem.Item1;\n                currentRadius = currentItem.Item2;\n \n                angleDelta = currentAngle - lastAngle;\n \n                path.LineTo(\n                    (float)(centerX + radii[currentRadius] * Math.Cos(lastAngle)),\n                    (float)(centerY + radii[currentRadius] * Math.Sin(lastAngle)));\n \n                SKRect r = new SKRect(\n                    centerX - radii[currentRadius],\n                    centerY - radii[currentRadius],\n                    centerX + radii[currentRadius],\n                    centerY + radii[currentRadius]);\n \n \n                path.ArcTo(r,\n                            (float)(lastAngle * 180.0 \/ Math.PI),\n                            (float)((currentAngle - lastAngle) * 180.0 \/ Math.PI), false);\n                lastAngle = currentAngle;\n            }\n \n            if (AreCogsOutward)\n            {\n                path.Close();\n                path.MoveTo(radii[0] + centerX, 0 + centerY);\n                SKRect r = new SKRect(centerX - radii[0], centerY - radii[0], centerX + radii[0], centerY + radii[0]);\n \n                path.ArcTo(r, 360, -180, false);\n                path.ArcTo(r, 180, -180, false);\n            }\n            path.Close();\n \n            context.DrawPath(path, p);\n            path.Dispose();\n            p.Dispose();\n            context.Restore();\n        }\n    }\n \n    public class IrisRenderer\n    {\n        public IrisRenderer()\n        {\n \n        }\n \n        private DateTime _lastRender = DateTime.Now;\n        private bool _forward = true;\n        private double _progress = 0;\n        private double _duration = 5000;\n        private Random _rand = new Random();\n \n        private static double Cubic(double p)\n        {\n            return p * p * p;\n        }\n \n        public static double CubicEase(double t)\n        {\n            if (t &lt; .5)\n            {\n                var fastTime = t * 2.0;\n                return .5 * Cubic(fastTime);\n            }\n \n            var outFastTime = (1.0 - t) * 2.0;\n            var y = 1.0 - Cubic(outFastTime);\n            return .5 * y + .5;\n        }\n \n        private bool _first = true;\n        public void RenderIris(SKSurface surface, SKImageInfo info)\n        {\n            if (_first)\n            {\n                _first = false;\n                _lastRender = DateTime.Now;\n            }\n            var currTime = DateTime.Now;\n            var elapsed = (currTime - _lastRender).TotalMilliseconds;\n \n            _lastRender = currTime;\n \n            if (_forward)\n            {\n                _progress += elapsed \/ _duration;\n            }\n            else\n            {\n                _progress -= elapsed \/ _duration;\n            }\n            if (_progress > 1.0)\n            {\n                _progress = 1.0;\n                _forward = false;\n                _duration = 1000 + 4000 * _rand.NextDouble();\n            }\n            if (_progress &lt; 0)\n            {\n                _progress = 0;\n                _forward = true;\n                _duration = 1000 + 4000 * _rand.NextDouble();\n            }\n \n            var context = surface.Canvas;\n            context.DrawColor(SKColors.Transparent, SKBlendMode.Clear);\n \n            \/\/Determine the center for the circle.\n            var centerX = info.Width \/ 2.0f;\n            var centerY = info.Height \/ 2.0f;\n \n            \/\/Determine the radius for the circle.\n            var rad = Math.Min(info.Width, info.Height) \/ 2.0f;\n \n            var fromR = 255;\n            var fromG = 0;\n            var fromB = 0;\n \n            var toR = 0;\n            var toG = 0;\n            var toB = 255;\n \n            var actualProgress = CubicEase(_progress);\n            var actualR = (byte)Math.Round(fromR + (double)(toR - fromR) * actualProgress);\n            var actualG = (byte)Math.Round(fromG + (double)(toG - fromG) * actualProgress);\n            var actualB = (byte)Math.Round(fromB + (double)(toB - fromB) * actualProgress);\n \n            \/\/Create the paint object to fill the circle.\n            using (SKPaint p = new SKPaint())\n            {\n                p.IsStroke = false;\n                p.IsAntialias = true;\n                p.Color = new SKColor(actualR, actualG, actualB);\n                \/\/Fill the circle.\n                context.DrawCircle(centerX, centerY, rad, p);\n            };\n        }\n    }\n}<\/pre>\n\n\n\n<p>I won\u2019t go into detail as to what is going on in this logic for this article, but if there is interest, I can break it down in a subsequent article. Still, I present this by way of showing how we can reuse a lot of complex logic across platforms. If we rerun our apps now, we should see a complex visual of intermeshing cogs counter rotating against each other:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/users.infragistics.com\/gmurray\/Blogs\/Images\/SkiaSharp\/iris.png\" alt=\" If we rerun our apps now, we should see a complex visual of intermeshing cogs counter rotating against each other\" title=\"If we rerun our apps now, we should see a complex visual of intermeshing cogs counter rotating against each other\"\/><\/figure>\n\n\n\n<p>And here\u2019s a video of it in motion:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/www.youtube.com\/watch?v=Qg6RATIZPgY\" target=\"_blank\" rel=\"noopener noreferrer\"><img decoding=\"async\" src=\"https:\/\/users.infragistics.com\/gmurray\/Blogs\/Images\/SkiaSharp\/IrisVideo.png\" alt=\" And here\u2019s a video of it in motion\" title=\"And here\u2019s a video of it in motion\"\/><\/a><\/figure>\n\n\n\n<p>Now do you believe me that this is <em>awesome<\/em>? As a result, you might be thinking: \u201cGraham, if SkiaSharp makes it so easy to do high performance rendering across platforms, wouldn\u2019t it be neat if someone built some really awesome UI stuff that we could use in cross platform apps?\u201d. Well, yes, actually, which is why we did <a href=\"\/products\/xamarin\">exactly that<\/a>:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"http:\/\/www.infragistics.com\/xamarin\" target=\"_blank\" rel=\"noopener noreferrer\"><img decoding=\"async\" src=\"https:\/\/users.infragistics.com\/gmurray\/Blogs\/Images\/SkiaSharp\/product.png\" alt=\"  really awesome UI stuff that we could use in cross platform apps\" title=\" really awesome UI stuff that we could use in cross platform apps\"\/><\/a><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" class=\"wp-block-heading\" id=\"wrapping-up\">Wrapping Up<\/h2>\n\n\n\n<p>If you have been following Infragistics + Xamarin for a while, you might know we\u2019ve had a Xamarin based product for some time (and that we have a new version <a href=\"\/products\/xamarin\">out now<\/a>!). What may not be obvious, though, is that the new version of the product has been significantly re-engineered to have a totally consistent API, performance, and behavior story between all platforms for the 17.0 release. Previous versions of our Xamarin product were a thin veneer over our native Android and iOS products. This was only feasible due to the fact that our native mobile APIs were similar enough to each other. However, in terms of shooting for maximal consistency, the APIs were not quite consistent enough with each other (and for some components were entirely divergent), which made this strategy more expensive and limiting than desired. That, and while we worked untold black magic under the covers so that you could bind your .NET based data directly against our (decidedly non-.NET) native components efficiently, this stuff was monstrously complicated behind the scenes.<\/p>\n\n\n\n<p>When SkiaSharp came along, we knew we had an opportunity to re-envision the product as a C# product \u201call the way down\u201d even to the rendering layer, and to refocus the API (and underlying logic) to be as identical as possible between Xamarin.Forms, Xamarin.Android, Xamarin.iOS, and, in addition, to be extremely similar to our desktop XAML platforms. Barring the fact that desktop has some unique WPF features, we\u2019ve universally made things extremely close, so that in lots of cases you can just paste logic between platforms with only minor or no adjustments. To top things off, when you use our new Xamarin components, you are, in most cases, running the exact same logic as you are in our popular desktop WPF products. We are very proud of the work we\u2019ve done, and hope it delights you! Let us know!<\/p>\n\n\n\n<p>&nbsp;<\/p>\n\n\n\n<p>-Graham<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Cross platform development can be tricky, especially with mobile platforms involved. You could avoid it entirely by just building a separate app per platform, but that is neither cost effective, nor especially fun. <\/p>\n","protected":false},"author":72,"featured_media":2367,"comment_status":"publish","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[17],"tags":[],"class_list":["post-545","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-how-to"],"_links":{"self":[{"href":"https:\/\/www.infragistics.com\/blogs\/wp-json\/wp\/v2\/posts\/545","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.infragistics.com\/blogs\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.infragistics.com\/blogs\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.infragistics.com\/blogs\/wp-json\/wp\/v2\/users\/72"}],"replies":[{"embeddable":true,"href":"https:\/\/www.infragistics.com\/blogs\/wp-json\/wp\/v2\/comments?post=545"}],"version-history":[{"count":2,"href":"https:\/\/www.infragistics.com\/blogs\/wp-json\/wp\/v2\/posts\/545\/revisions"}],"predecessor-version":[{"id":1693,"href":"https:\/\/www.infragistics.com\/blogs\/wp-json\/wp\/v2\/posts\/545\/revisions\/1693"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.infragistics.com\/blogs\/wp-json\/wp\/v2\/media\/2367"}],"wp:attachment":[{"href":"https:\/\/www.infragistics.com\/blogs\/wp-json\/wp\/v2\/media?parent=545"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.infragistics.com\/blogs\/wp-json\/wp\/v2\/categories?post=545"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.infragistics.com\/blogs\/wp-json\/wp\/v2\/tags?post=545"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}