コンテンツへスキップ
C#、Xamarin、SkiaSharp を使用して喜ばせ、驚かせる (プラットフォーム間)

C#、Xamarin、SkiaSharp を使用して喜ばせ、驚かせる (プラットフォーム間)

クロスプラットフォーム開発は、特にモバイルプラットフォームが関係する場合、注意が必要です。プラットフォームごとに別々のアプリを構築するだけで、それを完全に回避することもできますが、それは費用対効果が高くなく、特に楽しいものでもありません。

18min read

What is SkiaSharp?

SkiaSharp

Xamarinのようなツールは、少なくともすべての重要なプラットフォームで共通のプログラミング言語を使用するのに役立ちますが、アプリケーションのビジネス ロジックを共有するのには最適ですが、UI ロジックの共有が自動的に容易になるわけではありません。

Xamarin /Microsoft には、異なるプラットフォーム間で UI 実装を共有するのに役立つ複数のツールが用意されています。彼らは作成しましたXamarin。フォームこれにより、アプリケーションの UI ビューを一度抽象的に定義し、サポートされているプラットフォーム間で再利用できます。Xamarin。フォームはとてもかっこいい、少なくとも私たちはそう考えています、それが私たちが持っている理由です新製品あなたができることを確かめるために利用可能さらに素晴らしいものXamarinで。フォーム。しかし、Forms でできないことがまだあるが、それでも必要な場合はどうでしょうか。C#でプラットフォーム間でカスタムグラフィックスをレンダリングするのは素晴らしいことではないでしょうか。

数年前、私はまさにこれをやりたかったのですが、課題は、当時、すべての重要なプラットフォームで利用可能なクロスプラットフォームの C# 2D API がなかったことでした。そこで、さまざまなネイティブ 2D レンダリング API の周囲に抽象化を作成して、一部のレンダリング ロジックを一度記述し、一部の抽象化レイヤーでさまざまなネイティブ レンダリング API への呼び出しの正しいシーケンスに変換できるようにしました。しかし、私が見つけたのは、このパスを使用すると、多くの興味深いオーバーヘッドが発生するということです。私のロジックでは、C# で 2D グラフィックス プリミティブをいくつか構築し、それを Android で Java オブジェクトとして表現し、それらの Java オブジェクトを Android のネイティブ レンダリング レイヤーでネイティブ クラスとして表現する必要がありました。これほど多くの抽象化レイヤーがあると、2D レンダリング API と複雑な 2D グラフィックスを使用した場合の煩わしさと相まって、少なからずオーバーヘッドが発生していました。

まあ、Xamarin自身もこれと同じ痛みを感じているに違いありません、なぜならそれが彼らをSkiaSharpの作成に導いたからです。SkiaSharp は、クロス プラットフォームの 2D グラフィックス レンダリング API であり、Xamarin経由の Android/iOS プラットフォームなど、さまざまなプラットフォームで使用できます。SkiaSharp は、Skiaレンダリング ライブラリの C ネイティブ API を直接囲む C# バインディングです。Skiaは、AndroidやChromeブラウザ、その他多数の注目度の高いプロジェクトで頻繁に使用されている、高速なオープンソースレンダリングライブラリです。SkiaSharp を手にすると、C# API は Skia ネイティブ ライブラリと直接通信できるため、API との対話のオーバーヘッドを比較的少なくして、多くのプラットフォームで高速な 2D グラフィックス レンダリングを行うことができます。この記事では、API の使用を開始する方法について説明します。

はじめる

まず、Visual Studio を開き、新しいクロス プラットフォーム プロジェクトを作成します。これは、Xamarinが Visual Studio にインストールするテンプレートの 1 つです。

これは、Xamarinが VS にインストールするテンプレートの 1 つです

これは、好みに応じて、Xamarin.Forms アプリまたはネイティブ アプリにすることができます。このデモの目的のために、共有プロジェクトではなくPCL(ポータブルクラスライブラリ)を介してコードを共有するネイティブアプリを作成しました。これにより、テンプレートに関連付けられた既定のプラットフォームのホストが作成され、必要に応じて SkiaSharp もサポートしている場合は、プロジェクトを追加できます。

ネイティブ クロス プラットフォーム アプリを選択すると、プラットフォーム間でコードを共有するための PCL プロジェクト、Xamarin.Android プロジェクト、Xamarin.iOS プロジェクトが作成されましたが、WPF プロジェクトと UWP プロジェクトもレンダリング用に SkiaSharp をサポートしているため、これらのプロジェクトもミックスに追加しました。次に、PCL プロジェクトへの参照を、ソリューションに追加した 2 つの新しいプロジェクトに追加しました。

次に、PCL プロジェクトへの参照を、ソリューションに追加した 2 つの新しいプロジェクトに追加しました

次に、いくつかの NuGet パッケージをソリューションに追加する必要があります。ソリューション ノードを右クリックし、[ソリューションの Nuget パッケージの管理] を選択すると、ソリューション内のすべてのプロジェクトに SkiaSharp と SkiaSharp.Views をオンラインで検索してインストールできます。SkiaSharp.Views には、選択したプラットフォームで SkiaSharp のレンダリングをネイティブ UI ビューにブートストラップするのに役立つヘルパー クラスがいくつかあり、定型ロジックを節約できます。SkiaSharp.Views は、PCL を除くすべてのプロジェクトにインストールする必要がありますが、PCL にはユーティリティが提供されません (特定の UI プラットフォームにバインドされていないため)。

私たちの目標

単純な円をレンダリングする単純なものから始めますが、その後、かなり複雑なものに移ります。古いサンプルブラウザの1つには、きちんとしたアニメーションの虹彩効果がありました。

古いサンプルブラウザの1つには、きちんとしたアニメーションの虹彩効果がありました

これは、たくさんの画像を重ね合わせ、それらをさまざまな方向に回転させることによって行われました。その場合、画像は静的でしたが、代わりに、すべてのロジックをビューに配置して、すべてを動的にレンダリングできるかどうか興味がありました。その目標に向かって進みましょう。

まず、SkiaSharp.Views を活用して、Android 用のIrisViewというコンポーネントを作成します。後でこれをさらに拡張し、他のプラットフォームの詳細を記入します。

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);
            };
        }
    }
}

このコードの大部分は、SkiaSharp.Views が提供するビューの 1 つを拡張して、通常の Android ビュー内で SkiaSharp を使用してコンテンツをレンダリングできるようにする定型ロジックです。ペイントを行うロジックに焦点を当てると、次のようになります。

//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);
};

ここでは、

  • ペイントするスキアキャンバスを入手します。
  • キャンバスに表示される初期カラーをクリアします(以前のレンダリングをすべて消去します)。
  • ビューの中心と、ビューに描画できる円の半径を決定します。
  • アンチエイリアシングを使用して、赤で塗りつぶされる Skia Paint オブジェクトを作成します。
  • 構成されたペイントオブジェクトを使用して、キャンバスに円を描きます。

次に、メイン アクティビティに戻り、この IrisView をレイアウトに追加すると、次のように表示されます。

メインアクティビティに戻り、このIrisViewをレイアウトに追加すると、次のようなものが表示されます

わかりました、これは素晴らしいですが、明らかに、レンダリングロジックがネイティブのAndroidビューにある場合、プラットフォーム間で共有することはできませんよね?では、これを少しリファクタリングしてみましょう。PCL では、次の内容でIrisRenderer.csというクラスを作成します。

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);
            };
        }
    }
}

また、Android用のIrisViewを次のように修正します。

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);
        }
    }
}

このようにして、すべてのレンダリング ロジックを PCL にある共有クラスに分解し、ターゲットとするすべてのプラットフォーム間で共有できるようにしました。一度コーディングするだけで完了です。さらに、レンダラーが経過時間を分析し、線形補間を使用して変化をアニメーション化できるように、間隔を空けてビューを無効にし、再描画し続けるプリミティブアニメーションシステムを追加しました(3次イージング関数で緩和されます)。かなりクールだと思いませんか?青と赤の間のアニメーション中の円は次のようになります。

さて、この時点で、IrisViewの残りの実装を埋めることができます。各プラットフォームには、UI ビューの構成要素に関する要件や、アニメーション ループを駆動するために使用できるメカニズムが異なるため、これらは別々のクラスにする必要がありますが、これらのクラスの内容を最小限に抑えて、プラットフォーム固有の動作のみを含めるようにするという考え方です。また、これらのクラスのロジックをさらに減らす追加の抽象化 (たとえば、アニメーションに関する抽象化) を構築するオプションもあります。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);
        }
    }
}

そして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);
        }
    }
}

これで、これらのそれぞれを実行して、まったく同じレンダリング動作を観察できます。これがなぜこんなに素晴らしいのかまだ理解していないなら、物事をかなり複雑にしましょう。IrisRendererを次の内容で更新します。

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);
            };
        }
    }
}

この記事では、このロジックで何が起こっているのかについては詳しく説明しませんが、興味があれば、次の記事で詳しく説明できます。それでも、プラットフォーム間で多くの複雑なロジックを再利用する方法を示すことで、これを提示します。ここでアプリを再実行すると、互いに回転する歯車のカウンターが絡み合う複雑なビジュアルが表示されます。

ここでアプリを再実行すると、噛み合う歯車のカウンターが互いに回転する複雑なビジュアルが表示されます

そして、これが動いているビデオです。

そして、これが動いているビデオです

さて、これが素晴らしいと私を信じますか?その結果、「グラハム、SkiaSharp がプラットフォーム間で高パフォーマンスのレンダリングを非常に簡単に行えるのなら、クロスプラットフォーム アプリで使用できる本当に素晴らしい UI を誰かが構築したら素晴らしいと思いませんか?」と考えるかもしれません。ええ、はい、実際には、それがまさに私たちがそれをした理由です:

クロスプラットフォームアプリで使用できる本当に素晴らしいUIのもの

まとめ

Infragistics + Xamarinをしばらくフォローしている方は、以前からXamarinベースの製品を提供していたこと(そして現在、新しいバージョンがリリースされていること)をご存知かもしれません。しかし、明らかではないかもしれませんが、製品の新しいバージョンは、17.0リリースのすべてのプラットフォーム間で完全に一貫したAPI、パフォーマンス、および動作のストーリーを持つように大幅に再設計されました。Xamarin製品の以前のバージョンは、ネイティブのAndroidおよびiOS製品に対する薄いベニヤでした。これは、ネイティブのモバイルAPIが互いに十分に類似していたという事実によってのみ実現可能でした。しかし、最大限の一貫性を追求するという点では、APIの一貫性が十分でなかった(また、一部のコンポーネントでは完全に異なる)ため、この戦略は必要以上に高価で制限的なものになっていました。それと、私たちは .NET ベースのデータを (明らかに non-.NET) ネイティブ コンポーネントに対して直接効率的にバインドできるように、内部で計り知れない黒魔術を働かせましたが、これは舞台裏で非常に複雑でした。

SkiaSharp が登場したとき、レンダリング レイヤーまで "ずっと下" で製品を C# 製品として再構想し、API (および基になるロジック) を API 間で可能な限り同一になるように再焦点を当てる機会があることを知っていましたXamarin。フォーム、Xamarin。Android、Xamarin.iOS、さらに、デスクトップのXAMLプラットフォームと非常によく似ています。デスクトップにはいくつかの独自のWPF機能があるという事実を除けば、私たちは普遍的に物事を非常に近いものにしたので、多くの場合、プラットフォーム間でロジックを貼り付けるだけで、微調整はまったく必要ありません。さらに、新しいXamarinコンポーネントを使用すると、ほとんどの場合、人気のあるデスクトップ WPF 製品とまったく同じロジックを実行できます。私たちは自分たちが成し遂げた仕事を非常に誇りに思っており、それがあなたを喜ばせることを願っています!お知らせください!

 

-Graham

デモを予約