Android版に続き、Xamarin.Forms製アプリ「AEDオープンデータ検索」のiOS版をリリースしました!
iOS版のコードも公開中のGitHubリポジトリに含まれていますよ!
https://github.com/P3PPP/XFAedSearch
主な機能
近くにあるAEDを地図に表示します。
AED情報のソースは初音 玲さん( @hatsune_ )の AEDオープンデータプラットフォーム です。
当然といえば当然ですがAndroid版と同じです。
Xamarin.Forms.Maps.Mapのバグを発見しました。
AED検索アプリ作ってる途中で見つけた Xamarin.Forms.Maps のバグ、再現プロジェクト作って報告しようとしたら既に同様のチケットがあった。https://t.co/mYshXKozr4
— たまにXamarin.Forms解説マン (@ticktackmobile) 2016年2月26日
「Xamarin.Forms.Maps.MapのAndroid実装側で GoogleMap.CameraChange
イベントにハンドラを追加すると、 Map.VisibleRegion
(地図が表示してる範囲の情報)が更新されなくなる」というものバグを報告しようとしましたが、少し前に同様のチケットが登録されていたのでやめました。(バグ報告者になるチャンスを逃した)
せっかくなので確認コード上げときます。
実行するとMap.VisibleRegionの中味が常にnullになります。 ちなみにカスタムRendererの代わりにEffectでやると、中身は入ってるが更新されない状態になります。
ライブラリバージョン
AndroidManifest等、地図を使うのに必要な準備は終わってるものとします。
Shared / PCL プロジェクトのコード
using System; using Xamarin.Forms; using Xamarin.Forms.Maps; namespace XFApp17 { public class MyMap : Map { } public class App : Application { public App() { var map = new MyMap(); var label = new Label { HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, BackgroundColor = Color.Silver, TextColor = Color.White, InputTransparent = false, }; var button = new Button { Text = "Show VisibleRegion", HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, }; button.Clicked += (sender, e) => { if(map.VisibleRegion == null) { label.Text = "MyMap.VisibleRegion is null."; } else { label.Text = $"Latitude:{map.VisibleRegion.Center.Latitude}," + Environment.NewLine + $"Longitude:{map.VisibleRegion.Center.Longitude}, " + Environment.NewLine + $"Radius:{map.VisibleRegion.Radius.Meters}"; } }; MainPage = new ContentPage { Content = new Grid { Children = { map, label, button, }, }, }; } } }
Androidプロジェクトのコード
using System; using Xamarin.Forms; using Xamarin.Forms.Platform.Android; using Xamarin.Forms.Maps; using Xamarin.Forms.Maps.Android; using Android.Gms.Maps; using XFApp17; using XFApp17.Droid; [assembly:ExportRenderer (typeof(MyMap), typeof(MyMapRenderer))] namespace XFApp17.Droid { public class MyMapRenderer : MapRenderer, IOnMapReadyCallback { GoogleMap map; protected override void OnElementChanged(ElementChangedEventArgs<View> e) { base.OnElementChanged(e); if(e.OldElement != null) { map.CameraChange -= OnCameraChange; } if(e.NewElement != null) { ((MapView)Control).GetMapAsync(this); } } public void OnMapReady (GoogleMap googleMap) { map = googleMap; map.CameraChange += OnCameraChange; } void OnCameraChange (object sender, GoogleMap.CameraChangeEventArgs e) { Console.WriteLine("OnCameraChange"); } } }
先日、Xamarin.Forms製のAED検索アプリをGoogle Playストアにリリースしました。(iOS版は申請作業中)
AEDオープンデータ検索
ついでにソースコードもGitHubで公開してます。(公開事例ですよー!)
https://github.com/P3PPP/XFAedSearch
近くにあるAEDを地図に表示します。
AED情報のソースは初音 玲さん( @hatsune_ )の AEDオープンデータプラットフォーム です。
iOSの「マップ」のように、コンテンツを大きく見せるため、画面をタップするとナビゲーションバー(Xamarin.Formsではこの表記)が引っ込むアプリがありますね?
例(iOSの「マップ」アプリ)
NavigationPage.SetHasNavigationBar()
でナビゲーションバーの有無を切り替えればXamarin.Formsでこれを再現できるのではと考えましたが、結果は残念なものとなりました...
ナビゲーションバーが切り替わる際に、画面下部に空白ができているのが分かるでしょうか?
これはNavigationPageの中のPageがナビゲーションバーの高さ分縮小されるためです。
Pageのサイズが変わってしまっており、レイアウト調整などでは解決できないため諦めることにしました。 (頑張るならNavigationPageのカスタムRendererだろうか...)
再現コード
public App() { var boxView = new BoxView { Color = Color.Olive, }; var page = new ContentPage { Content = boxView, Title = "Title", }; boxView.GestureRecognizers.Add( new TapGestureRecognizer((_) => NavigationPage.SetHasNavigationBar(page, !NavigationPage.GetHasNavigationBar(page)) )); MainPage = new NavigationPage(page); }
RelativeLayout
の子要素に XConstraint
, YConstraint
Attached Property をセットすることでレイアウト位置を調整できます。
これはXY座標、つまり左上原点の指定となるため中央に配置するには一手間必要です。(AbsoluteLayoutの場合はいい感じに中央配置しれくるんですけどねー)
<ActivityIndicator HeightRequest="44" WidthRequest="44" RelativeLayout.XConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.5, Constant=-22}" RelativeLayout.YConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.5, Constant-22}" />
Factor=0.5
指定でViewの左上がRelativeLayoutの中央となるので Constant=-22
で幅高さの半分ずらす。
<Label x:Name="button" Text="{Binding Hoge}" RelativeLayout.XConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Width, Factor=0.5}" RelativeLayout.YConstraint = "{ConstraintExpression Type=RelativeToParent, Property=Height, Factor=0.5}" />
// Pageのコンストラクタ InitializeComponent (); // 中央揃えに見せるトリック button.SizeChanged += (sender, e) => { button.TranslationX = -(button.Width / 2); button.TranslationY = -(button.Height / 2); };
C#側でSizeChangedイベントをハンドリングし、TranslationX / Y を幅高さの半分ずらす。 XAMLで完結させたい場合は、この処理をBehaviorにまとめる。
RelativeLayoutへの追加、Constraintの設定を全てC#で行えば問題なく中央に配置できる。(Constraint設定時に座標を返す式を渡すため)
テキストの一部だけ色やフォントサイズを変えたい時、WPFならTextBlockを使いますね?
Xamarin.Formsでは Label.FormattedText
で実現できます。
XAMLで書くとこんな感じ
<Label> <Label.FormattedText> <FormattedString> <Span Text="名前:" ForegroundColor="Gray"/> <Span Text="あべなな" FontSize="Large" ForegroundColor="Maroon"/> <Span Text=" さん"/> <Span Text="{x:Static x:Environment.NewLine}" /> <Span Text="年齢:" ForegroundColor="Gray"/> <Span Text="17" FontSize="Large" FontAttributes="Bold" ForegroundColor="Maroon"/> <Span Text="歳" /> </FormattedString> </Label.FormattedText> </Label>
結果はこんな感じ
C#で書くとこんな感じ
new Label { FormattedText = new FormattedString { Spans = { new Span { Text = "名前:", ForegroundColor = Color.Gray }, new Span { Text = "あべなな", FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)), ForegroundColor = Color.Maroon }, new Span { Text = " さん" }, new Span { Text = System.Environment.NewLine }, new Span { Text = "年齢:", ForegroundColor = Color.Gray }, new Span { Text = "17", FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label)), FontAttributes = FontAttributes.Bold, ForegroundColor = Color.Maroon }, new Span { Text = " 歳" }, }, }, };
SpanクラスはBindableObjectではないのでBindningが使えません。のでUserVoiceに投票して待ちましょう。 あるいはサンプルに書いたようにStatic Propertyで何とかするぐらいでしょうか。
Extend Span with bindable Text and TapGesture – Customer Feedback for Xamarin Platform
Unseal Span or Make it Bindable – Customer Feedback for Xamarin Platform
Xamarin.Forms.Maps.Map
コントロールを使用する際に現在位置表示、ズーム機能をONにしていると、Android版では地図上にボタンが表示されます。(iOSでは表示されない)
iOSと揃えたい、デザインが他と馴染まないので自前で用意したいという場合は邪魔になるので、Effectでネイティブコントロールに介入して消してしまいましょう。
Androidプロジェクト内に新しいEffectクラスを作ります。
using System; using Xamarin.Forms; using Xamarin.Forms.Platform.Android; using Android.Gms.Maps; [assembly: ResolutionGroupName ("YourCompany")] [assembly: ExportEffect (typeof (SuppressMapButton.Droid.SuppressMapButtonEffect), "SuppressMapButtonEffect")] namespace SuppressMapButton.Droid { public class SuppressMapButtonEffect : PlatformEffect { #region implemented abstract members of Effect protected override void OnAttached() { var mapView = Control as MapView; if(mapView == null) return; // ズームボタン LinearLayout var zoomButton = mapView.FindViewById(1); if(zoomButton != null) { zoomButton.Visibility = Android.Views.ViewStates.Gone; } // 現在位置ボタン ImageView var locationButton = mapView.FindViewById(2); if(locationButton != null) { locationButton.Visibility = Android.Views.ViewStates.Gone; } } protected override void OnDetached() { } #endregion } }
2016年2月現在では Id:1 がズームボタン、 Id:2 が現在位置ボタンとなっていました。 値の根拠はmapViewStyleになると思うのですがソースを見つけられなかったので確認できてません。
使用する側はこんな感じ。
public App() { MainPage = new ContentPage { Content = new Xamarin.Forms.Maps.Map { IsShowingUser = true, HasZoomEnabled = true, Effects = { Effect.Resolve("YourCompany.SuppressMapButtonEffect"), }, }, }; }
見事、地図上のボタンを消せました。