SIN@SAPPOROWORKSの覚書

C#を中心に、夜な夜な試行錯誤したコードの記録です。

Xamarin.Forms BoxView(ボーダや角丸のプロパティを追加して、円を書いてみた) [ 改訂 ]

【 Xamarin 記事一覧 】

本記事は、過去(2014/07/28)に掲載したものの改訂版です。最新版のXamarin.Formsや、Windows Phone への対応が加筆されています。

BoxView は、Xamarin.Formsで提供されているViewのうちの1つで、四角形を描画するコントロールです。
今後、拡張されるのかも知れませんが、現時点(2014.11.08)では、プロパティとして指定できるのは、サイズと塗りつぶしの色のみです。

Xamarin Developers Guide 「Xamarin.Forms BoxView」

今回は、ターゲット毎のレンダラーを拡張して、このBoxViewに枠線(ボーダ)及び角丸を指定できるプロパティを追加してみました。

Android iOS Windows Phone
f:id:furuya02:20141108001437p:plain:w150 f:id:furuya02:20141108001440p:plain:w150 f:id:furuya02:20141108001439p:plain:w150


1 拡張BoxViewの定義

まずは、BoxViewを継承したMyBoxViewの定義です。ボーダの色・線幅、角丸を設定するためのプロパティを追加しました。
また、デフォルトのサイズやレイアウトをコンストラクタで設定しました。(こちらは、単にサンプルを書きやすくするためだけです)

public class MyBoxView : BoxView{

    public Color StrokeColor { get; set; } //ボーダ色
    public Color FillColor { get; set; } //塗りつぶし色
        
    public int LineWidth { get; set; } //ボーダの幅(0px~10px)
    public float Radius {get;set;} //角丸(0%~50%)

    public MyBoxView(Color fillColor, Color strokeColor, int lineWidth, float radius){
        FillColor = fillColor;
        StrokeColor = strokeColor;
        LineWidth = lineWidth;
        Radius = radius;

        //デフォルト値でサイズとレイアウトを設定
        WidthRequest = 100;
        HeightRequest = 100;
        HorizontalOptions = LayoutOptions.Center;
        VerticalOptions = LayoutOptions.Center;
    }
}

2 拡張BoxViewの利用

作成した拡張BoxViewを使用する側のコードは次のとおりです。
色々な、色・線幅・角丸を設定した拡張BoxViewを、スタックレイアウトで縦に並べただけです。

public class App{
    public static Page GetMainPage(){

        var layout = new StackLayout();
        layout.Children.Add(new MyBoxView(Color.Blue, Color.Blue, 0, 0));
        layout.Children.Add(new MyBoxView(Color.Aqua, Color.Navy, 2, 10));
        layout.Children.Add(new MyBoxView(Color.Yellow, Color.Olive, 3, 20));
        layout.Children.Add(new MyBoxView(Color.Red, Color.Purple, 4, 30));
        layout.Children.Add(new MyBoxView(Color.White, Color.Fuschia, 10, 50));
            
        return new ContentPage{
            //iPhone用に上に余白をとる
            Padding = new Thickness(0, Device.OnPlatform(20, 0, 0), 0, 0),
            Content = layout
        };
    }
}


3 レンダラーの定義

最後に、ExportRenderer属性を使用して、各ターゲットごとのレンダラーを定義し、実際の描画を担うDraw()を上書きします。
Draw()の中では、デフォルトの描画をすべて無効にして、自前で描画しています。

※XamarinFormsの各コントロールなどのレンダラークラスをまとめたBlogがあります。
http://magenic.com/Blog/PostId/31/xamarin-xamarinforms-renderer-reference

(1)Android

//MyBoxViewのレンダラーをMyBoxViewRendererに変更する
[assembly: ExportRenderer(typeof(MyBoxView), typeof(MyBoxViewRenderer))] 

namespace App1.Droid {
    class MyBoxViewRenderer :BoxRenderer{
        public override void Draw(Canvas canvas){

            //デフォルトの描画を無効にする
            //base.Draw(canvas);
            
            //モデルオブジェクトの取得
            var myBoxView = (MyBoxView)Element; //Xamarin.Forms 1.0.6186 では、Modelでした

            using (var paint = new Paint()) {
                //ボーダの分だけ四角のサイズを小さくする
                var rectangle = new RectF(myBoxView.LineWidth, myBoxView.LineWidth, Width - myBoxView.LineWidth, Height - myBoxView.LineWidth);
                //サイズ(幅)の半分を50%として、radiusを求める
                var radius = (float)((Width / 2) * (myBoxView.Radius / 50));

                //塗りつぶしの色を指定
                paint.Color = myBoxView.FillColor.ToAndroid();
                //四角形描画(塗りつぶし)
                canvas.DrawRoundRect(rectangle, radius, radius, paint);

                //塗りつぶしなし
                paint.SetStyle(Paint.Style.Stroke);
                //ボーダの幅を指定
                paint.StrokeWidth = myBoxView.LineWidth;
                //ボーダの色を指定
                paint.Color = myBoxView.StrokeColor.ToAndroid();
                //アンチエイリアス有効
                paint.AntiAlias = true;
                //四角形描画(ボーダのみ)
                canvas.DrawRoundRect(rectangle, radius, radius, paint);
            }
        }
    }
}

(2)iOS

//MyBoxViewのレンダラーをMyBoxViewRendererに変更する
[assembly: ExportRenderer(typeof(MyBoxView), typeof(MyBoxViewRenderer))] 

namespace App1.iOS {
    class MyBoxViewRenderer :BoxRenderer{

        public override void Draw(RectangleF rect){
            
            //デフォルトの描画を無効にする
            //base.Draw(rect); 
            
            //モデルオブジェクトの取得
            var myBoxView = (MyBoxView)Element; //Xamarin.Forms 1.0.6186 では、Modelでした
            using (var context = UIGraphics.GetCurrentContext()) {
                //塗りつぶしの色を指定
                context.SetFillColor(myBoxView.FillColor.ToCGColor());
                //ボーダの色を指定
                context.SetStrokeColor(myBoxView.StrokeColor.ToCGColor());
                //ボーダの幅を指定
                context.SetLineWidth(myBoxView.LineWidth);
                //ボーダの分だけ四角のサイズを小さくする
                var rectangle = Bounds.Inset(myBoxView.LineWidth, myBoxView.LineWidth);
                //サイズ(幅)の半分を50%として、radiusを求める
                var radius = (float)((rectangle.Width / 2) * (myBoxView.Radius / 50));
                //描画
                var path = CGPath.FromRoundedRect(rectangle, radius, radius);
                context.AddPath(path);
                context.DrawPath(CGPathDrawingMode.FillStroke);
            }
        }
    }
}


(3)Windows Phone

WindowsPhoneのBoxViewのレンダラーは、BoxViewRenderer(Xamarin.Forms 1.0.6186では使用できません)なのですが、残念ながらBoxViewRendererには、Drawメソッドがありません。


f:id:furuya02:20141108002602p:plain:w400:left

仕方がないので、ViewRendererを使用し、新たにユーザコントロールを作成して、これを使用することにしました。

ユーザコントロールの作成
f:id:furuya02:20141108003322p:plain:w400:left
最初に、プロジェクトに「Windows Phone ユーザコントロール」を追加します。ここでは、名前をMyBoxViewControl.xamlとしました。雛形として生成されたXAMLは、Gridのみになっていますが、これをCanvasに変更し「MyBoxViewCanvas」という名前をつけました。


<UserControl x:Class="App1.WinPhone.MyBoxViewControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    d:DesignHeight="480" d:DesignWidth="480">
    
    <!--
    <Grid x:Name="LayoutRoot" Background="{StaticResource PhoneChromeBrush}">
    </Grid>
    -->
    <Canvas x:Name="MyBoxViewCanvas"
            Background="Transparent"
            HorizontalAlignment="Stretch"
            VerticalAlignment="Stretch" />

</UserControl>

レンダラーの定義
ViewRendererでは、SetNativeControl()で作成したユーザコントロールをセットします。
ユーザコントロールの描画は、プロパティが変更された時にキックされるよう、OnElementPropertyChanged()をオーバーライドして呼び出しました。
なお、Forms側のMyBoxViewへのポインタは、ユーザコントロールのコンストラクタで送りました。

[assembly: ExportRenderer(typeof(MyBoxView), typeof(MyBoxViewRenderer))] 
namespace App1.WinPhone {
    class MyBoxViewRenderer : ViewRenderer<MyBoxView, MyBoxViewControl> {
        protected override void OnElementChanged(ElementChangedEventArgs<MyBoxView> e){
            base.OnElementChanged(e);
            
            //独自のユーザコントロールをセットする
            SetNativeControl(new MyBoxViewControl(Element));//パラメータにFormsのコントロールも渡しておく
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) {
            base.OnElementPropertyChanged(sender, e);
         if (e.PropertyName == "Height") { //プロパティHeight若しくはWidthが変更された時、再描画する
                Control.Draw();//再描画メソッド
            }
        }
    }
}


ユーザコントロールの描画
ユーザコントロールの描画は、独自のメソッド(Draw)を定義しています。
下記は、コンストラクタを含めたユーザコントロールの全コードです。

namespace App1.WinPhone {
    public partial class MyBoxViewControl : UserControl{
        private readonly MyBoxView _myBoxView;
        public MyBoxViewControl(MyBoxView myBoxView) {
            InitializeComponent();

            _myBoxView = myBoxView;//Formsのコントロールへのポインタを保存しておく
        }

        public void Draw(){
            //塗りつぶし色
            var fillColor = Color.FromArgb(
                            (byte)(_myBoxView.FillColor.A * 255),
                            (byte)(_myBoxView.FillColor.R * 255),
                            (byte)(_myBoxView.FillColor.G * 255),
                            (byte)(_myBoxView.FillColor.B * 255));
            //ボーダ色
            var strokeColor = Color.FromArgb(
                            (byte)(_myBoxView.StrokeColor.A * 255),
                            (byte)(_myBoxView.StrokeColor.R * 255),
                            (byte)(_myBoxView.StrokeColor.G * 255),
                            (byte)(_myBoxView.StrokeColor.B * 255));


            var rect = new System.Windows.Shapes.Rectangle();
            rect.Fill = new SolidColorBrush(fillColor);//塗りつぶし色
            rect.Stroke = new SolidColorBrush(strokeColor);//ボーダ色
            rect.Width = _myBoxView.Width;
            rect.Height = _myBoxView.Height;
            //サイズ(幅)の半分を50%として、radiusを求める
            var radius = (float)((rect.Width / 2) * (_myBoxView.Radius / 50));
            rect.RadiusX = radius;
            rect.RadiusY = radius;
            rect.StrokeThickness = _myBoxView.LineWidth;//ボーダの幅

            MyBoxViewCanvas.Children.Add(rect);

        }
    }
}


4 プロパティ値の動的変更

Android iOS Windows Phone
f:id:furuya02:20141108011257p:plain:w150 f:id:furuya02:20141108011258p:plain:w150 f:id:furuya02:20141108011300p:plain:w150


続いて、スライダーでプロパティ値を変更するサンプルを作成してみました。
今度は、拡張BoxViewは1つで、その下にスライダーを配置して、線幅(LineWidth)及び角丸(Radius)を変化させれるようにしてみました。

public class App{
    public static Page GetMainPage(){
            
        var layout = new StackLayout();
        
        //1つだけ拡張BoxViewを生成する
        var boxView = new MyBoxView(Color.Lime, Color.Accent, 0, 0);
        layout.Children.Add(boxView);

        //Radiusの値をスライダーで変更する
        var slider1 = new Slider{ Maximum = 50, Minimum = 0 };
        var label1 = new Label();
        slider1.ValueChanged += (s, a) => {
            boxView.Radius = (int) slider1.Value; //スライダー値でRadius値を設定する
            label1.Text = string.Format("Radius={0}", boxView.Radius);
        };
        layout.Children.Add(label1);
        layout.Children.Add(slider1);

        //LineWidthの値をスライダーで変更する
        var slider2 = new Slider{ Maximum = 10, Minimum = 0 };
        var label2 = new Label();
        slider2.ValueChanged += (s, a) => {
            boxView.LineWidth = (int)slider2.Value;//スライダー値でLineWidth値を設定する
            label2.Text = string.Format("LineWidth={0}", boxView.LineWidth);
        };
        layout.Children.Add(label2);
        layout.Children.Add(slider2);

        return new ContentPage{
            //iPhone用に上に余白をとる
            Padding = new Thickness(0, Device.OnPlatform(20, 0, 0), 0, 0),
            Content = layout
        };
    }
}

5 OnPropertyChangedのコール

しかし、現在の実装では、スライダーの変化で描画の変化は起こりません。理由は、拡張BoxViewで新設したプロパティ値を変化しても、基底クラスにそれが伝わらないためです。
そこで、プロパティ(LineWidth及びRadius)の実装を修正して、set時に、OnPropertyChangedを呼び出すようにしました。

public class MyBoxView : BoxView{

    // ------ 略 -----
        
    //public int LineWidth { get; set; } //ボーダの幅(0px~10px)
    //public float Radius {get;set;} //角丸(0%~50%)

    private float _radius;//角丸(0%~50%)
    public float Radius {
        set{
            //Radiusが変化した場合は、基底クラス(BoxView)のOnHandlePropertyChangedを呼び出す
            base.OnPropertyChanged("Radius");
            _radius = value;
        }
        get { return _radius; }
    }
    private float _lineWidth;//ボーダの幅(0px~5px)
    public float LineWidth {
        set {
            //LineWidthが変化した場合は、基底クラス(BoxView)のOnHandlePropertyChangedを呼び出す
            base.OnPropertyChanged("LineWidth");
            _lineWidth = value;
        }
        get {
            return _lineWidth;
        }
    } 

    public MyBoxView(Color fillColor, Color strokeColor, int lineWidth, float radius){

        // ------ 略 -----

    }
}


6 OnElementPropertyChangedのオーバーライド

続いて、各ターゲット側でも、OnPropertyChangedがコールされた際に、再描画を行うように、OnElementPropertyChangedのオーバーライドを追加します。(Xamarin.Forms 1.0.6186 では、OnHandlePropertyChangedでした)

(1)Android

namespace App1.Droid {
    class MyBoxViewRenderer :BoxRenderer{
        
        // ------ 略 -----

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e){
            base.OnElementPropertyChanged(sender, e);

            //Radius及びLineWidthプロパティが変更された時、再描画する
            if (e.PropertyName == "Radius" || e.PropertyName == "LineWidth") {
                Invalidate(); //再描画
            }
        }
    }
}

(2)iOS

namespace App1.iOS {
    class MyBoxViewRenderer :BoxRenderer{

        // ------ 略 -----

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e){
            base.OnElementPropertyChanged(sender, e);

            //Radius及びLineWidthプロパティが変更された時、再描画する
            if (e.PropertyName == "Radius" || e.PropertyName == "LineWidth") {
                SetNeedsDisplay(); //再描画
            }
        }
    }
}

(3)Windows Phone

namespace App1.WinPhone {
    class MyBoxViewRenderer : ViewRenderer<MyBoxView, MyBoxViewControl> {

        // ------ 略 -----

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) {
            base.OnElementPropertyChanged(sender, e);
            
            if (e.PropertyName == "Radius" || e.PropertyName=="LineWidth"){
            //if (e.PropertyName == "Height") { //プロパティHeight若しくはWidthが変更された時、再描画する
                Control.Draw();//再描画メソッド
            }
        }
    }
}


【 Xamarin 記事一覧 】