From 11b0de3481b8e9950b42fee8fc1869def260e397 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 8 Dec 2022 14:28:33 +0000 Subject: [PATCH] add hold gesture recognizer --- .../HoldGestureRecognizer.cs | 124 ++++++++++++++++++ src/Avalonia.Base/Input/Gestures.cs | 4 + .../Input/HoldGestureEventArgs.cs | 31 +++++ src/Avalonia.Controls/Control.cs | 19 +++ 4 files changed, 178 insertions(+) create mode 100644 src/Avalonia.Base/Input/GestureRecognizers/HoldGestureRecognizer.cs create mode 100644 src/Avalonia.Base/Input/HoldGestureEventArgs.cs diff --git a/src/Avalonia.Base/Input/GestureRecognizers/HoldGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/HoldGestureRecognizer.cs new file mode 100644 index 0000000000..02fd800baf --- /dev/null +++ b/src/Avalonia.Base/Input/GestureRecognizers/HoldGestureRecognizer.cs @@ -0,0 +1,124 @@ +using System.Timers; +using Avalonia.Input.GestureRecognizers; +using Avalonia.Threading; + +namespace Avalonia.Input +{ + public class HoldGestureRecognizer : StyledElement, IGestureRecognizer + { + private const int Tolerance = 30; + private IInputElement? _target; + private IGestureRecognizerActionsDispatcher? _actions; + private int _gestureId; + private IPointer? _tracking; + private PointerPressedEventArgs? _pointerEventArgs; + private Rect _trackingBounds; + private Timer? _holdTimer; + private bool _elasped; + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsHoldWithMouseEnabledProperty = + AvaloniaProperty.Register( + nameof(IsHoldWithMouseEnabled)); + + /// + /// Gets or sets whether to detect hold from the mouse + /// + public bool IsHoldWithMouseEnabled + { + get => GetValue(IsHoldWithMouseEnabledProperty); + set => SetValue(IsHoldWithMouseEnabledProperty, value); + } + + public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) + { + _target = target; + _actions = actions; + + _target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble); + _target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble); + + _holdTimer = new Timer(300); + _holdTimer.AutoReset = false; + _holdTimer.Elapsed += HoldTimer_Elapsed; + } + + private async void HoldTimer_Elapsed(object? sender, ElapsedEventArgs e) + { + _elasped = true; + _holdTimer?.Stop(); + + if(_tracking != null) + { + await Dispatcher.UIThread.InvokeAsync(() => _target?.RaiseEvent(new HoldGestureEventArgs(_gestureId, _pointerEventArgs, HoldingState.Started))); + } + } + + private void OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + PointerPressed(e); + } + + private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + PointerReleased(e); + } + + public void PointerCaptureLost(IPointer pointer) + { + if (_tracking == pointer) + { + EndHold(!_elasped); + } + } + + public void PointerMoved(PointerEventArgs e) + { + if (_tracking == e.Pointer && _target is Visual visual) + { + var currentPosition = e.GetPosition(visual); + + if (!_trackingBounds.Contains(currentPosition)) + { + EndHold(true); + } + } + } + + public void PointerPressed(PointerPressedEventArgs e) + { + if (_target != null && _target is Visual visual && (IsHoldWithMouseEnabled || e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + { + _elasped = false; + var position = e.GetPosition(visual); + _gestureId = HoldGestureEventArgs.GetNextFreeId(); + _tracking = e.Pointer; + _pointerEventArgs = e; + + _trackingBounds = new Rect(position.X - Tolerance / 2, position.Y - Tolerance / 2, Tolerance, Tolerance); + + _holdTimer?.Start(); + } + } + + public void PointerReleased(PointerReleasedEventArgs e) + { + if (_tracking == e.Pointer) + { + EndHold(!_elasped); + } + } + + private void EndHold(bool cancelled) + { + _holdTimer?.Stop(); + + _tracking = null; + _trackingBounds = default; + + _target?.RaiseEvent(new HoldGestureEventArgs(_gestureId, _pointerEventArgs, cancelled ? HoldingState.Cancelled : HoldingState.Completed)); + } + } +} diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index b4d5feaf3b..1c2220baf4 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -58,6 +58,10 @@ namespace Avalonia.Input RoutedEvent.Register( "PullGesture", RoutingStrategies.Bubble, typeof(Gestures)); + public static readonly RoutedEvent HoldGestureEvent = + RoutedEvent.Register( + "HoldGesture", RoutingStrategies.Bubble, typeof(Gestures)); + public static readonly RoutedEvent PullGestureEndedEvent = RoutedEvent.Register( "PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); diff --git a/src/Avalonia.Base/Input/HoldGestureEventArgs.cs b/src/Avalonia.Base/Input/HoldGestureEventArgs.cs new file mode 100644 index 0000000000..63a508b1f4 --- /dev/null +++ b/src/Avalonia.Base/Input/HoldGestureEventArgs.cs @@ -0,0 +1,31 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + public class HoldGestureEventArgs : RoutedEventArgs + { + public int Id { get; } + public Vector Delta { get; } + public HoldingState HoldingState { get; } + public PointerEventArgs? PointerEventArgs { get; } + + private static int _nextId = 1; + + internal static int GetNextFreeId() => _nextId++; + + public HoldGestureEventArgs(int id, PointerEventArgs? pointerEventArgs, HoldingState holdingState) : base(Gestures.HoldGestureEvent) + { + Id = id; + HoldingState = holdingState; + PointerEventArgs = pointerEventArgs; + } + } + + public enum HoldingState + { + Started, + Completed, + Cancelled, + } +} diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 88c9823952..de91fb83c9 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -216,6 +216,12 @@ namespace Avalonia.Controls /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; + public Control() + { + // Add a default HoldGestureRecognizer + GestureRecognizers.Add(new HoldGestureRecognizer()); + } + /// void ISetterValue.Initialize(ISetter setter) { @@ -366,16 +372,29 @@ namespace Avalonia.Controls { base.OnAttachedToVisualTreeCore(e); + AddHandler(Gestures.HoldGestureEvent, OnHoldEvent); + InitializeIfNeeded(); ScheduleOnLoadedCore(); } + private void OnHoldEvent(object? sender, HoldGestureEventArgs e) + { + if(e.HoldingState == HoldingState.Started) + { + // Trigger ContentRequest when hold starts + RaiseEvent(new ContextRequestedEventArgs(e.PointerEventArgs!)); + } + } + /// protected sealed override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTreeCore(e); + RemoveHandler(Gestures.HoldGestureEvent, OnHoldEvent); + OnUnloadedCore(); }