0
votes

enter image description here

Hi everyone, i had to create custom info-window in xamarin forms as shown in the screenshot above. I have created a custom renderer for that but the problem i am having right now is that buttons are not getting clicked. Xamarin is taking entire info-window as a clickable view. Please guide me what i am doing wrong or is it possible to achieve button clicks in Xamarin forms. Thanks in advance.

Here is the code for custom renderer:

using System;
using System.Collections.Generic;
using Android.Content;
using Android.Gms.Maps;
using Android.Gms.Maps.Model;
using Xamarin.Forms;
using Xamarin.Forms.Maps;
using Xamarin.Forms.Maps.Android;
using SalesApp.Droid.CustomRenderer;
using Android.Widget;
using SalesApp.CustomControls;
using SalesApp.Droid.Listeners;

[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace SalesApp.Droid.CustomRenderer
{
    public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter, IOnMapReadyCallback
    {
        GoogleMap map;
        List<CustomPin> customPins;
        bool isDrawn;

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

            if (e.OldElement != null)
            {
                map.InfoWindowClick -= OnInfoWindowClick;
            }

            if (e.NewElement != null)
            {
                var formsMap = (CustomMap)e.NewElement;
                customPins = formsMap.CustomPins
                Control.GetMapAsync(this);
            }
        }

        public void OnMapReady(GoogleMap googleMap)
        {
            map = googleMap;
            map.InfoWindowClick += OnInfoWindowClick;
            map.SetInfoWindowAdapter(this);
        }

        protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);
            if (e.PropertyName.Equals("VisibleRegion") && !isDrawn)
            {
                map.Clear();

                if (customPins != null)
                {
                    foreach (var pin in customPins)
                    {
                        var marker = new MarkerOptions();
                        marker.SetPosition(new LatLng(pin.Pin.Position.Latitude, pin.Pin.Position.Longitude));
                        marker.SetTitle(pin.Pin.Label);
                        marker.SetSnippet(pin.Pin.Address);
                        marker.SetIcon(BitmapDescriptorFactory.FromResource((int)typeof(Resource.Drawable).GetField(pin.Image).GetValue(null)));

                        map.AddMarker(marker);
                    }
                    isDrawn = true;
                }
            }
        }

        protected override void OnLayout(bool changed, int l, int t, int r, int b)
        {
            base.OnLayout(changed, l, t, r, b);

            if (changed)
            {
                isDrawn = false;
            }
        }

        void OnInfoWindowClick(object sender, GoogleMap.InfoWindowClickEventArgs e)
        {
            var customPin = GetCustomPin(e.Marker);
            if (customPin == null)
            {
                throw new Exception("Custom pin not found");
            }

            //if (!string.IsNullOrWhiteSpace(customPin.Url))
            //{
            //    var url = Android.Net.Uri.Parse(customPin.Url);
            //    var intent = new Intent(Intent.ActionView, url);
            //    intent.AddFlags(ActivityFlags.NewTask);
            //    Android.App.Application.Context.StartActivity(intent);
            //}

            Android.App.Application.Context.StartActivity(new Intent(Android.App.Application.Context, typeof(DialogActivity)));

        }

        void IOnMapReadyCallback.OnMapReady(GoogleMap googleMap)
        {
            InvokeOnMapReadyBaseClassHack(googleMap);
            map = googleMap;
            map.SetInfoWindowAdapter(this);
            map.InfoWindowClick += OnInfoWindowClick;
        }

        public Android.Views.View GetInfoContents(Marker marker)
        {
            var inflater = Android.App.Application.Context.GetSystemService(Context.LayoutInflaterService) as Android.Views.LayoutInflater;
            if (inflater != null)
            {
                Android.Views.View view;

                var customPin = GetCustomPin(marker);
                if (customPin == null)
                {
                    throw new Exception("Custom pin not found");
                }

                if (customPin.Id == "Xamarin")
                {
                    view = inflater.Inflate(Resource.Layout.XamarinMapInfoWindow, null);
                }
                else
                {
                    view = inflater.Inflate(Resource.Layout.MapInfoWindow, null);
                }

                var infoTitle = view.FindViewById<TextView>(Resource.Id.InfoWindowTitle);
                var address = view.FindViewById<TextView>(Resource.Id.Address);
                var contactPerson = view.FindViewById<TextView>(Resource.Id.ContactPerson);
                var phone = view.FindViewById<TextView>(Resource.Id.Phone);
                var zip = view.FindViewById<TextView>(Resource.Id.Zip);
                var email = view.FindViewById<TextView>(Resource.Id.Email);
                var cvr = view.FindViewById<TextView>(Resource.Id.CVR);
                var turnover = view.FindViewById<TextView>(Resource.Id.Turnover);
                var noOfEmp = view.FindViewById<TextView>(Resource.Id.NoOfEmp);
                var leadStatus = view.FindViewById<TextView>(Resource.Id.LeadStatus);
                var category = view.FindViewById<TextView>(Resource.Id.Cat);

                if (infoTitle != null)
                {
                    infoTitle.Text = customPin.LeedsAndCustomersData.FullName;
                }
                if (address != null)
                {
                    address.Text = customPin.LeedsAndCustomersData.Address + " " + customPin.LeedsAndCustomersData.CityName;
                }
                if (contactPerson != null)
                {
                    contactPerson.Text = customPin.LeedsAndCustomersData.FirstName;
                }
                if (phone != null)
                {
                    phone.Text = customPin.LeedsAndCustomersData.Phone;
                }
                if (zip != null)
                {
                    zip.Text = customPin.LeedsAndCustomersData.ZipCode;
                }
                if (email != null)
                {
                    email.Text = customPin.LeedsAndCustomersData.Email;
                }
                if (cvr != null)
                {
                    cvr.Text = customPin.LeedsAndCustomersData.CVR;
                }
                if (turnover != null)
                {
                    turnover.Text = customPin.LeedsAndCustomersData.Turnover;
                }
                if (noOfEmp != null)
                {
                    noOfEmp.Text = customPin.LeedsAndCustomersData.NoOfEmployees.ToString();
                }
                if (leadStatus != null)
                {
                    leadStatus.Text = customPin.LeedsAndCustomersData.LeadStatus;
                }
                if (category != null)
                {
                    category.Text = customPin.LeedsAndCustomersData.BusinessType;
                }

                //add listeners
                OnTouchPhoneListener callButtonListener = new OnTouchPhoneListener();
                phone.SetOnTouchListener(callButtonListener);

                return view;
            }
            return null;
        }

        public Android.Views.View GetInfoWindow(Marker marker)
        {
            return null;
        }

        CustomPin GetCustomPin(Marker annotation)
        {
            var position = new Position(annotation.Position.Latitude, annotation.Position.Longitude);
            foreach (var pin in customPins)
            {
                if (pin.Pin.Position == position)
                {
                    return pin;
                }
            }
            return null;
        }

        void InvokeOnMapReadyBaseClassHack(GoogleMap googleMap)
        {
            System.Reflection.MethodInfo onMapReadyMethodInfo = null;

            Type baseType = typeof(MapRenderer);
            foreach (var currentMethod in baseType.GetMethods(System.Reflection.BindingFlags.NonPublic |
                                                             System.Reflection.BindingFlags.Instance |
                                                              System.Reflection.BindingFlags.DeclaredOnly))
            {
                if (currentMethod.IsFinal && currentMethod.IsPrivate)
                {
                    if (string.Equals(currentMethod.Name, "OnMapReady", StringComparison.Ordinal))
                    {
                        onMapReadyMethodInfo = currentMethod;
                        break;
                    }

                    if (currentMethod.Name.EndsWith(".OnMapReady", StringComparison.Ordinal))
                    {
                        onMapReadyMethodInfo = currentMethod;
                        break;
                    }
                }
            }

            if (onMapReadyMethodInfo != null)
            {
                onMapReadyMethodInfo.Invoke(this, new[] { googleMap });
            }
        }
    }
}

OnInfoWindowElemTouchListener:

using Android.OS;
using Android.Views;
using Android.Widget;
using System.Threading.Tasks;
using Android.Gms.Maps.Model;
using Android.Graphics.Drawables;
using Java.Lang;

namespace SalesApp.Droid.Listeners
{
    public abstract class OnInfoWindowElemTouchListener : Java.Lang.Object, View.IOnTouchListener
    {
        private View view;

        private Drawable bgDrawableNormal;
        private Drawable bgDrawablePressed;
        private Handler handler = new Handler();
        private Marker marker;
        private static bool endPressStatus = false;
        private bool pressed = false;


        //public OnInfoWindowElemTouchListener(View view, Drawable bgDrawableNormal, Drawable bgDrawablePressed)
        //{
        //  this.view = this.view;
        //  this.bgDrawableNormal = this.bgDrawableNormal;
        //  this.bgDrawablePressed = this.bgDrawablePressed;
        //}

        public OnInfoWindowElemTouchListener(View view)
        {
            this.view = this.view;
        }

        public OnInfoWindowElemTouchListener(Button button)
        {

        }
        public OnInfoWindowElemTouchListener()
        {

        }

        public void setMarker(Marker marker)
        {
            this.marker = this.marker;
        }

        /*public bool OnTouch(View v, MotionEvent e)
        {
            if (e.Action == MotionEventActions.Down)
            {
                // do stuff
                return true;
            }
            if (e.Action == MotionEventActions.Up)
            {
                // do other stuff
                return true;
            }

            return false;
        }*/


        public bool OnTouch(View vv, MotionEvent e)
        {
            if (0 <= e.GetX() && e.GetX() <= vv.Width && 0 <= e.GetY() && e.GetY() <= vv.Height)
            {
                switch (e.ActionMasked)
                {
                    case MotionEventActions.Down:
                        startPress();
                        break;

                    // We need to delay releasing of the view a little so it shows the
                    // pressed state on the screen
                    case MotionEventActions.Up:
                        //handler.PostDelayed(ConfirmClickRunnable, 150);
                        //Task.Factory.StartNew(() =>onClickConfirmed(view, marker));
                        Task.Factory.StartNew(() => onClickConfirmed());
                        Task.Delay(150);
                        break;

                    case MotionEventActions.Cancel:
                        endPress();
                        break;
                    default:
                        break;
                }
            }
            else
            {
                // If the touch goes outside of the view's area
                // (like when moving finger out of the pressed button)
                // just release the press
                endPress();
            }

            return false;
        }


        private void startPress()
        {
            if (!pressed)
            {
                pressed = true;
                //handler.RemoveCallbacks(ConfirmClickRunnable);

                if ((marker != null))
                {
                    marker.ShowInfoWindow();
                }

            }

        }

        public bool endPress()
        {
            if (pressed)
            {
                this.pressed = false;
                //handler.RemoveCallbacks(ConfirmClickRunnable);
                view.SetBackgroundColor(Android.Graphics.Color.Green);


                if ((marker != null))
                {
                    marker.ShowInfoWindow();
                }
                endPressStatus = true;
                return true;
            }
            else
            {
                endPressStatus = false;
                return false;
            }

        }

        //private Runnable confirmClickRunnable = new RunnableAnonymousInnerClassHelper(this);
        private Runnable ConfirmClickRunnable = new Java.Lang.Runnable(() =>
        {
            if (endPressStatus)
            {

                //onClickConfirmed(view, marker);
            }
        });




        /*private  class RunnableAnonymousInnerClassHelper : Java.Lang.Object, Java.Lang.IRunnable
        {
            private readonly Context outerInstance;     

            public RunnableAnonymousInnerClassHelper(Context outerInstance)
            {
                this.outerInstance = outerInstance;         
            }

            public void Run()
            {
                if (endPressStatus)
                {
                    onClickConfirmed();
                }
            }
        }*/


        //public abstract void onClickConfirmed(View v, Marker marker);
        public abstract void onClickConfirmed();

    }
}

OnTouchPhoneListener:

using Android.Widget;
using System;

namespace SalesApp.Droid.Listeners
{
    public class OnTouchPhoneListener : OnInfoWindowElemTouchListener
    //public class OnTouchPhoneListener 
    {
        Button button;
        public OnTouchPhoneListener(Button button)
        {

        }

        public OnTouchPhoneListener()
        {

        }
        public override void onClickConfirmed() { 
            Console.WriteLine("call Button Clicked");
        }
    }
}
1
It would be very helpful, if you instead created a minimal reproducible example you can reproduce the issue with. - Paul Kertscher
thanks @Paul, i have created one for my self and having the same issue. Button listeners are not getting triggered. - Zubair Rehman

1 Answers

0
votes

Why is the info window implemented in your custom renderer? This makes the class CustomMapRenderer do (at least) do two things, violate the SRP and makes the code way harder to understand.

Instead I'd go with the custom renderer just for the map. Then in Xamarin.Forms you can implement the overlay with Xamarin.Forms means (e.g. the map view in an absolute or relative layout and the overlay in the same layout but a reduced size). Please see the following pseudo-XAML

<ContentPage [...]>
  <AbsoluteLayout>
    <local:MapView x:Name="MapView" AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds="0,0,1,1" />
    <local:MapOverlay AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds=".5, .5, .5, .5" />
  </AbsoluteLayout>
</ContentPage>

Then you can introduce a viewmodel PinInfoViewModel which is exposed by MapView.SelectedPinInfo and can be set as MapOverlay.BindingContext

<local:MapOverlay AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds=".5, .5, .5, .5" BindingContext="{Binding Source={x:Reference MapView}, Path=SelectedPinInfo}" /> 

MapOverlay - in turn - binds to all the properties of PinInfoViewModel. Last thing we'll have to do is making the overlay invisible if no pin is selected. For this purpose we expose MapView.IsPinSelected and bind MapOverlay.IsVisible to it

<local:MapOverlay AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds=".5, .5, .5, .5" BindingContext="{Binding Source={x:Reference MapView}, Path=SelectedPinInfo}" IsVisible="{Binding Source=MapView, Path=IsPinSelected}" /> 

While up to this point we have not done anything to solve your issue, you can now implement the overlay way simpler, e.g. with a StackLayout. And you can bind the Buttons commands to do whatever you'd like to do.