Xamarin Forms Maps监听拖拽事件显示用户位置重定位图标
Xamarin.Forms Map 拖拽显示重定位按钮实现方案
核心实现逻辑是通过自定义渲染器监听各平台原生地图的相机/区域变化完成事件,替代原生控件缺失的拖拽结束事件,不要使用叠层手势识别、坐标轮询这类兼容性、性能都很差的方案。
步骤1:共享项目自定义Map控件
首先在共享代码项目中继承原生Map,新增需要的属性和事件,用于和平台渲染器、业务逻辑通信:
public class CustomMap : Map { // 控制重定位按钮显示状态的可绑定属性 public static readonly BindableProperty ShowRelocateButtonProperty = BindableProperty.Create( nameof(ShowRelocateButton), typeof(bool), typeof(CustomMap), false); public bool ShowRelocateButton { get => (bool)GetValue(ShowRelocateButtonProperty); set => SetValue(ShowRelocateButtonProperty, value); } // 标记是否为代码主动移动地图,避免误判用户拖拽 public bool IsProgrammaticMove { get; set; } // 用户拖拽/缩放地图完成后触发的事件 public event EventHandler<MapDragEndedEventArgs> MapDragEnded; public void TriggerDragEnded(Position newMapCenter) { MapDragEnded?.Invoke(this, new MapDragEndedEventArgs(newMapCenter)); } } public class MapDragEndedEventArgs : EventArgs { public Position CurrentCenter { get; } public MapDragEndedEventArgs(Position center) => CurrentCenter = center; }
步骤2:iOS平台渲染器实现
iOS原生MKMapView自带RegionChanged事件,会在地图拖拽、缩放等操作导致区域变化完成后触发,直接监听该事件即可:
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))] namespace YourApp.iOS.Renderers { public class CustomMapRenderer : MapRenderer { MKMapView _nativeMap; protected override void OnElementChanged(ElementChangedEventArgs<View> e) { base.OnElementChanged(e); if (e.NewElement != null) { _nativeMap = Control as MKMapView; if (_nativeMap != null) { _nativeMap.RegionChanged += OnMapRegionChanged; } } if (e.OldElement != null && _nativeMap != null) { _nativeMap.RegionChanged -= OnMapRegionChanged; } } private void OnMapRegionChanged(object sender, MKMapViewChangeEventArgs e) { var customMap = Element as CustomMap; // 仅在非代码触发的移动结束时抛事件 if (!customMap.IsProgrammaticMove) { var center = new Position( _nativeMap.CenterCoordinate.Latitude, _nativeMap.CenterCoordinate.Longitude); customMap.TriggerDragEnded(center); } } } }
步骤3:Android平台渲染器实现
Android原生GoogleMap提供OnCameraIdle回调,会在地图拖拽、缩放结束,相机停止移动时触发,刚好匹配需求:
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))] namespace YourApp.Droid.Renderers { public class CustomMapRenderer : MapRenderer, GoogleMap.IOnCameraIdleListener { GoogleMap _nativeMap; protected override void OnMapReady(GoogleMap map) { base.OnMapReady(map); _nativeMap = map; _nativeMap.SetOnCameraIdleListener(this); } public void OnCameraIdle() { var customMap = Element as CustomMap; if (!customMap.IsProgrammaticMove) { var cameraTarget = _nativeMap.CameraPosition.Target; var center = new Position(cameraTarget.Latitude, cameraTarget.Longitude); customMap.TriggerDragEnded(center); } } protected override void Dispose(bool disposing) { if (disposing && _nativeMap != null) { _nativeMap.SetOnCameraIdleListener(null); } base.Dispose(disposing); } } }
步骤4:业务逻辑实现
- 页面布局中替换原有Map控件为
CustomMap,在地图上层叠加重定位按钮,初始状态隐藏 - 监听
MapDragEnded事件,不要直接判断坐标是否完全相等(GPS漂移、微小移动都会导致判断失效),通过两点距离判断是否偏离用户位置:
// 计算两个坐标的地表直线距离,返回值单位为米 private double CalculateDistance(Position p1, Position p2) { var lat1 = p1.Latitude * Math.PI / 180; var lon1 = p1.Longitude * Math.PI / 180; var lat2 = p2.Latitude * Math.PI / 180; var lon2 = p2.Longitude * Math.PI / 180; var deltaLat = lat2 - lat1; var deltaLon = lon2 - lon1; var calc = Math.Pow(Math.Sin(deltaLat / 2), 2) + Math.Cos(lat1) * Math.Cos(lat2) * Math.Pow(Math.Sin(deltaLon / 2), 2); return 6376500 * 2 * Math.Atan2(Math.Sqrt(calc), Math.Sqrt(1 - calc)); } // 拖拽结束事件处理 private void OnMapDragEnded(object sender, MapDragEndedEventArgs e) { // 阈值设为50米,小于该距离认为还在用户位置范围内 var distance = CalculateDistance(e.CurrentCenter, _lastUserPosition); ((CustomMap)sender).ShowRelocateButton = distance > 50; } // 重定位按钮点击逻辑 private void OnRelocateClicked(object sender, EventArgs e) { customMap.IsProgrammaticMove = true; var region = MapSpan.FromCenterAndRadius( _lastUserPosition, Distance.FromMeters(500)); customMap.MoveToRegion(region); customMap.ShowRelocateButton = false; // 移动完成后重置标记,延迟是为了等平台地图动画执行完成 Device.StartTimer(TimeSpan.FromMilliseconds(300), () => { customMap.IsProgrammaticMove = false; return false; }); }
常见踩坑说明
- 直接给Map或上层容器加手势识别器无效:Map本身是原生控件封装,手势会被原生层优先消费;上层容器如果开启输入接收会挡住地图交互,设置
InputTransparent=true又无法接收手势,完全不可行 - 坐标轮询方案不推荐:后台持续轮询坐标会造成不必要的性能消耗,响应延迟也高
- 不要用坐标完全相等作为判断依据:GPS本身存在漂移,用户操作地图也不可能刚好停在和用户坐标完全一致的点,必须设置合理的距离阈值
内容的提问来源于stack exchange,提问作者WoistdasNiveau
相关产品推荐
相关产品推荐

