[MAUI]集成高德地图组件至.NET MAUI Blazor项目
@
地图组件在手机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>
最终效果如下:
项目地址
[MAUI]集成高德地图组件至.NET MAUI Blazor项目的更多相关文章
- 基于 React 封装的高德地图组件,帮助你轻松的接入地图到 React 项目中。
react-amap 这是一个基于 React 封装的高德地图组件,帮助你轻松的接入地图到 React 项目中. 文档实例预览: Github Web | Gitee Web 特性 ️ 自动加载高德地 ...
- vue集成高德地图
vue集成高德地图 前言 二.使用步骤 1.注册高德开发平台 2.vue 结尾 前言 之前玩Thymeleaf的时候玩过高德地图,现在无聊Vue项目也整个地图进去~ 二.使用步骤 1.注册高德开发平台 ...
- AngularJS指令封装高德地图组件
1 概述 公司移动门户原来是基于AngularJS指令封装的百度地图组件,用于签到.签退.定位等功能,在使用过程中发现百度地图频繁的弹出广告,所以打算重新引用其它地图组件,最后决定基于AngularJ ...
- Vue项目(vuecli3.0搭建)集成高德地图实现路线轨迹绘制
先看最后实现的效果图 高德地图api文档 https://lbs.amap.com/api/javascript-api/summary 使用 1.在index.html里面引入高德地图js文件 2. ...
- web集成高德地图
1.使用高德地图API需到官网添加一个Key,http://lbs.amap.com/dev/key/app 2.页面头引入 <div id="addressMap"> ...
- Android集成高德地图如何自定义marker
高德地图自定义Marker 高德地图默认的marker样式是这种 一般的修改样式是通过icon接口来调整 MarkerOptions markerOptions = new MarkerOptions ...
- Android 集成高德地图
先上一张图片看看实现的效果啦!!! 首先登陆高德的开发者平台进行创建自己的应用程序,填写对应的包名,填写sHA1值(这个我这博客中写了获取的代码,可以直接复制粘贴),说了这么多其实都是废话,来我们看重 ...
- VUE 高德地图选取地址组件开发
高德地图文档地址 http://lbs.amap.com/api/lightmap/guide/picker/ 结合步骤: 1.通过iframe内嵌引入高德地图组件 key就选你自己申请的key &l ...
- objective-c高德地图时时定位
这篇随笔是对上一遍servlet接口的实现. 一.项目集成高德地图 应为我这个项目使用了cocopods这个第三方库管理工具,所以只需要很简单的步骤,就能将高德地图集成到项目中,如果你没使用过这工具, ...
- iOS - 高德地图步行线路规划多点多条线路
项目集成高德地图遇到的问题: 高德地图的官方步行导航只针对单个起始点单条线路,驾车导航才有途径点多线路.现在项目是要步行导航多个点多条线路
随机推荐
- 同一份代码怎能在不同环境表现不同?记一个可选链因为代码压缩造成的bug
壹 ❀ 引 某一天,CSM日常找我反馈客户紧急工单,说有一个私有部署客户升级版本后,发现一个功能使用不太正常.因为我们公司客户分为两种,一种是SaaS客户,客户侧使用的版本被动跟随主版本变动,而私有部 ...
- Linux 中iostat 命令详解
iostat命令详解 iostat 主要是统计 磁盘活动情况. iostat有以下缺陷: iostat的输出结果大多数是一段时间内的平均值,因此难以反映峰值情况iostat仅能对系统整体情况进行分析汇 ...
- 【Unity3D】顶点和片元着色器
1 前言 上文介绍了渲染管线.固定管线着色器和表面着色器,如下: 渲染管线 固定管线着色器一 固定管线着色器二 表面着色器 固定管线着色器通过命令方式实现光照和贴图等效果,表面着色器通过给 S ...
- Java判断一个字符串中是否包含数字
知识点 本例考察以下Java知识点: 正则表达式 关于正则表达式: https://www.runoob.com/java/java-regular-expressions.html Characte ...
- 代码+案例,实战解析BeautifulSoup4
本文分享自华为云社区<从HTML到实战:深入解析BeautifulSoup4的爬虫奇妙世界>,作者:柠檬味拥抱. 网络上的信息浩如烟海,而爬虫技术正是帮助我们从中获取有用信息的重要工具.在 ...
- win32 - 写入安全日志(AuthzRegisterSecurityEventSource和AuthzReportSecurityEvent)
微软文档介绍说, 安全日志在其他两个重要方面与其他日志不同.首先,在默认配置中,它受到强大的访问控制列表(ACL)和特权检查的保护,这将可以读取其内容的个人的范围限制为本地系统,管理员和安全特权的持有 ...
- django项目中使用nginx+fastdfs上传图片和使用图片的流程
自定义文件存储类 1.先弄清楚django中默认的上传文件存储FileSystemStorage类 https://docs.djangoproject.com/zh-hans/2.2/ref/fil ...
- python代码,读取一个txt文件,将其中的每一行开头加上一个字母a,每一行的结尾加上一个字母b
with open('name.txt', 'r+') as file: lines = file.readlines() file.seek(0) # 将文件指针移回文件开头 file.trunca ...
- Huggingface初上手即ERNIE-gram句子相似性实战
大模型如火如荼的今天,不学点语言模型(LM)相关的技术实在是说不过去了.只不过由于过往项目用到LM较少,所以学习也主要停留在直面--动眼不动手的水平.Huggingface(HF)也是现在搞LM离不开 ...
- react start 后 url 后面不带/ 解决思路
> navigator@0.1.0 dev H:\2020home\giteez\navigator > node scripts/start.js Compiled successful ...