Xamarin.Forms — определение координат нажатия на экран

от автора

Ссылка на 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-ов и авторами больше не поддерживаются. Поэтому приходится некоторые заменять.


ссылка на оригинал статьи https://habr.com/ru/post/692308/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *