Xamarin Forms地图 - 如何刷新/更新地图 - 自定义地图渲染器

5

如果你正在寻找完整的折线、标记、瓷砖、UI选项(以及即将推出的3D效果)的渲染/实现,你应该看一下我在公共Github上创建的XamarinByEmixam23/..../Map


我搜了很多但我仍然有同样的问题:

我如何更新、刷新或重新加载Xamarin.Forms.Maps?

在类定义中(class CustomMap:Map),没有更新地图的方法。也许MVVM逻辑可以解决问题,但我在网上找不到它。

我按照这个地图教程:Working with maps.

为了自定义它,我按照这个教程:Highlight a Route on a Map.

所以,在这些教程之后(我做了相同的事情,没有改变),我用两个RouteCoordinates尝试了一条直线...... 然后我写了一个算法,它完美地工作。

DirectionMap

public class DirectionMap
{
    public Distance distance { get; set; }
    public Duration duration { get; set; }
    public Address address_start { get; set; }
    public Address address_end { get; set; }
    public List<Step> steps { get; set; }

    public class Distance
    {
        public string text { get; set; }
        public int value { get; set; }
    }
    public class Duration
    {
        public string text { get; set; }
        public int value { get; set; }
    }
    public class Address
    {
        public string text { get; set; }
        public Position position { get; set; }
    }
    public class Step
    {
        public Position start { get; set; }
        public Position end { get; set; }
    }
}

ResponseHttpParser

public static void parseDirectionGoogleMapsResponse(HttpStatusCode httpStatusCode, JObject json, Action<DirectionMap, string> callback)
{
    switch (httpStatusCode)
    {
        case HttpStatusCode.OK:

            DirectionMap directionMap = null;
            string strException = null;

            try
            {
                directionMap = new DirectionMap()
                {
                    distance = new DirectionMap.Distance()
                    {
                        text = (json["routes"][0]["legs"][0]["distance"]["text"]).ToString(),
                        value = Int32.Parse((json["routes"][0]["legs"][0]["distance"]["value"]).ToString())
                    },
                    duration = new DirectionMap.Duration()
                    {
                        text = (json["routes"][0]["legs"][0]["duration"]["text"]).ToString(),
                        value = Int32.Parse((json["routes"][0]["legs"][0]["duration"]["value"]).ToString())
                    },
                    address_start = new DirectionMap.Address()
                    {
                        text = (json["routes"][0]["legs"][0]["start_address"]).ToString(),
                        position = new Position(Double.Parse((json["routes"][0]["legs"][0]["start_location"]["lat"]).ToString()), Double.Parse((json["routes"][0]["legs"][0]["start_location"]["lng"]).ToString()))
                    },
                    address_end = new DirectionMap.Address()
                    {
                        text = (json["routes"][0]["legs"][0]["end_address"]).ToString(),
                        position = new Position(Double.Parse((json["routes"][0]["legs"][0]["end_location"]["lat"]).ToString()), Double.Parse((json["routes"][0]["legs"][0]["end_location"]["lng"]).ToString()))
                    }
                };

                bool finished = false;
                directionMap.steps = new List<Step>();
                int index = 0;

                while (!finished)
                {
                    try
                    {
                        Step step = new Step()
                        {
                            start = new Position(Double.Parse((json["routes"][0]["legs"][0]["steps"][index]["start_location"]["lat"]).ToString()), Double.Parse((json["routes"][0]["legs"][0]["steps"][index]["start_location"]["lng"]).ToString())),
                            end = new Position(Double.Parse((json["routes"][0]["legs"][0]["steps"][index]["end_location"]["lat"]).ToString()), Double.Parse((json["routes"][0]["legs"][0]["steps"][index]["end_location"]["lng"]).ToString()))
                        };
                        directionMap.steps.Add(step);
                        index++;
                    }
                    catch (Exception e)
                    {
                        finished = true;
                    }
                }
            }
            catch (Exception e)
            {
                directionMap = null;
                strException = e.ToString();
            }
            finally
            {
                callback(directionMap, strException);
            }
            break;
        default:
            switch (httpStatusCode)
            {

            }
            callback(null, json.ToString());
            break;
    }
}

我只是为了一些私人计算获取距离和持续时间,并将每个步骤放入List<>中。

当所有事情都完成时,我使用我的回调函数将我们带回控制器(MapPage.xaml.cs XAML表单页面(Xamarin Portable))。

现在,一切变得奇怪了。就像地图没有意识到已经进行了更改。

public partial class MapPage : ContentPage
{
    public MapPage()
    {
        InitializeComponent();
        setupMap();
        setupMapCustom();
    }

    public void setupMapCustom()
    {
        customMap.RouteCoordinates.Add(new Position(37.785559, -122.396728));
        customMap.RouteCoordinates.Add(new Position(37.780624, -122.390541));
        customMap.RouteCoordinates.Add(new Position(37.777113, -122.394983));
        customMap.RouteCoordinates.Add(new Position(37.776831, -122.394627));

        customMap.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(37.79752, -122.40183), Xamarin.Forms.Maps.Distance.FromMiles(1.0)));
    }       

    public async void setupMap()
    {
        customMap.MapType = MapType.Satellite;

        string origin = "72100 Le Mans";
        string destination = "75000 Paris";

        HttpRequest.getDirections(origin, destination, callbackDirections);

        customMap.RouteCoordinates.Add(await MapUtilities.GetMapPointOfStreetAddress(origin));
        Position position = await MapUtilities.GetMapPointOfStreetAddress(destination);
        //customMap.RouteCoordinates.Add(position);

        var pin = new Pin
        {
            Type = PinType.Place,
            Position = position,
            Label = "Destination !!",
        };
        customMap.Pins.Add(pin);
    }

    private async void callbackDirections(Object obj, string str)
    {
        if (obj != null)
        {
            DirectionMap directionMap = obj as DirectionMap;

            foreach (Step step in directionMap.steps)
            {
                customMap.RouteCoordinates.Add(step.start);
                System.Diagnostics.Debug.WriteLine("add step");
            }

            customMap.RouteCoordinates.Add(directionMap.address_end.position);
            System.Diagnostics.Debug.WriteLine("add last step");
        }
        else
        {
            System.Diagnostics.Debug.WriteLine(str);
        }
    }
}

我运行我的应用程序,一切正常,直到出现某些快速的情况,因为算法所花费的时间等原因,回调函数来得太晚,然后我需要刷新、重新加载或更新我的地图... 无论如何,我未来需要更新我的地图,所以... 如果有人能帮忙,这个人就受欢迎了!
编辑1 我看了你的答案(非常感谢!;))但它不起作用 :/
我像你做的那样更新了CustomMap。
public class CustomMap : Map
{
    public static readonly BindableProperty RouteCoordinatesProperty =
    BindableProperty.Create<CustomMap, List<Position>>(p => p.RouteCoordinates, new List<Position>());

    public List<Position> RouteCoordinates
    {
        get { return (List<Position>)GetValue(RouteCoordinatesProperty); }
        set { SetValue(RouteCoordinatesProperty, value); }
    }

    public CustomMap()
    {
        RouteCoordinates = new List<Position>();
    }
}

同样适用于 CustomMapRenderer(Droid)

public class CustomMapRenderer : MapRenderer, IOnMapReadyCallback
{
    GoogleMap map;
    Polyline polyline;

    protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.View> e)
    {
        base.OnElementChanged(e);

        if (e.OldElement != null)
        {
            // Unsubscribe
        }

        if (e.NewElement != null)
        {
            ((MapView)Control).GetMapAsync(this);
        }
    }

    protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);
        if (this.Element == null || this.Control == null)
            return;

        if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
        {
            UpdatePolyLine();
        }
    }

    private void UpdatePolyLine()
    {
        if (polyline != null)
        {
            polyline.Remove();
            polyline.Dispose();
        }

        var polylineOptions = new PolylineOptions();
        polylineOptions.InvokeColor(0x66FF0000);

        foreach (var position in ((CustomMap)this.Element).RouteCoordinates)
        {
            polylineOptions.Add(new LatLng(position.Latitude, position.Longitude));
        }

        polyline = map.AddPolyline(polylineOptions);
    }

    public void OnMapReady(GoogleMap googleMap)
    {
        map = googleMap;
        UpdatePolyLine();
    }
}

那么,针对最后一次更改,在我的MapPage.xaml.cs文件中,我根据您的说明对callbackDirections进行了更改(希望我做得好)。

private async void callbackDirections(Object obj, string str)
    {
        if (obj != null)
        {
            Device.BeginInvokeOnMainThread(() =>
            {
                DirectionMap directionMap = obj as DirectionMap;
                var list = new List<Position>(customMap.RouteCoordinates);

                foreach (Step step in directionMap.steps)
                {
                    list.Add(directionMap.address_end.position);
                    System.Diagnostics.Debug.WriteLine("add step");
                }

                System.Diagnostics.Debug.WriteLine("last step");
                customMap.RouteCoordinates = list;
                System.Diagnostics.Debug.WriteLine("finished?");
            });
        }
        else
        {
            System.Diagnostics.Debug.WriteLine(str);
        }
    }

地图仍然没有显示折线 :/ 我只做了这些更改,没有改变我之前代码中的其他内容。
我没有告诉你,但我不是MVVM绑定方面的专家,所以如果我忘记了什么,请原谅 :/ 编辑2 在阅读了您的回答并反复阅读了它后,在MapPage.xaml.cs中有我的“测试代码”。
public MapPage()
    {
        InitializeComponent();
        //HttpRequest.getDirections(origin, destination, callbackDirections);

        Device.BeginInvokeOnMainThread(() =>
        {
            customMap.RouteCoordinates = new List<Position>
            {
                new Position (37.797534, -122.401827),
                new Position (37.776831, -122.394627)
            };
        });

        //setupMap();
        //setupMapCustom();
    }

因为它对我无效,所以我查看了我的代码,然后发现public static readonly BindableProperty RouteCoordinatesProperty = BindableProperty.Create<CustomMap, List<Position>>( p => p.RouteCoordinates, new List<Position>());已被弃用。
因此,我在这个帖子上找到了一种不同的实现绑定的方法,但它也说这种方式已经被弃用请看这里... 我还看了一些关于绑定的教程,他们说他们将一些代码放入了他们的xaml中,让我想起了我的。
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
     xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
     xmlns:local="clr-namespace:NAMESPACE;assembly=NAMESPACE"
     x:Class="NAMESPACE.Controlers.MapPage">
         <ContentPage.Content>
             <local:CustomMap x:Name="customMap"/>
         </ContentPage.Content>
</ContentPage>

我没有使用ItemSource="{PolylineBindable}"这样的东西。

4个回答

8

这个示例中的自定义渲染器并不适用于动态更新路径。它只是针对路径的所有点在初始化地图/第一次绘制路径之前已知的情况实现的。所以你遇到了这个竞争条件,因为你正在从Web服务加载方向。

所以你需要做一些改变:

RouteCoordinates必须是一个BindableProperty

public class CustomMap : Map
{
    public static readonly BindableProperty RouteCoordinatesProperty =
        BindableProperty.Create<CustomMap, List<Position>>(p => p.RouteCoordinates, new List<Position>());

    public List<Position> RouteCoordinates
    {
        get { return (List<Position>)GetValue(RouteCoordinatesProperty); }
        set { SetValue(RouteCoordinatesProperty, value); }
    }

    public CustomMap ()
    {
        RouteCoordinates = new List<Position>();
    }
}

在坐标改变时更新折线

  • 将折线的创建从OnMapReady移动到UpdatePolyLine
  • OnMapReadyOnElementPropertyChanged中调用UpdatePolyLine
public class CustomMapRenderer : MapRenderer, IOnMapReadyCallback
{
    GoogleMap map;
    Polyline polyline;

    protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<View> e)
    {
        base.OnElementChanged(e);

        if (e.OldElement != null)
        {
            // Unsubscribe
        }

        if (e.NewElement != null)
        {
            ((MapView)Control).GetMapAsync(this);
        }
    }

    protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);
        if (this.Element == null || this.Control == null)
            return;

        if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
        {
            UpdatePolyLine();
        }
    }

    private void UpdatePolyLine()
    {
        if (polyline != null)
        {
            polyline.Remove();
            polyline.Dispose();
        }               

        var polylineOptions = new PolylineOptions();
        polylineOptions.InvokeColor(0x66FF0000);

        foreach (var position in ((CustomMap)this.Element).RouteCoordinates)
        {
            polylineOptions.Add(new LatLng(position.Latitude, position.Longitude));
        }

        polyline = map.AddPolyline(polylineOptions);
    }

    public void OnMapReady(GoogleMap googleMap)
    {
        map = googleMap;
        UpdatePolyLine();
    }
}

设置数据

更新位置的过程有所变化。不是将位置添加到现有列表中,而是需要(创建一个新的列表)并将其设置为RouteCoordinates。您可以使用Device.BeginInvokeOnMainThread来确保操作在UI线程上执行。否则,折线将不会更新。

Device.BeginInvokeOnMainThread(() =>
{
    customMap.RouteCoordinates = new List<Position>
    {
        new Position (37.797534, -122.401827),
        new Position (37.776831, -122.394627)
    };
}) 

在您的情况下,类似于以下内容:
var list = new List<Position>(customMap.RouteCoordinates);
list.Add(directionMap.address_end.position);
customMap.RouteCoordinates = list;

待办事项

在iOS上,您现在需要实现类似于UpdatePolyLine的行为。

注意

这可能不是最高效的实现方式,因为您重新绘制了所有内容而不是添加一个点。但是只要没有性能问题,那么就没问题 :)


谢谢您的回答!请看一下我的编辑1 :) - Emixam23
1
请仔细阅读我的回答(todo 上面的部分)。我说过,您必须更改向列表添加位置的方式。渲染器不会注意到如果您调用 customMap.RouteCoordinates.Add(...),您必须分配列表,因为 setter 会触发 OnElementPropertyChanged。这可理解吗? - Sven-Michael Stübe
你好,我想知道自从上一次以来,你是否能够给我一个答案?:/ 我还是卡住了...我尝试了很多次,这真的很烦人:( - Emixam23
1
我给你的肯定是有效的。我在一个运行Marshmallow的Nexus 5上尝试过了。所以我只能想象加载数据和地图初始化之间存在某种竞争条件。 - Sven-Michael Stübe
好的,谢谢,我会重新检查的 :) 但是,你有关于BindableProperty.Create的提示吗?非常感谢您! - Emixam23
显示剩余2条评论

2
我按照Xamarin文档中提供的教程进行操作,并根据@Sven-Michael Stübe的回答进行了一些更改,最终实现了功能。
我从WebService中加载坐标,然后创建一个单独的列表,将新列表设置为自定义地图上RouteCoordinates属性的值。
在Android Renderer上做了一些更改。
我正在使用MVVM。 CustomMap类:
public static readonly BindableProperty RouteCoordinatesProperty =
        BindableProperty.Create(nameof(RouteCoordinates), typeof(List<Position>), typeof(CustomMap), new List<Position>(), BindingMode.TwoWay);

public List<Position> RouteCoordinates
{
    get { return (List<Position>)GetValue(RouteCoordinatesProperty); }
    set { SetValue(RouteCoordinatesProperty, value); }
}

public CustomMap()
{
    RouteCoordinates = new List<Position>();
}

ViewModel (Codebehind, in your case):

private async void LoadCoordinates(string oidAula, CustomMap mapa)
{
    IsBusy = true;

    var percurso = await ComunicacaoServidor.GetPercurso(oidAula); // Get coordinates from WebService
    var pontos = percurso.Select(p => new Position(p.Latitude, p.Longitude)).ToList(); // Create coordinates list from webservice result

    var latitudeMedia = percurso[percurso.Count / 2].Latitude;
    var longitudeMedia = percurso[percurso.Count / 2].Longitude;

    mapa.RouteCoordinates = pontos;
    mapa.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(latitudeMedia, longitudeMedia), Distance.FromMiles(1.0)));

    IsBusy = false;
}

XAML:

<maps:CustomMap
        AbsoluteLayout.LayoutFlags  = "All"
        AbsoluteLayout.LayoutBounds = "0, 0, 1, 1"
        VerticalOptions             = "FillAndExpand"
        HorizontalOptions           = "FillAndExpand"
        x:Name                      = "PercursoMapa" />

Android 渲染器:

public class CustomMapRenderer : MapRenderer
{
    bool isDrawn;

    protected override void OnElementChanged(ElementChangedEventArgs<Map> e)
    {
        base.OnElementChanged(e);

        if (e.OldElement != null)
        {
            // Unsubscribe
        }

        if (e.NewElement != null)
            Control.GetMapAsync(this);
    }

    protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);

        if ((e.PropertyName == "RouteCoordinates" || e.PropertyName == "VisibleRegion") && !isDrawn)
        {
            var polylineOptions = new PolylineOptions();
            polylineOptions.InvokeColor(0x66FF0000);

            var coordinates = ((CustomMap)Element).RouteCoordinates;

            foreach (var position in coordinates)
                polylineOptions.Add(new LatLng(position.Latitude, position.Longitude));

            NativeMap.AddPolyline(polylineOptions);
            isDrawn = coordinates.Count > 0;
        }
    }
}

这个例子有超过3600个位置点,折线在设备上正确显示:

截图


我修改了问题,但还是感谢MVVM实现,它仍然很好用! - Emixam23

1
在这些答案的基础上,以下是我在iOS上使其正常工作的步骤。这样可以在加载地图后甚至更改路线,与Xamarin示例不同。
首先,按照@Sven-Michael Stübe的自定义地图类并使用@Emixam23的更新:
public class CustomMap : Map
{
    public static readonly BindableProperty RouteCoordinatesProperty =
        BindableProperty.Create(nameof(RouteCoordinates), typeof(List<Position>), typeof(CustomMap), new List<Position>(), BindingMode.TwoWay);

    public List<Position> RouteCoordinates
    {
        get { return (List<Position>)GetValue(RouteCoordinatesProperty); }
        set { SetValue(RouteCoordinatesProperty, value); }
    }

    public CustomMap()
    {
        RouteCoordinates = new List<Position>();
    }
}

下一步是iOS自定义渲染器:
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace KZNTR.iOS
{
    public class CustomMapRenderer : MapRenderer
    {
        MKPolylineRenderer polylineRenderer;
        CustomMap map;

        protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if ((this.Element == null) || (this.Control == null))
                return;

            if (e.PropertyName == CustomMap.RouteCoordinatesProperty.PropertyName)
            {
                map = (CustomMap)sender;
                UpdatePolyLine();
            }
        }

        [Foundation.Export("mapView:rendererForOverlay:")]
        MKOverlayRenderer GetOverlayRenderer(MKMapView mapView, IMKOverlay overlay)
        {
            if (polylineRenderer == null)
            {
                var o = ObjCRuntime.Runtime.GetNSObject(overlay.Handle) as MKPolyline;

                polylineRenderer = new MKPolylineRenderer(o);
                //polylineRenderer = new MKPolylineRenderer(overlay as MKPolyline);
                polylineRenderer.FillColor = UIColor.Blue;
                polylineRenderer.StrokeColor = UIColor.Red;
                polylineRenderer.LineWidth = 3;
                polylineRenderer.Alpha = 0.4f;
            }
            return polylineRenderer;
        }

        private void UpdatePolyLine()
        {

            var nativeMap = Control as MKMapView;

            nativeMap.OverlayRenderer = GetOverlayRenderer;

            CLLocationCoordinate2D[] coords = new CLLocationCoordinate2D[map.RouteCoordinates.Count];

            int index = 0;
            foreach (var position in map.RouteCoordinates)
            {
                coords[index] = new CLLocationCoordinate2D(position.Latitude, position.Longitude);
                index++;
            }

            var routeOverlay = MKPolyline.FromCoordinates(coords);
            nativeMap.AddOverlay(routeOverlay);
        }
    }
}

最后,将折线添加到地图上:
            Device.BeginInvokeOnMainThread(() =>
            {
                customMap.RouteCoordinates.Clear();

                var plist = new List<Position>(customMap.RouteCoordinates);

                foreach (var point in track.TrackPoints)
                {
                    plist.Add(new Position(double.Parse(point.Latitude, CultureInfo.InvariantCulture), double.Parse(point.Longitude, CultureInfo.InvariantCulture)));
                }

                customMap.RouteCoordinates = plist;

                var firstpoint = (from pt in track.TrackPoints select pt).FirstOrDefault();
                customMap.MoveToRegion(MapSpan.FromCenterAndRadius(new Position(double.Parse(firstpoint.Latitude, CultureInfo.InvariantCulture), double.Parse(firstpoint.Longitude, CultureInfo.InvariantCulture)), Distance.FromMiles(3.0)));

            });

我不确定这是否是最佳或最有效的方法,我对渲染器并不了解,但它似乎可以工作。


1
这是否允许在人行走或奔跑时呈现地图,即更新地图?您对此有何看法?在2018年,有更好的方法吗? - c-sharp-and-swiftui-devni

0

所以经过了许多搜索和当然是@Sven-Michael Stübe的答案,您可以拥有适用于每个平台“Android,iOS,WinPhone”的地图。跟随我的代码,然后根据@Sven-Michael Stübe的答案进行编辑。

一旦你完成了所有事情,它可能会像@Sven-Michael Stübe一样工作,但也可能不会像我一样工作。如果它无法正常工作,请尝试更改以下代码:

public static readonly BindableProperty RouteCoordinatesProperty =
    BindableProperty.Create<CustomMap, List<Position>>(
        p => p.RouteCoordinates, new List<Position>());

public static readonly BindableProperty RouteCoordinatesProperty =
    BindableProperty.Create(nameof(RouteCoordinates), typeof(List<Position>), typeof(CustomMap), new List<Position>(), BindingMode.TwoWay);

有关此内容的更多信息,请参阅文档(已弃用实现)

然后代码就可以工作了!

附注:您可能会遇到折线在最后没有正确跟随道路的一些问题,我正在解决。

附注2:我还将制作一个视频来解释如何编写自定义地图,以便无需安装NuGet包即可在最后编辑所有内容! (第一个将是法语,第二个将是英语,在制作视频时将编辑此帖子)

再次感谢@Sven-Michael Stübe!!也要感谢他的回答 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接