Xamarin開発記03 - 線形と放射状のグラデーションを使い分けて矩形と円を描画

# はじめに
[前回の記事](https://qiita.com/kojimadev/items/9a280466a9d06941b6ef)にて、Xamarin.FormsでSkiaSharpを用いて線形グラデーションの矩形を描画するコントロールを作成しました。
本記事では、前回作成したコントロールを修正して、線形と放射状のグラデーションを使い分けて矩形と円を描画するコントロールを作成します。

# クラスの構造
グラデーションで、なんらかの形状を描画するabstractのクラスとして、GradientShapeクラスを作成します。
前回作成した矩形を描画する GradientRectクラスは、GradientShapeクラスのサブクラスとなります。さらに、GradientShapeクラスのサブクラスとして、円を描画するGradientCircleクラスを作成します。以下にクラス図を記します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403606/5095c0a6-19c7-7378-fb93-d5eebd1c2b69.png" width=50%>

# グラデーションの描画に必要なクラス
今回も各コントロールは、グラデーションのenum値(GradientColor)をプロパティに持ち、そのenum値で定義されたグラデーションを表示する構成とします。
以下に、そのenum値(GradientColor)と、グラデーションの設定(GradientModel)と、それを作成するクラス(GradientModelFactory)のソースコードを記します。
前回との大きな違いは、GradientModelクラスが、線形グラデーションと放射状グラデーションを識別するためのDrawTypeプロパティを持っている点です。

```c#
	/// <summary>
	/// グラデーションの色のタイプ
	/// </summary>
	public enum GradientColor
	{
		Transparent,
		// 以下、 線形グラデーション
		LinerDarkRed,
		LinerLightBlue,
		LinerDarkYellow,
		LinerBlack,
		LinerGray,
		// 以下、放射状グラデーション
		RadialRedBrush,
		RadialBlueBrush,
		RadialYellowBrush,
	}

	/// <summary>
	/// グラデーション設定
	/// </summary>
	public class GradientModel
	{
		/// <summary>
		/// グラデーション形状のタイプ
		/// </summary>
		public ShaderType ShaderType { get; set; }

		/// <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.LinerDarkRed:
					return new GradientModel()
					{
						ShaderType = ShaderType.Liner,
						Colors = new SKColor[] { SKColors.Red, SKColors.DarkRed },
						ColorPos = new float[] { 0, 1 }
					};
				case GradientColor.LinerLightBlue:
					return new GradientModel()
					{
						ShaderType = ShaderType.Liner,
						Colors = new SKColor[] { SKColors.AliceBlue, SKColors.LightBlue },
						ColorPos = new float[] { 0, 1 }
					};
				case GradientColor.LinerDarkYellow:
					return new GradientModel()
					{
						ShaderType = ShaderType.Liner,
						Colors = new SKColor[] { Color.FromHex("#FFFFA500").ToSKColor(), Color.FromHex("#FF90B000").ToSKColor() },
						ColorPos = new float[] { 0, 1 }
					};
				case GradientColor.LinerBlack:
					return new GradientModel()
					{
						// 3色のグラデーションの場合はColorsとColorPosに3つを指定
						ShaderType = ShaderType.Liner,
						Colors = new SKColor[] { SKColors.White, SKColors.LightGray, SKColors.Black },
						ColorPos = new float[] { 0, 0.5f, 1 }
					};
				case GradientColor.LinerGray:
					return new GradientModel()
					{
						ShaderType = ShaderType.Liner,
						Colors = new SKColor[] { SKColors.LightGray, SKColors.Gray },
						ColorPos = new float[] { 0, 1 }
					};
				case GradientColor.RadialRedBrush:
					return new GradientModel()
					{
						// 放射状グラデーション
						ShaderType = ShaderType.Radial,
						Colors = new SKColor[] { SKColors.White, Color.FromHex("#FFFF2020").ToSKColor() },
						ColorPos = new float[] { 0, 1 }
					};
				case GradientColor.RadialBlueBrush:
					return new GradientModel()
					{
						// 放射状グラデーション
						ShaderType = ShaderType.Radial,
						Colors = new SKColor[] { SKColors.White, Color.FromHex("#FF2020FF").ToSKColor() },
						ColorPos = new float[] { 0, 1 }
					};
				case GradientColor.RadialYellowBrush:
					return new GradientModel()
					{
						// 放射状グラデーション
						ShaderType = ShaderType.Radial,
						Colors = new SKColor[] { SKColors.White, SKColors.Yellow },
						ColorPos = new float[] { 0, 1 }
					};
				default:
					throw new Exception("Invalid GradientColor");
			}
		}
```

# グラデーション表示するコントロールの作成
上記のクラスを用いて、GradientShapeおよびそのサブクラスを作成します。
GradientShapeのxaml部分は以下です(前回記事のGradientRectと同じです)。

```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.GradientShape"
             HorizontalOptions="Fill" VerticalOptions="Fill">
	<ContentView.Content>
		<Grid x:Name="_MainGrid">
			<forms:SKCanvasView x:Name="_SkCanvasView" IgnorePixelScaling="True" PaintSurface="SkCanvasView_OnPaintSurface"
			                    HorizontalOptions="Fill" VerticalOptions="Fill"/>
		</Grid>
	</ContentView.Content>
</ContentView>
```
GradientShapeクラスのコードビハインドは以下です。
ポイントは、PaintSurfaceメソッド内で、背景を描画するDrawFillメソッドとDrawStrokeメソッド(いずれもabstractメソッド)を呼び出している点です。この2つのメソッドをサブクラスで実装することで、矩形と円の描画を実現します。
(SkCanvasViewなどのSkiaSharpの使い方は前回記事を参照ください)

```c#
	/// <summary>
	/// 背景をグラデーション描画するコントロール
	/// </summary>
	public abstract partial class GradientShape : ContentView
	{
		// コンストラクタとプロパティの定義は省略(詳細はGitHubのソースコード参照)

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

		/// <summary>
		/// 対象キャンバスを描画する
		/// </summary>
		/// <param name="canvas">描画対象のキャンバス</param>
		/// <param name="info">描画対象領域のサイズなどの情報</param>
		protected virtual void PaintSurface(SKCanvas canvas, SKImageInfo info)
		{
			// 初期描画済みとする
			Initialized = true;

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

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

			// グラデーション設定を取得
			GradientModel gradientModel = GradientModelFactory.Instance.CreateGradientModel(BackGradientColor);
			if (gradientModel == null && BackgroundColor == Color.Transparent)
			{
				// グラデーション設定もグラデーション無しの背景色も、どちらも未設定であれば透明とする
				return;
			}

			// 塗りつぶしのグラデーション用のSKPaintを作成
			using (SKPaint paint = CreateSKPaint())
			{
				if (gradientModel != null)
				{
					// 背景グラデーション用のShaderの設定
					paint.Shader = CreateShader(gradientModel, rect);
				}
				else
				{
					// グラデーション設定がない場合はBackgroundColorから取得
					paint.Color = BackgroundColor.ToSKColor();
				}

				// 背景を描画
				DrawFill(canvas, paint, rect);
			}

			// 枠線の太さが0に近ければ枠線は描画しない
			if (StrokeWidth < 0.01) return;

			// 枠線用のSKPaintを作成
			using (SKPaint paint = CreateSKPaint(BorderColor.ToSKColor(), SKPaintStyle.Stroke, StrokeWidth))
			{
				// 枠線を描画
				DrawStroke(canvas, paint, rect);
			}
		}

		/// <summary>
		/// 背景を描画する
		/// </summary>
		/// <param name="canvas">描画対象のキャンバス</param>
		/// <param name="paint">描画する色の設定</param>
		/// <param name="rect">描画サイズ</param>
		protected abstract void DrawFill(SKCanvas canvas, SKPaint paint, SKRect rect);

		/// <summary>
		/// 枠線を描画する
		/// </summary>
		/// <param name="canvas">描画対象のキャンバス</param>
		/// <param name="paint">描画する色の設定</param>
		/// <param name="rect">描画サイズ</param>
		protected abstract void DrawStroke(SKCanvas canvas, SKPaint paint, SKRect rect);

		/// <summary>
		/// 矩形から円の半径を算出する
		/// </summary>
		/// <param name="rect">矩形</param>
		/// <returns>半径</returns>
		protected static float GetRadius(SKRect rect)
		{
			// 円から絶対にはみ出ないように半径は1小さくする
			return (float)Math.Min(rect.Width, rect.Height) / 2 - 1;
		}

		/// <summary>
		/// SKPaintを作成する
		/// </summary>
		/// <param name="style">塗りつぶしか枠線か</param>
		/// <param name="strokeWidth">枠線の場合の線の太さ</param>
		/// <returns>作成したSKPaint</returns>
		protected static SKPaint CreateSKPaint(SKPaintStyle style = SKPaintStyle.Fill, float strokeWidth = 0)
		{
			return CreateSKPaint(SKColors.Black, style, strokeWidth);
		}

		/// <summary>
		/// SKPaintを作成する
		/// </summary>
		/// <param name="color">色</param>
		/// <param name="style">塗りつぶしか枠線か</param>
		/// <param name="strokeWidth">枠線の場合の線の太さ</param>
		/// <returns>作成したSKPaint</returns>
		protected static SKPaint CreateSKPaint(SKColor color, SKPaintStyle style = SKPaintStyle.Fill, float strokeWidth = 0)
		{
			return new SKPaint()
			{
				IsAntialias = true,
				Color = color,
				Style = style,
				StrokeWidth = strokeWidth,
			};
		}

		/// <summary>
		/// グラデーション用のShaderを作成する
		/// </summary>
		/// <param name="gradientModel">グラデーション設定</param>
		/// <param name="rect">対象の矩形範囲</param>
		/// <returns>作成したShader</returns>
		protected static SKShader CreateShader(GradientModel gradientModel, SKRect rect)
		{
			switch (gradientModel.ShaderType)
			{
				case ShaderType.Liner:
					// 矩形の場合
					return SKShader.CreateLinearGradient(
						new SKPoint(rect.Left, rect.Top),
						new SKPoint(rect.Left, rect.Bottom),
						gradientModel.Colors,
						gradientModel.ColorPos,
						SKShaderTileMode.Clamp);
				case ShaderType.Radial:
					// 円の場合
					// 矩形のサイズから円の半径を算出する
					float radius = GetRadius(rect);
					// グラデーションの始点位置を中心から左上にずらし、そのずれた分だけ半径を大きくする
					return SKShader.CreateRadialGradient(
						new SKPoint(rect.MidX - radius * 0.5f, rect.MidY - radius * 0.5f),
						radius + radius * 0.5f,
						gradientModel.Colors,
						gradientModel.ColorPos,
						SKShaderTileMode.Clamp);
				default:
					return null;
			}
		}
	}
```
矩形を描画するGradientRectクラスと、円を描画するGradientCircleクラスは以下です。これらのサブクラスでの実装内容は、abstractメソッド2つの実装のみです。

```c#
	/// <summary>
	/// グラデーションを実現する矩形
	/// </summary>
	public class GradientRect : GradientShape
	{
		/// <summary>
		/// 背景を描画する
		/// </summary>
		/// <param name="canvas">描画対象のキャンバス</param>
		/// <param name="paint">描画する色の設定</param>
		/// <param name="rect">描画サイズ</param>
		protected override void DrawFill(SKCanvas canvas, SKPaint paint, SKRect rect)
		{
			canvas.DrawRoundRect(rect, CornerRadius, CornerRadius, paint);
		}

		/// <summary>
		/// 枠線を描画する
		/// </summary>
		/// <param name="canvas">描画対象のキャンバス</param>
		/// <param name="paint">描画する色の設定</param>
		/// <param name="rect">描画サイズ</param>
		protected override void DrawStroke(SKCanvas canvas, SKPaint paint, SKRect rect)
		{
			canvas.DrawRoundRect(rect, CornerRadius, CornerRadius, paint);
		}
	}

	/// <summary>
	/// グラデーションを実現する円
	/// </summary>
	public class GradientCircle : GradientShape
	{
	    /// <summary>
	    /// 背景を描画する
	    /// </summary>
	    /// <param name="canvas">描画対象のキャンバス</param>
	    /// <param name="paint">描画する色の設定</param>
	    /// <param name="rect">描画サイズ</param>
	    protected override void DrawFill(SKCanvas canvas, SKPaint paint, SKRect rect)
	    {
			float radius = GetRadius(rect);
			canvas.DrawCircle((float)rect.MidX, (float)rect.MidY, radius, paint);
		}

	    /// <summary>
	    /// 枠線を描画する
	    /// </summary>
	    /// <param name="canvas">描画対象のキャンバス</param>
	    /// <param name="paint">描画する色の設定</param>
	    /// <param name="rect">描画サイズ</param>
	    protected override void DrawStroke(SKCanvas canvas, SKPaint paint, SKRect rect)
	    {
		    float radius = GetRadius(rect);
		    canvas.DrawCircle(rect.MidX, rect.MidY, radius, paint);
	    }
	}
```

# 放射状グラデーションによる円の描画方法
上記で作成したコントロールの使い方を説明します。
以下のように、各コントロールの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 />
			<RowDefinition Height="0.5*"/>
		</Grid.RowDefinitions>

		<!--円を線形グラデーションで描画-->
		<controls:GradientCircle Grid.Row="0" BackGradientColor="LinerDarkRed" />
		<!--円を放射状グラデーションで描画-->
		<controls:GradientCircle Grid.Row="1" BackGradientColor="RadialBlueBrush" />
		<!--矩形にCornerRadiusを設定すると角丸になる-->
		<controls:GradientRect Grid.Row="2" BackGradientColor="LinerLightBlue" CornerRadius="30" />
		<!--矩形にBorderColorとStrokeWidthを設定すると枠線も描画する-->
		<controls:GradientRect Grid.Row="3" BackGradientColor="LinerBlack" BorderColor="DarkBlue" StrokeWidth="10" />
		<Button Grid.Row="4" Text="バインドで値が変わる確認用ボタン" Clicked="Button_OnClicked" />
	</Grid>
</ContentPage>
```

上記を実行すると、以下のようになります(画像はAndroidで実行した場合)。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403606/fac9da84-7bc7-1d76-8153-0b0cb4e56c42.png" width=50%>

# まとめ
線形と放射状のグラデーションを使い分けて矩形と円を描画するコントロールを作成しました。
本記事で紹介したソースコードを、GitHubの以下にアップしました。よろしければ、自由に改変してご利用ください。
使う場合は、GradientColorとGradientFactoryにグラデーションの定義を自由に追加してもらうことを想定しています。
[GitHubのソースコードはこちら](https://github.com/kojimadev/XamarinGradientShape)

次回は、日本語を既定の言語にして多言語対応する方法を紹介します。
[次回の記事はこちら](https://qiita.com/kojimadev/items/11d322bdead876de2521)