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)を表示する方法を解説する。