LoginSignup
3
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-04-26

2019/5/9 改訂:クラス名をGradationXXXからGradientXXXに変更しました。

はじめに

前回の記事にて、Xamarin.Formsでスマホアプリを開発するための初期設定を行いました。
本記事では、Xamarin標準のコントロールでは実現できない「グラデーションの描画」について、SkiaSharpを用いたコントロールの作成方法を紹介します。

SkiaSharpとは

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

インストール方法

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

チュートリアル

チュートリアルとして、基本的な使い方は下記の記事を参照ください。
SkiaSharp の単純な円を描画

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

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

上記は以下のような円を描画します。

グラデーションの描画方法

グラデーションを描画する方法の詳細は以下を参照ください。
SkiaSharp の線形グラデーション
ポイントは、以下のように、SKPaintのShaderプロパティに、グラデーションの始点、終点、色を設定することです。それ以外は、チュートリアルとだいたい同じです。

        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)を以下のように作成します。

    /// <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 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 クラスのコードビハインドは以下です。

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 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で実行した場合)。

バインドを用いてグラデーションを変更

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

    // ステータスを表す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に合わせて背景色を変更させることができます。バインドの書き方の例は以下です。

<controls:GradientRect BackGradientColor="{Binding Path=Status, Converter={StaticResource StatusToGradientColorConverter}, Mode=OneWay}" />

本記事で紹介したコードを用いて、実際にItemクラスのStatusプロパティを変更させてグラデーションを切り替えるソースコード一式をGitHubにアップしています(まとめ参照)。

まとめ

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

次回は、線形グラデーションと放射状グラデーションを使い分けて、矩形と円を描画するコントロールを紹介します。
次回の記事はこちら

3
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2