Xamarin.Forms 2.1.0 がリリースされたのでEffectを利用したトリックを解説します。
今回は Xamarin.Forms.Maps.Map
(にアタッチしたBehavior)に地図のタップイベントを生やすというものです。
同様のことがカスタムMapクラス&カスタムRendererで実現できますが、これから紹介する手法の方が再利用製に優れます。
(既存のカスタムMapライブラリに対しても適用できたり) 一方で、Effectを使う都合上インスタンス生成に介入できない点には注意が必要です。
地図を使うための準備(AndroidManifestやAPIキー、info.plist等)については割愛します。
(大体はここに書いてある→『 Map Control - Xamarin 』)
作るもの
- MapExtensionBehavior
- Mapクラスの拡張機能を提供するクラス、タップイベントはここに生やす
- MapEffect (iOS/Android)
- Effectの実装クラス、ネイティブのイベントをハンドルする
コード
今回のサンプルはSharedプロジェクト構成です。
PCLの場合は System.Runtime.CompilerServices.InternalsVisibleToAttribute
を使うなどする必要があります。多分。
MapExtensionBehavior
Sharedプロジェクトに追加します。
using System;
using Xamarin.Forms;
using Xamarin.Forms.Maps;
namespace EffectsTric
{
public class MapExtensionBehavior : Behavior<Map>
{
private readonly string effectId = "MapExtensions.MapEffect";
private Effect effect;
private IMapEffect mapEffect;
protected override void OnAttachedTo(Map bindable)
{
base.OnAttachedTo(bindable);
if(bindable as Map == null)
return;
effect = Effect.Resolve(effectId);
mapEffect = effect as IMapEffect;
mapEffect.MapTapped += (sender, e) =>
MapTapped?.Invoke(this, new MapTappedEventArgs(e.Position));
bindable.Effects.Add (effect);
}
protected override void OnDetachingFrom(Map bindable)
{
bindable?.Effects?.Remove(effect);
effect = null;
mapEffect = null;
base.OnDetachingFrom(bindable);
}
public event EventHandler<MapTappedEventArgs> MapTapped;
internal interface IMapEffect
{
event EventHandler<MapTappedEventArgs> MapTapped;
}
}
[Serializable]
public sealed class MapTappedEventArgs : EventArgs
{
public Position Position
{
get;
}
public MapTappedEventArgs(Position position)
{
Position = position;
}
}
}
MapEffect (iOS)
iOSプロジェクトに追加します。
using System;
using System.Linq;
using Xamarin.Forms;
using Xamarin.Forms.Maps;
using Xamarin.Forms.Platform.iOS;
using MapKit;
using CoreLocation;
using UIKit;
using EffectsTric;
[assembly: ResolutionGroupName ("MapExtensions")]
[assembly: ExportEffect (typeof (EffectsTric.iOS.MapEffect), "MapEffect")]
namespace EffectsTric.iOS
{
public class MapEffect : PlatformEffect, MapExtensionBehavior.IMapEffect
{
private MKMapView mapView;
private MapExtensionBehavior behavior;
private UITapGestureRecognizer tapGesture;
#region implemented abstract members of Effect
protected override void OnAttached()
{
mapView = Control as MKMapView;
if(mapView == null)
return;
behavior = (Element as Map)?.Behaviors?.OfType<MapExtensionBehavior>()?.FirstOrDefault();
if(behavior == null)
return;
tapGesture = new UITapGestureRecognizer((recognizer) =>
{
var point = recognizer.LocationInView(mapView);
var coordinate = mapView.ConvertPoint(point, mapView);
MapTapped?.Invoke(this, new MapTappedEventArgs(new Position(
coordinate.Latitude, coordinate.Longitude)));
})
{
NumberOfTapsRequired = 1,
};
mapView.AddGestureRecognizer(tapGesture);
}
protected override void OnDetached()
{
mapView.RemoveGestureRecognizer(tapGesture);
tapGesture.Dispose();
tapGesture = null;
mapView = null;
behavior = null;
}
#endregion
public event EventHandler<MapTappedEventArgs> MapTapped;
}
}
MapEffect (Android)
Androidプロジェクトに追加します。
using System;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Android.Gms.Maps;
using Xamarin.Forms.Maps;
using System.Linq;
using EffectsTric;
[assembly: ResolutionGroupName ("MapExtensions")]
[assembly: ExportEffect (typeof (EffectsTric.Droid.MapEffect), "MapEffect")]
namespace EffectsTric.Droid
{
public class MapEffect : PlatformEffect, MapExtensionBehavior.IMapEffect
{
public event EventHandler<MapTappedEventArgs> MapTapped;
private GoogleMap googleMap;
private MapExtensionBehavior behavior;
#region implemented abstract members of Effect
protected override void OnAttached()
{
var mapView = Control as MapView;
if(mapView == null)
return;
behavior = (Element as Map)?.Behaviors?.OfType<MapExtensionBehavior>()?.FirstOrDefault();
if(behavior == null)
return;
var callback = new OnMapReadyCallback();
callback.OnMapReadyAction = (gMap) => {
googleMap = gMap;
googleMap.MapClick += GoogleMap_MapClick;
};
mapView.GetMapAsync(callback);
}
protected override void OnDetached()
{
if(googleMap != null)
{
googleMap.MapClick -= GoogleMap_MapClick;
googleMap = null;
}
}
#endregion
void GoogleMap_MapClick (object sender, GoogleMap.MapClickEventArgs e)
{
MapTapped?.Invoke(this, new MapTappedEventArgs(
new Position(e.Point.Latitude,
e.Point.Longitude)));
}
class OnMapReadyCallback : Java.Lang.Object, IOnMapReadyCallback
{
public Action<GoogleMap> OnMapReadyAction;
public void OnMapReady(GoogleMap googleMap)
{
OnMapReadyAction?.Invoke(googleMap);
}
}
}
}
動作サンプル
使用側のサンプルコード
public App()
{
var label = new Label {
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center,
InputTransparent = true,
BackgroundColor = Color.Gray.MultiplyAlpha(0.3),
TextColor = Color.Black,
};
var behavior = new MapExtensionBehavior();
behavior.MapTapped += (sender, e) => {
label.Text = e.Position.Latitude + ", " + e.Position.Longitude;
};
var map = new Map {
IsShowingUser = true,
Behaviors = {
behavior
},
};
var position = new Position(35.675889, 139.744972);
map.MoveToRegion(new MapSpan(
position, 0.8, 0.8));
MainPage = new ContentPage {
Content = new Grid {
Children = {
map,
label,
},
},
};
}
Androidのエミュレータで地図使えるようにするのはちょっと大変なので動画はiOSだけ。