Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
Xamarin逆引きTips

Xamarin逆引きTips

Xamarin.FormsでBoxViewコントロールを拡張するには?

2015年2月10日

四角形を描画するBoxViewコントロールを拡張してネイティブ側で描画することで、角丸・枠線・影付きなどを実現する方法を説明する。

古谷 誠進(@furuya02
  • このエントリーをはてなブックマークに追加

 Xamarin.Formsには、BoxViewという四角形を描画するコントロールがある。しかし、このコントロールは、大きさや色などを変更するプロパティしか用意されておらず、角丸・枠線・影付きなどには対応していない。

 今回は、このBoxViewコントロールを拡張して、ネイティブ側で自由に描画する方法を解説する*1

  • *1 なお本Tipsは、Windows上でVisual Studio 2013を使用してXamarin.Forms開発をすることを前提としている(編集部注: Mac上のXamarin Studioでも同様の手順で、本稿の内容が実現できることは確認している)。使用しているXamarin.Formsのバージョンは、執筆時点で最新の「1.3.1.6296」である(バージョンの確認方法は、本稿の最後にあるコラムで紹介している)。

1. シナリオ

 最初に、角丸・影付きの効果を付加したBoxViewコントロールを表示する。続いて、スライダーコントロールを使用して、BoxViewコントロールのプロパティ値を変更することで、角丸のサイズが動的に変更できるように修正する。

2. Xamarin.Formsプロジェクトを作成する

 メニューバーの[ファイル]-[新規作成]-[プロジェクト]から表示したダイアログで、[テンプレート]-[Visual C#]-[Mobile Apps]-[Blank App (Xamarin.Forms Portable)]を選択し、名前を「ExBoxViewSample」として[OK]ボタンを押す(図1)。

図1 「Blank App (Xamarin.Forms Portable)」プロジェクトの新規作成

 ExBoxViewSampleプロジェクトにExBoxView.csファイルを追加し、以下のコードを追記する。

C#
using Xamarin.Forms;

public class ExBoxView : BoxView {

  public int Radius { get; set; }     // 角丸のサイズ
  public int ShadowSize { get; set; } // 影の幅

  public ExBoxView() {
    Radius = 10;
    ShadowSize = 5;
    WidthRequest = 150;
    HeightRequest = 150;
  }
}
リスト1 BoxViewクラスを継承したExBoxViewクラスのコード(ExBoxView.cs)

 このコードでは、Xamarin.FormsのBoxViewクラスを拡張してExBoxViewクラスを作成し、

  • 角丸のサイズを指定するためのRadiusプロパティと
  • 影の幅を指定するためのShadowSizeプロパティ

を実装している。

 本稿のサンプルでは、BoxViewを少し重なった状態で2つ配置したページを作成する。そこでApp.csファイルは、以下のように修正する。

C#
……省略……
public class App : Application {
  public App() {

    var boxViewRed = new ExBoxView {
      Color = Color.Red
    };
    var boxViewBlue = new ExBoxView {
      Color = Color.Blue
    };

    var layout = new AbsoluteLayout();
    layout.Children.Add(boxViewRed, new Point(100, 100));
    layout.Children.Add(boxViewBlue, new Point(50, 50));

    MainPage = new ContentPage {
      BackgroundColor = Color.White,
      Content = layout,
    };
  }

  ……省略……
}
リスト2 ExBoxViewコントロールを2つ配置したページのコード(App.cs)

 このコードを実行すると次の画面のようになる。まだ、角丸や影は描画されていない。

図2 iOSの画面: BoxViewコントロールの表示例 図2 Androidの画面: BoxViewコントロールの表示例
図2 BoxViewコントロールの表示例: iOSの画面 | Androidの画面

3. iOSで角丸および影を描画する

 ExBoxViewSample.iOSプロジェクトに、ExBoxViewRenderer.csファイルを追加し、以下のコードのように実装する。

C#
using System.Drawing;
using CoreGraphics;
using ExBoxViewSample.iOS;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(ExBoxView), typeof(ExBoxViewRenderer))] //←2
namespace ExBoxViewSample.iOS {
  internal class ExBoxViewRenderer : BoxRenderer { //←1
    public override void Draw(CGRect rect) {       //←3
      //base.Draw(rect);                           //←4

      var exBoxView = (ExBoxView) Element;         //←5
      using (var context = UIGraphics.GetCurrentContext()) {   //←6

        var shadowSize = exBoxView.ShadowSize;     //←7
        var blur = shadowSize; 
        var radius = exBoxView.Radius;             //←8

        context.SetFillColor(exBoxView.Color.ToCGColor());     //←9
        var bounds = Bounds.Inset(shadowSize*2, shadowSize*2); //←10
        context.AddPath(CGPath.FromRoundedRect(bounds, radius, radius));
        context.SetShadow(new SizeF(shadowSize, shadowSize), blur); 
        context.DrawPath(CGPathDrawingMode.Fill);
      }
    }
  }
}
リスト3 iOS用のレンダラー実装コード(ExBoxViewRenderer.cs)

 Xamarin.Formsの描画は、各コントロールに用意されているRendererによって行われており、BoxViewの場合はBoxRendererがその役割を担う。1で、BoxRendererクラスを継承してExBoxViewRendererクラスを作成している。これがiOSでの描画を行う。

 2では、ExportRenderer属性によって、「ExBoxViewコントロールの描画にはExBoxViewRendererクラスを使用する」と定義している。Xamarin.Formsのフレームワークは、この属性が定義されていると、コントロールの描画をその属性に指定されたクラスに委譲する。

 BoxRendererクラスにはコントロールの描画を担任するDrawメソッドがあるが、ExBoxViewRendererクラスでこれをoverrideし(3)、デフォルトの描画を無効にすることで(4)、完全に自前で描画を行っている。

 5を見ると分かるように、レンダラークラスでは、Elementプロパティで、Xamarin.Forms側のコントロールが取得できる。そして、2の定義により、その実体は必ずExBoxView型である。

 UIGraphics.GetCurrentContextメソッドでコンテキストを取得し(6)、各種の描画を行っているが、これは、iOSでCoreイメージを描画するコードそのものである。影のサイズ(7)、角丸のサイズ(8)、塗りつぶし色(9)は、それぞれXamarin.Forms側のExBoxViewコントロールのプロパティ値を使用している。また、影を描画するために、矩形(=長方形)のサイズは、その分だけ小さくなっている(10)。

 このコードを実行すると次の画面のようになる。角丸や影が描画されていることを確認できる。

図3 iOSの画面: BoxViewコントロールで角丸や影を描画する例
図3 iOSの画面: BoxViewコントロールで角丸や影を描画する例

角丸および、影のサイズは、ExBoxViewコントロールのデフォルト値である「10」と「5」になっている。

4. Androidで角丸および影を描画する

 Android側の実装もiOS側と同じ要領だ。ExBoxViewSample.Droidプロジェクトに、ExBoxViewRenderer.csファイルを追加し、以下のコードのように実装する。

C#
using Android.Graphics;
using ExBoxViewSample.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(ExBoxView), typeof(ExBoxViewRenderer))]
namespace ExBoxViewSample.Droid {
  internal class ExBoxViewRenderer : BoxRenderer {

    public override void Draw(Canvas canvas) {
      //base.Draw(canvas);

      var exBoxView = (ExBoxView) Element; 

      using (var paint = new Paint()) {

        var shadowSize = exBoxView.ShadowSize;
        var blur = shadowSize;
        var radius = exBoxView.Radius;

        paint.AntiAlias = true; 

        // 影の描画(1
        paint.Color = (Xamarin.Forms.Color.FromRgba(0, 0, 0, 112)).ToAndroid();
        paint.SetMaskFilter(new BlurMaskFilter(blur, BlurMaskFilter.Blur.Normal));
        var rectangle = new RectF(shadowSize, shadowSize, Width - shadowSize, Height - shadowSize);
        canvas.DrawRoundRect(rectangle, radius, radius, paint);

        // 本体の描画(2
        paint.Color = exBoxView.Color.ToAndroid();
        paint.SetMaskFilter(null);
        rectangle = new RectF(0, 0, Width - shadowSize*2, Height - shadowSize*2);
        canvas.DrawRoundRect(rectangle, radius, radius, paint);
      }

    }
  }
}
リスト4 Android用のレンダラー実装コード(ExBoxViewRenderer.cs)

 プラットフォーム固有である描画のコード以外は、iOSとほとんど同じコードである。Androidの描画では、影を描画するAPIが無いため、輪郭をぼかした影部分(1)と、本体(2)を重ねて描画している。

 これにより、Androidでも角丸や影が描画されていることを確認できる。

図4 Androidの画面: BoxViewコントロールで角丸や影を描画する例
図4 Androidの画面: BoxViewコントロールで角丸や影を描画する例

角丸および、影のサイズは、ExBoxViewコントロールのデフォルト値である「10」と「5」になっている。

5. プロパティ値の動的変更

 続いて、スライダー(Slider)コントロールを追加して、角丸サイズのRadiusプロパティを動的に変更してみよう。

 スライダーコントロールを配置するには、App.csファイルを以下のように修正する。

C#
public class App : Application {
  public App() {

    ……省略……

    var sliderRed = new Slider {
      Maximum = 100,
      WidthRequest = 200,
    };
    sliderRed.PropertyChanged += (s, a) => {
      boxViewRed.Radius = (int)sliderRed.Value;   //←1
    };

    var sliderBlue = new Slider {
      Maximum = 100,
      WidthRequest = 200,
    };
    sliderBlue.PropertyChanged += (s, a) => {
      boxViewBlue.Radius = (int)sliderBlue.Value; //←1
    };

    var layout = new AbsoluteLayout();
    layout.Children.Add(sliderRed, new Point(50, 300));  //←2
    layout.Children.Add(sliderBlue, new Point(50, 350)); //←2

    ……省略……
  }
  ……省略……
}
リスト5 Sliderコントロールを2つ追加したページのコード(App.cs)

リスト2の「var layout = new AbsoluteLayout();」の1行を、このように書き換える。

 スライダーコントロールは、先のExBoxViewコントロールの下に配置した(2)。また、スライダーコントロールの変化で、ExBoxViewコントロールのRadiusプロパティを変更している(1)。

6. BindableProperty

 しかし、これだけでは正常に動作しない。結論から言ってしまうと、この実装だと、拡張クラスで追加したプロパティの変化がレンダラー側に伝わっていないのである。

 レンダラー側にプロパティ値の変化を伝えるためには、次のようにプロパティの宣言を修正する必要がある。

C#
public class ExBoxView : BoxView {

  ……省略……

  //public int Radius { get; set; } //角丸のサイズ

  public static readonly BindableProperty RadiusProperty =
    BindableProperty.Create<ExBoxView, int>(p => p.Radius, 20);
  public int Radius {
    get { return (int)GetValue(RadiusProperty); }
    set { SetValue(RadiusProperty, value); }
  }

  ……省略……
}
リスト6 BindablePropertyでRadiusプロパティを実装する(ExBoxView.cs)

 BindablePropertyで実装されたプロパティは、値の変化がレンダラー側に伝えられ、OnElementPropertyChangedメソッドが呼び出される。そのOnElementPropertyChangedメソッドをオーバーライドするには、ExBoxViewRenderer.csファイルは、以下のように修正する。

C#
using System.ComponentModel;
……省略……

internal class ExBoxViewRenderer : BoxRenderer {

  ……省略……

  protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) {
    base.OnElementPropertyChanged(sender, e);
    if (e.PropertyName == "Radius") { //←1
      SetNeedsDisplay();              //←2 再描画
    }
  }
}
リスト7 [iOSの場合]OnElementPropertyChangedメソッドをオーバーライドする(ExBoxViewRenderer.cs)

 Androidの場合(リスト8)もiOSの場合(リスト7)とほぼ同じコードになるが、iOSで再描画を行うSetNeedsDisplayメソッドがAndroidのInvalidateメソッドになる点が異なる。

C#
using System.ComponentModel;
……省略……

internal class ExBoxViewRenderer : BoxRenderer {

  ……省略……

  protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) {
    base.OnElementPropertyChanged(sender, e);
    if (e.PropertyName == "Radius") { //←1
      Invalidate();                   //←2 再描画
    }
  }
}
リスト8 [Androidの場合]OnElementPropertyChangedメソッドをオーバーライドする(ExBoxViewRenderer.cs)

 OnElementPropertyChangedメソッド内で、変化したプロパティ名が「Radius」だった場合(1)、再描画を行うSetNeedsDisplayメソッド(iOSの場合)もしくはInvalidate(Androidの場合)を呼び出す(2)。このことで結果的にDrawメソッドが呼び出されることになる。

 実行すると、スライダーコントロールで角丸のサイズを変更できることが確認できる(図5)。

図5 iOSの画面: スライダーコントロールで角丸のサイズを変更する例 図5 Androidの画面: スライダーコントロールで角丸のサイズを変更する例
図5 スライダーコントロールで角丸のサイズを変更する例: iOSの画面 | Androidの画面

7. まとめ

 今回は、Xamarin.FormsのレンダラーでDrawイベントを処理して、各プラットフォーム固有の描画を行う例を紹介した。この手法を使用すると、各プラットフォームで表現可能な描画は、全てが対応可能となる。

 なおXamarin.Formsは、まだ誕生したばかりなので、仕様変更の可能性がまだ十分に有り得る。実装に際しては、最新の情報を入手することをお勧めする。

【コラム】利用中のXamarin.Formsのバージョン確認

 本記事は、2015年2月5日現在の「Stable」最新バージョンである「Xamarin.Forms 1.3.1.6296」を基に記載している。利用中のバージョンは、Visual Studioの[パッケージ マネージャー コンソール]で、次のコマンドを使って確認できる。

コンソール
PM> Get-Package

Id                           Version        Description/Release Notes
--                           -------        -------------------------
WPtoolkit                    4.2013.08.16   Windows Phone toolkit ....
Xamarin.Android.Support.v4   19.0.2         C# bindings for android  ....
Xamarin.Forms                1.3.1.6296     Build native UIs for iOS,  ....
Visual Studioの[パッケージ マネージャー コンソール]を使って、利用中のバージョンを確認しているところ

※以下では、本稿の前後を合わせて5回分(第32回~第36回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

Xamarin逆引きTips
32. Xamarin.iOSでZipファイルを使用するには?(ZipFileクラス編)

iOSアプリ開発標準のZipArchiveライブラリではなく、.NET標準のZipFileクラス編を使って、ZIPファイルの圧縮・展開を行う方法を解説する。

Xamarin逆引きTips
33. Xamarin Studio/Visual Studioで「Ricty Diminished」プログラミング用フォントを使うには?

「Ricty Diminished」や「Source Code Pro」などのプログラミング用フォントを、Xamarin Studio/Visual Studioのコードエディターのフォントとして設定する方法。

Xamarin逆引きTips
34. 【現在、表示中】≫ Xamarin.FormsでBoxViewコントロールを拡張するには?

四角形を描画するBoxViewコントロールを拡張してネイティブ側で描画することで、角丸・枠線・影付きなどを実現する方法を説明する。

Xamarin逆引きTips
35. Xamarin.Formsでタッチイベントを処理するには?(iOS/Androidの各種ジェスチャー対応)

iOS/Androidにおけるタップやスワイプなどの各種ジェスチャーを、Xamarin.Formsで処理する方法を解説する。

Xamarin逆引きTips
36. Xamarin.Formsでツールバーアイテムによるメニューを設置するには?

PageクラスのToolbarItemsプロパティを使って、画面の上部にツールバー(Android)/ナビゲーションバー(iOS)を表示する方法を解説する。

サイトからのお知らせ

Twitterでつぶやこう!