amay077 / Xamarin.Forms.GoogleMaps

Map library for Xamarin.Forms using Google maps API
https://www.nuget.org/packages/Xamarin.Forms.GoogleMaps/
MIT License
545 stars 346 forks source link

BitmapDescriptor not rendered #754

Closed orwo1 closed 2 years ago

orwo1 commented 3 years ago

VERSIONS

PLATFORMS

ACTUAL BEHAVIOR

Getting a BitmapDescriptor from xaml view of Image, is not rendered on the map every time. When it is not rendered on the pin, no error is displayed, and no exception is thrown. The pin renders the image one out of every couple of tries.

ACTUAL SCREENSHOTS/STACKTRACE

https://pasteboard.co/JyCVGyR.gif

EXPECTED BEHAVIOR

Each time I navigate to page with a map control, and I add pins through MapObjects property, To have them rendered with the images provided.

HOW TO REPRODUCE

  1. Add pin to ObservableCollection
  2. Supply DataTemplate for pin, where Icon property gets a View using a converter
  3. Navigate to page with map control to display pin.
Pin pin = new Pin()
            {
                Type = PinType.Generic,
                IsDraggable = false,
                ZIndex = 100,
                Anchor = new Point(0.5, 0.5),
                Flat = true,
                Label = "Blabla",
                Tag = "Blabla",
                Position = _somePosition;
            };
MapObjetcs.Add(pin);

Xaml:

<map:PinIconConverter x:Key="PinIconConverter">
        <map:PinIconConverter.PinTypeOneView>
            <Image Source="{customControls:ImageMultiResource PinTypeOneViewImage.png}"
                                  HeightRequest="22"
                                  WidthRequest="22"
                                  Aspect="Fill"/>
        </map:PinIconConverter.PinTypeOneView>
        <map:PinIconConverter.PinTypeTwoView>
            <Image Source="{customControls:ImageMultiResource PinTypeTwoViewImage.png}"
                                  HeightRequest="20"
                                  WidthRequest="22"
                                  Aspect="Fill" />
        </map:PinIconConverter.PinTypeTwoView>
    </map:PinIconConverter>
<DataTemplate x:Key="DataTemplatePin">
        <googlemaps:Pin Position="{Binding Position}"
                        Icon="{Binding Converter={StaticResource PinIconConverter}}"
                        Label="{Binding Label}"
                        IsDraggable="{Binding IsDraggable}"
                        ZIndex="{Binding ZIndex}"
                        Flat="{Binding Flat}"
                        Rotation="{Binding Rotation}"
                        Address="{Binding Address}"
                        Anchor="{Binding Anchor}"
                        InfoWindowAnchor="{Binding InfoWindowAnchor}"
                        IsVisible="{Binding IsVisible}"
                        Transparency="{Binding Transparency}"
                        Type="{Binding Type}"/>
</DataTemplate>

PinIconConverter returns BitmapDescriptorFactory.FromView(PinTypeOneView/PinTypeTwoView);

<googlemaps:Map x:Name="map"
                            MapType="{Binding MapType, Source={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MapView}}}"
                            MyLocationEnabled="False"
                            IsShowingUser="{Binding IsShowingOwnerOnMap}"
                            HasRotationEnabled="{Binding IsRotationEnabled}"
                            HasScrollEnabled="{Binding IsScrollEnabled}"
                            HasZoomEnabled="{Binding IsZoomEnabled}"
                            ItemsSource="{Binding MapObjetcs}"
                            ItemTemplate="{StaticResource DataTemplatePin}"/>
JordanBird commented 3 years ago

I've encountered, and recently worked around this issue.

Basically as far as I can tell the image load is async, so the view finishes loading without the image being ready; which is why if you were to use an element with a background colour, it would render correctly.

The workaround for me was to generate the image as a bitmap using SkiaSharp without using the view and cache the result.

I don't imagine there will be a solve for Xamarin.Forms.GoogleMaps until we can block the UI in Xamarin Forms until all images have loaded unfortunately.

orwo1 commented 3 years ago

@JordanBird I have tried skipping the middleman and use BitmapDescriptorFactory.FromStream(), but I was getting almost the same results. Are you using SkiaSharp to generate a bitmap from your xaml view? or other source? How do you pass it from the shared project into the map control?

JordanBird commented 3 years ago

It's a bit of a hack.. But I use FFImageLoading to pull in an SVG file, I've reworked the code from my project a little below:

var iconWidth = 100;
var iconHeight = 100;

var stream = await ImageService.Instance.LoadEmbeddedResource($"resource://REMOVED.png?Assembly=REMOVED")
    .WithCache(FFImageLoading.Cache.CacheType.Memory)
    .AsPNGStreamAsync();

var half = (float)iconWidth / 2f;

var iconBitmap = new SKBitmap(iconWidth, iconHeight);
using (SKCanvas canvas = new SKCanvas(iconBitmap))
{
    canvas.Clear();

    canvas.DrawCircle(half, half, half, new Paint() { Color = new SKColor(255, 0, 0) });

    var iconSkBitmap= SKBitmap.FromImage(SKImage.FromEncodedData(part));
        canvas.DrawBitmap(iconSkBitmap, ((float)iconWidth - iconSkBitmap.Width) / 2f, ((float)iconHeight - iconSkBitmap.Height) / 2f);
}

Icon = BitmapDescriptorFactory.FromStream(SKImage.FromBitmap(iconBitmap).Encode(SKEncodedImageFormat.Png, 100).AsStream());

So if you're able to get your icon into a stream, you should be able to just replace the ImageService stuff with your implementation.

As for setting the Icon from the shared project, you'll need your observable collection of pins/dtos; and then set your data template up like the below:

<maps:Map.ItemTemplate>
    <DataTemplate>
        <maps:Pin Position="{Binding Position}" Label="{Binding Label}" Icon="{Binding MapIcon}"/>
    </DataTemplate>
</maps:Map.ItemTemplate>

I've roughly pulled this out of our project, so it might not work fully as expected, but there's hopefully enough there to get you on the right path; it's the best workaround I've found so far for this icon issue unfortunately.

orwo1 commented 3 years ago

@JordanBird This solved my issue.

I used:

`var iconSkBitmap = SKBitmap.Decode(embeddedResourceStream);

var density = DeviceDisplay.MainDisplayInfo.Density;

var width = (int)Math.Round(density * iconSkBitmap.Width);

var height = (int)Math.Round(density * iconSkBitmap.Height);`

To get the icons height and width scaled up per platform.

Thank you so much for sharing this. I still don't understand why simply using: BitmapDescriptor.FromStream(embedded_png_stream) wasn't good enough?