Ссылка на github

Некоторые элементы управления в приложении могут реагировать на жесты - например кнопки, CollectionView, выезжающее сбоку меню у FlyoutPage. Ещё есть распознаватели жестов (GestureRecognizers), которые можно добавить элементам управления, не умеющим реагировать на касания. Можно использовать эффекты (Effects) - Invoking Events from Effects.

Но иногда надо узнать о касании в любом месте экрана и координаты этого касания. Например, для рисования пальцем или для написания пользовательских элементов управления. На андроид и iOS есть способ это сделать при помощи DependencyService. Это странно, но часть такого полезного и можно сказать базового функционала надо писать самому или покупать (https://www.mrgestures.com/).

Если хотите покороче, то ссылка на код вверху статьи. А далее будет более длинный вариант с объяснениями.

На мульти-тач экранах одновременных касаний может быть несколько. В данном примере берётся только первое из них. Как обрабатывать несколько одновременно написано здесь: Multi-Touch Finger Tracking.

Итак, нам понадобятся:

В кроссплатформенном проекте:

  • интерфейс для DependencyService - IGlobalTouch.cs

  • TouchEventArgs.cs для передачи данных из андроид/iOS

В андроид-проекте:

  • GlobalTouch.cs

  • дополнить MainActivity.cs

В iOS-проекте:

  • GlobalTouch.cs

  • TouchCoordinatesRecognizer.cs

  • правки в AppDelegate.cs

Поэкспериментировав, добавил кроме самого определения координат нажатия еще несколько полезных для этой задачи методов:

  • получение координат того view на который нажали (GetPosition);

  • получение размера SafeArea для iOS (GetSafeAreaTop / GetSafeAreaBottom);

  • получение размера панели навигации (GetNavBarHeight).

Они все пригодятся для расчётов при нажатии.

Кроссплатформенный проект

Здесь нужен интерфейс IGlobalTouch для DependencyService и класс-наследник EventArgs с дополнительным свойством для передачи координат.

public interface IGlobalTouch
{
    void Subscribe(EventHandler handler);
    void Unsubscribe(EventHandler handler);
    Point GetPosition(VisualElement element);
    double GetSafeAreaTop();
    double GetSafeAreaBottom();
    int GetNavBarHeight();
}
public class TouchEventArgs<T> : EventArgs
{
    public T EventData { get; private set; }
    public TouchEventArgs(T EventData)
    {
        this.EventData = EventData;
    }
}

Android

MainActivity.cs:

В классе MainActivity два изменения: EventHandler для события изменения координат и переопределить метод DispatchTouchEvent. Координаты здесь сразу делятся на DeviceDisplay.MainDisplayInfo.Density чтобы в кроссплатформенный проект приходили одинаковые с андроида и iOS.

public EventHandler globalTouchHandler;
public override bool DispatchTouchEvent(MotionEvent e)
{
    var d = DeviceDisplay.MainDisplayInfo.Density;
    globalTouchHandler?.Invoke(null, new TouchEventArgs<Point>(new Point(e.GetX() / d, e.GetY() / d)));
    return base.DispatchTouchEvent(e);
}

Также о нажатии на View в андроиде можно узнать, если создать свой View и переопределить метод OnTouchEvent или если подписаться на событие Touch этого View.

GlobalTouch.cs:

В реализации IGlobalTouch даём подписаться/отписаться на нажатие и вспомогательные методы. SafeArea это про айфон, поэтому здесь нули.

public class GlobalTouch : IGlobalTouch
{
    public void Subscribe(EventHandler handler) => 
        (Platform.CurrentActivity as MainActivity).globalTouchHandler += handler;

    public void Unsubscribe(EventHandler handler) => 
        (Platform.CurrentActivity as MainActivity).globalTouchHandler -= handler;

    public Point GetPosition(VisualElement element)
    {
        var d = DeviceDisplay.MainDisplayInfo.Density;
        var view = Xamarin.Forms.Platform.Android.Platform.GetRenderer(element).View;
        return new Point(view.GetX() / d, view.GetY() / d);
    }

    public double GetSafeAreaBottom() => 0;

    public double GetSafeAreaTop() => 0;

    public int GetNavBarHeight()
    {
        var d = DeviceDisplay.MainDisplayInfo.Density;
        int statusBarHeight = -1;
        int resourceId = Platform.CurrentActivity.Resources.GetIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0)
        {
            statusBarHeight = Platform.CurrentActivity.Resources.GetDimensionPixelSize(resourceId);
        }
        return (int)(statusBarHeight / d);
    }
}

iOS

На iOS можно узнать о нажатии на UIView если создать свой UIView и переопределить в нём методы TouchesBegan, TouchesMoved, TouchesEnded и TouchesCancelled. А также эти методы есть в UIGestureRecognizer, чем мы и воспользуемся, создав свой и добавив его к UIApplication.KeyWindow

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

Затем в классе AppDelegate в методе FinishedLaunching добавить его к "главному" представлению UIApplication.KeyWindow:

public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate, IUIGestureRecognizerDelegate
{
    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;
    }
}

Использовать можно так:

service = DependencyService.Get<IGlobalTouch>();
DependencyService.Get<IGlobalTouch>().Subscribe((sender, e) =>
{
    var touchPoint = (e as TouchEventArgs<Point>).EventData;
    var touchX = touchPoint.X;
    var touchY = touchPoint.Y;

    var viewPosition = service.GetPosition(this);
    var navBarHeight = service.GetNavBarHeight();
    var viewX = viewPosition.X;
    var viewY = viewPosition.Y + navBarHeight;

    if (touchX >= viewX && touchX <= viewX + Width
    && touchY >= viewY && touchY <= viewY + Height)
    {
        item.TranslationX = touchPoint.X - screenWidth / 2;
    }
});

В сочетании с TouchEffect из Xamarin.CommunityToolkit уже можно реализовать более сложное поведение чем доступно по умолчанию. В демке на гитхабе, например, заготовка для своего CarouselView.

Немного громоздко, да, согласен. Но столкнулся в очередной раз с тем что шаг влево - шаг вправо и стандартные контролы не позволяют сделать элементарных вещей - определить, что пользователь закончил взаимодействие или задать направление вращения CollectionView итп. Также сейчас актуален вопрос перехода на .net maui и обнаруживается, что многие удобные элементы управления были взяты из когда-то популярных nuget-ов и авторами больше не поддерживаются. Поэтому приходится некоторые заменять.

Комментарии (0)