[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を選択します。
App Nameを入力し、そのまま「次へ」をクリックします。
プロジェクト作成場所などを確認(必要であれば変更)し、「作成」をクリックします。
以下のようにPCL、Android、iOSのプロジェクトが作成されます。
必要なパッケージのインストール
PCL、Android、iOSのプロジェクト全てに以下のパッケージをインストールします。
Xamarin.Forms.Maps
Xamarin.FormsのMapコントロールを使うにはXamarin.Forms.Mapsパッケージをインストールする必要があります。
パッケージをインストールするにはプロジェクトのパッケージを右クリックし、「パッケージの追加」を選択します。
すると、パッケージを検索するダイアログが表示されるので、「Xamarin.Forms.Maps」と入力し、該当パッケージを選択して「Add Package」をクリックします。
※ AndroidはGooglePlayServicesのパッケージも必要なのでライセンスに同意する必要があります。
Xam.Plugin.Geolocator
今回はボタンをタップした時に現在位置を取得したいのでXam.Plugin.Geolocatorをインストールします。
Plugin.Permissions
位置情報を使う際、Android 6.0のRuntime Permissionに対応するためPlugin.Permissionsをインストールします。
プラットフォーム固有の準備
地図を表示したり、位置情報を使うためにはiOS、Androidそれぞれで固有の準備をする必要があります。
iOS
iOSで位置情報を使うにはInfo.plistに位置情報を使う目的を記述する必要があります。
位置情報をバックグラウンドでも使うのか、アプリを使用中のみ使うのかによって項目が異なります。
今回はアプリを使用中のみ位置情報を使うので「Location When In Use Usage Description」を設定しました。
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
マップの初期化
ここまでで準備が終わったのでここからはコードを書いていきます。
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
地図が表示されました!
Android
ちゃんとPermissionのダイアログ出ました!
現在地の表示もできています。IsShowingUser
をtrue
にするとマップの右上に標準の現在地ボタンが表示されるみたいなので下に配置した現在地ボタンが微妙ですね...
おわりに
今回はXamarin.FormsのMapコントロールを使って地図を表示してみました。
ググると記事がいくつか出てくるのですが、Android 6.0のRuntime Permissionに対応したものがなかなか無かったので記事にしてみました。
この記事の内容が少しでも誰かのお役にたてば幸いです。