ぴーさんログ

だいたいXamarin.Formsのブログ

【ソース公開】Xamarin.Forms製のAED検索アプリ、iOS版もリリースしました!

Android版に続き、Xamarin.Forms製アプリ「AEDオープンデータ検索」のiOS版をリリースしました!

iOS版のコードも公開中のGitHubリポジトリに含まれていますよ!

https://github.com/P3PPP/XFAedSearch

f:id:ticktack623:20160304165532j:plain

主な機能

近くにあるAEDを地図に表示します。

AED情報のソースは初音 玲さん( @hatsune_ )の AEDオープンデータプラットフォーム です。

f:id:ticktack623:20160304165746p:plain:w220 f:id:ticktack623:20160304165802p:plain:w220

当然といえば当然ですがAndroid版と同じです。

一応Android版のリンクのリンクも

Get it on Google Play

Xamarin.Forms.Mapsのバグを報告しようとしたら既にチケット上がってた

Xamarin.Forms.Maps.Mapのバグを発見しました。

「Xamarin.Forms.Maps.MapのAndroid実装側で GoogleMap.CameraChange イベントにハンドラを追加すると、 Map.VisibleRegion (地図が表示してる範囲の情報)が更新されなくなる」というものバグを報告しようとしましたが、少し前に同様のチケットが登録されていたのでやめました。(バグ報告者になるチャンスを逃した)

せっかくなので確認コード上げときます。

実行するとMap.VisibleRegionの中味が常にnullになります。 ちなみにカスタムRendererの代わりにEffectでやると、中身は入ってるが更新されない状態になります。

ライブラリバージョン

  • Xamarin.Forms 2.0.1
  • Xamarin.Forms.Maps 2.0.1
  • Xamarin.GooglePlayService 26.0.0

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製のアプリをリリースしました (Android版)

先日、Xamarin.Forms製のAED検索アプリをGoogle Playストアにリリースしました。(iOS版は申請作業中)

AEDオープンデータ検索

f:id:ticktack623:20160225143901j:plain:w450

Get it on Google Play

ついでにソースコードGitHubで公開してます。(公開事例ですよー!)

https://github.com/P3PPP/XFAedSearch

主な機能

近くにあるAEDを地図に表示します。

AED情報のソースは初音 玲さん( @hatsune_ )の AEDオープンデータプラットフォーム です。

f:id:ticktack623:20160225144109p:plain:w250  f:id:ticktack623:20160225144125p:plain:w250

Xamarin.Formsで諦めたこと(全画面コンテンツとNavigationBar)

iOSの「マップ」のように、コンテンツを大きく見せるため、画面をタップするとナビゲーションバー(Xamarin.Formsではこの表記)が引っ込むアプリがありますね?

例(iOSの「マップ」アプリ)

f:id:ticktack623:20160222194448g:plain:w250

NavigationPage.SetHasNavigationBar() でナビゲーションバーの有無を切り替えればXamarin.Formsでこれを再現できるのではと考えましたが、結果は残念なものとなりました...

iOS / Android

f:id:ticktack623:20160222194545g:plain:w250 f:id:ticktack623:20160222194625g:plain:w265

ナビゲーションバーが切り替わる際に、画面下部に空白ができているのが分かるでしょうか?

これはNavigationPageの中のPageがナビゲーションバーの高さ分縮小されるためです。

  • バー非表示 → 表示
    1. Pageが縮小される(下に空白ができる)
    2. バーのアニメーションに合わせてPageが移動する
    3. 移動が完了して空白が埋まる
  • バー表示 → 非表示
    1. バーのアニメーションに合わせてPageが移動する
    2. 移動が完了して空白ができる
    3. 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);
}

【Xamarin.Forms】RelativeLayoutで中央に配置する

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設定時に座標を返す式を渡すため)

Xamarin.FormsでTextBlockのような事をする

テキストの一部だけ色やフォントサイズを変えたい時、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>

結果はこんな感じ

f:id:ticktack623:20160211175321j:plain

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

Effectを使ってAndroidのMapViewからボタンを消す

Xamarin.Forms.Maps.Map コントロールを使用する際に現在位置表示、ズーム機能をONにしていると、Android版では地図上にボタンが表示されます。(iOSでは表示されない)

f:id:ticktack623:20160205000621p:plain:w300

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"),
            },
        },
    };
}

見事、地図上のボタンを消せました。

f:id:ticktack623:20160205000656p:plain:w300

参考

stackoverflow.com