6

How can I intercept a tapped/clicked event on application level in an Xamarin.Forms app?

I tried on the MainPage to use something like

    <Grid.GestureRecognizers>
        <TapGestureRecognizer Command="{Binding TappedCommand}" Tapped="TapGestureRecognizer_OnTapped"></TapGestureRecognizer>
    </Grid.GestureRecognizers>

but it works only if I tap empty areas, touching a button doesn't fire the events.

Additionally I often navigate by changing the MainPage, so I would have to add such recognizer even if it worked.

I want to handle any touch/tap/click event in my app globally, is it possible in Xamarin.Forms?

alek kowalczyk
  • 4,896
  • 1
  • 26
  • 55
  • You can set InputTransparent on an element if you want it to pass input events down instead of capturing them. I don't know of a way to have a global handler - but you could create a base page class with a gesture recognizer that all of your pages inherit from. – Jason Dec 19 '16 at 04:45

3 Answers3

11

This can be easily setup within the platform-specific apps and then use a Xamarin.Forms dependency service to subscribe/unsubscribe to the events.

What you capture within those events is up to your needs, in this example I am just capturing and eventing the x/y values.

A generic EventArgs Class and DP Interface:

public class TouchEventArgs<T> : EventArgs
{
    public T EventData { get; private set; }

    public TouchEventArgs(T EventData)
    {
        this.EventData = EventData;
    }
}

public interface IGlobalTouch
{
    void Subscribe(EventHandler handler);
    void Unsubscribe(EventHandler handler);
}

Add the following to the MainActivity of your Xamarin.Android application:

public EventHandler globalTouchHandler;
public override bool DispatchTouchEvent(MotionEvent ev)
{
    globalTouchHandler?.Invoke(null, new TouchEventArgs<Point>(new Point(ev.GetX(), ev.GetY())));
    return base.DispatchTouchEvent(ev);
}

Android Dependency Implementation:

public class GlobalTouch : IGlobalTouch
{
    public GlobalTouch() {}

    public void Subscribe(EventHandler handler)
    {
        (Forms.Context as MainActivity).globalTouchHandler += handler;
    }

    public void Unsubscribe(EventHandler handler)
    {
        (Forms.Context as MainActivity).globalTouchHandler -= handler;
    }
}

Usage in your Xamarin.Forms project:

DependencyService.Get<IGlobalTouch>().Subscribe((sender, e) =>
{
    var point = (e as TouchEventArgs<Point>).EventData;
    System.Diagnostics.Debug.WriteLine($"{point.X}:{point.Y}");
});

Note: You should be assigning a delegate so can unsubscribe...

Output:

307.628997802734:365.563842773438
309.280151367188:365.197265625
311.883605957031:365.390991210938
312.694641113281:380.148590087891
308.030578613281:387.823364257813
291.513244628906:396.339416503906
286.220489501953:396.339416503906
282.100006103516:396.339416503906

The same technique can be used for iOS, there are TouchesBegan, TouchesEnded, TouchesMoved and TouchesCancelled that you can attach to depending upon your needs....

SushiHangover
  • 73,120
  • 10
  • 106
  • 165
  • Implemented slight differently according to my requirements, but based on your snippets, worked like a charm, thanks! :) – alek kowalczyk Dec 19 '16 at 18:47
  • This works but be aware that these locations are device dependent (have DPI) on newer devices so divide results by `Context.Resources.DisplayMetrics.Density` to get DIPs (device independent pixels) – tkefauver Jul 31 '21 at 01:27
  • I meant density not device, Xam points won't match up with the touches see here for converting: https://stackoverflow.com/a/51422564/105028 – tkefauver Jul 31 '21 at 06:48
  • Dont forget to register in mainActivity and AppDelegate: Xamarin.Forms.DependencyService.Register(); – innom Feb 16 '22 at 16:04
3

Using SushiHangover resolution, for iOS, it would be something like this:

In AppDelegate.cs implement IUIGestureRecognizerDelegate

public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate, IUIGestureRecognizerDelegate
{

Inside AppDelegate.cs class, in FinishedLaunching method, add this:

Xamarin.Forms.DependencyService.Register<GlobalTouch>();
bool ret = base.FinishedLaunching(app, options);
if (ret)
{
    UITapGestureRecognizer tap = new UITapGestureRecognizer(Self, new ObjCRuntime.Selector("gestureRecognizer:shouldReceiveTouch:"));
    tap.Delegate = (IUIGestureRecognizerDelegate)Self;
    app.KeyWindow.AddGestureRecognizer(tap);
}
return ret;

Add this method in AppDelegate.cs:

[Export("gestureRecognizer:shouldReceiveTouch:")]
public bool ShouldReceiveTouch(UIGestureRecognizer gestureRecognizer, UITouch touch)
{
    Xamarin.Forms.DependencyService.Get<IGlobalTouch>().TapScreen();
    return false;
}

Add this into GlobalTouch:

EventHandler globalTouchHandler;

public void TapScreen()
{
    globalTouchHandler?.Invoke(this, null);
}

Add this into IGlobalTouch:

void TapScreen();
Osiris Pujols
  • 176
  • 1
  • 6
0

iOS part for SushiHangover solution:

TouchCoordinatesRecognizer.cs:

public class TouchCoordinatesRecognizer : UIGestureRecognizer
{
    public override void TouchesBegan(NSSet touches, UIEvent evt)
    {
        base.TouchesBegan(touches, evt);
        SendCoords(touches);
    }

    public override void TouchesMoved(NSSet touches, UIEvent evt)
    {
        base.TouchesMoved(touches, evt);
        SendCoords(touches);
    }

    public override void TouchesEnded(NSSet touches, UIEvent evt)
    {
        base.TouchesEnded(touches, evt);
        SendCoords(touches);
    }

    public override void TouchesCancelled(NSSet touches, UIEvent evt)
    {
        base.TouchesCancelled(touches, evt);
        SendCoords(touches);
    }

    void SendCoords(NSSet touches)
    {
        foreach (UITouch touch in touches.Cast<UITouch>())
        {
            var window = UIApplication.SharedApplication.KeyWindow;
            var vc = window.RootViewController;

            AppDelegate.Current.globalTouchHandler?.Invoke(null,
                new TouchEventArgs<Point>(
                    new Point(
                        touch.LocationInView(vc.View).X,
                        touch.LocationInView(vc.View).Y)));
            return;
        }
    }
}

AppDelegate.cs:

public static AppDelegate Current { get; private set; }

public EventHandler globalTouchHandler;

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    Current = this;

    global::Xamarin.Forms.Forms.Init();
    LoadApplication(new App());
    var res = base.FinishedLaunching(app, options);
    app.KeyWindow.AddGestureRecognizer(new TouchCoordinatesRecognizer()); // <--
    return res;
}

GlobalTouch.cs:

public class GlobalTouch : IGlobalTouch
{
    public void Subscribe(EventHandler handler) => AppDelegate.Current.globalTouchHandler += handler;

    public void Unsubscribe(EventHandler handler) => AppDelegate.Current.globalTouchHandler -= handler;
}

Don't forget to divide x and y coordinates by DeviceDisplay.MainDisplayInfo.Density in necessary places of android and iOS parts.

user3125174
  • 154
  • 2
  • 11