この記事はXamarin.Formsの標準コントロールだけでは対応しきれなくなった時、ViewRendererを駆使した独自コントロールで乗り越えるためのチュートリアルです。
目次
- Xamarin.Formsコントロールの仕組み
- 独自のコントロールを作る
- Binding可能なプロパティでNaitiveコントロールと連携する
- NativeコントロールからFormsコントロールにメッセージを送る
- FormsコントロールからNativeコントロールにメッセージを送る
- さらなるステップアップ
Xamarin.Formsコントロールの仕組み
最初にXamarin.Formsのコントロールがどのような仕組みで成り立っているか確認しておきましょう。
Xamarin.Formsでは各プラットフォームのコントロールをラップし、抽象化したコントロールを定義する事でView層コードの共通化を可能としています。 (XAMLで記述できますがWPFのように自由なUIレンダリングができるわけではありません)
この記事内では抽象化されたコントロールの事を Formsコントロール 、各プラットフォームのコントロールの事を Nativeコントロールと呼ぶ事にします。そして、FormsコントロールとNativeコントロールを結びつけるのがRendererです。
Rendererの主な役割はNativeコンロールの生成、Formsコントロールのプロパティ値変更をNativeコントロールへ伝達すること、逆にNativeコントロールのイベトをFormsコントロールに伝播させることなどです。
独自のコントロールを作るには、PCLプロジェクトでFomrsコントロールを定義、各プラットフォームプロジェクトでRendererを定義し、必要に応じて各プラットフォームプロジェクトでカスタムコントロールを定義します。
Xamarin.Forms公開当初は、既存コントロールのちょっとしたカスタマイズでもCustom Rendererが推奨されていました(それしか方法がなかったとも言う)、しかし現在ではそういった場合にはEffectsを利用するのが良いでしょう。
独自のコントロールを作る
さて、概要を把握したところで実際に独自のコントロールを作って表示してみましょう。 今回はBoxViewっぽいもの作ります。
※以降の解説コードではPCLプロジェクト構成を前提として、iOSで実装したサンプルを示します。
Formsコントロールの定義
PCLプロジェクトに Xamarin.Forms.View
を継承した MyBoxView
を定義。中身はありません。
using System; using Xamarin.Forms; namespace ViewRendererTutorial { public class MyBoxView : View { } }
Rendererの定義
iOSプロジェクトに MyBoxViewRenderer
を定義します。
using System; using Xamarin.Forms; using Xamarin.Forms.Platform.iOS; using UIKit; using ViewRendererTutorial; using ViewRendererTutorial.iOS; // FormsコントロールとRendererの対応を宣言 [assembly: ExportRenderer (typeof (MyBoxView), typeof (MyBoxViewRenderer))] namespace ViewRendererTutorial.iOS { // ViewRendererの型引数にFormsコントロールとNativeコントロールを与える public class MyBoxViewRenderer : ViewRenderer<MyBoxView, UIView> { protected override void OnElementChanged (ElementChangedEventArgs<MyBoxView> e) { // Nativeコントロールのインスタンス生成はココ! if (Control == null) { var nativeControl = new UIView (); nativeControl.BackgroundColor = Color.Lime.ToUIColor (); SetNativeControl (nativeControl); } base.OnElementChanged (e); } } }
Nativeコンロールのインスタンス生成は OnElementChanged
内で行います。
ButtonRendererやImageRendererなどから派生する場合はこの処理は実装済みですが、ViewRendererから派生する場合は自分で書かなくてはなりません。
これまでの実装で独自のコントロールを表示する事ができます。
<StackLayout VerticalOptions="Center"> <local:MyBoxView x:Name="myBoxView" HeightRequest="100" /> <Label Text="ViewRenderer tutorial" VerticalOptions="Center" HorizontalOptions="Center" /> </StackLayout>
Binding可能なプロパティでNaitiveコントロールと連携する
続いて、FormsコントロールにBindablePropertyを追加してみましょう。(BindingはXAMLの華ですからね!)
BoxViewらしくColorPropertyを追加する事にします。 (実は前段までの実装で、BackGroundColorを変えるだけで同じ事ができますが説明のために目を瞑ってください)
Formsコントロールの定義
中身が空だったMyBoxViewにBindablePropertyを追加します。
using System; using Xamarin.Forms; namespace ViewRendererTutorial { public class MyBoxView : View { // BindablePropertyを追加 public static readonly BindableProperty ColorProperty = BindableProperty.Create (nameof (Color), typeof (Color), typeof (MyBoxView), default (Color), propertyChanged: (bindable, oldValue, newValue) => ((MyBoxView)bindable).Color = (Color)newValue); public Color Color { get { return (Color)GetValue (ColorProperty); } set { SetValue (ColorProperty, value); } } } }
Rendererの定義
using System; using Xamarin.Forms; using Xamarin.Forms.Platform.iOS; using UIKit; using ViewRendererTutorial; using ViewRendererTutorial.iOS; [assembly: ExportRenderer (typeof (MyBoxView), typeof (MyBoxViewRenderer))] namespace ViewRendererTutorial.iOS { public class MyBoxViewRenderer : ViewRenderer<MyBoxView, UIView> { protected override void OnElementChanged (ElementChangedEventArgs<MyBoxView> e) { if (Control == null) { var nativeControl = new UIView (); SetNativeControl (nativeControl); } if (e.NewElement != null) { // Formsコントロールのプロパティ値を反映 UpdateColor (); } base.OnElementChanged (e); } protected override void OnElementPropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e) { base.OnElementPropertyChanged (sender, e); // プロパティ値の変更を反映 if (e.PropertyName == MyBoxView.ColorProperty.PropertyName) { UpdateColor (); } } private void UpdateColor () { if (Element == null) return; Control.BackgroundColor = Element.Color.ToUIColor (); } } }
OnElementChanged
で最初のプロパティ値を反映、 OnElementPropertyChanged
で変更されたプロパティ値をFormsコントロールに反映させればOKです。
<StackLayout VerticalOptions="Center"> <local:MyBoxView x:Name="myBoxView" HeightRequest="100" Color="Lime" /> <Label Text="ViewRenderer tutorial" VerticalOptions="Center" HorizontalOptions="Center" /> <Button Text="Change Color" Clicked="ButtonClicked" /> </StackLayout>
Random random = new Random(); void ButtonClicked (object sender, System.EventArgs e) { myBoxView.Color = Color.FromRgb ( random.Next (255), random.Next (255), random.Next (255)); }
BindablePropertyの変更が反映されていますね!
NativeコントロールからFormsコントロールにメッセージを送る
MyBoxViewにクリック機能を追加してみましょう。 FormsコントロールにClickedイベントを追加し、Naitiveコントロールがハンドルしたユーザー操作を伝播させます。
クリック対応自体はTapGestureRecognizerを使うとFormsコントロール側だけで完結できますが、これから解説する方法は他にも応用が効きますよ。
Formsコントロールの定義
using System; using Xamarin.Forms; using System.Runtime.CompilerServices; // 指定したassemblyにinternal要素へのアクセスを許可する [assembly: InternalsVisibleTo ("ViewRendererTutorial.iOS")] namespace ViewRendererTutorial { public class MyBoxView : View { public static readonly BindableProperty ColorProperty = BindableProperty.Create (nameof (Color), typeof (Color), typeof (MyBoxView), default (Color), propertyChanged: (bindable, oldValue, newValue) => ((MyBoxView)bindable).Color = (Color)newValue); public Color Color { get { return (Color)GetValue (ColorProperty); } set { SetValue (ColorProperty, value); } } // イベントを追加 public event EventHandler Clicked; // Rendererからのシグナルを受け取る internal void SendClicked () { Clicked?.Invoke (this, EventArgs.Empty); } } }
Formsコントロールにイベントを追加、Rendererから発火してもらうために SendClicked
メソッドを用意します。
また、internalメソッドを呼んでもらうために InternalsVisibleToAttribute
でNativeプラットフォームAssemblyからのアクセスを許可します。
このアプローチはXamarin.Forms標準のButton、ButtonRendererのそれとほぼ同じです。
Rendererの定義
using System; using Xamarin.Forms; using Xamarin.Forms.Platform.iOS; using UIKit; using ViewRendererTutorial; using ViewRendererTutorial.iOS; [assembly: ExportRenderer (typeof (MyBoxView), typeof (MyBoxViewRenderer))] namespace ViewRendererTutorial.iOS { public class MyBoxViewRenderer : ViewRenderer<MyBoxView, UIView> { protected override void OnElementChanged (ElementChangedEventArgs<MyBoxView> e) { if (Control == null) { var nativeControl = new UIView (); SetNativeControl (nativeControl); // NativeコントロールがタップされたらFormsコントロールにシグナルを送る nativeControl.AddGestureRecognizer ( new UITapGestureRecognizer (() => Element?.SendClicked ())); } if (e.NewElement != null) { UpdateColor (); } base.OnElementChanged (e); } protected override void OnElementPropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e) { base.OnElementPropertyChanged (sender, e); if (e.PropertyName == MyBoxView.ColorProperty.PropertyName) { UpdateColor (); } } private void UpdateColor () { if (Element == null) return; Control.BackgroundColor = Element.Color.ToUIColor (); } } }
RendererはFormsコントロールの事を知っているのでメソッドを叩くのも容易です。
サンプルでは省略していますが、本番ではDisposeのタイミングでイベントハンドラの解除などをきちんと行いましょう。
<StackLayout VerticalOptions="Center"> <local:MyBoxView x:Name="myBoxView" HeightRequest="100" Color="Lime" Clicked="MyBoxViewClicked" /> <Label Text="ViewRenderer tutorial" VerticalOptions="Center" HorizontalOptions="Center" /> </StackLayout>
void MyBoxViewClicked (object sender, System.EventArgs e) { DisplayAlert ("ViewRendererTutorial", "MyBoxView Clicked.", "OK"); }
クリックイベントをハンドルできました!
FormsコントロールからNativeコントロールにメッセージを送る
先程とは逆にFormsコントロールからNativeコントロールにメッセージを送る場面を考えてみます。
例えばNativeコントロールを操作するAPIを抽象化するケースがそれにあたります。
サンプルとして簡単なMapコントロールを定義し、地図の表示位置を移動させるAPIを実装してみましょう。 (これはXamarin.Forms.Mapsの実装を簡略化したものです)
Formsコントロールの定義
using System; using Xamarin.Forms; namespace ViewRendererTutorial { public class MyMap : View { public void MoveToResion (double latitude, double longitude) { // Rendererにメッセージを送る MessagingCenter.Send (this, "MyMapMoveToRegion", new Tuple<double, double> (latitude, longitude)); } } }
Formsコントロールに抽象化したAPIを定義、内部でMessagingCenterを利用してRendererにメッセージを送ります。
Rendererの定義
using System; using Xamarin.Forms; using Xamarin.Forms.Platform.iOS; using UIKit; using MapKit; using CoreLocation; using ViewRendererTutorial; using ViewRendererTutorial.iOS; [assembly: ExportRenderer (typeof (MyMap), typeof (MyMapRenderer))] namespace ViewRendererTutorial.iOS { public class MyMapRenderer : ViewRenderer<MyMap, MKMapView> { protected override void OnElementChanged (ElementChangedEventArgs<MyMap> e) { if (Control == null) { var nativeControl = new MKMapView (); SetNativeControl (nativeControl); } // Formsコントロールからのメッセージを受け取る MessagingCenter.Subscribe<MyMap, Tuple<double, double>> (this, "MyMapMoveToRegion", (sender, args) => MoveToRegion(args.Item1, args.Item2), Element); base.OnElementChanged (e); } private void MoveToRegion (double latitude, double longitude) { var mapRegion = new MKCoordinateRegion ( new CLLocationCoordinate2D (latitude, longitude), new MKCoordinateSpan (1.0d, 1.0d)); Control.SetRegion (mapRegion, true); } } }
OnElementChanged
でFormsコントロールからのメッセージを購読します。
(本当はElement更新時のSubscribe、UnSubscribe、Dispose時のUnSbscribeをしないとダメですよ)
FormsコントロールからRenderer経由でNativeコントロールを操作できていますね。
さらなるステップアップ
Xamarin.FormsはOSSなので既存のソースコードを見て勉強することができます。
FormsコントロールはXamarin.Forms.Core、RendererはXamarin.Forms.Platform.【プラットフォーム】/Renderers にあります。
Xamarin.Formsのソースを読んでクールなコントロールを作りましょう。