Xamarin.Form 使用Google地圖
李政輝C.H. Lee
- 恆逸教育訓練中心-資深講師
- 技術分類:App開發
Visual Studio Xamarin.Form是一個產生雙平台(ANDROID/IOS)原生的成熟開發專案,即使鬆散偶合MAUI的出現, Xamarin.Form使用緊密散偶的開發方式直覺的與ANDROID/IOS手機的使用者體驗完美結合。在這個文章中,我將製作一個APP 來顯示GOOGLE地圖,並結合大數據CSV資料加入至PIN標示點。
技術主題:- 透過MainPage內容頁,產生Menu對應至3個建立的內容頁
- 透過Map內容頁,顯示Google Map
- 透過PinPage內容頁,在地圖加入Pin
- 透過Covid19Page內容頁,加入定位功能做為地圖的中心點,同時透過Data.GOV取出公費COVID-19家用快篩試劑診所名單的CSV檔並將其資料產生Pin
預設畫面預覽:
- MainPage內容頁(Menu)
- MainPage內容頁(Map-顯示Google Map)
- PinPage內容頁(Pins-在地圖加入Pin)
- Covid19Page內容頁(快篩試劑地圖-公費COVID-19家用快篩試劑診所名單)
主題1:透過MainPage內容頁,產生Menu對應至3個建立的內容頁
-
1.1建立新專案,選擇:行動應用程式式(Xamarin.Forms),按「下一步」
專案名稱:GoogleMaps,按「建立」。
-
1.2 在App.xaml啟用NavigationPage;
App.xaml.cs
public App() { InitializeComponent(); //1.2 //MainPage = new MainPage(); MainPage = new NavigationPage(newMainPage()); }
-
1.3 在MainPage.xaml使用TableView加入MENU目錄,對應至3個建立的內容頁
-
1.4建立MapPage.xaml
專案(按右鍵)加入新增項目。
Xamarin.Forms內容頁面名稱:MapPage.xaml按「新增」
1.5重覆上一步驟,加入PinPage.xaml
1.6重覆上一步驟,加入Covid19Page.xaml
1.7在MainPage加入TableView及TableCell,做為目錄清單。透過TableCell的CommandParameter連結三個內容頁。
MainPage.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" x:Class="GoogleMaps.MainPage" xmlns:local="clr-namespace:GoogleMaps"> <TableView Intent="Menu"> <TableRoot> <TableSection> <TextCell Text="Map" Detail="顯示Google Map" Command="{Binding NavigateCommand}" CommandParameter="{x:Type local:MapPage}" /> <TextCell Text="Pins" Detail="在地圖加入Pin" Command="{Binding NavigateCommand}" CommandParameter="{x:Type local:PinPage}" /> <TextCell Text="快篩試劑地圖" Detail="公費COVID-19家用快篩試劑診所名單" Command="{Binding NavigateCommand}" CommandParameter="{x:Type local:Covid19Page}" /> </TableSection> </TableRoot> </TableView> </ContentPage>
-
1.8在MainPage.cs透過NavigateCommand將內容頁Push至NavigationPage。
MainPage.cs
public partial class MainPage : ContentPage { public ICommand NavigateCommand { get; set; } public MainPage() { InitializeComponent(); NavigateCommand = new Command<Type>(async (Type pageType) => { Page page = (Page)Activator.CreateInstance(pageType); await Navigation.PushAsync(page); }); BindingContext = this; } }
主題2:透過Map內容頁,顯示Google Map
-
2.1 將NuGet 套件:Xamarin.Forms.Maps 新增至方案中
-
2.2 安裝Xamarin.Forms.Maps NuGet 套件之後,必須在每個平台專案中初始化它
Android專案的 MainActivity.cs將 Xamarin.FormsMaps.Init(this, savedInstanceState);
加至 global::Xamarin.Forms.Forms.Init(this, savedInstanceState);的後面MainActivity.cs
protected void override OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); Xamarin.Essentials.Platform.Init(this, savedInstanceState); global::Xamarin.Forms.Forms.Init(this, savedInstanceState); Xamarin.FormsMaps.Init(this, savedInstanceState); LoadApplication(new App()); }
iOS專案的AppDelegate.cs 將Xamarin.FormsMaps.Init();加至global::Xamarin.Forms.Forms.Init();的後面
AppDelegate.cs
public override bool FinishedLaunching(UIApplication app, NSDictionary options) { global::Xamarin.Forms.Forms.Init(); Xamarin.FormsMaps.Init(); LoadApplication(new App()); return base.FinishedLaunching(app, options); }
-
2.3 在iOS專案上,存取使用者的位置需要已授與應用程式的位置許可權。
在iOS上顯示地圖並與其互動不需要任何其他設定。不過,若要存取位置服務,您必須在Info.plist中設定下列金鑰:
Info.plist加入這些索引鍵的 XML 標記,值是字串類型
<key>NSLocationAlwaysUsageDescription</key >
<string>Can we use your location at all times?</string >
<key>NSLocationWhenInUseUsageDescription</key >
<string>Can we use your location when your application is being used?</string >
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key >
<string>Can we use your location at all times?</string >
-
2.4 在Android 專案上,存取使用者的位置需要已授與應用程式的位置許可權。
在 Android 上顯示和互動地圖的設定程式如下:
-
取得 Google 地圖 API 金鑰,並將其新增至資訊清單。 在Google Developer Console
https://console.developers.google.com/
建立一個專案,然後加入API- Maps SDK for Android
- Maps SDK for iOS
- 憑證建立憑證API金鑰
<application...> <!-- 取得 Google 地圖 API 金鑰 --> <meta-data android:name="com.google.android.geo.API_KEY" android:value="AIzaSyACo9xxxxxxxxxxxxxUcihwMyiASw" /> </application>
-
在Android專案Properties/AndroidManifest.xml檔案資訊清單中指定 Google Play 服務版本號碼。
<application...> <!-- 指定 Google Play 服務版本號碼 --> <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" /> </application>
-
在資訊清單中指定 Apache HTTP 舊版程式庫的需求。
Xamarin.Forms如果您的應用程式以 API 28 或更高版本為目標,您必須在<application>AndroidManifest.xml的元素內新增下列宣告:<application...> <!-- 如果您的應用程式以 API 28 (Android 9.0)或更高版本為目標必須新增下列宣告 --> <uses-library android:name="org.apache.http.legacy" android:required="false" /> </application>
-
在資訊清單中指定位置許可權。
如果您的應用程式需要存取使用者的位置,您必須藉由將或許可權新增ACCESS_COARSE_LOCATION 至資訊清單(或兩者),以要求許可權,做為專案的子系<manifest>:ACCESS_FINE_LOCATION<manifest xmlns:.....> <!-- 需要存取使用者的位置必須宣告指定位置許可權 --> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> </manifest>
許可權 ACCESS_COARSE_LOCATION 可讓 API 使用 WiFi 或行動資料,或兩者來判斷裝置的位置。 許可權 ACCESS_FINE_LOCATION 可讓 API 使用全域定位系統 (GPS) 、WiFi 或行動資料,以盡可能判斷位置。
-
要求類別中的 MainActivity 執行時間位置許可權。
如果您的應用程式是以 API 23 或更新版本為目標,而且需要存取使用者的位置,則必須檢查它是否具有執行時間的必要許可權,並在沒有時要求它。 執行下列工作即可達成這點:- 在類別中 MainActivity ,新增下欄欄位:
- 在類別中 MainActivity ,新增下列 OnStart 覆寫:
- 如果應用程式是以 API 23 或更新版本為目標,此程式碼會執行許可權的 AccessFineLocation 執行時間許可權檢查。 如果未授與許可權,呼叫 方法就會提出 RequestPermissions 許可權要求。
Android專案的MainActivity.cs
//在類別中 MainActivity ,新增下欄欄位: const int RequestLocationId = 0; readonly string[] LocationPermissions = { //using Android; Manifest.Permission.AccessCoarseLocation, Manifest.Permission.AccessFineLocation }; //在類別中 MainActivity ,新增下列 OnStart 覆寫: protected override void OnStart() { base.OnStart(); if ((int)Build.VERSION.SdkInt >= 23) { if (CheckSelfPermission(Manifest.Permission.AccessFineLocation) != Permission.Granted) { RequestPermissions(LocationPermissions, RequestLocationId); } else { // Permissions already granted - display a message. } } } //如果應用程式是以 API 23 或更新版本為目標,此程式碼會執行許可權的 AccessFineLocation 執行時間許可權檢查。 如果未授與許可權,呼叫 方法就會提出 RequestPermissions 許可權要求。 public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) { Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); //base.OnRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == RequestLocationId) { if ((grantResults.Length == 1) && (grantResults[0] == (int)Permission.Granted)) { // Permissions granted - display a message. } else { // Permissions denied - display a message. } } else { base.OnRequestPermissionsResult(requestCode, permissions, grantResults); } }
-
2.5 在MapPage內容頁新增Map控制項:
MapPage.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" x:Class="GoogleMaps.MapPage" xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps" Title="Map"> <ContentPage.Content> <StackLayout> <!--顯示地圖--> <!-- MapType : Street 指定將會顯示街道地圖。 Satellite 指定將會顯示包含衛星影像的地圖。 Hybrid 會指定將顯示結合街道和衛星資料的地圖。 TrafficEnabled="true" 顯示流量資料 IsShowingUser="true" 顯示使用者的位置 OnMapClicked 事件處理常式:會輸出代表點選地圖位置的緯度和經度。 --> <maps:Map x:Name="map" MapType="Street" MapClicked="OnMapClicked"> <x:Arguments> <maps:MapSpan> <x:Arguments> <maps:Position> <x:Arguments> <!--在地圖上顯示特定位置為中心--> <x:Double>25.052992</x:Double> <x:Double>121.544633</x:Double> </x:Arguments> </maps:Position> <!--地圖縮放比例--> <x:Double>0.1</x:Double> <x:Double>0.1</x:Double> </x:Arguments> </maps:MapSpan> </x:Arguments> </maps:Map> </StackLayout> </ContentPage.Content> </ContentPage>
-
2.6 在MapPage.xaml.cs 加入OnMapClicked事件委派
MapPage.xaml.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Xamarin.Forms; using Xamarin.Forms.Maps; using Xamarin.Forms.Xaml; namespace GoogleMaps { [XamlCompilation(XamlCompilationOptions.Compile)] public partial class MapPage : ContentPage { public MapPage() { InitializeComponent(); } async void OnMapClicked(object sender, MapClickedEventArgs e) { await DisplayAlert("經緯度位置", $"您點選的位置是: {e.Position.Latitude}, {e.Position.Longitude}" ,"OK"); } } }
-
2.7 測試
主題3. 透過PinPage內容頁,在地圖加入Pin
控制項 Xamarin.Forms.Map 允許使用物件標記位置:Pin
Pin是點選時開啟資訊視窗的地圖標記:
類別 Pin 具有下列屬性:
- Label類型為string,這通常代表Pin標題。
- Address類型為string,這通常代表Pin釘選位置的位址。 不過,它可以是任何 string 內容,而不只是位址。
- Position類型為Position ,表示Pin的緯度和經度。
- Type類型為PinType列舉 ,表示Pin的類型。
-
3.1 在PinPage.xaml加入Pin
PinPage.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" x:Class="GoogleMaps.PinPage" xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps" Title="Pins"> <ContentPage.Content> <StackLayout> <maps:Map x:Name="map" MapType="Street"> <x:Arguments> <maps:MapSpan> <x:Arguments> <maps:Position> <x:Arguments> <!-->在地圖上顯示特定位置為中心--> <x:Double>25.042171 </x:Double> <x:Double>121.566376</x:Double> </x:Arguments> </maps:Position> <!--地圖縮放比例--> <x:Double>0.1</x:Double> <x:Double>0.1</x:Double> </x:Arguments> </maps:MapSpan> </x:Arguments> <maps:Map.Pins> <maps:Pin Label="恆逸資訊教育訓練中心" Address="台北市松山區復興北路99號12樓,14樓,16樓" Type="Place"> <maps:Pin.Position> <maps:Position> <x:Arguments> <x:Double>25.052992</x:Double> <x:Double>121.544633</x:Double> </x:Arguments> </maps:Position> </maps:Pin.Position> </maps:Pin> </maps:Map.Pins> </maps:Map> </StackLayout> </ContentPage.Content> </ContentPage>
-
3.2 也可在PinPage.xaml.cs加入Pin
PinPage.xaml.cs
public partial class PinPage : ContentPage { public PinPage() { InitializeComponent(); Pin boardwalkPin = new Pin { Position = new Position(25.035473, 121.565297), Label = "台北101", Address = "台北市信義區信義路五段7號", Type = PinType.Generic }; boardwalkPin.MarkerClicked += OnMarkerClickedAsync; map.Pins.Add(boardwalkPin); } async void OnMarkerClickedAsync(object sender, PinClickedEventArgs e) { e.HideInfoWindow = true; string pinName = ((Pin)sender).Label; await DisplayAlert($"{pinName} 簡介", $"{pinName} 是高聳的地標性摩天大樓,設有商店、餐廳,以及位於 89 樓的觀景台。", "Ok"); } }
-
3.3 測試
主題4:透過Covid19Page內容頁,加入定位功能做為地圖的中心點,同時透過Data.gov取出公費COVID-19家用快篩試劑診所名單的CSV檔並將其資料產生Pin。
Xamarin.Essentials的Geolocation類別會提供 API 來擷取裝置的目前地理位置座標。
-
4.1 存取地理位置功能,Android平台的特定設定
需要粗略和精確位置的權限,並且必須在 Android 專案中設定。Android 10-Q (API 層級29或更高) 並要求 LocationAlways的權限。 開啟Android專案中 [Properties] 資料夾下的 AndroidManifest.xml 檔案,並在 [manifest] 節點內新增下列內容:
AndroidManifest.xml
<!--需要粗略和精確定位位置的權限--> <uses-feature android:name="android.hardware.location" android:required="false" /> <uses-feature android:name="android.hardware.location.gps" android:required="false" /> <uses-feature android:name="android.hardware.location.network" android:required="false" /> <!--Android 10-Q (API 層級29或更高) 並要求 LocationAlways--> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
-
4.2 存取定位功能,ios平台的特定設定
開啟 plist 編輯器並新增 Privacy - Location When In Use Usage Description 屬性,並填寫一個值以向使用者顯示。
<key>NSLocationWhenInUseUsageDescription</key>
<string>Can we use your location when your application is being used?</string> -
4.3 加入手機定位功能,使用Xamarin.Essentials的Geolocation
GetLastKnownLocationAsync()回傳經緯度Covid19Page.xaml.cs
public partial class Covid19Page : ContentPage { public Covid19Page() { InitializeComponent(); var geo = GetLastLocation().Result; Position center = new Position(geo.Item1,geo.Item2); MapSpan mapSpan = new MapSpan(center,0.05,0.05); } async Task<Tuple<double, double>> GetLastLocation() { var location = await Geolocation.GetLastKnownLocationAsync(); if (location != null) { return new Tuple<double, double>(location.Latitude, location.Longitude); } else { return new Tuple<double, double>(25.035707, 121.564868); } } }
-
4.4 建立 Pin 資料來源的 NhiFst 類別
建立DAL資料夾,加入NhiFst 類別。CSV 來源提供7個欄位(診所名稱、診所地址、診所電話、Long,Lat),在此只需提取其中五個欄位:(診所名稱、診所地址、診所電話、Long、Lat)
NhiFst.cs
using System; using System.Collections.Generic; using System.Text; namespace GoogleMaps.DAL { public class NhiFst { //診所名稱,診所地址,診所電話,Long,Lat public string Name { get; set; } public string Phone { get; set; } public double Lng { get; set; } public double Lat { get; set; } public NhiFst( string name, string address, string phone,double lng,double lat ) { Name = name; Address = address; Phone = phone; Lng = lng; Lat = lat; } } }
-
4.5 使用HttpClients取出Data.gov的CSV檔
加入NhiFstRepository類別,使用HttpClients取出CSV檔 https://quality.data.gov.tw/dq_download_csv.php?nid=150692&md5_url=38da966c8e5eda5b457a7366ce7f2c53NhiFstRepository.cs
using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; using System.IO; using System.Net; using Newtonsoft.Json; using Xamarin.Forms; using System.Diagnostics; namespace GoogleMaps.DAL { public class NhiFstRepository { //公費COVID-19家用快篩試劑社區定點診所名單 //縣市別,鄉鎮市區別,診所名稱,診所地址,診所電話,Long,Lat //https://data.gov.tw/dataset/150692 //下載網址:https://quality.data.gov.tw/dq_download_csv.php?nid=150692&md5_url=38da966c8e5eda5b457a7366ce7f2c53 const string Url = "https://quality.data.gov.tw/dq_download_csv.php?nid=150692&md5_url=38da966c8e5eda5b457a7366ce7f2c53"; List<NhiFst> fstData=new List<NhiFst>(); public async Task<IEnumerable<NhiFst>> GetAll() { HttpClient client = new HttpClient(); client.DefaultRequestHeaders.Add("Accept", "text/csv"); var stream = await client.GetStreamAsync(Url).ConfigureAwait(false); using (var sr = new StreamReader(stream)) { string[] read; char[] seperators = { ',' }; string data = sr.ReadLine(); Debug.WriteLine("DATA:" + data); while ((data = sr.ReadLine()) != null) { read = data.Split(seperators, StringSplitOptions.RemoveEmptyEntries); //縣市別[0],鄉鎮市區別[1],診所名稱[2],診所地址[3],診所電話[4],Long[5],Lat[6] fstData.Add(new NhiFst(read[2], read[3], read[4], float.Parse(read[5]), float.Parse(read[6]))); } } return fstData; } } }
-
4.6 建立Pin連繫所需的Location類別,對應至Pin類別
Location.cs
using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using Xamarin.Forms.Maps; namespace GoogleMaps.DAL { public class Location { Position _position; public string Address { get; } public string Description { get; } public Position Position { get; set; } public Location(string description,string address, Position position) { Address = address; Description = description; Position = position; } } }
-
4.7 建立ViewMode類別:PinItemsSourcePageViewModel,將CSV資料來源載入至Location
PinItemsSourcePageViewModel.cs
using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Text; using Xamarin.Essentials; using Xamarin.Forms.Maps; namespace GoogleMaps.DAL { public class PinItemsSourcePageViewModel { public PinItemsSourcePageViewModel() { GetJsonData(); } NhiFstRepository manager = new NhiFstRepository(); ObservableCollection<Location> _locations; public IEnumerable Locations => _locations; async void GetJsonData() { _locations = new ObservableCollection<Location>() { new Location("Taipei101", "高聳的地標性摩天大樓,設有商店、餐廳,以及位於 89 樓的觀景台。", new Position(25.035940, 121.565148)), new Location("恆逸資訊教育訓練中心", "台北市松山區復興北路99號12樓,14樓,16樓", new Position(25.052992, 121.544633)) }; var fstCollection = await manager.GetAll(); foreach (NhiFst d in fstCollection) { _locations.Add( new Location(d.Name,$"地址:{d.Address},電話:{d.Phone}",new Position(d.Lat,d.Lng))); } } } }
-
4.8 在Covid19Page內容頁加入Map及Pin控制項連繫至Location類別
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="GoogleMaps.Covid19Page" xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps" Title="快篩試劑地圖"> <ContentPage.Content> <StackLayout> <maps:Map x:Name="map" ItemsSource="{Binding Locations}"> <maps:Map.ItemTemplate> <DataTemplate> <maps:Pin Position="{Binding Position}" Address="{Binding Address}" Label="{Binding Description}" /> </DataTemplate> </maps:Map.ItemTemplate> </maps:Map> </StackLayout> </ContentPage.Content> </ContentPage>
-
4.9 在Covid19Page原始碼將BindingContext與ViewModel來源PinItemsSourcePageViewModel連繫,同時透過Map的MoveToRegion方法將Map中心點移至第3步驟提供的手機定位點
Covid19Page.xaml.cs
using GoogleMaps.DAL; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Xamarin.Essentials; using Xamarin.Forms; using Xamarin.Forms.Maps; using Xamarin.Forms.Xaml; namespace GoogleMaps { [XamlCompilation(XamlCompilationOptions.Compile)] public partial class Covid19Page : ContentPage { public Covid19Page() { InitializeComponent(); var geo = GetLastLocation().Result; Position center = new Position(geo.Item1,geo.Item2); MapSpan mapSpan = new MapSpan(center ,0.05,0.05); BindingContext = new PinItemsSourcePageViewModel(); map.MoveToRegion(mapSpan); } async Task<Tuple<double, double>> GetLastLocation() { var location = await Geolocation.GetLastKnownLocationAsync(); if (location != null) { return new Tuple<double, double>(location.Latitude, location.Longitude); } else { return new Tuple<double, double>(25.035707, 121.564868); } } } }
-
4.10 測試