[Xamarin.Forms] Mapコントロールを使って地図を表示する(Android 6.0のRuntime Permission対応)

はじめに

こんにちは!モバイルアプリサービス部の加藤潤です。
昨年、Xamarinに入門してみましたが、その時の対象はXamarin.iOSだったので今回はXamarin.Formsを触ってみました。
Xamarin.FormsのMapコントロールを使って地図を表示してみました。

この記事でわかること

この記事ではXamarin.Formsを使った以下の方法について記載しています。

  • Mapコントロールを使ってiOS、Androidそれぞれで地図を表示する。
  • 位置情報を使って現在位置を取得し、地図に表示する。
    • Android 6.0のRuntime Permissionに対応

開発環境

環境は以下の通りです。

  • Xamarin Studio Community バージョン 6.1.5(build 0)
  • Xamarin.Forms 2.3.3.180

また、NuGetでインストールしたパッケージは以下の通りです。

パッケージ一覧

  • Xamarin.Forms.Maps 2.3.3.180
  • Xam.Plugin.Geolocator 3.0.4
  • Plugin.Permissions 1.2.1

Xamarin.Formsプロジェクトの作成

まずはXamarin StudioでFormsのプロジェクトを作成します。

Multiplatform > アプリ > Forms Appを選択します。 new_project_001

App Nameを入力し、そのまま「次へ」をクリックします。 new_project_002

プロジェクト作成場所などを確認(必要であれば変更)し、「作成」をクリックします。 new_project_003

以下のようにPCL、Android、iOSのプロジェクトが作成されます。

created_project

必要なパッケージのインストール

PCL、Android、iOSのプロジェクト全てに以下のパッケージをインストールします。

Xamarin.Forms.Maps

Xamarin.FormsのMapコントロールを使うにはXamarin.Forms.Mapsパッケージをインストールする必要があります。
パッケージをインストールするにはプロジェクトのパッケージを右クリックし、「パッケージの追加」を選択します。

add_package

すると、パッケージを検索するダイアログが表示されるので、「Xamarin.Forms.Maps」と入力し、該当パッケージを選択して「Add Package」をクリックします。

xamarin_forms_maps

※ AndroidはGooglePlayServicesのパッケージも必要なのでライセンスに同意する必要があります。 android_add_xamarin_forms_maps

Xam.Plugin.Geolocator

今回はボタンをタップした時に現在位置を取得したいのでXam.Plugin.Geolocatorをインストールします。

xam_plugin_geolocator

Plugin.Permissions

位置情報を使う際、Android 6.0のRuntime Permissionに対応するためPlugin.Permissionsをインストールします。

plugin_permissions

プラットフォーム固有の準備

地図を表示したり、位置情報を使うためにはiOS、Androidそれぞれで固有の準備をする必要があります。

iOS

iOSで位置情報を使うにはInfo.plistに位置情報を使う目的を記述する必要があります。
位置情報をバックグラウンドでも使うのか、アプリを使用中のみ使うのかによって項目が異なります。
今回はアプリを使用中のみ位置情報を使うので「Location When In Use Usage Description」を設定しました。 NSLocationWhenInUseUsageDescription

Android

AndroidでGoogleマップを表示するためにはAPIキーを生成し、アプリに組み込む必要があります。 APIキーの生成方法は下記が詳しいので参照してください。

APIキーを生成したら、以下のようにAndroidManifest.xmlにキーを設定します。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="jp.classmethod.mapapp">
	<application android:label="MapApp">
		<meta-data android:name="com.google.android.maps.v2.API_KEY" android:value="ここに生成したキーを入力する" />
	</application>
	<uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23" />
</manifest>

また、「必要なアクセス許可」も設定します。 Map Controlに記載のある以下の項目を設定しました。

  • AccessCoarseLocation
  • AccessFineLocation
  • AccessLocationExtraCommands
  • AccessMockLocation
  • AccessNetworkState
  • AccessWifiState
  • Internet

required_permissions

マップの初期化

ここまでで準備が終わったのでここからはコードを書いていきます。
NuGetでXamarin.Forms.Mapsパッケージをインストールしましたが、マップを使うには初期化コードがプラットフォーム毎に必要です。

iOS

iOSの場合はAppDelegate.csのFinishedLaunchingメソッド内に初期化コードを記述します。

namespace MapApp.iOS
{
	[Register("AppDelegate")]
	public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
	{
		public override bool FinishedLaunching(UIApplication app, NSDictionary options)
		{
			global::Xamarin.Forms.Forms.Init();

			// マップの初期化コード。これを呼ぶことで、PCL内でXamarin.Forms.Maps APIが使えるようになる。
			global::Xamarin.FormsMaps.Init();

			LoadApplication(new App());

			return base.FinishedLaunching(app, options);
		}
	}
}

Android

Androidの場合はMainActivity.csのOnCreateメソッド内に初期化コードを記述します。

namespace MapApp.Droid
{
	[Activity(Label = "MapApp.Droid", Icon = "@drawable/icon", Theme = "@style/MyTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
	public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
	{
		protected override void OnCreate(Bundle bundle)
		{
			TabLayoutResource = Resource.Layout.Tabbar;
			ToolbarResource = Resource.Layout.Toolbar;

			base.OnCreate(bundle);

			global::Xamarin.Forms.Forms.Init(this, bundle);

			// マップの初期化コード。これを呼ぶことで、PCL内でXamarin.Forms.Maps APIが使えるようになる。
			global::Xamarin.FormsMaps.Init(this, bundle);

			LoadApplication(new App());
		}
	}
}

実装

ここから実際に地図を表示するコードを書いていきます。
Formsなので主な処理はPCLプロジェクト内の初期ページ(今回はMapAppPage)に書いていきます。

MapAppPage.xaml

今回はコントロールをコードで書くのでXAMLの方はトップにContentPageがあるだけで中身は空にしておきます。
※もちろんXAMLに書くことも可能です。

<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:MapApp" x:Class="MapApp.MapAppPage">
</ContentPage>

MapAppPage.xaml.cs

マップの表示は以下のように実装しました。

CheckLocationPermissionStatusAsyncで位置情報のPermissionの状態を取得、確認しています。
※ この部分がAndroid 6.0のRuntime Permissionに対応する部分です。iOSでは位置情報の使用許可ダイアログの表示をOSが自動で行ってくれるのでこの処理は(クラッシュしないという意味で)必須ではありません。

位置情報の使用がユーザーに許可されているかどうかの取得は非同期で行われるため、最初はIsShowingUserをfalseにして現在位置を非表示にしています。

「現在地」ボタンをタップするとgetCurrentUserLocationを呼んで現在位置を取得します。 現在地の緯度経度が取得できたらMapの中心を当該緯度経度に変更しています。

/// <summary>
/// マップの初期位置(東京駅)
/// </summary>
private readonly Position MapInitialPosition = new Position(35.681298, 139.766247);

/// <summary>
/// マップの表示距離
/// </summary>
private readonly Distance MapDistance = Distance.FromMiles(0.3);

public MapAppPage()
{
    InitializeComponent();

    var map = new Map(MapSpan.FromCenterAndRadius(MapInitialPosition, MapDistance))
    {
        IsShowingUser = false,
        VerticalOptions = LayoutOptions.FillAndExpand
    };

    var stack = new StackLayout { Spacing = 0 };
    stack.Children.Add(map);

    var button = new Button() { HorizontalOptions = LayoutOptions.FillAndExpand, HeightRequest = 44, Text = "現在地" };
    button.Clicked += async (sender, e) =>
    {
        var userLocation = await getCurrentUserLocation();
        if (userLocation.HasValue)
        {
            map.IsShowingUser = true;
            map.MoveToRegion(MapSpan.FromCenterAndRadius(userLocation.Value, MapDistance));
        }
    };
    stack.Children.Add(button);
    Content = stack;

    CheckLocationPermissionStatusAsync(map);
}

現在位置を取得するgetCurrentUserLocationメソッドは以下のように実装しました。 await CrossGeolocator.Current.GetPositionAsyncで非同期で現在位置を取得しています。
タイムアウト(ミリ秒指定)は10秒に設定しています。

private async Task<Position?> getCurrentUserLocation()
{
    try
    {
        var location = await CrossGeolocator.Current.GetPositionAsync(10000);
        if (location != null)
            return new Position(location.Latitude, location.Longitude);
        else
            return null;
    }
    catch (Exception ex)
    {
        Debug.WriteLine("Unable to get location, may need to increase timeout: " + ex);
        return null;
    }
}

位置情報のPermissionの状態を取得、確認するCheckLocationPermissionStatusAsyncは以下のように実装しました。 LocationのPermission状態を取得し、許可されていなければユーザーに許可してもらうためにPermissionのリクエストを行います。

private async Task CheckLocationPermissionStatusAsync(Map map)
{
    // LocationのPermission状態を取得
    var status = await CrossPermissions.Current.CheckPermissionStatusAsync(Permission.Location);
    if (status != PermissionStatus.Granted)
    {
        // 許可されていなければユーザーに許可してもらうためにPermissionのリクエストを行う。
        status = (await CrossPermissions.Current.RequestPermissionsAsync(Permission.Location))[Permission.Location];
    }
    if (status == PermissionStatus.Granted)
    {
        // 許可されたらマップ上の現在地を表示する。
        map.IsShowingUser = true;
    }
}

位置情報の使用許可結果をハンドリングする

CrossPermissions.Current.RequestPermissionsAsyncを実行することで許可ダイアログが表示されますが、 その結果(ユーザーが許可したか拒否したか)を受け取るためにMainActivity側でOnRequestPermissionsResultをオーバーライドし、PermissionsImplementation.Current.OnRequestPermissionsResultを実行します。

public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Android.Content.PM.Permission[] grantResults)
{
    // この処理を行うことでPCL側でRequestPermissionの結果が受け取れるようになる。
    PermissionsImplementation.Current.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}

動作確認

iOS

地図が表示されました!

ios_001

Android

ちゃんとPermissionのダイアログ出ました!

android_001

現在地の表示もできています。IsShowingUsertrueにするとマップの右上に標準の現在地ボタンが表示されるみたいなので下に配置した現在地ボタンが微妙ですね...

android_002

おわりに

今回はXamarin.FormsのMapコントロールを使って地図を表示してみました。
ググると記事がいくつか出てくるのですが、Android 6.0のRuntime Permissionに対応したものがなかなか無かったので記事にしてみました。
この記事の内容が少しでも誰かのお役にたてば幸いです。

参考