SIN@SAPPOROWORKSの覚書

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

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

【 Xamarin 記事一覧 】

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


Xamarin Developers Guide 「Xamarin.Forms BoxView」

今回は、ターゲット毎のレンダラーを拡張して、このBoxViewに枠線(ボーダ)及び角丸を指定できるプロパティを追加してみました。(※実装は、iOS及びAndroidのみです)
f:id:furuya02:20140728012425p:plain:w150:leftf:id:furuya02:20140728012423p:plain:w150:left


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()の中では、デフォルトの描画をすべて無効にして、全部、自前で描画しています。

(1)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)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);
            }
        }
    }
}

(2)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)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);
            }
        }
    }
}


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

f:id:furuya02:20140728013434p:plain:w150:leftf:id:furuya02:20140728013435p:plain:w150:left
続いて、スライダーでプロパティ値を変更するサンプルを作成してみました。
今度は、拡張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 OnHandlePropertyChangedのオーバーライド

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

(1)iOS

namespace App1.iOS {
    class MyBoxViewRenderer :BoxRenderer{

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

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

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

(2)Android

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

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

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

【 Xamarin 記事一覧 】