Xamarin Forms Maps - how to refresh/update the map - CustomMap Renderer

前端 未结 4 1883
予麋鹿
予麋鹿 2021-02-06 14:06

If you are searching for a full polylines, pins, tiles, UIOptions (and 3D effects soon) renderings/implementations, you should take a loot at the public github I made at

相关标签:
4条回答
  • 2021-02-06 14:11

    So after lot of searches and, of course, the answer of @Sven-Michael Stübe, you can have your proper maps which works on each platform "Android, iOS, WinPhone". Follow my code, then edit it following the @Sven-Michael Stübe's answer.

    Once you finished everything, it could works (like for @Sven-Michael Stübe), but it also couldn't work (like for me). If it doesn't works, try to change the following code:

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

    by

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

    See the documentation for more information about it. (Deprecated implementation)

    Then the code works !

    PS: You can have some troubles with the polyline at the end, which not following the road right, I'm working on it.

    PS2: I'll also make a video to explain how to code your customMap to don't have to install a NuGet package, to be able to edit everything at the end ! (The first one will be in French, the second in English, this post will be edited when the video will be made)

    Thank angain to @Sven-Michael Stübe !! Thank to up his answer as well :)

    0 讨论(0)
  • 2021-02-06 14:12

    The custom renderer from the example is not made for dynamic updating the path. It is just implemented for the case, where all points of the paths are known before initializing the map / drawing the path the first time. So you have this race condition, you ran into, because you are loading the directions from a web service.

    So you have to do some changes:

    RouteCoordinates must be a 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>();
        }
    }
    

    Update the Polyline whenever the coordinates change

    • Move the creation of the polyline from OnMapReady to UpdatePolyLine
    • call UpdatePolyLine from OnMapReady and OnElementPropertyChanged
    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();
        }
    }
    

    Setting the data

    Updating the positions changes a bit. Instead of adding the positions to the existing list, you have to (create a new list) and set it to RouteCoordinates. You can use Device.BeginInvokeOnMainThread to ensure, that the operation is performed on the UI thread. Else the polyline will not update.

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

    In your case it's something like

    var list = new List<Position>(customMap.RouteCoordinates);
    list.Add(directionMap.address_end.position);
    customMap.RouteCoordinates = list;
    

    Todo

    On iOS you have now to implement a similar behavior (like UpdatePolyLine)

    Note

    That might not the most performant implementation, because you redraw everything instead of adding one point. But it's fine as long as you have no performance issues :)

    0 讨论(0)
  • 2021-02-06 14:23

    I followed the tutorial available on Xamarin Docs and it worked for me with some changes based on @Sven-Michael Stübe answer

    I load the coordinates from a WebService and then I create a separate List, and after this, I set the new list to the RouteCoordinates property on Custom Map.

    Some changes are made on Android Renderer

    I'm using MVVM.

    CustomMap Class:

    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 Renderer:

    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;
            }
        }
    }
    

    This example have more than 3600 points of location and the polyline shows correctly on device:

    Screenshot

    0 讨论(0)
  • 2021-02-06 14:26

    Building on these answers, here is what I did to get it to work on iOS. This allows changing the route even after the map is loaded, unlike the Xamarin sample.

    Firstly, custom map class as per @Sven-Michael Stübe with the update from @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>();
        }
    }
    

    Next, the iOS custom renderer:

    [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);
            }
        }
    }
    

    And finally, adding a polyline to the map:

                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)));
    
                });
    

    Not sure if this is the best way to do it, or the most efficient, I don't know much about renderers, but it does seem to work.

    0 讨论(0)
提交回复
热议问题