@

地图组件在手机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

[MAUI]集成高德地图组件至.NET MAUI Blazor项目的更多相关文章

  1. 基于 React 封装的高德地图组件,帮助你轻松的接入地图到 React 项目中。

    react-amap 这是一个基于 React 封装的高德地图组件,帮助你轻松的接入地图到 React 项目中. 文档实例预览: Github Web | Gitee Web 特性 ️ 自动加载高德地 ...

  2. vue集成高德地图

    vue集成高德地图 前言 二.使用步骤 1.注册高德开发平台 2.vue 结尾 前言 之前玩Thymeleaf的时候玩过高德地图,现在无聊Vue项目也整个地图进去~ 二.使用步骤 1.注册高德开发平台 ...

  3. AngularJS指令封装高德地图组件

    1 概述 公司移动门户原来是基于AngularJS指令封装的百度地图组件,用于签到.签退.定位等功能,在使用过程中发现百度地图频繁的弹出广告,所以打算重新引用其它地图组件,最后决定基于AngularJ ...

  4. Vue项目(vuecli3.0搭建)集成高德地图实现路线轨迹绘制

    先看最后实现的效果图 高德地图api文档 https://lbs.amap.com/api/javascript-api/summary 使用 1.在index.html里面引入高德地图js文件 2. ...

  5. web集成高德地图

    1.使用高德地图API需到官网添加一个Key,http://lbs.amap.com/dev/key/app 2.页面头引入 <div id="addressMap"> ...

  6. Android集成高德地图如何自定义marker

    高德地图自定义Marker 高德地图默认的marker样式是这种 一般的修改样式是通过icon接口来调整 MarkerOptions markerOptions = new MarkerOptions ...

  7. Android 集成高德地图

    先上一张图片看看实现的效果啦!!! 首先登陆高德的开发者平台进行创建自己的应用程序,填写对应的包名,填写sHA1值(这个我这博客中写了获取的代码,可以直接复制粘贴),说了这么多其实都是废话,来我们看重 ...

  8. VUE 高德地图选取地址组件开发

    高德地图文档地址 http://lbs.amap.com/api/lightmap/guide/picker/ 结合步骤: 1.通过iframe内嵌引入高德地图组件 key就选你自己申请的key &l ...

  9. objective-c高德地图时时定位

    这篇随笔是对上一遍servlet接口的实现. 一.项目集成高德地图 应为我这个项目使用了cocopods这个第三方库管理工具,所以只需要很简单的步骤,就能将高德地图集成到项目中,如果你没使用过这工具, ...

  10. iOS - 高德地图步行线路规划多点多条线路

    项目集成高德地图遇到的问题: 高德地图的官方步行导航只针对单个起始点单条线路,驾车导航才有途径点多线路.现在项目是要步行导航多个点多条线路

随机推荐

  1. Map与WeakMap

    Map与WeakMap Map对象用来保存键值对,并且能够记住键的原始插入顺序,任何对象或者原始值都可以作为键或者是值. WeakMap对象同样用来保存键值对,对于键是弱引用的而且必须为一个对象,而值 ...

  2. Python 潮流周刊第 39 期(摘要)

    本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章.教程.开源项目.软件工具.播客和视频.热门话题等内容.愿景:帮助所有读者精进 Python 技术,并增长职 ...

  3. 彻底搞懂Java中的Runnable和Thread

    写在前面 今天在阅读ThreadPoolExecutor源码的时候觉得有些地方理解起来似是而非,很别扭!最后才猛然发现,原来是我自己的问题:没有真正理解Runnable和Thread的含义! 我之前对 ...

  4. Docker进阶之02-Swarm集群入门实践

    Docker集群概述 Docker集群有2种方案: 1.在Docker Engine 1.12之前的集群模式被称为经典集群,这是通过API代理系统实现的集群,目前已经不再维护. 2.自Docker E ...

  5. CSDN的Markdown编辑器使用说明

    这里写自定义目录标题 欢迎使用Markdown编辑器 新的改变 功能快捷键 合理的创建标题,有助于目录的生成 如何改变文本的样式 插入链接与图片 如何插入一段漂亮的代码片 生成一个适合你的列表 创建一 ...

  6. JSON排除指定字段的4种方法

    转自:https://blog.csdn.net/Sn_Keys/article/details/122443407

  7. 阿里云 SMS 短信 Java SDK 封装

    Github & Issues: https://github.com/cn-src/aliyun-sms 官方文档:https://help.aliyun.com/document_deta ...

  8. Gin框架入门

    参考文档 Gin: https://gin-gonic.com/zh-cn/docs/quickstart/ net/http: https://pkg.go.dev/net/http 代码分析 pa ...

  9. 【Azure 应用服务】App Service for Linux环境中,如何解决字体文件缺失的情况

    问题描述 部署在App Service for Linux环境中的Web App.出现了字体文件缺失的问题,页面显示本来时中文的地方,区别变为方框占位. 问题分析 在应用中,通常涉及到显示问题的有两个 ...

  10. 【Azure 应用服务】Function App中的函数(Functions)删除问题

    问题描述 Function App 中的函数如何删除问题 问题分析 1)在Function App的门户上,点击"Delete"进行删除 2) 进入Function App的高级管 ...