经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » ASP.net » 查看文章
[MAUI]集成高德地图组件至.NET MAUI Blazor项目
来源:cnblogs  作者:林晓lx  时间:2024/3/25 8:55:04  对本文有异议

@


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

使用.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对象并设置安全密钥。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
  6. <title>AmapApp</title>
  7. <base href="/" />
  8. <link href="css/app2.css" rel="stylesheet" />
  9. </head>
  10. <body>
  11. <div class="status-bar-safe-area"></div>
  12. <div id="app">Loading...</div>
  13. <div id="blazor-error-ui">
  14. An unhandled error has occurred.
  15. <a href="" class="reload">Reload</a>
  16. <a class="dismiss">??</a>
  17. </div>
  18. <script src="_framework/blazor.webview.js" autostart="false"></script>
  19. <script src="lib/amap/loader.js"></script>
  20. <script type="text/javascript">
  21. window._AMapSecurityConfig = {
  22. securityJsCode: "764832459a38e824a0d555b62d8ec1f0",
  23. };
  24. </script>
  25. </body>
  26. </html>

配置权限

打开Android端AndroidManifest.xml文件

在这里插入图片描述

添加权限:

  1. <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  2. <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  3. <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

打开Info.plist文件,添加权限描述信心

在这里插入图片描述

  1. <key>NSLocationWhenInUseUsageDescription</key>
  2. <string>允许使用设备的GPS更新您的位置信息。</string>

创建定义

创建Position,Poi,Location等类型,用于描述位置信息。由于篇幅这里不展开介绍。

创建模型

创建一个MainPageViewModel类,用于处理页面逻辑。代码如下:

  1. public class MainPageViewModel : ObservableObject
  2. {
  3. public event EventHandler<FinishedChooiseEvenArgs> OnFinishedChooise;
  4. private static AsyncLock asyncLock = new AsyncLock();
  5. public static RateLimitedAction throttledAction = Debouncer.Debounce(null, TimeSpan.FromMilliseconds(1500), leading: false, trailing: true);
  6. public MainPageViewModel()
  7. {
  8. Search = new Command(SearchAction);
  9. Done = new Command(DoneAction);
  10. Remove = new Command(RemoveAction);
  11. }
  12. private void RemoveAction(object obj)
  13. {
  14. this.Address=null;
  15. this.CurrentLocation=null;
  16. OnFinishedChooise?.Invoke(this, new FinishedChooiseEvenArgs(Address, CurrentLocation));
  17. }
  18. private void DoneAction(object obj)
  19. {
  20. OnFinishedChooise?.Invoke(this, new FinishedChooiseEvenArgs(Address, CurrentLocation));
  21. }
  22. private void SearchAction(object obj)
  23. {
  24. Init();
  25. }
  26. public async void Init()
  27. {
  28. var location = await GeoLocationHelper.GetNativePosition();
  29. if (location==null)
  30. {
  31. return;
  32. }
  33. var amapLocation = new Location.Location()
  34. {
  35. Latitude=location.Latitude,
  36. Longitude=location.Longitude
  37. };
  38. CurrentLocation=amapLocation;
  39. }
  40. private Location.Location _currentLocation;
  41. public Location.Location CurrentLocation
  42. {
  43. get { return _currentLocation; }
  44. set
  45. {
  46. if (_currentLocation != value)
  47. {
  48. if (value!=null &&_currentLocation!=null&&Location.Location.CalcDistance(value, _currentLocation)<100)
  49. {
  50. return;
  51. }
  52. _currentLocation = value;
  53. OnPropertyChanged();
  54. }
  55. }
  56. }
  57. private string _address;
  58. public string Address
  59. {
  60. get { return _address; }
  61. set
  62. {
  63. _address = value;
  64. OnPropertyChanged();
  65. }
  66. }
  67. private ObservableCollection<Poi> _pois;
  68. public ObservableCollection<Poi> Pois
  69. {
  70. get { return _pois; }
  71. set
  72. {
  73. _pois = value;
  74. OnPropertyChanged();
  75. }
  76. }
  77. private Poi _selectedPoi;
  78. public Poi SelectedPoi
  79. {
  80. get { return _selectedPoi; }
  81. set
  82. {
  83. _selectedPoi = value;
  84. OnPropertyChanged();
  85. }
  86. }
  87. public Command Search { get; set; }
  88. public Command Done { get; set; }
  89. public Command Remove { get; set; }
  90. }

注意这里的Init方法,用于初始化位置。

GeoLocationHelper.GetNativePosition()方法用于从你设备的GPS模块,获取当前位置。它调用的是Microsoft.Maui.Devices.Sensors提供的设备传感器访问功能
,详情可参考官方文档地理位置 - .NET MAUI

创建地图组件

创建Blazor页面AMapPage.razor以及AMapPage.razor.js

AMapPage.razor中引入

  1. protected override async Task OnAfterRenderAsync(bool firstRender)
  2. {
  3. if (!firstRender)
  4. return;
  5. await JSRuntime.InvokeAsync<IJSObjectReference>(
  6. "import", "./AMapPage.razor.js");
  7. await Refresh();
  8. await JSRuntime.InvokeVoidAsync("window.initObjRef", this.objRef);
  9. }

razor页面的 @Code 代码段中,放置MainPageViewModel属性,以及一个DotNetObjectReference对象,用于在JS中调用C#方法。

  1. @code {
  2. [Parameter]
  3. public MainPageViewModel MainPageViewModel { get; set; }
  4. private DotNetObjectReference<AMapPage> objRef;
  5. protected override void OnInitialized()
  6. {
  7. objRef = DotNetObjectReference.Create(this);
  8. }
  9. private async Task Refresh()
  10. {
  11. ...
  12. }

AMapPage.razor.js我们加载地图,并设置地图的中心点。和一些地图挂件。此外,我们还需要监听地图的中心点变化,更新中心点。 这些代码可以从官方示例中复制。(https://lbs.amap.com/demo/javascript-api-v2/example/map/map-moving)。

  1. console.info("start load")
  2. window.viewService = {
  3. map: null,
  4. zoom: 13,
  5. amaplocation: [116.397428, 39.90923],
  6. SetAmapContainerSize: function (width, height) {
  7. console.info("setting container size")
  8. var div = document.getElementById("container");
  9. div.style.height = height + "px";
  10. },
  11. SetLocation: function (longitude, latitude) {
  12. console.info("setting loc", longitude, latitude)
  13. window.viewService.amaplocation = [longitude, latitude];
  14. if (window.viewService.map) {
  15. window.viewService.map.setZoomAndCenter(window.viewService.zoom, window.viewService.amaplocation);
  16. console.info("set loc", window.viewService.zoom, window.viewService.map)
  17. }
  18. },
  19. isHotspot: true
  20. }
  21. AMapLoader.load({ //首次调用 load
  22. key: '0896cedc056413f83ca0aee5b029c65d',//首次load key为必填
  23. version: '2.0',
  24. plugins: ['AMap.Scale', 'AMap.ToolBar', 'AMap.InfoWindow', 'AMap.PlaceSearch']
  25. }).then((AMap) => {
  26. console.info("loading..")
  27. var opt = {
  28. resizeEnable: true,
  29. center: window.viewService.amaplocation,
  30. zoom: window.viewService.zoom,
  31. isHotspot: true
  32. }
  33. var map = new AMap.Map('container', opt);
  34. console.info(AMap, map, opt)
  35. map.addControl(new AMap.Scale())
  36. map.addControl(new AMap.ToolBar())
  37. window.viewService.marker = new AMap.Marker({
  38. position: map.getCenter()
  39. })
  40. map.add(window.viewService.marker);
  41. var placeSearch = new AMap.PlaceSearch(); //构造地点查询类
  42. var infoWindow = new AMap.InfoWindow({});
  43. map.on('hotspotover', function (result) {
  44. placeSearch.getDetails(result.id, function (status, result) {
  45. if (status === 'complete' && result.info === 'OK') {
  46. onPlaceSearch(result);
  47. }
  48. });
  49. });
  50. map.on('moveend', onMapMoveend);
  51. // map.on('zoomend', onMapMoveend);
  52. //回调函数
  53. window.viewService.map = map;
  54. function onMapMoveend() {
  55. var zoom = window.viewService.map.getZoom(); //获取当前地图级别
  56. var center = window.viewService.map.getCenter(); //获取当前地图中心位置
  57. if (window.viewService.marker) {
  58. window.viewService.marker.setPosition(center);
  59. }
  60. window.objRef.invokeMethodAsync('OnMapMoveend', center);
  61. }
  62. function onPlaceSearch(data) { //infoWindow.open(map, result.lnglat);
  63. var poiArr = data.poiList.pois;
  64. if (poiArr[0]) {
  65. var location = poiArr[0].location;
  66. infoWindow.setContent(createContent(poiArr[0]));
  67. infoWindow.open(window.viewService.map, location);
  68. }
  69. }
  70. function createContent(poi) { //信息窗体内容
  71. var s = [];
  72. s.push('<div class="info-title">' + poi.name + '</div><div class="info-content">' + "地址:" + poi.address);
  73. s.push("电话:" + poi.tel);
  74. s.push("类型:" + poi.type);
  75. s.push('<div>');
  76. return s.join("<br>");
  77. }
  78. console.info("loaded")
  79. }).catch((e) => {
  80. console.error(e);
  81. });
  82. window.initObjRef = function (objRef) {
  83. window.objRef = objRef;
  84. }

地图中心点改变时,我们需要使用window.objRef.invokeMethodAsync('OnMapMoveend', center);从JS runtime中通知到C#代码。

同时,在AMapPage.razor中配置一个方法,用于接收从JS runtime发来的回调通知。
在此赋值CurrentLocation属性。

  1. [JSInvokable]
  2. public async Task OnMapMoveend(dynamic location)
  3. {
  4. await Task.Run(() =>
  5. {
  6. var locationArray = JsonConvert.DeserializeObject<double[]>(location.ToString());
  7. MainPageViewModel.CurrentLocation=new Location.Location()
  8. {
  9. Longitude=locationArray[0],
  10. Latitude =locationArray[1]
  11. };
  12. });
  13. }

同时监听CurrentLocation属性的值,一旦发生变化,则调用JS runtime中的viewService.SetLocation方法,更新地图中心点。

  1. protected override async Task OnInitializedAsync()
  2. {
  3. MainPageViewModel.PropertyChanged += async (o, e) =>
  4. {
  5. if (e.PropertyName==nameof(MainPageViewModel.CurrentLocation))
  6. {
  7. if (MainPageViewModel.CurrentLocation!=null)
  8. {
  9. var longitude = MainPageViewModel.CurrentLocation.Longitude;
  10. var latitude = MainPageViewModel.CurrentLocation.Latitude;
  11. await JSRuntime.InvokeVoidAsync("viewService.SetLocation", longitude, latitude);
  12. }
  13. }
  14. };
  15. }

MainPageViewModel类中,我们添加一个PropertyChanged事件,用于监听CurrentLocation属性的改变。

当手指滑动地图触发位置变化,导致CurrentLocation属性改变时,将当前的中心点转换为具体的地址。这里使用了高德逆地理编码API服务(https://restapi.amap.com/v3/geocode/regeo)解析CurrentLocation的值, 还需使用了防抖策略,避免接口的频繁调用。

  1. public MainPageViewModel()
  2. {
  3. PropertyChanged+=MainPageViewModel_PropertyChanged;
  4. ...
  5. }
  6. private async void MainPageViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
  7. {
  8. if (e.PropertyName == nameof(CurrentLocation))
  9. {
  10. if (CurrentLocation!=null)
  11. {
  12. // 使用防抖
  13. using (await asyncLock.LockAsync())
  14. {
  15. var amapLocation = new Location.Location()
  16. {
  17. Latitude=CurrentLocation.Latitude,
  18. Longitude=CurrentLocation.Longitude
  19. };
  20. var amapInverseHttpRequestParamter = new AmapInverseHttpRequestParamter()
  21. {
  22. Locations= new Location.Location[] { amapLocation }
  23. };
  24. ReGeocodeLocation reGeocodeLocation = null;
  25. try
  26. {
  27. reGeocodeLocation = await amapHttpRequestClient.InverseAsync(amapInverseHttpRequestParamter);
  28. }
  29. catch (Exception ex)
  30. {
  31. Console.WriteLine(ex.ToString());
  32. }
  33. throttledAction.Update(() =>
  34. {
  35. MainThread.BeginInvokeOnMainThread(() =>
  36. {
  37. CurrentLocation=amapLocation;
  38. if (reGeocodeLocation!=null)
  39. {
  40. Address = reGeocodeLocation.Address;
  41. Pois=new ObservableCollection<Poi>(reGeocodeLocation.Pois);
  42. }
  43. });
  44. });
  45. throttledAction.Invoke();
  46. }
  47. }
  48. }
  49. }

至此我们完成了地图组件的基本功能。

创建交互逻辑

在MainPage.xaml中,创建一个选择器按钮,以及一个卡片模拟选择器按钮点击后的弹窗。

  1. <Button Clicked="Button_Clicked"
  2. Grid.Row="1"
  3. x:Name="SelectorButton"
  4. HorizontalOptions="Center"
  5. VerticalOptions="Center"
  6. Text="{Binding Address, TargetNullValue=请选择地点}"></Button>
  7. <Border StrokeShape="RoundRectangle 10"
  8. Grid.RowSpan="2"
  9. x:Name="SelectorPopup"
  10. IsVisible="False"
  11. Margin="5,50"
  12. MinimumHeightRequest="500">
  13. <Grid Padding="0">
  14. <Grid.RowDefinitions>
  15. <RowDefinition Height="Auto"></RowDefinition>
  16. <RowDefinition Height="Auto"></RowDefinition>
  17. <RowDefinition></RowDefinition>
  18. </Grid.RowDefinitions>
  19. <Grid Grid.Row="0">
  20. <Grid.ColumnDefinitions>
  21. <ColumnDefinition Width="*"></ColumnDefinition>
  22. <ColumnDefinition></ColumnDefinition>
  23. </Grid.ColumnDefinitions>
  24. <Label FontSize="Large"
  25. Margin="10, 10, 10, 0"
  26. FontAttributes="Bold"
  27. Text="选择地点"></Label>
  28. <HorizontalStackLayout Grid.Column="1"
  29. HorizontalOptions="End">
  30. <Button Text="删除"
  31. Margin="5,0"
  32. Command="{Binding Remove}"></Button>
  33. <Button Text="完成"
  34. Margin="5,0"
  35. Command="{Binding Done}"></Button>
  36. </HorizontalStackLayout>
  37. </Grid>
  38. <Grid Grid.Row="1"
  39. Margin="10, 10, 10, 0">
  40. <Grid.RowDefinitions>
  41. <RowDefinition></RowDefinition>
  42. <RowDefinition Height="Auto"></RowDefinition>
  43. </Grid.RowDefinitions>
  44. <Label HorizontalTextAlignment="Center"
  45. VerticalOptions="Center"
  46. x:Name="ContentLabel"
  47. Text="{Binding Address}"></Label>
  48. <Border IsVisible="False"
  49. Grid.RowSpan="2"
  50. x:Name="ContentFrame">
  51. <Entry Text="{Binding Address, Mode=TwoWay}"
  52. Placeholder="请输入地址, 按Enter键完成"
  53. Completed="Entry_Completed"
  54. Unfocused="Entry_Unfocused"
  55. ClearButtonVisibility="WhileEditing"></Entry>
  56. </Border>
  57. <Border x:Name="ContentButton"
  58. Grid.Row="1"
  59. HorizontalOptions="Center"
  60. VerticalOptions="Center">
  61. <Label>
  62. <Label.FormattedText>
  63. <FormattedString>
  64. <Span FontFamily="FontAwesome"
  65. Text="&#xf044;"></Span>
  66. <Span Text=" 修改"></Span>
  67. </FormattedString>
  68. </Label.FormattedText>
  69. </Label>
  70. <Border.GestureRecognizers>
  71. <TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped">
  72. </TapGestureRecognizer>
  73. </Border.GestureRecognizers>
  74. </Border>
  75. </Grid>
  76. <BlazorWebView Grid.Row="2"
  77. Margin="-10, 0"
  78. x:Name="mainMapBlazorWebView"
  79. HostPage="wwwroot/amap_index.html">
  80. <BlazorWebView.RootComponents>
  81. <RootComponent Selector="#app"
  82. x:Name="rootComponent"
  83. ComponentType="{x:Type views:AMapPage}" />
  84. </BlazorWebView.RootComponents>
  85. </BlazorWebView>
  86. </Grid>
  87. </Border>

在这里插入图片描述

最终效果如下:

在这里插入图片描述

项目地址

Github:maui-samples

原文链接:https://www.cnblogs.com/jevonsflash/p/18091763

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号