 
Xamarin逆引きTips
Xamarin.FormsでBoxViewコントロールを拡張するには?
四角形を描画するBoxViewコントロールを拡張してネイティブ側で描画することで、角丸・枠線・影付きなどを実現する方法を説明する。
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)。
 ExBoxViewSampleプロジェクトにExBoxView.csファイルを追加し、以下のコードを追記する。
| 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;   } } | 
 このコードでは、Xamarin.FormsのBoxViewクラスを拡張してExBoxViewクラスを作成し、
- 角丸のサイズを指定するためのRadiusプロパティと
- 影の幅を指定するためのShadowSizeプロパティ
を実装している。
 本稿のサンプルでは、BoxViewを少し重なった状態で2つ配置したページを作成する。そこでApp.csファイルは、以下のように修正する。
| ……省略…… 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,     };   }   ……省略…… } | 
このコードを実行すると次の画面のようになる。まだ、角丸や影は描画されていない。
 
 
3. iOSで角丸および影を描画する
 ExBoxViewSample.iOSプロジェクトに、ExBoxViewRenderer.csファイルを追加し、以下のコードのように実装する。
| 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);       }     }   } } | 
 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)。
このコードを実行すると次の画面のようになる。角丸や影が描画されていることを確認できる。

角丸および、影のサイズは、ExBoxViewコントロールのデフォルト値である「10」と「5」になっている。
4. Androidで角丸および影を描画する
 Android側の実装もiOS側と同じ要領だ。ExBoxViewSample.Droidプロジェクトに、ExBoxViewRenderer.csファイルを追加し、以下のコードのように実装する。
| 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);       }     }   } } | 
プラットフォーム固有である描画のコード以外は、iOSとほとんど同じコードである。Androidの描画では、影を描画するAPIが無いため、輪郭をぼかした影部分(1)と、本体(2)を重ねて描画している。
これにより、Androidでも角丸や影が描画されていることを確認できる。

角丸および、影のサイズは、ExBoxViewコントロールのデフォルト値である「10」と「5」になっている。
5. プロパティ値の動的変更
 続いて、スライダー(Slider)コントロールを追加して、角丸サイズのRadiusプロパティを動的に変更してみよう。
 スライダーコントロールを配置するには、App.csファイルを以下のように修正する。
| 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     ……省略……   }   ……省略…… } | 
リスト2の「var layout = new AbsoluteLayout();」の1行を、このように書き換える。
 スライダーコントロールは、先のExBoxViewコントロールの下に配置した(2)。また、スライダーコントロールの変化で、ExBoxViewコントロールのRadiusプロパティを変更している(1)。
6. BindableProperty
しかし、これだけでは正常に動作しない。結論から言ってしまうと、この実装だと、拡張クラスで追加したプロパティの変化がレンダラー側に伝わっていないのである。
レンダラー側にプロパティ値の変化を伝えるためには、次のようにプロパティの宣言を修正する必要がある。
| 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); }   }   ……省略…… } | 
 BindablePropertyで実装されたプロパティは、値の変化がレンダラー側に伝えられ、OnElementPropertyChangedメソッドが呼び出される。そのOnElementPropertyChangedメソッドをオーバーライドするには、ExBoxViewRenderer.csファイルは、以下のように修正する。
| 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 再描画     }   } } | 
 Androidの場合(リスト8)もiOSの場合(リスト7)とほぼ同じコードになるが、iOSで再描画を行うSetNeedsDisplayメソッドがAndroidのInvalidateメソッドになる点が異なる。
| 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 再描画     }   } } | 
 OnElementPropertyChangedメソッド内で、変化したプロパティ名が「Radius」だった場合(1)、再描画を行うSetNeedsDisplayメソッド(iOSの場合)もしくはInvalidate(Androidの場合)を呼び出す(2)。このことで結果的にDrawメソッドが呼び出されることになる。
実行すると、スライダーコントロールで角丸のサイズを変更できることが確認できる(図5)。
 
 
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,  .... | 
※以下では、本稿の前後を合わせて5回分(第32回~第36回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
 
32. Xamarin.iOSでZipファイルを使用するには?(ZipFileクラス編)
iOSアプリ開発標準のZipArchiveライブラリではなく、.NET標準のZipFileクラス編を使って、ZIPファイルの圧縮・展開を行う方法を解説する。
 
33. Xamarin Studio/Visual Studioで「Ricty Diminished」プログラミング用フォントを使うには?
「Ricty Diminished」や「Source Code Pro」などのプログラミング用フォントを、Xamarin Studio/Visual Studioのコードエディターのフォントとして設定する方法。
 
34. 【現在、表示中】≫ Xamarin.FormsでBoxViewコントロールを拡張するには?
四角形を描画するBoxViewコントロールを拡張してネイティブ側で描画することで、角丸・枠線・影付きなどを実現する方法を説明する。
 
35. Xamarin.Formsでタッチイベントを処理するには?(iOS/Androidの各種ジェスチャー対応)
iOS/Androidにおけるタップやスワイプなどの各種ジェスチャーを、Xamarin.Formsで処理する方法を解説する。
 
36. Xamarin.Formsでツールバーアイテムによるメニューを設置するには?
PageクラスのToolbarItemsプロパティを使って、画面の上部にツールバー(Android)/ナビゲーションバー(iOS)を表示する方法を解説する。





 
 
