From 8297e5bb65f442616e1af71defc028cfea90c527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EC=88=98/Common=20Platform=20Lab=28SR?= =?UTF-8?q?=29/Staff=20Engineer/=EC=82=BC=EC=84=B1=EC=A0=84=EC=9E=90?= Date: Thu, 12 Aug 2021 09:58:36 +0900 Subject: [PATCH] Add GestureManager (#96) * Add GestureManager * Fix DragGestureHandler * Fix GestureManager * Check nullable enable --- .../GestureManager/GestureManager.Tizen.cs | 121 +++- .../Core/Platform/Tizen/DragGestureHandler.cs | 243 +++++++ .../Core/Platform/Tizen/DropGestureHandler.cs | 126 ++++ .../Tizen/Extensions/DragDropExtensions.cs | 114 ++++ .../Core/Platform/Tizen/GestureDetector.cs | 636 ++++++++++++++++++ .../src/Core/Platform/Tizen/GestureHandler.cs | 55 ++ .../Core/Platform/Tizen/IGestureController.cs | 15 + .../Core/Platform/Tizen/PanGestureHandler.cs | 43 ++ .../Platform/Tizen/PinchGestureHandler.cs | 58 ++ .../Platform/Tizen/SwipeGestureHandler.cs | 29 + .../Core/Platform/Tizen/TapGestureHandler.cs | 46 ++ 11 files changed, 1485 insertions(+), 1 deletion(-) create mode 100644 src/Controls/src/Core/Platform/Tizen/DragGestureHandler.cs create mode 100644 src/Controls/src/Core/Platform/Tizen/DropGestureHandler.cs create mode 100644 src/Controls/src/Core/Platform/Tizen/Extensions/DragDropExtensions.cs create mode 100644 src/Controls/src/Core/Platform/Tizen/GestureDetector.cs create mode 100644 src/Controls/src/Core/Platform/Tizen/GestureHandler.cs create mode 100644 src/Controls/src/Core/Platform/Tizen/IGestureController.cs create mode 100644 src/Controls/src/Core/Platform/Tizen/PanGestureHandler.cs create mode 100644 src/Controls/src/Core/Platform/Tizen/PinchGestureHandler.cs create mode 100644 src/Controls/src/Core/Platform/Tizen/SwipeGestureHandler.cs create mode 100644 src/Controls/src/Core/Platform/Tizen/TapGestureHandler.cs diff --git a/src/Controls/src/Core/Platform/GestureManager/GestureManager.Tizen.cs b/src/Controls/src/Core/Platform/GestureManager/GestureManager.Tizen.cs index 344c17b7cce6..f231e5a4771e 100644 --- a/src/Controls/src/Core/Platform/GestureManager/GestureManager.Tizen.cs +++ b/src/Controls/src/Core/Platform/GestureManager/GestureManager.Tizen.cs @@ -1,18 +1,137 @@ #nullable enable using System; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; namespace Microsoft.Maui.Controls.Platform { class GestureManager : IDisposable { + IViewHandler? _handler; + GestureDetector? _gestureDetector; + bool _disposed = false; + + protected virtual VisualElement? Element => _handler?.VirtualView as VisualElement; + public GestureManager(IViewHandler handler) { - //TODO: Need to impl + _handler = handler; + _gestureDetector = null; + SetupElement(null, Element); + } + + void SetupElement(VisualElement? oldElement, VisualElement? newElement) + { + if (oldElement != null) + { + if (oldElement is View ov && + ov.GestureRecognizers is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= OnGestureRecognizerCollectionChanged; + _gestureDetector?.Clear(); + } + oldElement.PropertyChanged -= OnElementPropertyChanged; + } + + if (newElement != null) + { + if (newElement is View ov && + ov.GestureRecognizers is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnGestureRecognizerCollectionChanged; + if (ov.GestureRecognizers.Count > 0) + { + _gestureDetector = new GestureDetector(_handler); + _gestureDetector.AddGestures(ov.GestureRecognizers); + } + } + newElement.PropertyChanged += OnElementPropertyChanged; + } + + UpdateInputTransparent(); + UpdateIsEnabled(); + } + + void OnElementPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == VisualElement.InputTransparentProperty.PropertyName) + UpdateInputTransparent(); + else if (e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) + UpdateIsEnabled(); } public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + _disposed = true; + + if (disposing) + { + SetupElement(Element, null); + if (_gestureDetector != null) + { + _gestureDetector.Dispose(); + _gestureDetector = null; + } + _handler = null; + } + } + + void UpdateInputTransparent() + { + if (Element != null && _gestureDetector != null) + { + _gestureDetector.InputTransparent = Element.InputTransparent; + } + } + + void UpdateIsEnabled() + { + if (Element != null && _gestureDetector != null) + { + _gestureDetector.IsEnabled = Element.IsEnabled; + } + } + + void OnGestureRecognizerCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (_gestureDetector == null) + { + _gestureDetector = new GestureDetector(_handler); + } + + // Gestures will be registered/unregistered according to changes in the GestureRecognizers list + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + _gestureDetector.AddGestures(e.NewItems?.OfType()); + break; + + case NotifyCollectionChangedAction.Replace: + _gestureDetector.RemoveGestures(e.OldItems?.OfType()); + _gestureDetector.AddGestures(e.NewItems?.OfType()); + break; + + case NotifyCollectionChangedAction.Remove: + _gestureDetector.RemoveGestures(e.OldItems?.OfType()); + break; + + case NotifyCollectionChangedAction.Reset: + _gestureDetector.Clear(); + break; + } } } } diff --git a/src/Controls/src/Core/Platform/Tizen/DragGestureHandler.cs b/src/Controls/src/Core/Platform/Tizen/DragGestureHandler.cs new file mode 100644 index 000000000000..eceef1d84191 --- /dev/null +++ b/src/Controls/src/Core/Platform/Tizen/DragGestureHandler.cs @@ -0,0 +1,243 @@ +using System; +using System.Threading.Tasks; +using ElmSharp; +using Microsoft.Maui.Controls.Internals; +using Tizen.UIExtensions.ElmSharp; +using EGestureType = ElmSharp.GestureLayer.GestureType; +using TImage = Tizen.UIExtensions.ElmSharp.Image; +using TLabel = Tizen.UIExtensions.ElmSharp.Label; + +namespace Microsoft.Maui.Controls.Platform +{ + public class DragGestureHandler : GestureHandler + { + DragDropExtensions.Interop.DragIconCreateCallback _iconCallback; + DragDropExtensions.Interop.DragStateCallback _dragDoneCallback; + + static bool s_isDragging; + static CustomDragStateData s_currentDragStateData; + + protected virtual IView Element => Handler?.VirtualView as IView; + + public DragGestureHandler(IGestureRecognizer recognizer, IViewHandler handler) : base(recognizer) + { + _iconCallback = OnIconCallback; + _dragDoneCallback = OnDragDoneCallback; + Handler = handler; + } + + public override EGestureType Type => EGestureType.LongTap; + + public IViewHandler Handler { get; } + + public static CustomDragStateData CurrentStateData + { + get + { + return s_currentDragStateData; + } + } + + EvasObject NativeView + { + get + { + return Handler.NativeView as EvasObject; + } + } + + public void ResetCurrentStateData() + { + s_currentDragStateData = null; + } + + protected override void OnStarted(View sender, object data) + { + } + + protected override void OnMoved(View sender, object data) + { + //Workaround to prevent an error occuring by multiple StartDrag calling in Tizen 6.5 + if (!s_isDragging) + { + ResetCurrentStateData(); + StartDrag(); + } + } + + protected override void OnCompleted(View sender, object data) + { + } + + protected override void OnCanceled(View sender, object data) + { + } + + void StartDrag() + { + if (Recognizer is DragGestureRecognizer dragGestureRecognizer && dragGestureRecognizer.CanDrag) + { + if (Handler == null) + return; + + var arg = dragGestureRecognizer.SendDragStarting(Element); + + if (arg.Cancel) + return; + + s_currentDragStateData = new CustomDragStateData(); + s_currentDragStateData.DataPackage = arg.Data; + + var target = DragDropExtensions.DragDropContentType.Text; + var strData = string.IsNullOrEmpty(arg.Data.Text) ? " " : arg.Data.Text; + + s_isDragging = true; + + DragDropExtensions.StartDrag(NativeView, + target, + strData, + DragDropExtensions.DragDropActionType.Move, + _iconCallback, + null, + null, + _dragDoneCallback); + } + } + + IntPtr OnIconCallback(IntPtr data, IntPtr window, ref int xoff, ref int yoff) + { + EvasObject icon = null; + EvasObject parent = new CustomWindow(NativeView, window); + + if (s_currentDragStateData.DataPackage.Image != null) + { + icon = GetImageIconAsync(parent).Result; + } + else if (NativeView is ShapeView) + { + icon = GetShapeView(parent); + } + else + { + icon = GetDefaultIcon(parent); + } + var bound = NativeView.Geometry; + bound.X = 0; + bound.Y = 0; + icon.Geometry = bound; + + if (icon is TLabel) + { + icon.Resized += (s, e) => + { + var map = new EvasMap(4); + map.PopulatePoints(icon.Geometry, 0); + map.Zoom(0.5, 0.5, 0, 0); + icon.IsMapEnabled = true; + icon.EvasMap = map; + }; + } + else + { + var map = new EvasMap(4); + map.PopulatePoints(icon.Geometry, 0); + map.Zoom(0.5, 0.5, 0, 0); + icon.IsMapEnabled = true; + icon.EvasMap = map; + } + + + return icon; + } + + EvasObject GetDefaultIcon(EvasObject parent) + { + if (!string.IsNullOrEmpty(s_currentDragStateData.DataPackage.Text)) + { + var label = new TLabel(parent); + label.Text = s_currentDragStateData.DataPackage.Text; + + if (Element is IFontElement fe) + label.FontSize = fe.FontSize; + + return label; + } + else + { + var box = new ElmSharp.Rectangle(parent); + box.Color = new ElmSharp.Color(128, 128, 128, 128); + return box; + } + } + + async Task GetImageIconAsync(EvasObject parent) + { + var image = new TImage(parent); + var mImage = s_currentDragStateData.DataPackage.Image; + var services = Handler.MauiContext?.Services; + var provider = services.GetService(typeof(IImageSourceServiceProvider)) as IImageSourceServiceProvider; + var service = provider?.GetImageSourceService(mImage); + var result = await service.LoadImageAsync(mImage, image); + if (result == null) + return null; + return image; + } + + EvasObject GetShapeView(EvasObject parent) + { + var copiedImg = new EvasImage(parent) + { + IsFilled = true + }; + + if (NativeView is ShapeView shapeView) + { + var canvas = shapeView.SKCanvasView; + var realHandle = DragDropExtensions.Interop.elm_object_part_content_get(canvas, "elm.swallow.content"); + + DragDropExtensions.Interop.evas_object_image_size_get(realHandle, out int w, out int h); + DragDropExtensions.Interop.evas_object_image_size_set(copiedImg, w, h); + + var imgData = DragDropExtensions.Interop.evas_object_image_data_get(realHandle, false); + DragDropExtensions.Interop.evas_object_image_data_set(copiedImg, imgData); + } + + return copiedImg; + } + + void OnDragDoneCallback(IntPtr data, IntPtr obj) + { + s_isDragging = false; + if (Recognizer is DragGestureRecognizer dragGestureRecognizer && dragGestureRecognizer.CanDrag) + { + dragGestureRecognizer.SendDropCompleted(new DropCompletedEventArgs()); + } + } + + public class CustomWindow : EvasObject + { + IntPtr _handle; + + public CustomWindow(EvasObject parent, IntPtr handle) : base() + { + _handle = handle; + Realize(parent); + } + + public CustomWindow(EvasObject handle) : base(handle) + { + } + + protected override IntPtr CreateHandle(EvasObject parent) + { + return _handle; + } + } + + public class CustomDragStateData + { + public DataPackage DataPackage { get; set; } + public DataPackageOperation AcceptedOperation { get; set; } = DataPackageOperation.Copy; + } + } +} diff --git a/src/Controls/src/Core/Platform/Tizen/DropGestureHandler.cs b/src/Controls/src/Core/Platform/Tizen/DropGestureHandler.cs new file mode 100644 index 000000000000..8b71d638d9c1 --- /dev/null +++ b/src/Controls/src/Core/Platform/Tizen/DropGestureHandler.cs @@ -0,0 +1,126 @@ +using System; +using System.Linq; +using ElmSharp; +using Tizen.Common; +using Tizen.UIExtensions.ElmSharp; +using EGestureType = ElmSharp.GestureLayer.GestureType; + +namespace Microsoft.Maui.Controls.Platform +{ + public class DropGestureHandler : GestureHandler + { + DragDropExtensions.Interop.DragStateCallback _dragEnterCallback; + DragDropExtensions.Interop.DragStateCallback _dragLeaveCallback; + DragDropExtensions.Interop.DropCallback _dropCallback; + + public override EGestureType Type => default(EGestureType); + + public DropGestureHandler(IGestureRecognizer recognizer, IViewHandler handler) : base(recognizer) + { + _dragEnterCallback = OnEnterCallback; + _dragLeaveCallback = OnLeaveCallback; + _dropCallback = OnDropCallback; + Handler = handler; + } + + public IViewHandler Handler { get; } + + EvasObject NativeView + { + get + { + var native = Handler.NativeView as EvasObject; + if (native is Canvas canvas) + { + var child = canvas.Children.LastOrDefault(); + + if (child != null) + { + if (child.PassEvents) + child.PassEvents = false; + + return child; + } + } + return native; + } + } + + + public void AddDropGesture() + { + if (Handler == null) + return; + + var target = DragDropExtensions.DragDropContentType.Targets; + + DragDropExtensions.AddDropTarget(NativeView, + target, + _dragEnterCallback, + _dragLeaveCallback, null, + _dropCallback); + } + + void OnEnterCallback(IntPtr data, IntPtr obj) + { + var currentStateData = DragGestureHandler.CurrentStateData; + if (currentStateData == null) + return; + + var arg = new DragEventArgs(currentStateData.DataPackage); + + if (Recognizer is DropGestureRecognizer dropRecognizer && dropRecognizer.AllowDrop) + dropRecognizer.SendDragOver(arg); + + DragGestureHandler.CurrentStateData.AcceptedOperation = arg.AcceptedOperation; + } + + void OnLeaveCallback(IntPtr data, IntPtr obj) + { + var currentStateData = DragGestureHandler.CurrentStateData; + if (currentStateData == null) + return; + + var arg = new DragEventArgs(currentStateData.DataPackage); + + if (Recognizer is DropGestureRecognizer dropRecognizer && dropRecognizer.AllowDrop) + dropRecognizer.SendDragLeave(arg); + + DragGestureHandler.CurrentStateData.AcceptedOperation = arg.AcceptedOperation; + } + + bool OnDropCallback(IntPtr data, IntPtr obj, IntPtr selectionData) + { + var currentStateData = DragGestureHandler.CurrentStateData; + + if (currentStateData.DataPackage == null || currentStateData.AcceptedOperation == DataPackageOperation.None) + return false; + + Device.BeginInvokeOnMainThread(async () => + { + if (Recognizer is DropGestureRecognizer dropRecognizer && dropRecognizer.AllowDrop) + await dropRecognizer.SendDrop(new DropEventArgs(currentStateData.DataPackage.View)); + }); + + return true; + } + + #region GestureHandler + protected override void OnStarted(View sender, object data) + { + } + + protected override void OnMoved(View sender, object data) + { + } + + protected override void OnCompleted(View sender, object data) + { + } + + protected override void OnCanceled(View sender, object data) + { + } + #endregion + } +} diff --git a/src/Controls/src/Core/Platform/Tizen/Extensions/DragDropExtensions.cs b/src/Controls/src/Core/Platform/Tizen/Extensions/DragDropExtensions.cs new file mode 100644 index 000000000000..1d3f8108f15a --- /dev/null +++ b/src/Controls/src/Core/Platform/Tizen/Extensions/DragDropExtensions.cs @@ -0,0 +1,114 @@ +using System; +using System.Runtime.InteropServices; +using ElmSharp; + +namespace Microsoft.Maui.Controls.Platform +{ + public static class DragDropExtensions + { + public static void AddDropTarget(EvasObject obj, DragDropContentType contentType, + Interop.DragStateCallback enterCallback, + Interop.DragStateCallback leaveCallback, + Interop.DragPositionCallback positionCallback, + Interop.DropCallback dropCallback) + { + Interop.elm_drop_target_add(obj.RealHandle, contentType, + enterCallback, IntPtr.Zero, + leaveCallback, IntPtr.Zero, + positionCallback, IntPtr.Zero, + dropCallback, IntPtr.Zero); + } + + public static void StartDrag(EvasObject obj, DragDropContentType contentType, + string data, DragDropActionType actionType, + Interop.DragIconCreateCallback iconCallback, + Interop.DragPositionCallback positionCallback, + Interop.DragAcceptCallback acceptCallback, + Interop.DragStateCallback statCallback) + { + var strData = Marshal.StringToHGlobalAnsi(data); + Interop.elm_drag_start(obj.RealHandle, contentType, strData, actionType, + iconCallback, IntPtr.Zero, + positionCallback, IntPtr.Zero, + acceptCallback, IntPtr.Zero, + statCallback, IntPtr.Zero); + } + + public enum DragDropContentType + { + Targets = -1, + None = 0, + Text = 1, + MarkUp = 2, + Image = 4, + VCard = 8, + Html = 16 + } + + public enum DragDropActionType + { + Unknown = 0, + Copy, + Move, + Private, + Ask, + List, + Link, + Description + } + + public class Interop + { + public const string LibElementary = "libelementary.so.1"; + public const string LibEvas = "libevas.so.1"; + + + public delegate IntPtr DragIconCreateCallback(IntPtr data, IntPtr window, ref int xoff, ref int yoff); + public delegate void DragPositionCallback(IntPtr data, IntPtr obj, int x, int y, int actionType); + public delegate void DragAcceptCallback(IntPtr data, IntPtr obj, bool accept); + public delegate void DragStateCallback(IntPtr data, IntPtr obj); + public delegate bool DropCallback(IntPtr data, IntPtr obj, IntPtr selectionData); + + [DllImport(LibElementary)] + internal static extern void elm_drop_target_add(IntPtr obj, + DragDropContentType type, + DragStateCallback enterCallback, + IntPtr enterData, + DragStateCallback leaveCallback, + IntPtr leaveData, + DragPositionCallback positionCallback, + IntPtr positionData, + DropCallback dropcallback, + IntPtr dropData); + + [DllImport(LibElementary)] + internal static extern void elm_drag_start(IntPtr obj, + DragDropContentType contentType, + IntPtr data, + DragDropActionType actionType, + DragIconCreateCallback iconCreateCallback, + IntPtr iconCreateData, + DragPositionCallback dragPositionCallback, + IntPtr dragPositonData, + DragAcceptCallback dragAcceptCallback, + IntPtr dragAcceptData, + DragStateCallback dragStateCallback, + IntPtr dragStateData); + + [DllImport(LibElementary)] + internal static extern IntPtr elm_object_part_content_get(IntPtr obj, string part); + + [DllImport(LibEvas)] + internal static extern IntPtr evas_object_image_data_get(IntPtr obj, bool forWriting); + + [DllImport(LibEvas)] + internal static extern void evas_object_image_data_set(IntPtr obj, IntPtr data); + + [DllImport(LibEvas)] + internal static extern void evas_object_image_size_get(IntPtr obj, out int w, out int h); + + [DllImport(LibEvas)] + internal static extern void evas_object_image_size_set(IntPtr obj, int w, int h); + } + } +} diff --git a/src/Controls/src/Core/Platform/Tizen/GestureDetector.cs b/src/Controls/src/Core/Platform/Tizen/GestureDetector.cs new file mode 100644 index 000000000000..3c15da840ac2 --- /dev/null +++ b/src/Controls/src/Core/Platform/Tizen/GestureDetector.cs @@ -0,0 +1,636 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using ElmSharp; +using Microsoft.Maui.Controls.Internals; +using EGestureType = ElmSharp.GestureLayer.GestureType; + +namespace Microsoft.Maui.Controls.Platform +{ + class GestureDetector : IDisposable + { + readonly IDictionary> _handlerCache; + + IViewHandler? _handler; + GestureLayer? _gestureLayer; + double _doubleTapTime = 0; + double _longTapTime = 0; + bool _inputTransparent = false; + bool _isEnabled; + + protected virtual VisualElement? Element => _handler?.VirtualView as VisualElement; + + View? View => Element as View; + + protected virtual EvasObject? Control => _handler?.NativeView as EvasObject; + + public bool IsEnabled + { + get + { + return _isEnabled; + } + set + { + _isEnabled = value; + UpdateGestureLayerEnabled(); + } + } + + public bool InputTransparent + { + get + { + return _inputTransparent; + } + set + { + _inputTransparent = value; + UpdateGestureLayerEnabled(); + } + } + + public GestureDetector(IViewHandler? handler) + { + _handlerCache = new Dictionary>(); + _handler = handler; + _isEnabled = View?.IsEnabled ?? false; + _inputTransparent = View?.InputTransparent ?? false; + } + + public void Dispose() + { + Clear(); + _handler = null; + } + + public void Clear() + { + // this will clear all callbacks in ElmSharp GestureLayer + _gestureLayer?.Unrealize(); + _gestureLayer = null; + foreach (var handlers in _handlerCache.Values) + { + foreach (var handler in handlers) + { + handler.PropertyChanged -= OnGestureRecognizerPropertyChanged; + } + } + _handlerCache?.Clear(); + + if (Device.Idiom == TargetIdiom.TV) + { + if (Control != null) + Control.KeyDown -= OnKeyDown; + } + } + + public void AddGestures(IEnumerable? recognizers) + { + if (_gestureLayer == null) + { + CreateGestureLayer(); + } + if (recognizers == null) + return; + foreach (var item in recognizers) + { + AddGesture(item); + } + } + + public void RemoveGestures(IEnumerable? recognizers) + { + if (recognizers == null) + return; + foreach (var item in recognizers) + RemoveGesture(item); + } + + void CreateGestureLayer() + { + _gestureLayer = new GestureLayer(Control); + _gestureLayer.Attach(Control); + _gestureLayer.Deleted += (s, e) => + { + _gestureLayer = null; + Clear(); + }; + UpdateGestureLayerEnabled(); + + if (Device.Idiom == TargetIdiom.TV) + { + if (Control != null) + Control.KeyDown += OnKeyDown; + } + } + + void UpdateGestureLayerEnabled() + { + if (_gestureLayer != null) + { + _gestureLayer.IsEnabled = !_inputTransparent && _isEnabled; + } + } + + void AddGesture(IGestureRecognizer recognizer) + { + var handler = CreateHandler(recognizer); + if (handler == null) + return; + + var gestureType = handler.Type; + var timeout = handler.Timeout; + var cache = _handlerCache; + if (!cache.ContainsKey(gestureType)) + { + cache[gestureType] = new List(); + } + + handler.PropertyChanged += OnGestureRecognizerPropertyChanged; + cache[gestureType].Add(handler); + if (cache[gestureType].Count == 1) + { + switch (gestureType) + { + case EGestureType.Tap: + case EGestureType.TripleTap: + AddTapGesture(gestureType); + break; + + case EGestureType.DoubleTap: + AddDoubleTapGesture(gestureType, timeout); + break; + + case EGestureType.LongTap: + AddLongTapGesture(gestureType, timeout); + break; + + case EGestureType.Line: + AddLineGesture(gestureType); + break; + + case EGestureType.Flick: + AddFlickGesture(gestureType, timeout); + break; + + case EGestureType.Rotate: + AddRotateGesture(gestureType); + break; + + case EGestureType.Momentum: + AddMomentumGesture(gestureType); + break; + + case EGestureType.Zoom: + AddPinchGesture(gestureType); + break; + + default: + break; + } + } + + if (handler is DropGestureHandler dropGestureHandler) + { + dropGestureHandler.AddDropGesture(); + } + } + + void RemoveGesture(IGestureRecognizer recognizer) + { + var cache = _handlerCache; + var handler = LookupHandler(recognizer); + if (handler == null) return; + + var gestureType = cache.FirstOrDefault(x => x.Value.Contains(handler)).Key; + + handler.PropertyChanged -= OnGestureRecognizerPropertyChanged; + cache[gestureType].Remove(handler); + + if (cache[gestureType].Count == 0) + { + switch (gestureType) + { + case EGestureType.Tap: + case EGestureType.DoubleTap: + case EGestureType.TripleTap: + case EGestureType.LongTap: + RemoveTapGesture(gestureType); + break; + + case EGestureType.Line: + RemoveLineGesture(); + break; + + case EGestureType.Flick: + RemoveFlickGesture(); + break; + + case EGestureType.Rotate: + RemoveRotateGesture(); + break; + + case EGestureType.Momentum: + RemoveMomentumGesture(); + break; + + case EGestureType.Zoom: + RemovePinchGesture(); + break; + + default: + break; + } + } + } + + void AddLineGesture(EGestureType type) + { + _gestureLayer?.SetLineCallback(GestureLayer.GestureState.Start, (data) => { OnGestureStarted(type, data); }); + _gestureLayer?.SetLineCallback(GestureLayer.GestureState.Move, (data) => { OnGestureMoved(type, data); }); + _gestureLayer?.SetLineCallback(GestureLayer.GestureState.End, (data) => { OnGestureCompleted(type, data); }); + _gestureLayer?.SetLineCallback(GestureLayer.GestureState.Abort, (data) => { OnGestureCanceled(type, data); }); + } + + void AddPinchGesture(EGestureType type) + { + _gestureLayer?.SetZoomCallback(GestureLayer.GestureState.Start, (data) => { OnGestureStarted(type, data); }); + _gestureLayer?.SetZoomCallback(GestureLayer.GestureState.Move, (data) => { OnGestureMoved(type, data); }); + _gestureLayer?.SetZoomCallback(GestureLayer.GestureState.End, (data) => { OnGestureCompleted(type, data); }); + _gestureLayer?.SetZoomCallback(GestureLayer.GestureState.Abort, (data) => { OnGestureCanceled(type, data); }); + } + + void AddTapGesture(EGestureType type) + { + _gestureLayer?.SetTapCallback(type, GestureLayer.GestureState.Start, (data) => { OnGestureStarted(type, data); }); + _gestureLayer?.SetTapCallback(type, GestureLayer.GestureState.End, (data) => { OnGestureCompleted(type, data); }); + _gestureLayer?.SetTapCallback(type, GestureLayer.GestureState.Abort, (data) => { OnGestureCanceled(type, data); }); + } + + void AddDoubleTapGesture(EGestureType type, double timeout) + { + if (_gestureLayer == null) + return; + if (timeout > 0) + _gestureLayer.DoubleTapTimeout = timeout; + + _gestureLayer.SetTapCallback(type, GestureLayer.GestureState.Start, (data) => { OnDoubleTapStarted(type, data); }); + _gestureLayer.SetTapCallback(type, GestureLayer.GestureState.End, (data) => { OnDoubleTapCompleted(type, data); }); + _gestureLayer.SetTapCallback(type, GestureLayer.GestureState.Abort, (data) => { OnGestureCanceled(type, data); }); + } + + void AddLongTapGesture(EGestureType type, double timeout) + { + if (_gestureLayer == null) + return; + if (timeout > 0) + _gestureLayer.LongTapTimeout = timeout; + + _gestureLayer.SetTapCallback(type, GestureLayer.GestureState.Start, (data) => { OnLongTapStarted(type, data); }); + _gestureLayer.SetTapCallback(type, GestureLayer.GestureState.Move, (data) => { OnLongTapMoved(type, data); }); + _gestureLayer.SetTapCallback(type, GestureLayer.GestureState.End, (data) => { OnLongTapCompleted(type, data); }); + _gestureLayer.SetTapCallback(type, GestureLayer.GestureState.Abort, (data) => { OnGestureCanceled(type, data); }); + } + + void AddFlickGesture(EGestureType type, double timeout) + { + if (_gestureLayer == null) + return; + if (timeout > 0) + _gestureLayer.FlickTimeLimit = (int)(timeout * 1000); + + // Task to correct wrong coordinates information when applying EvasMap(Xamarin ex: Translation, Scale, Rotate property) + // Always change to the absolute coordinates of the pointer. + int startX = 0; + int startY = 0; + _gestureLayer.SetFlickCallback(GestureLayer.GestureState.Start, (data) => + { + startX = _gestureLayer.EvasCanvas.Pointer.X; + startY = _gestureLayer.EvasCanvas.Pointer.Y; + data.X1 = startX; + data.Y1 = startY; + OnGestureStarted(type, data); + }); + _gestureLayer.SetFlickCallback(GestureLayer.GestureState.Move, (data) => + { + data.X1 = startX; + data.Y1 = startY; + data.X2 = _gestureLayer.EvasCanvas.Pointer.X; + data.Y2 = _gestureLayer.EvasCanvas.Pointer.Y; + OnGestureMoved(type, data); + }); + _gestureLayer.SetFlickCallback(GestureLayer.GestureState.End, (data) => + { + data.X1 = startX; + data.Y1 = startY; + data.X2 = _gestureLayer.EvasCanvas.Pointer.X; + data.Y2 = _gestureLayer.EvasCanvas.Pointer.Y; + OnGestureCompleted(type, data); + }); + _gestureLayer.SetFlickCallback(GestureLayer.GestureState.Abort, (data) => { OnGestureCanceled(type, data); }); + } + + void AddRotateGesture(EGestureType type) + { + _gestureLayer?.SetRotateCallback(GestureLayer.GestureState.Start, (data) => { OnGestureStarted(type, data); }); + _gestureLayer?.SetRotateCallback(GestureLayer.GestureState.Move, (data) => { OnGestureMoved(type, data); }); + _gestureLayer?.SetRotateCallback(GestureLayer.GestureState.End, (data) => { OnGestureCompleted(type, data); }); + _gestureLayer?.SetRotateCallback(GestureLayer.GestureState.Abort, (data) => { OnGestureCanceled(type, data); }); + } + + void AddMomentumGesture(EGestureType type) + { + // Task to correct wrong coordinates information when applying EvasMap(Xamarin ex: Translation, Scale, Rotate property) + // Always change to the absolute coordinates of the pointer. + int startX = 0; + int startY = 0; + _gestureLayer?.SetMomentumCallback(GestureLayer.GestureState.Start, (data) => + { + startX = _gestureLayer.EvasCanvas.Pointer.X; + startY = _gestureLayer.EvasCanvas.Pointer.Y; + OnGestureStarted(type, data); + }); + _gestureLayer?.SetMomentumCallback(GestureLayer.GestureState.Move, (data) => + { + data.X1 = startX; + data.Y1 = startY; + data.X2 = _gestureLayer.EvasCanvas.Pointer.X; + data.Y2 = _gestureLayer.EvasCanvas.Pointer.Y; + OnGestureMoved(type, data); + }); + _gestureLayer?.SetMomentumCallback(GestureLayer.GestureState.End, (data) => { OnGestureCompleted(type, data); }); + _gestureLayer?.SetMomentumCallback(GestureLayer.GestureState.Abort, (data) => { OnGestureCanceled(type, data); }); + } + + void RemoveLineGesture() + { + _gestureLayer?.SetLineCallback(GestureLayer.GestureState.Start, null); + _gestureLayer?.SetLineCallback(GestureLayer.GestureState.Move, null); + _gestureLayer?.SetLineCallback(GestureLayer.GestureState.End, null); + _gestureLayer?.SetLineCallback(GestureLayer.GestureState.Abort, null); + } + + void RemovePinchGesture() + { + _gestureLayer?.SetZoomCallback(GestureLayer.GestureState.Start, null); + _gestureLayer?.SetZoomCallback(GestureLayer.GestureState.Move, null); + _gestureLayer?.SetZoomCallback(GestureLayer.GestureState.End, null); + _gestureLayer?.SetZoomCallback(GestureLayer.GestureState.Abort, null); + } + + void RemoveTapGesture(EGestureType type) + { + _gestureLayer?.SetTapCallback(type, GestureLayer.GestureState.Start, null); + _gestureLayer?.SetTapCallback(type, GestureLayer.GestureState.End, null); + _gestureLayer?.SetTapCallback(type, GestureLayer.GestureState.Abort, null); + } + + void RemoveFlickGesture() + { + _gestureLayer?.SetFlickCallback(GestureLayer.GestureState.Start, null); + _gestureLayer?.SetFlickCallback(GestureLayer.GestureState.Move, null); + _gestureLayer?.SetFlickCallback(GestureLayer.GestureState.End, null); + _gestureLayer?.SetFlickCallback(GestureLayer.GestureState.Abort, null); + } + + void RemoveRotateGesture() + { + _gestureLayer?.SetRotateCallback(GestureLayer.GestureState.Start, null); + _gestureLayer?.SetRotateCallback(GestureLayer.GestureState.Move, null); + _gestureLayer?.SetRotateCallback(GestureLayer.GestureState.End, null); + _gestureLayer?.SetRotateCallback(GestureLayer.GestureState.Abort, null); + } + + void RemoveMomentumGesture() + { + _gestureLayer?.SetMomentumCallback(GestureLayer.GestureState.Start, null); + _gestureLayer?.SetMomentumCallback(GestureLayer.GestureState.Move, null); + _gestureLayer?.SetMomentumCallback(GestureLayer.GestureState.End, null); + _gestureLayer?.SetMomentumCallback(GestureLayer.GestureState.Abort, null); + } + + #region GestureCallback + + void OnGestureStarted(EGestureType type, object data) + { + var cache = _handlerCache; + if (cache.ContainsKey(type)) + { + foreach (var handler in cache[type]) + { + (handler as IGestureController)?.SendStarted(View, data); + } + } + } + + void OnGestureMoved(EGestureType type, object data) + { + var cache = _handlerCache; + if (cache.ContainsKey(type)) + { + foreach (var handler in cache[type]) + { + (handler as IGestureController)?.SendMoved(View, data); + } + } + } + + void OnGestureCompleted(EGestureType type, object data) + { + var cache = _handlerCache; + if (cache.ContainsKey(type)) + { + foreach (var handler in cache[type]) + { + (handler as IGestureController)?.SendCompleted(View, data); + } + } + } + + void OnGestureCanceled(EGestureType type, object data) + { + var cache = _handlerCache; + if (cache.ContainsKey(type)) + { + foreach (var handler in cache[type]) + { + (handler as IGestureController)?.SendCanceled(View, data); + } + } + } + + void OnDoubleTapStarted(EGestureType type, object data) + { + _doubleTapTime = ((GestureLayer.TapData)data).Timestamp; + OnGestureStarted(type, data); + } + + void OnDoubleTapCompleted(EGestureType type, object data) + { + _doubleTapTime = ((GestureLayer.TapData)data).Timestamp - _doubleTapTime; + var cache = _handlerCache; + + if (cache.ContainsKey(type)) + { + foreach (var handler in cache[type]) + { + if ((handler.Timeout * 1000) >= _longTapTime) + (handler as IGestureController)?.SendCompleted(View, data); + else + (handler as IGestureController)?.SendCanceled(View, data); + } + } + } + + void OnLongTapStarted(EGestureType type, object data) + { + _longTapTime = ((GestureLayer.TapData)data).Timestamp; + OnGestureStarted(type, data); + } + + void OnLongTapMoved(EGestureType type, object data) + { + OnGestureMoved(type, data); + } + + void OnLongTapCompleted(EGestureType type, object data) + { + _longTapTime = ((GestureLayer.TapData)data).Timestamp - _longTapTime; + var cache = _handlerCache; + + if (cache.ContainsKey(type)) + { + foreach (var handler in cache[type]) + { + if ((handler.Timeout * 1000) <= _longTapTime) + (handler as IGestureController)?.SendCompleted(View, data); + else + (handler as IGestureController)?.SendCanceled(View, data); + } + } + } + + #endregion GestureCallback + + GestureHandler CreateHandler(IGestureRecognizer recognizer) + { + if (recognizer is TapGestureRecognizer) + { + return new TapGestureHandler(recognizer); + } + else if (recognizer is PinchGestureRecognizer) + { + return new PinchGestureHandler(recognizer, _handler); + } + else if (recognizer is PanGestureRecognizer) + { + return new PanGestureHandler(recognizer); + } + else if (recognizer is SwipeGestureRecognizer) + { + return new SwipeGestureHandler(recognizer); + } + else if (recognizer is DragGestureRecognizer) + { + return new DragGestureHandler(recognizer, _handler); + } + else if (recognizer is DropGestureRecognizer) + { + return new DropGestureHandler(recognizer, _handler); + } + return Registrar.Registered.GetHandlerForObject(recognizer, recognizer); + } + + GestureHandler? LookupHandler(IGestureRecognizer recognizer) + { + var cache = _handlerCache; + + foreach (var handlers in cache.Values) + { + foreach (var handler in handlers) + { + if (handler.Recognizer == recognizer) + return handler; + } + } + return null; + } + + void UpdateTapGesture(GestureHandler handler) + { + if (handler == null) + return; + RemoveGesture(handler.Recognizer); + AddGesture(handler.Recognizer); + + if (_gestureLayer == null) + return; + if (handler.Timeout > _gestureLayer.DoubleTapTimeout) + _gestureLayer.DoubleTapTimeout = handler.Timeout; + } + + void UpdateLongTapGesture(GestureHandler handler) + { + if (_gestureLayer == null) + return; + if (handler.Timeout > 0 && handler.Timeout < _gestureLayer.LongTapTimeout) + _gestureLayer.LongTapTimeout = handler.Timeout; + } + + void UpdateFlickGesture(GestureHandler handler) + { + if (_gestureLayer == null) + return; + if (handler.Timeout > _gestureLayer.FlickTimeLimit) + _gestureLayer.FlickTimeLimit = (int)(handler.Timeout * 1000); + } + + void OnGestureRecognizerPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + var handler = sender as GestureHandler; + if (handler != null) + { + switch (handler.Type) + { + case EGestureType.Tap: + case EGestureType.DoubleTap: + case EGestureType.TripleTap: + UpdateTapGesture(handler); + break; + + case EGestureType.LongTap: + UpdateLongTapGesture(handler); + break; + + case EGestureType.Flick: + UpdateFlickGesture(handler); + break; + + default: + break; + } + } + } + + void OnKeyDown(object? sender, EvasKeyEventArgs e) + { + if (_gestureLayer == null) + return; + if (e.KeyName == "Return" && _gestureLayer.IsEnabled) + { + var cache = _handlerCache; + if (cache.ContainsKey(EGestureType.Tap)) + { + foreach (var handler in cache[EGestureType.Tap]) + { + (handler as IGestureController)?.SendStarted(View, null); + (handler as IGestureController)?.SendCompleted(View, null); + } + } + } + } + } +} diff --git a/src/Controls/src/Core/Platform/Tizen/GestureHandler.cs b/src/Controls/src/Core/Platform/Tizen/GestureHandler.cs new file mode 100644 index 000000000000..623357bc30c6 --- /dev/null +++ b/src/Controls/src/Core/Platform/Tizen/GestureHandler.cs @@ -0,0 +1,55 @@ +using System.ComponentModel; +using ElmSharp; + +namespace Microsoft.Maui.Controls.Platform +{ + public abstract class GestureHandler : IGestureController, INotifyPropertyChanged, IRegisterable + { + public IGestureRecognizer Recognizer { get; private set; } + + public abstract GestureLayer.GestureType Type { get; } + + public virtual double Timeout { get; } + + protected GestureHandler(IGestureRecognizer recognizer) + { + Recognizer = recognizer; + Recognizer.PropertyChanged += OnRecognizerPropertyChanged; + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected abstract void OnStarted(View sender, object data); + + protected abstract void OnMoved(View sender, object data); + + protected abstract void OnCompleted(View sender, object data); + + protected abstract void OnCanceled(View sender, object data); + + void IGestureController.SendStarted(View sender, object data) + { + OnStarted(sender, data); + } + + void IGestureController.SendCompleted(View sender, object data) + { + OnCompleted(sender, data); + } + + void IGestureController.SendMoved(View sender, object data) + { + OnMoved(sender, data); + } + + void IGestureController.SendCanceled(View sender, object data) + { + OnCanceled(sender, data); + } + + protected virtual void OnRecognizerPropertyChanged(object sender, PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, e); + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Tizen/IGestureController.cs b/src/Controls/src/Core/Platform/Tizen/IGestureController.cs new file mode 100644 index 000000000000..a3fda1bb1e64 --- /dev/null +++ b/src/Controls/src/Core/Platform/Tizen/IGestureController.cs @@ -0,0 +1,15 @@ +using System; + +namespace Microsoft.Maui.Controls.Platform +{ + public interface IGestureController + { + void SendStarted(View sender, object data); + + void SendMoved(View sender, object data); + + void SendCompleted(View sender, object data); + + void SendCanceled(View sender, object data); + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Tizen/PanGestureHandler.cs b/src/Controls/src/Core/Platform/Tizen/PanGestureHandler.cs new file mode 100644 index 000000000000..4a18bda0413e --- /dev/null +++ b/src/Controls/src/Core/Platform/Tizen/PanGestureHandler.cs @@ -0,0 +1,43 @@ +using ElmSharp; + +namespace Microsoft.Maui.Controls.Platform +{ + public class PanGestureHandler : GestureHandler + { + int _currentPanGestureId; + + public PanGestureHandler(IGestureRecognizer recognizer) : base(recognizer) + { + } + + public override GestureLayer.GestureType Type + { + get + { + return GestureLayer.GestureType.Momentum; + } + } + + protected override void OnStarted(View sender, object data) + { + _currentPanGestureId++; + (Recognizer as IPanGestureController)?.SendPanStarted(sender, _currentPanGestureId); + } + + protected override void OnMoved(View sender, object data) + { + var lineData = (GestureLayer.MomentumData)data; + (Recognizer as IPanGestureController)?.SendPan(sender, DPExtensions.ConvertToScaledDP(lineData.X2 - lineData.X1), DPExtensions.ConvertToScaledDP(lineData.Y2 - lineData.Y1), _currentPanGestureId); + } + + protected override void OnCompleted(View sender, object data) + { + (Recognizer as IPanGestureController)?.SendPanCompleted(sender, _currentPanGestureId); + } + + protected override void OnCanceled(View sender, object data) + { + (Recognizer as IPanGestureController)?.SendPanCanceled(sender, _currentPanGestureId); + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Tizen/PinchGestureHandler.cs b/src/Controls/src/Core/Platform/Tizen/PinchGestureHandler.cs new file mode 100644 index 000000000000..2c4e56ce912b --- /dev/null +++ b/src/Controls/src/Core/Platform/Tizen/PinchGestureHandler.cs @@ -0,0 +1,58 @@ +using ElmSharp; + +namespace Microsoft.Maui.Controls.Platform +{ + public class PinchGestureHandler : GestureHandler + { + Graphics.Point _currentScalePoint; + int _previousPinchRadius; + double _originalPinchScale; + IViewHandler _handler; + + public PinchGestureHandler(IGestureRecognizer recognizer, IViewHandler handler) : base(recognizer) + { + _handler = handler; + } + + public override GestureLayer.GestureType Type + { + get + { + return GestureLayer.GestureType.Zoom; + } + } + + protected override void OnStarted(View sender, object data) + { + var geometry = (_handler.NativeView as EvasObject).Geometry; + var zoomData = (GestureLayer.ZoomData)data; + _currentScalePoint = new Graphics.Point((zoomData.X - geometry.X) / (double)geometry.Width, (zoomData.Y - geometry.Y) / (double)geometry.Height); + _originalPinchScale = sender.Scale; + _previousPinchRadius = zoomData.Radius; + (Recognizer as IPinchGestureController)?.SendPinchStarted(sender, _currentScalePoint); + } + + protected override void OnMoved(View sender, object data) + { + var zoomData = (GestureLayer.ZoomData)data; + if (_previousPinchRadius <= 0) + _previousPinchRadius = 1; + // functionality limitation: _currentScalePoint is not updated + (Recognizer as IPinchGestureController)?.SendPinch(sender, + 1 + _originalPinchScale * (zoomData.Radius - _previousPinchRadius) / _previousPinchRadius, + _currentScalePoint + ); + _previousPinchRadius = zoomData.Radius; + } + + protected override void OnCompleted(View sender, object data) + { + (Recognizer as IPinchGestureController)?.SendPinchEnded(sender); + } + + protected override void OnCanceled(View sender, object data) + { + (Recognizer as IPinchGestureController)?.SendPinchCanceled(sender); + } + } +} diff --git a/src/Controls/src/Core/Platform/Tizen/SwipeGestureHandler.cs b/src/Controls/src/Core/Platform/Tizen/SwipeGestureHandler.cs new file mode 100644 index 000000000000..be7f26059e3d --- /dev/null +++ b/src/Controls/src/Core/Platform/Tizen/SwipeGestureHandler.cs @@ -0,0 +1,29 @@ +using ElmSharp; + +namespace Microsoft.Maui.Controls.Platform +{ + public class SwipeGestureHandler : GestureHandler + { + public SwipeGestureHandler(IGestureRecognizer recognizer) : base(recognizer) + { + } + + public override GestureLayer.GestureType Type => GestureLayer.GestureType.Flick; + + protected override void OnStarted(View sender, object data) { } + + protected override void OnMoved(View sender, object data) { } + + protected override void OnCompleted(View sender, object data) + { + if (Recognizer is SwipeGestureRecognizer swipeGesture) + { + var lineData = (GestureLayer.LineData)data; + (swipeGesture as ISwipeGestureController)?.SendSwipe(sender, DPExtensions.ConvertToScaledDP(lineData.X2 - lineData.X1), DPExtensions.ConvertToScaledDP(lineData.Y2 - lineData.Y1)); + (swipeGesture as ISwipeGestureController)?.DetectSwipe(sender, swipeGesture.Direction); + } + } + + protected override void OnCanceled(View sender, object data) { } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Tizen/TapGestureHandler.cs b/src/Controls/src/Core/Platform/Tizen/TapGestureHandler.cs new file mode 100644 index 000000000000..21ee68ea1ebc --- /dev/null +++ b/src/Controls/src/Core/Platform/Tizen/TapGestureHandler.cs @@ -0,0 +1,46 @@ +using ElmSharp; + +namespace Microsoft.Maui.Controls.Platform +{ + public class TapGestureHandler : GestureHandler + { + public TapGestureHandler(IGestureRecognizer recognizer) : base(recognizer) + { + } + + public override GestureLayer.GestureType Type + { + get + { + var recognizer = Recognizer as TapGestureRecognizer; + if (recognizer != null) + { + int numberOfTaps = recognizer.NumberOfTapsRequired; + + if (numberOfTaps > 2) + return GestureLayer.GestureType.TripleTap; + else if (numberOfTaps > 1) + return GestureLayer.GestureType.DoubleTap; + } + return GestureLayer.GestureType.Tap; + } + } + + protected override void OnStarted(View sender, object data) + { + } + + protected override void OnMoved(View sender, object data) + { + } + + protected override void OnCompleted(View sender, object data) + { + (Recognizer as TapGestureRecognizer)?.SendTapped(sender); + } + + protected override void OnCanceled(View sender, object data) + { + } + } +}