oxyplot / oxyplot-xamarin

OxyPlot for Xamarin.Mac and Xamarin.Forms
MIT License
64 stars 59 forks source link

Missing constructor (IntPtr,JniHandleOwnership) in Android.PlotView #6

Closed scastria closed 8 years ago

scastria commented 8 years ago

Strange. I just updated to XF 2.2 now that both OxyPlot and DevExpress supports it. I ran into a new issue on Android.

My app needs to be able to tap on an OxyPlot and get plot objects that were tapped. Since this isn't builtin, I extended PlotView like this:

public class TapPlotView : PlotView
{
    public static BindableProperty DrillDownCommandProperty = BindableProperty.Create<TapPlotView, ICommand>(x => x.TapCommand,
        null,
        BindingMode.OneWay
    );
    public ICommand TapCommand {
        get { return (ICommand)GetValue(DrillDownCommandProperty); }
        set { SetValue(DrillDownCommandProperty, value); }
    }

    public void OnItemTapped(IEnumerable<HitTestResult> results)
    {
        if (TapCommand != null)
            TapCommand.Execute (results);
    }
}

I then implemented Renderers for both iOS and Android. iOS renderer adds a TapGestureRecognizer and works fine. However, my Android renderer:

public class TapPlotViewRenderer : PlotViewRenderer
{
    private const int HIT_TOLERANCE = 15;

    protected override void OnElementChanged (ElementChangedEventArgs<PlotView> e)
    {
        base.OnElementChanged (e);
        if (Control != null) {
            Control.Touch += delegate(object sender, TouchEventArgs tea) {
                //Don't steal touch event which prevents pan/zoom
                tea.Handled = false;
                double scale = OxyPlot.Xamarin.Android.PlotView.Scale;
                if(tea.Event.Action != MotionEventActions.Down)
                    return;
                IEnumerable<HitTestResult> results = Control.Model.HitTest(new HitTestArguments(new ScreenPoint(tea.Event.GetX() / scale,tea.Event.GetY() / scale),HIT_TOLERANCE));
                ((TapPlotView)Element).OnItemTapped(results);
            };
        }
    }
}

is failing with XF2.2 complaining about "Missing constructor (IntPtr,JniHandleOwnership) in Android.PlotView". My google searches says this means something else is bad. Strange that this worked fine on Android for XF2.1.

I wonder if OxyPlot had builtin support for Tapping on a plot would cause this issue to go away.

scastria commented 8 years ago

After more investigation, this has nothing to do with XF 2.2. Still investigating...

scastria commented 8 years ago

Maybe before I spend a lot of time debugging, I should ask: "What is the best way to detect taps on plot objects in Xamarin.Forms?"

andrii-borysov-me commented 8 years ago

This issue must be fixed by upgrading to latest stable Xamarin platform - https://releases.xamarin.com/stable-release-cycle-7/.

ahmedalejo commented 8 years ago

You should just add the constructor, it´s a Mono issue where by it trys to reconstruct an object after it has been disposed/GC.

public TapPlotViewRenderer()
{ }

public TapPlotViewRenderer(System.IntPtr javaReference, Android.Runtime.JniHandleOwnership transfer)
{ }
scastria commented 8 years ago

I actually tried that, and then I got another error, and it kept leading me down a dark path. I actually found a different way to solve this problem by using things that already exist in OxyPlot.

ahmedalejo commented 8 years ago

Could you share such things that you used?

ahmedalejo commented 8 years ago

Oh i think i get your point. You meant, use GestureListeners present in `PlotModel that totally makes sense.

Thanks for closing. -1 issue

scastria commented 8 years ago

First I created my TapPlotView like this:

    public class TapPlotView : PlotView
    {
    }

Then I created custom renderers, but actually ONLY needed one for iOS:

    public class TapPlotViewRenderer : PlotViewRenderer
    {
        protected override void OnElementChanged (ElementChangedEventArgs<PlotView> e)
        {
            base.OnElementChanged (e);
            if ((Control != null) && (Control.Controller != null)) {
                UITapGestureRecognizer tgr = new UITapGestureRecognizer (delegate(UITapGestureRecognizer gr) {
                    CGPoint viewPt = gr.LocationInView(Control);
                    Control.Controller.HandleTouchStarted(Control,ToTouchEventArgs(viewPt));
                    Control.Controller.HandleTouchCompleted(Control, ToTouchEventArgs(viewPt));
                });
                Control.AddGestureRecognizer (tgr); 
            }
        }

        private OxyTouchEventArgs ToTouchEventArgs(CGPoint location)
        {
            return(new OxyTouchEventArgs
            {
                Position = new ScreenPoint(location.X, location.Y),
                DeltaTranslation = new ScreenVector(0, 0),
                DeltaScale = new ScreenVector(1, 1)
            });
        }
    }

Then I used a TouchManipulator which is the thing I didn't know about when I first posted this thread:

    public class TapTouchManipulator : TouchManipulator
    {
        private const int TAP_TOLERANCE = 10;
        private const int HIT_TOLERANCE = 15;

        private ICommand _tapCommand = null;
        private ScreenPoint _startPos;

        public TapTouchManipulator(IPlotView plotView,ICommand tapCommand) : base(plotView)
        {
            _tapCommand = tapCommand;
        }

        public override void Started(OxyTouchEventArgs e)
        {
            base.Started(e);
            _startPos = e.Position;
            //Always handle event so we get Completed below which handles Tap
            e.Handled = true;
        }

        public override void Completed(OxyTouchEventArgs e)
        {
            base.Completed(e);
            if (e.Position.DistanceTo(_startPos) >= TAP_TOLERANCE)
                return;
            IEnumerable<HitTestResult> results = PlotView.ActualModel.HitTest(new HitTestArguments(e.Position, HIT_TOLERANCE));
            if (_tapCommand != null)
                _tapCommand.Execute(results);
        }
    }

Then in my application code where I create the plot view, I do this:

            PlotController prodPC = new PlotController();
            prodPC.BindTouchDown(new DelegatePlotCommand<OxyTouchEventArgs>((view, controller, args) => controller.AddTouchManipulator(view, new TapTouchManipulator(view, PlotCommand), args)));
            TapPlotView lineChartV = new TapPlotView {
                Model = _prodPM,
                Controller = prodPC
            };
ahmedalejo commented 8 years ago

Thanks a mil for sharing

And oh! for formatting your snippets, remember to put the csharp language hint for the markup

```csharp
await vm.LoadMapPOIsAsync();

will generate something like the following

``` csharp
await this.MapVM.LoadPOIsAsync();

notice the syntax highlighting

scastria commented 8 years ago

done

objorke commented 8 years ago

Can #7 be updated with this solution?

objorke commented 8 years ago

Did you still have to create a derived PlotView? I think this should be supported by default.

objorke commented 8 years ago

ps. it is a bit difficult to follow discussions on closed issues.. :-)