@
- 前期準備:註冊高德開發者並建立 key
- 登入控制檯
- 建立 key
- 獲取 key 和金鑰
- 建立專案
- 建立JS API Loader
- 配置許可權
- 建立定義
- 建立模型
- 建立地圖元件
- 建立互動邏輯
- 專案地址
地圖元件在手機App中常用地理相關業務,如檢視線下門店,設定導航,或選取地址等。是一個較為常見的元件。
在.NET MAUI 中,有兩種方案可以整合高德地圖,一種是使用原生庫繫結。網上也有人實現過:https://blog.csdn.net/sD7O95O/article/details/125827031
但這種方案需要大量平臺原生開發的知識,而且需要對每一個平臺進行適配。
在這裡我介紹第二種方案:.NET MAUI Blazor + 高德地圖JS API 2.0 庫的實現。
JS API 2.0 是高德開放平臺基於WebGL的地圖元件,可以將高德地圖模組整合到.NET MAUI Blazor中的BlazorWebView控制元件,由於BlazorWebView的跨平臺特性,可以達到一次開發全平臺通用,無需為每個平臺做適配。
今天用此方法實現一個地圖選擇器,使用手機的GPS定位初始化當前位置,使用高德地圖JS API庫實現地點選擇功能。混合開發方案涉及本機程式碼與JS runtime的互動,如果你對這一部分還不太瞭解,可以先閱讀這篇文章:[MAUI]深入瞭解.NET MAUI Blazor與Vue的混合開發
使用.NET MAU實現跨平臺支援,本專案可執行於Android、iOS平臺。
前期準備:註冊高德開發者並建立 key
登入控制檯
登入 高德開放平臺控制檯,如果沒有開發者賬號,請 註冊開發者。
建立 key
進入應用管理,建立新應用,新應用中新增 key,服務平臺選擇 Web端(JS API)
。再建立一個Web服務
型別的Key,用於解析初始位置地址。
獲取 key 和金鑰
建立成功後,可獲取 key 和安全金鑰。
建立專案
新建.NET MAUI Blazor專案,命名AMap
建立JS API Loader
前往https://webapi.amap.com/loader.js
另存js檔案至專案wwwroot資料夾
在wwwroot建立amap_index.html
檔案,將loader.js引用到頁面中。建立_AMapSecurityConfig物件並設定安全金鑰。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>AmapApp</title>
<base href="/" />
<link href="css/app2.css" rel="stylesheet" />
</head>
<body>
<div class="status-bar-safe-area"></div>
<div id="app">Loading...</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webview.js" autostart="false"></script>
<script src="lib/amap/loader.js"></script>
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: "764832459a38e824a0d555b62d8ec1f0",
};
</script>
</body>
</html>
配置許可權
開啟Android端AndroidManifest.xml檔案
新增許可權:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
開啟Info.plist檔案,新增許可權描述信心
<key>NSLocationWhenInUseUsageDescription</key>
<string>允許使用裝置的GPS更新您的位置資訊。</string>
建立定義
建立Position,Poi,Location等型別,用於描述位置資訊。由於篇幅這裡不展開介紹。
建立模型
建立一個MainPageViewModel
類,用於處理頁面邏輯。程式碼如下:
public class MainPageViewModel : ObservableObject
{
public event EventHandler<FinishedChooiseEvenArgs> OnFinishedChooise;
private static AsyncLock asyncLock = new AsyncLock();
public static RateLimitedAction throttledAction = Debouncer.Debounce(null, TimeSpan.FromMilliseconds(1500), leading: false, trailing: true);
public MainPageViewModel()
{
Search = new Command(SearchAction);
Done = new Command(DoneAction);
Remove = new Command(RemoveAction);
}
private void RemoveAction(object obj)
{
this.Address=null;
this.CurrentLocation=null;
OnFinishedChooise?.Invoke(this, new FinishedChooiseEvenArgs(Address, CurrentLocation));
}
private void DoneAction(object obj)
{
OnFinishedChooise?.Invoke(this, new FinishedChooiseEvenArgs(Address, CurrentLocation));
}
private void SearchAction(object obj)
{
Init();
}
public async void Init()
{
var location = await GeoLocationHelper.GetNativePosition();
if (location==null)
{
return;
}
var amapLocation = new Location.Location()
{
Latitude=location.Latitude,
Longitude=location.Longitude
};
CurrentLocation=amapLocation;
}
private Location.Location _currentLocation;
public Location.Location CurrentLocation
{
get { return _currentLocation; }
set
{
if (_currentLocation != value)
{
if (value!=null &&_currentLocation!=null&&Location.Location.CalcDistance(value, _currentLocation)<100)
{
return;
}
_currentLocation = value;
OnPropertyChanged();
}
}
}
private string _address;
public string Address
{
get { return _address; }
set
{
_address = value;
OnPropertyChanged();
}
}
private ObservableCollection<Poi> _pois;
public ObservableCollection<Poi> Pois
{
get { return _pois; }
set
{
_pois = value;
OnPropertyChanged();
}
}
private Poi _selectedPoi;
public Poi SelectedPoi
{
get { return _selectedPoi; }
set
{
_selectedPoi = value;
OnPropertyChanged();
}
}
public Command Search { get; set; }
public Command Done { get; set; }
public Command Remove { get; set; }
}
注意這裡的Init方法,用於初始化位置。
GeoLocationHelper.GetNativePosition()
方法用於從你裝置的GPS模組,獲取當前位置。它呼叫的是Microsoft.Maui.Devices.Sensors
提供的裝置感測器訪問功能
,詳情可參考官方文件地理位置 - .NET MAUI
建立地圖元件
建立Blazor頁面AMapPage.razor
以及AMapPage.razor.js
在AMapPage.razor
中引入
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
return;
await JSRuntime.InvokeAsync<IJSObjectReference>(
"import", "./AMapPage.razor.js");
await Refresh();
await JSRuntime.InvokeVoidAsync("window.initObjRef", this.objRef);
}
razor頁面的 @Code
程式碼段中,放置MainPageViewModel屬性,以及一個DotNetObjectReference
物件,用於在JS中呼叫C#方法。
@code {
[Parameter]
public MainPageViewModel MainPageViewModel { get; set; }
private DotNetObjectReference<AMapPage> objRef;
protected override void OnInitialized()
{
objRef = DotNetObjectReference.Create(this);
}
private async Task Refresh()
{
...
}
在AMapPage.razor.js
我們載入地圖,並設定地圖的中心點。和一些地圖掛件。此外,我們還需要監聽地圖的中心點變化,更新中心點。 這些程式碼可以從官方示例中複製。(https://lbs.amap.com/demo/javascript-api-v2/example/map/map-moving)。
console.info("start load")
window.viewService = {
map: null,
zoom: 13,
amaplocation: [116.397428, 39.90923],
SetAmapContainerSize: function (width, height) {
console.info("setting container size")
var div = document.getElementById("container");
div.style.height = height + "px";
},
SetLocation: function (longitude, latitude) {
console.info("setting loc", longitude, latitude)
window.viewService.amaplocation = [longitude, latitude];
if (window.viewService.map) {
window.viewService.map.setZoomAndCenter(window.viewService.zoom, window.viewService.amaplocation);
console.info("set loc", window.viewService.zoom, window.viewService.map)
}
},
isHotspot: true
}
AMapLoader.load({ //首次呼叫 load
key: '0896cedc056413f83ca0aee5b029c65d',//首次load key為必填
version: '2.0',
plugins: ['AMap.Scale', 'AMap.ToolBar', 'AMap.InfoWindow', 'AMap.PlaceSearch']
}).then((AMap) => {
console.info("loading..")
var opt = {
resizeEnable: true,
center: window.viewService.amaplocation,
zoom: window.viewService.zoom,
isHotspot: true
}
var map = new AMap.Map('container', opt);
console.info(AMap, map, opt)
map.addControl(new AMap.Scale())
map.addControl(new AMap.ToolBar())
window.viewService.marker = new AMap.Marker({
position: map.getCenter()
})
map.add(window.viewService.marker);
var placeSearch = new AMap.PlaceSearch(); //構造地點查詢類
var infoWindow = new AMap.InfoWindow({});
map.on('hotspotover', function (result) {
placeSearch.getDetails(result.id, function (status, result) {
if (status === 'complete' && result.info === 'OK') {
onPlaceSearch(result);
}
});
});
map.on('moveend', onMapMoveend);
// map.on('zoomend', onMapMoveend);
//回撥函式
window.viewService.map = map;
function onMapMoveend() {
var zoom = window.viewService.map.getZoom(); //獲取當前地圖級別
var center = window.viewService.map.getCenter(); //獲取當前地圖中心位置
if (window.viewService.marker) {
window.viewService.marker.setPosition(center);
}
window.objRef.invokeMethodAsync('OnMapMoveend', center);
}
function onPlaceSearch(data) { //infoWindow.open(map, result.lnglat);
var poiArr = data.poiList.pois;
if (poiArr[0]) {
var location = poiArr[0].location;
infoWindow.setContent(createContent(poiArr[0]));
infoWindow.open(window.viewService.map, location);
}
}
function createContent(poi) { //資訊窗體內容
var s = [];
s.push('<div class="info-title">' + poi.name + '</div><div class="info-content">' + "地址:" + poi.address);
s.push("電話:" + poi.tel);
s.push("型別:" + poi.type);
s.push('<div>');
return s.join("<br>");
}
console.info("loaded")
}).catch((e) => {
console.error(e);
});
window.initObjRef = function (objRef) {
window.objRef = objRef;
}
地圖中心點改變時,我們需要使用window.objRef.invokeMethodAsync('OnMapMoveend', center);
從JS runtime中通知到C#程式碼。
同時,在AMapPage.razor
中配置一個方法,用於接收從JS runtime發來的回撥通知。
在此賦值CurrentLocation
屬性。
[JSInvokable]
public async Task OnMapMoveend(dynamic location)
{
await Task.Run(() =>
{
var locationArray = JsonConvert.DeserializeObject<double[]>(location.ToString());
MainPageViewModel.CurrentLocation=new Location.Location()
{
Longitude=locationArray[0],
Latitude =locationArray[1]
};
});
}
同時監聽CurrentLocation
屬性的值,一旦發生變化,則呼叫JS runtime中的viewService.SetLocation
方法,更新地圖中心點。
protected override async Task OnInitializedAsync()
{
MainPageViewModel.PropertyChanged += async (o, e) =>
{
if (e.PropertyName==nameof(MainPageViewModel.CurrentLocation))
{
if (MainPageViewModel.CurrentLocation!=null)
{
var longitude = MainPageViewModel.CurrentLocation.Longitude;
var latitude = MainPageViewModel.CurrentLocation.Latitude;
await JSRuntime.InvokeVoidAsync("viewService.SetLocation", longitude, latitude);
}
}
};
}
在MainPageViewModel
類中,我們新增一個PropertyChanged
事件,用於監聽CurrentLocation
屬性的改變。
當手指滑動地圖觸發位置變化,導致CurrentLocation
屬性改變時,將當前的中心點轉換為具體的地址。這裡使用了高德逆地理編碼API服務(https://restapi.amap.com/v3/geocode/regeo)解析CurrentLocation的值, 還需使用了防抖策略,避免介面的頻繁呼叫。
public MainPageViewModel()
{
PropertyChanged+=MainPageViewModel_PropertyChanged;
...
}
private async void MainPageViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(CurrentLocation))
{
if (CurrentLocation!=null)
{
// 使用防抖
using (await asyncLock.LockAsync())
{
var amapLocation = new Location.Location()
{
Latitude=CurrentLocation.Latitude,
Longitude=CurrentLocation.Longitude
};
var amapInverseHttpRequestParamter = new AmapInverseHttpRequestParamter()
{
Locations= new Location.Location[] { amapLocation }
};
ReGeocodeLocation reGeocodeLocation = null;
try
{
reGeocodeLocation = await amapHttpRequestClient.InverseAsync(amapInverseHttpRequestParamter);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
throttledAction.Update(() =>
{
MainThread.BeginInvokeOnMainThread(() =>
{
CurrentLocation=amapLocation;
if (reGeocodeLocation!=null)
{
Address = reGeocodeLocation.Address;
Pois=new ObservableCollection<Poi>(reGeocodeLocation.Pois);
}
});
});
throttledAction.Invoke();
}
}
}
}
至此我們完成了地圖元件的基本功能。
建立互動邏輯
在MainPage.xaml中,建立一個選擇器按鈕,以及一個卡片模擬選擇器按鈕點選後的彈窗。
<Button Clicked="Button_Clicked"
Grid.Row="1"
x:Name="SelectorButton"
HorizontalOptions="Center"
VerticalOptions="Center"
Text="{Binding Address, TargetNullValue=請選擇地點}"></Button>
<Border StrokeShape="RoundRectangle 10"
Grid.RowSpan="2"
x:Name="SelectorPopup"
IsVisible="False"
Margin="5,50"
MinimumHeightRequest="500">
<Grid Padding="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label FontSize="Large"
Margin="10, 10, 10, 0"
FontAttributes="Bold"
Text="選擇地點"></Label>
<HorizontalStackLayout Grid.Column="1"
HorizontalOptions="End">
<Button Text="刪除"
Margin="5,0"
Command="{Binding Remove}"></Button>
<Button Text="完成"
Margin="5,0"
Command="{Binding Done}"></Button>
</HorizontalStackLayout>
</Grid>
<Grid Grid.Row="1"
Margin="10, 10, 10, 0">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Label HorizontalTextAlignment="Center"
VerticalOptions="Center"
x:Name="ContentLabel"
Text="{Binding Address}"></Label>
<Border IsVisible="False"
Grid.RowSpan="2"
x:Name="ContentFrame">
<Entry Text="{Binding Address, Mode=TwoWay}"
Placeholder="請輸入地址, 按Enter鍵完成"
Completed="Entry_Completed"
Unfocused="Entry_Unfocused"
ClearButtonVisibility="WhileEditing"></Entry>
</Border>
<Border x:Name="ContentButton"
Grid.Row="1"
HorizontalOptions="Center"
VerticalOptions="Center">
<Label>
<Label.FormattedText>
<FormattedString>
<Span FontFamily="FontAwesome"
Text=""></Span>
<Span Text=" 修改"></Span>
</FormattedString>
</Label.FormattedText>
</Label>
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped">
</TapGestureRecognizer>
</Border.GestureRecognizers>
</Border>
</Grid>
<BlazorWebView Grid.Row="2"
Margin="-10, 0"
x:Name="mainMapBlazorWebView"
HostPage="wwwroot/amap_index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app"
x:Name="rootComponent"
ComponentType="{x:Type views:AMapPage}" />
</BlazorWebView.RootComponents>
</BlazorWebView>
</Grid>
</Border>
最終效果如下:
專案地址
Github:maui-samples