Xamarin開発記02 - バインドに対応したグラデーションコントロールの作成

2019/5/9 改訂：クラス名をGradationXXXからGradientXXXに変更しました。
# はじめに
[前回の記事](https://qiita.com/kojimadev/items/2a459832a5732ab47066)にて、Xamarin.Formsでスマホアプリを開発するための初期設定を行いました。
本記事では、Xamarin標準のコントロールでは実現できない「グラデーションの描画」について、SkiaSharpを用いたコントロールの作成方法を紹介します。

# SkiaSharpとは
SkiaSharp は Skia Graphics Library という Google のオープンソースのグラフィックエンジンを利用した2D グラフィックスシステムです。Microsoftの公式サイトで紹介しているライブラリです。基本的に、共通プロジェクトでコードを記載することで、Android、iOS、UWPのすべてで同じ描画ができます。
背景色をグラデーションで描画する方法は他にもありますが、SkiaSharpは描画全般に対応しており、画像変換やパス(曲線や多角形など)の描画にも利用できて活用範囲が広いため、SkiaSharpの利用をお勧めします(厳密には、今回紹介するのはSkiaSharp.Formsですが)。
SkiaSharpの詳細は以下を参照ください。
[Xamarin.Forms での SkiaSharp グラフィックス](https://docs.microsoft.com/ja-jp/xamarin/xamarin-forms/user-interface/graphics/skiasharp/)

# インストール方法
SkiaSharp.Views.Forms を nuget からインストールしてください。
(本記事執筆時点でのバージョンは1.68.0)

# チュートリアル
チュートリアルとして、基本的な使い方は下記の記事を参照ください。
[SkiaSharp の単純な円を描画](https://docs.microsoft.com/ja-jp/xamarin/xamarin-forms/user-interface/graphics/skiasharp/basics/circle)

以下にポイントなるソースコードにコメントを付けて解説します。

```c#
public class SimpleCirclePage : ContentPage
{
	public SimpleCirclePage()
	{
	    // SkiaSharpの描画用コントロールのSKCanvasViewを作成してハンドラを登録
	    SKCanvasView canvasView = new SKCanvasView();
	    canvasView.PaintSurface += OnCanvasViewPaintSurface;
	    Content = canvasView;
	}

	// SKCanvasViewの描画時に呼び出されるハンドラ
	private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
	{
	    SKImageInfo info = args.Info;
	    SKSurface surface = args.Surface;
	    SKCanvas canvas = surface.Canvas;

	    // canvas上にすでに何かを描画済みであればいったんクリアする(透明色にする)
	    canvas.Clear();

	    // 線を描画するためのSKPaintを作成(WPFでのBrushのようなもの)
	    SKPaint paint = new SKPaint
	    {
	        Style = SKPaintStyle.Stroke,
	        Color = Color.Red.ToSKColor(),
	        StrokeWidth = 25
	    };
	    // キャンバスに円を描画する(引数は中心座標X、中心座標Y、半径、SKPaintの順)
	    canvas.DrawCircle(info.Width / 2, info.Height / 2, 100, paint);

	    // SKPaintを背景を塗りつぶす用に変更
	    paint.Style = SKPaintStyle.Fill;
	    paint.Color = SKColors.Blue;
	    // キャンバスを円で塗りつぶす
	    canvas.DrawCircle(info.Width / 2, info.Height / 2, 100, paint);
	}
}
```
上記は以下のような円を描画します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403606/7dec6ba2-edfd-64d5-dcb8-1b1afc45bf2d.png" width=25%>

# グラデーションの描画方法
グラデーションを描画する方法の詳細は以下を参照ください。
[SkiaSharp の線形グラデーション](https://docs.microsoft.com/ja-jp/xamarin/xamarin-forms/user-interface/graphics/skiasharp/effects/shaders/linear-gradient)
ポイントは、以下のように、SKPaintのShaderプロパティに、グラデーションの始点、終点、色を設定することです。それ以外は、チュートリアルとだいたい同じです。

```c#
        paint.Shader = SKShader.CreateLinearGradient(
                            new SKPoint(rect.Left, rect.Top),
                            new SKPoint(rect.Right, rect.Bottom),
                            new SKColor[] { SKColors.Red, SKColors.Blue },
                            new float[] { 0, 1 },
                            SKShaderTileMode.Repeat);
```


# グラデーション表示できるコントロールの作成
では、グラデーション表示できるコントロールを作成していきます。
私の経験として、アプリで利用するグラデーションは、あらかじめ定義しておいた数十パターンから選択して使うことが多いと思います。そのため、今回作成するコントロールは、グラデーションのenum値(GradientColor)をプロパティに持ち、そのenum値で定義されたグラデーションを表示する構成とします。
また、今回はシンプルに線形グラデーションのみを紹介します(放射状グラデーションは次回)。

まずは、そのenum値(GradientColor)と、グラデーションの設定(GradientModel)と、それを作成するクラス(GradientModelFactory)を以下のように作成します。

```c#
	/// <summary>
	/// グラデーションの色のタイプ
	/// </summary>
	public enum GradientColor
	{
		Transparent,
		DarkRed,
		LightBlue,
		DarkYellow,
		Black,
		Gray,
	}

	/// <summary>
	/// グラデーション設定
	/// </summary>
	public class GradientModel
	{
		/// <summary>
		/// グラデーションで使用する色
		/// </summary>
		public SKColor[] Colors { get; set; }

		/// <summary>
		/// グラデーションのポジション
		/// </summary>
		public float[] ColorPos { get; set; }
	}

	/// <summary>
	/// グラデーションの設定を作成するクラス
	/// </summary>
	public class GradientModelFactory
	{
	    /// <summary>
	    /// コンストラクタ
	    /// </summary>
	    private GradientModelFactory() { }

	    /// <summary>
	    /// インスタンスを取得
	    /// </summary>
	    public static GradientModelFactory Instance => new GradientModelFactory();

	    /// <summary>
	    /// グラデーションの設定を作成して返す
	    /// </summary>
	    /// <param name="gradientColor">グラデーションのタイプ</param>
	    /// <returns>グラデーションの設定</returns>
	    public GradientModel CreateGradientModel(GradientColor gradientColor)
	    {
			switch (gradientColor)
			{
				case GradientColor.Transparent:
					// 透明色の場合はnullを返す
					return null;
				case GradientColor.DarkRed:
					return new GradientModel()
					{
						Colors = new SKColor[] { SKColors.Red, SKColors.DarkRed },
						ColorPos = new float[] { 0, 1 }
					};
				case GradientColor.LightBlue:
					return new GradientModel()
					{
						Colors = new SKColor[] { SKColors.AliceBlue, SKColors.LightBlue },
						ColorPos = new float[] { 0, 1 }
					};
				case GradientColor.DarkYellow:
					return new GradientModel()
					{
						Colors = new SKColor[] { Color.FromHex("#FFFFA500").ToSKColor(), Color.FromHex("#FF90B000").ToSKColor() },
						ColorPos = new float[] { 0, 1 }
					};
				case GradientColor.Black:
					// 3色のグラデーションの場合はColorsとColorPosに3つを指定
					return new GradientModel()
					{
						Colors = new SKColor[] { SKColors.White, SKColors.LightGray, SKColors.Black },
						ColorPos = new float[] { 0, 0.5f, 1 }
					};
				case GradientColor.Gray:
					return new GradientModel()
					{
						Colors = new SKColor[] { SKColors.LightGray, SKColors.Gray },
						ColorPos = new float[] { 0, 1 }
					};
				default:
					throw new Exception("Invalid GradientColor");
		    }
		}
	}
```
上記のGradientModelを用いて背景を描画するコントロール GradientRect クラスを作成します。xamal部分は以下です。

```xml

<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:forms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="XamarinGradientShape.Controls.GradientRect"
             HorizontalOptions="Fill" VerticalOptions="Fill">
	<ContentView.Content>
		<forms:SKCanvasView x:Name="_SkCanvasView" 
		                    PaintSurface="SkCanvasView_OnPaintSurface"
		                    HorizontalOptions="Fill" VerticalOptions="Fill"/>
	</ContentView.Content>
</ContentView>
```
GradientRect クラスのコードビハインドは以下です。

```c#
public partial class GradientRect : ContentView
{
	/// <summary>
	/// BackGradientColor 依存関係プロパティ
	/// </summary>
	public static readonly BindableProperty BackGradientColorProperty = BindableProperty.Create(
		"BackGradientColor", // プロパティ名
		typeof(GradientColor), // プロパティの型
		typeof(GradientRect), // プロパティを持つ View の型
		GradientColor.Transparent, // 初期値
		BindingMode.TwoWay, // バインド方向
		null, // バリデーションメソッド
		BackGradientColorPropertyChanged, // 変更後イベントハンドラ
		null, // 変更時イベントハンドラ
		null);

	/// <summary>
	/// 背景のグラデーションのタイプ
	/// </summary>
	public GradientColor BackGradientColor
	{
		get => (GradientColor)GetValue(BackGradientColorProperty);
		set => SetValue(BackGradientColorProperty, value);
	}

	/// <summary>
	/// Borderの色
	/// </summary>
	public Color BorderColor { get; set; }

	/// <summary>
	/// Borderの線の太さ
	/// </summary>
	public float StrokeWidth { get; set; }

	/// <summary>
	/// 矩形を描画する場合の角丸にする円の半径
	/// </summary>
	public int CornerRadius { get; set; }

	/// <summary>
	/// 初期描画済みか
	/// </summary>
	public bool Initialized { get; private set; } = false;

	// 描画するハンドラ
	private void SkCanvasView_OnPaintSurface(object sender, SKPaintSurfaceEventArgs e)
	{
		SKImageInfo info = e.Info;
		SKSurface surface = e.Surface;
		SKCanvas canvas = surface.Canvas;
		canvas.Clear();

		// 描画範囲をコントロール全体として指定
		SKRect rect = new SKRect(0, 0, info.Width, info.Height);

		// グラデーション設定を取得
		GradientModel gradientModel = GradientModelFactory.Instance.CreateGradientModel(BackGradientColor);
		if (gradientModel == null)
		{
			return;
		}

		// 塗りつぶしのグラデーション用のSKPaintを作成
		SKPaint paint = new SKPaint
		{
			IsAntialias = true,
			Style = SKPaintStyle.Fill,
			Shader = SKShader.CreateLinearGradient(new SKPoint(rect.Left, rect.Top), new SKPoint(rect.Left, rect.Bottom),
				gradientModel.Colors, gradientModel.ColorPos, SKShaderTileMode.Clamp)
		};
		// 背景を描画
		canvas.DrawRoundRect(rect, CornerRadius, CornerRadius, paint);

		// 枠線用にSKPaintを変更
		paint.Style = SKPaintStyle.Stroke;
		paint.Shader = null;
		paint.Color = BorderColor.ToSKColor();
		paint.StrokeWidth = StrokeWidth;
		// 枠線の描画
		canvas.DrawRoundRect(rect, CornerRadius, CornerRadius, paint);

		// 初期描画済み
		Initialized = true;
	}

	// BackGradientColor変更後ハンドラ
	private static void BackGradientColorPropertyChanged(BindableObject bindable, object oldValue, object newValue)
	{
		if (bindable == null || newValue == null ||
		    newValue.GetType() != BackGradientColorProperty.ReturnType)
		{
			return;
		}
		GradientRect gradientRect = (GradientRect)bindable;

		// 初期描画前であれば何もしない
		if (gradientRect.Initialized == false)
		{
			return;
		}

		// 再描画する
		gradientRect._SkCanvasView.InvalidateSurface();
	}
}
```
上記のコントロールは、利用時にBackGradientColorを設定すれば、定義したグラデーションの矩形を描画します。使い方は以下のような感じになります。

```xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamarinGradientShape"
             xmlns:controls="clr-namespace:XamarinGradientShape.Controls;assembly=XamarinGradientShape"
             x:Class="XamarinGradientShape.MainPage">
	<Grid>
		<Grid.RowDefinitions>
			<RowDefinition />
			<RowDefinition />
			<RowDefinition />
			<RowDefinition />
		</Grid.RowDefinitions>

		<controls:GradientRect Grid.Row="0" BackGradientColor="DarkRed" />
		<controls:GradientRect Grid.Row="1" BackGradientColor="LightBlue" />
		<!--CornerRadiusを設定すると角丸になる-->
		<controls:GradientRect Grid.Row="2" BackGradientColor="DarkYellow" CornerRadius="30" />
		<!--BorderColorとStrokeWidthを設定すると枠線も描画する-->
		<controls:GradientRect Grid.Row="3" BackGradientColor="Black" BorderColor="DarkBlue" StrokeWidth="10" />
	</Grid>
</ContentPage>
```
上記を実行すると、以下のようになります(画像はAndroidで実行した場合)。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403606/e7614577-89ab-2377-a886-65dba5f9aaf4.png" width=50%>

### バインドを用いてグラデーションを変更
上記のGradientRectコントロールは、あるオブジェクトをステータスごとに異なる背景色にしたい場合などに活用できます。BackGradientColorプロパティをバインドすれば、バインド先の値に合わせて背景色を変更できます。それができるように、BackGradientColorは依存関係プロパティとして定義した上で、値が変わった時のハンドラ(BackGradientColorPropertyChangedメソッド)で再描画を実行しています。
実際に、ItemクラスのStatusとバインドさせる例を以下に挙げます。

```c#
	// ステータスを表すenum
	public enum Status
	{
		ToDo,
		Doing,
		Done,
	}

	// Statusをプロパティに持つクラス
	public class Item : INotifyPropertyChanged
	{
		private Status _Status;
		public Status Status
		{
			get => _Status;
			set
			{
				_Status = value;
				OnPropertyChanged();
			}
		}
		public event PropertyChangedEventHandler PropertyChanged;
		protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
		{
			PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
		}
	}

	// StatusをGradientColorに変換するコンバーター
	public class StatusToGradientColorConverter : IValueConverter
	{
		public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
		{
			var status = (Status)value;
			switch (status)
			{
				case Status.ToDo:
					return GradientColor.DarkRed;
				case Status.Doing:
					return GradientColor.DarkYellow;
				case Status.Done:
					return GradientColor.LightBlue;
				default:
					throw new Exception("Invalid status");
			}
		}

		public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
		{
			throw new NotImplementedException();
		}
	}
```
上記でバインドさせる準備は完了です。あとは、ItemクラスをGradientRectのBindingContextプロパティに設定して、BackGradientColorに Statusプロパティをバインドさせれば、ItemクラスのStatusに合わせて背景色を変更させることができます。バインドの書き方の例は以下です。 

```xml
<controls:GradientRect BackGradientColor="{Binding Path=Status, Converter={StaticResource StatusToGradientColorConverter}, Mode=OneWay}" />
```
本記事で紹介したコードを用いて、実際にItemクラスのStatusプロパティを変更させてグラデーションを切り替えるソースコード一式をGitHubにアップしています(まとめ参照)。

# まとめ
ステータスとバインドさせて背景色を変更させられるグラデーションコントロールを作成しました。
本記事で紹介したコードはGitHubの以下にアップしています。よろしければ自由に改変してご利用ください。
使う場合は、GradientColorとGradientFactoryにグラデーションの定義を自由に追加してもらうことを想定しています。
[GitHubのソースコードはこちら](https://github.com/kojimadev/XamarinGradientShape/tree/0ac8263ba9c7bb571104a1b7b4ee2d642d791611)

次回は、線形グラデーションと放射状グラデーションを使い分けて、矩形と円を描画するコントロールを紹介します。
[次回の記事はこちら](https://qiita.com/kojimadev/items/f1f5a5c84ad1db524249)