iOS
Xaml
Xamarin
Xamarin.Forms

[Xamarin.Forms]NativeのカスタムビューをXamlから使う

はじめに

Xamarin.Formsで、iOSのUIViewを拡張したビューを使いたかったのですが、やり方がよくわからなかったので手順を残しておきます。

用意するクラス

必要なクラスは下記リストのとおり。

  • プラットフォーム共通部分

    • カスタムView (Viewの子クラス)
  • 各プラットフォーム (iOS & Android)

    • カスタムView (iOSならUIViewの子クラス)
    • カスタムRenderer

実装

プラットフォーム共通部分

Viewの子クラスとして作成します。
単純にするためにプロパティなどは何もなしにしています。

CustomView.cs
using Xamarin.Forms;

namespace CustomView.Forms {
    public class CustomView : View {
    }
}

定義したカスタムビューをXamlで使ってみます。

MainPage.xaml
<?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:CustomView.Forms"
             x:Class="CustomView.Forms.MainPage">
    <StackLayout>
        <StackLayout.Margin>
            <!-- iOSはトップに 20 の余白が必要 -->
            <OnPlatform x:TypeArguments="Thickness">
                <On Platform="iOS">0, 20, 0, 0</On>
                <On Platform="Android">0, 0, 0, 0</On>
            </OnPlatform>
        </StackLayout.Margin>

        <Label Text="Custom View Sample" HorizontalOptions="Center" />
        <local:CustomView
            HorizontalOptions="FillAndExpand"
            VerticalOptions="FillAndExpand" />
    </StackLayout>
</ContentPage>

local:CustomView のところでカスタムビューを使っています。

iOS

カスタムビュー

Native側では、Drawメソッドをオーバーライドして好きなように描画します。
ここでは、公式のサンプルを参考に図形を描画してみます。

iOS/NativeCustomView.cs
using UIKit;
using CoreGraphics;

namespace CustomView.Forms.iOS {
    public class NativeCustomView : UIView {
        public override void Draw(CGRect rect) {
            base.Draw(rect);

            using (CGContext g = UIGraphics.GetCurrentContext()) {
                // calc points
                float height = (float)rect.Height / 2;
                float centerX = (float)rect.Width / 2;
                CGPoint p1 = new CGPoint(centerX - height / 2, height / 2 + height);
                CGPoint p2 = new CGPoint(centerX, height / 2);
                CGPoint p3 = new CGPoint(centerX + height / 2, height / 2 + height);

                //set up drawing attributes
                g.SetLineWidth(10);
                UIColor.Blue.SetFill();
                UIColor.Red.SetStroke();

                //create geometry
                var path = new CGPath();
                path.AddLines(new CGPoint[]{
                    p1,
                    p2,
                    p3
                });
                path.CloseSubpath();

                //add geometry to graphics context and draw it
                g.AddPath(path);
                g.DrawPath(CGPathDrawingMode.FillStroke);
            }
        }
    }
}

カスタムレンダラー

Nativeのカスタムビューとプラットフォーム共通のカスタムビューを結びつけるためのRendererを用意します。
Rendererという名前ですが、どちらかというとNativeと共通部分の橋渡し的な役割だと思います。

(プラットフォーム共通の)Viewの初期化時に OnElementChanged が呼ばれるので(複数回呼ばれることもあるらしいので注意)、そこでNativeのカスタムビューを初期化します。

iOS/NativeCustomRenderer.cs
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(CustomView.Forms.CustomView), typeof(CustomView.Forms.iOS.NativeCustomRenderer))]
namespace CustomView.Forms.iOS {
    public class NativeCustomRenderer : ViewRenderer<CustomView, NativeCustomView> {
        NativeCustomView customView; // この例では必要ないが、あとで必要になったときのため。

        protected override void OnElementChanged(ElementChangedEventArgs<CustomView> e) {
            base.OnElementChanged(e);

            if (Control == null) {
                customView = new NativeCustomView();  // Nativeカスタムビューの生成
                SetNativeControl(customView);  // 生成したビューをNativeコントロールとしてセット
            }
            if (e.OldElement != null) {
                // Unsubscribe
                // 何かイベントをSubscribeしているときには、必ずここでUnsubscribeしておく。
            }
            if (e.NewElement != null) {
                // Subscribe
                // 何かイベントをSubscribeしたいときには、ここでSubscribeする。
            }        
        }
    }
}

大事なのは、

[assembly: ExportRenderer(typeof(CustomView.Forms.CustomView), typeof(CustomView.Forms.iOS.NativeCustomRenderer))]

の行で、これで共通のカスタムビューとNativeのレンダラーを結びつけます。
ExportRendererの引数は ExportRenderer(typeof(プラットフォーム共通のカスタムビュークラス), typeof(Nativeのカスタムレンダラークラス)) なので間違えないように注意。

また、ViewRendererの型パラメーターのほうは、ViewRenderer<プラットフォーム共通のカスタムビュークラス, Nativeのカスタムビュークラス> なので混同しないように。

Android

Androidも細かい違いはあるがiOSと同様なので、コードだけ載せておく。
図形の描画は、公式サイトを参考にした。

Droid/NativeCustomView.cs
using Android.Views;
using Android.Content;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Graphics.Drawables.Shapes;

namespace CustomView.Forms.Droid {
    public class NativeCustomView : View {
        private readonly ShapeDrawable _shape;

        public NativeCustomView(Context context) : base(context) {
            var paint = new Paint();
            paint.SetARGB(255, 200, 255, 0);
            paint.SetStyle(Paint.Style.Stroke);
            paint.StrokeWidth = 4;

            _shape = new ShapeDrawable(new OvalShape());
            _shape.Paint.Set(paint);
        }

        protected override void OnDraw(Android.Graphics.Canvas canvas) {
            base.OnDraw(canvas);

            int width = canvas.Width / 2;
            int left = (canvas.Width - width) / 2;
            int top = (canvas.Height - width) / 2;
            _shape.SetBounds(left, top, left + width, top + width);
            _shape.Draw(canvas);
        }
    }
}

カスタムレンダラー

Droid/NativeCustomRenderer.cs
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Android.Content;

[assembly: ExportRenderer(typeof(CustomView.Forms.CustomView), typeof(CustomView.Forms.Droid.NativeCustomRenderer))]
namespace CustomView.Forms.Droid {
    public class NativeCustomRenderer : ViewRenderer<CustomView, NativeCustomView> {
        NativeCustomView customView; // この例では必要ないが、あとで必要になったときのため。

        public NativeCustomRenderer(Context context) : base(context) {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<CustomView> e) {
            base.OnElementChanged(e);

            if (Control == null) {
                customView = new NativeCustomView(Context);
                SetNativeControl(customView);
            }
            if (e.OldElement != null) {
                // Unsubscribe
                // 何かイベントをSubscribeしているときには、必ずここでUnsubscribeしておく。
            }
            if (e.NewElement != null) {
                // Subscribe
                // 何かイベントをSubscribeしたいときには、ここでSubscribeする。
            }
        }
    }
}

ソース

ソースはこちら

参考

Implementing a View