committed by
GitHub
21 changed files with 644 additions and 144 deletions
@ -0,0 +1,127 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Data; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.Input.GestureRecognizers |
|||
{ |
|||
public class GestureRecognizerCollection : IReadOnlyCollection<IGestureRecognizer>, IGestureRecognizerActionsDispatcher |
|||
{ |
|||
private readonly IInputElement _inputElement; |
|||
private List<IGestureRecognizer> _recognizers; |
|||
private Dictionary<IPointer, IGestureRecognizer> _pointerGrabs; |
|||
|
|||
|
|||
public GestureRecognizerCollection(IInputElement inputElement) |
|||
{ |
|||
_inputElement = inputElement; |
|||
} |
|||
|
|||
public void Add(IGestureRecognizer recognizer) |
|||
{ |
|||
if (_recognizers == null) |
|||
{ |
|||
// We initialize the collection when the first recognizer is added
|
|||
_recognizers = new List<IGestureRecognizer>(); |
|||
_pointerGrabs = new Dictionary<IPointer, IGestureRecognizer>(); |
|||
} |
|||
|
|||
_recognizers.Add(recognizer); |
|||
recognizer.Initialize(_inputElement, this); |
|||
|
|||
// Hacks to make bindings work
|
|||
|
|||
if (_inputElement is ILogical logicalParent && recognizer is ISetLogicalParent logical) |
|||
{ |
|||
logical.SetParent(logicalParent); |
|||
if (recognizer is IStyleable styleableRecognizer |
|||
&& _inputElement is IStyleable styleableParent) |
|||
styleableRecognizer.Bind(StyledElement.TemplatedParentProperty, |
|||
styleableParent.GetObservable(StyledElement.TemplatedParentProperty)); |
|||
} |
|||
} |
|||
|
|||
static readonly List<IGestureRecognizer> s_Empty = new List<IGestureRecognizer>(); |
|||
|
|||
public IEnumerator<IGestureRecognizer> GetEnumerator() |
|||
=> _recognizers?.GetEnumerator() ?? s_Empty.GetEnumerator(); |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
|
|||
public int Count => _recognizers?.Count ?? 0; |
|||
|
|||
|
|||
internal bool HandlePointerPressed(PointerPressedEventArgs e) |
|||
{ |
|||
if (_recognizers == null) |
|||
return false; |
|||
foreach (var r in _recognizers) |
|||
{ |
|||
if(e.Handled) |
|||
break; |
|||
r.PointerPressed(e); |
|||
} |
|||
|
|||
return e.Handled; |
|||
} |
|||
|
|||
internal bool HandlePointerReleased(PointerReleasedEventArgs e) |
|||
{ |
|||
if (_recognizers == null) |
|||
return false; |
|||
if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) |
|||
{ |
|||
capture.PointerReleased(e); |
|||
} |
|||
else |
|||
foreach (var r in _recognizers) |
|||
{ |
|||
if (e.Handled) |
|||
break; |
|||
r.PointerReleased(e); |
|||
} |
|||
return e.Handled; |
|||
} |
|||
|
|||
internal bool HandlePointerMoved(PointerEventArgs e) |
|||
{ |
|||
if (_recognizers == null) |
|||
return false; |
|||
if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) |
|||
{ |
|||
capture.PointerMoved(e); |
|||
} |
|||
else |
|||
foreach (var r in _recognizers) |
|||
{ |
|||
if (e.Handled) |
|||
break; |
|||
r.PointerMoved(e); |
|||
} |
|||
return e.Handled; |
|||
} |
|||
|
|||
internal void HandlePointerCaptureLost(PointerCaptureLostEventArgs e) |
|||
{ |
|||
if (_recognizers == null) |
|||
return; |
|||
_pointerGrabs.Remove(e.Pointer); |
|||
foreach (var r in _recognizers) |
|||
{ |
|||
if(e.Handled) |
|||
break; |
|||
r.PointerCaptureLost(e); |
|||
} |
|||
} |
|||
|
|||
void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer) |
|||
{ |
|||
pointer.Capture(_inputElement); |
|||
_pointerGrabs[pointer] = recognizer; |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
namespace Avalonia.Input.GestureRecognizers |
|||
{ |
|||
public interface IGestureRecognizer |
|||
{ |
|||
void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions); |
|||
void PointerPressed(PointerPressedEventArgs e); |
|||
void PointerReleased(PointerReleasedEventArgs e); |
|||
void PointerMoved(PointerEventArgs e); |
|||
void PointerCaptureLost(PointerCaptureLostEventArgs e); |
|||
} |
|||
|
|||
public interface IGestureRecognizerActionsDispatcher |
|||
{ |
|||
void Capture(IPointer pointer, IGestureRecognizer recognizer); |
|||
} |
|||
|
|||
public enum GestureRecognizerResult |
|||
{ |
|||
None, |
|||
Capture, |
|||
ReleaseCapture |
|||
} |
|||
} |
|||
@ -0,0 +1,183 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.Input.GestureRecognizers |
|||
{ |
|||
public class ScrollGestureRecognizer |
|||
: StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise
|
|||
IGestureRecognizer |
|||
{ |
|||
private bool _scrolling; |
|||
private Point _trackedRootPoint; |
|||
private IPointer _tracking; |
|||
private IInputElement _target; |
|||
private IGestureRecognizerActionsDispatcher _actions; |
|||
private bool _canHorizontallyScroll; |
|||
private bool _canVerticallyScroll; |
|||
private int _gestureId; |
|||
|
|||
// Movement per second
|
|||
private Vector _inertia; |
|||
private ulong? _lastMoveTimestamp; |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="CanHorizontallyScroll"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty = |
|||
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>( |
|||
nameof(CanHorizontallyScroll), |
|||
o => o.CanHorizontallyScroll, |
|||
(o, v) => o.CanHorizontallyScroll = v); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="CanVerticallyScroll"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanVerticallyScrollProperty = |
|||
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>( |
|||
nameof(CanVerticallyScroll), |
|||
o => o.CanVerticallyScroll, |
|||
(o, v) => o.CanVerticallyScroll = v); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
|
|||
/// </summary>
|
|||
public bool CanHorizontallyScroll |
|||
{ |
|||
get => _canHorizontallyScroll; |
|||
set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
|
|||
/// </summary>
|
|||
public bool CanVerticallyScroll |
|||
{ |
|||
get => _canVerticallyScroll; |
|||
set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); |
|||
} |
|||
|
|||
|
|||
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) |
|||
{ |
|||
_target = target; |
|||
_actions = actions; |
|||
} |
|||
|
|||
public void PointerPressed(PointerPressedEventArgs e) |
|||
{ |
|||
if (e.Pointer.IsPrimary && e.Pointer.Type == PointerType.Touch) |
|||
{ |
|||
EndGesture(); |
|||
_tracking = e.Pointer; |
|||
_gestureId = ScrollGestureEventArgs.GetNextFreeId();; |
|||
_trackedRootPoint = e.GetPosition(null); |
|||
} |
|||
} |
|||
|
|||
// Arbitrary chosen value, probably need to move that to platform settings or something
|
|||
private const double ScrollStartDistance = 30; |
|||
|
|||
// Pixels per second speed that is considered to be the stop of inertiall scroll
|
|||
private const double InertialScrollSpeedEnd = 5; |
|||
|
|||
public void PointerMoved(PointerEventArgs e) |
|||
{ |
|||
if (e.Pointer == _tracking) |
|||
{ |
|||
var rootPoint = e.GetPosition(null); |
|||
if (!_scrolling) |
|||
{ |
|||
if (CanHorizontallyScroll && Math.Abs(_trackedRootPoint.X - rootPoint.X) > ScrollStartDistance) |
|||
_scrolling = true; |
|||
if (CanVerticallyScroll && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > ScrollStartDistance) |
|||
_scrolling = true; |
|||
if (_scrolling) |
|||
{ |
|||
_actions.Capture(e.Pointer, this); |
|||
} |
|||
} |
|||
|
|||
if (_scrolling) |
|||
{ |
|||
var vector = _trackedRootPoint - rootPoint; |
|||
var elapsed = _lastMoveTimestamp.HasValue ? |
|||
TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) : |
|||
TimeSpan.Zero; |
|||
|
|||
_lastMoveTimestamp = e.Timestamp; |
|||
_trackedRootPoint = rootPoint; |
|||
if (elapsed.TotalSeconds > 0) |
|||
_inertia = vector / elapsed.TotalSeconds; |
|||
_target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); |
|||
e.Handled = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void PointerCaptureLost(PointerCaptureLostEventArgs e) |
|||
{ |
|||
if (e.Pointer == _tracking) EndGesture(); |
|||
} |
|||
|
|||
void EndGesture() |
|||
{ |
|||
_tracking = null; |
|||
if (_scrolling) |
|||
{ |
|||
_inertia = default; |
|||
_scrolling = false; |
|||
_target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); |
|||
_gestureId = 0; |
|||
_lastMoveTimestamp = null; |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
public void PointerReleased(PointerReleasedEventArgs e) |
|||
{ |
|||
if (e.Pointer == _tracking && _scrolling) |
|||
{ |
|||
e.Handled = true; |
|||
if (_inertia == default |
|||
|| e.Timestamp == 0 |
|||
|| _lastMoveTimestamp == 0 |
|||
|| e.Timestamp - _lastMoveTimestamp > 200) |
|||
EndGesture(); |
|||
else |
|||
{ |
|||
var savedGestureId = _gestureId; |
|||
var st = Stopwatch.StartNew(); |
|||
var lastTime = TimeSpan.Zero; |
|||
DispatcherTimer.Run(() => |
|||
{ |
|||
// Another gesture has started, finish the current one
|
|||
if (_gestureId != savedGestureId) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var elapsedSinceLastTick = st.Elapsed - lastTime; |
|||
lastTime = st.Elapsed; |
|||
|
|||
var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); |
|||
var distance = speed * elapsedSinceLastTick.TotalSeconds; |
|||
_target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); |
|||
|
|||
|
|||
|
|||
if (Math.Abs(speed.X) < InertialScrollSpeedEnd || Math.Abs(speed.Y) <= InertialScrollSpeedEnd) |
|||
{ |
|||
EndGesture(); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
}, TimeSpan.FromMilliseconds(16), DispatcherPriority.Background); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public class ScrollGestureEventArgs : RoutedEventArgs |
|||
{ |
|||
public int Id { get; } |
|||
public Vector Delta { get; } |
|||
private static int _nextId = 1; |
|||
|
|||
public static int GetNextFreeId() => _nextId++; |
|||
|
|||
public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent) |
|||
{ |
|||
Id = id; |
|||
Delta = delta; |
|||
} |
|||
} |
|||
|
|||
public class ScrollGestureEndedEventArgs : RoutedEventArgs |
|||
{ |
|||
public int Id { get; } |
|||
|
|||
public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) |
|||
{ |
|||
Id = id; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue