ScrollGestureRecognizer.cs 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. using System;
  2. using System.Diagnostics;
  3. using Avalonia.Threading;
  4. namespace Avalonia.Input.GestureRecognizers
  5. {
  6. public class ScrollGestureRecognizer
  7. : StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise
  8. IGestureRecognizer
  9. {
  10. private bool _scrolling;
  11. private Point _trackedRootPoint;
  12. private IPointer? _tracking;
  13. private IInputElement? _target;
  14. private IGestureRecognizerActionsDispatcher? _actions;
  15. private bool _canHorizontallyScroll;
  16. private bool _canVerticallyScroll;
  17. private int _gestureId;
  18. private int _scrollStartDistance = 30;
  19. private Point _pointerPressedPoint;
  20. private VelocityTracker? _velocityTracker;
  21. // Movement per second
  22. private Vector _inertia;
  23. private ulong? _lastMoveTimestamp;
  24. /// <summary>
  25. /// Defines the <see cref="CanHorizontallyScroll"/> property.
  26. /// </summary>
  27. public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty =
  28. AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
  29. nameof(CanHorizontallyScroll),
  30. o => o.CanHorizontallyScroll,
  31. (o, v) => o.CanHorizontallyScroll = v);
  32. /// <summary>
  33. /// Defines the <see cref="CanVerticallyScroll"/> property.
  34. /// </summary>
  35. public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanVerticallyScrollProperty =
  36. AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
  37. nameof(CanVerticallyScroll),
  38. o => o.CanVerticallyScroll,
  39. (o, v) => o.CanVerticallyScroll = v);
  40. /// <summary>
  41. /// Defines the <see cref="ScrollStartDistance"/> property.
  42. /// </summary>
  43. public static readonly DirectProperty<ScrollGestureRecognizer, int> ScrollStartDistanceProperty =
  44. AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, int>(
  45. nameof(ScrollStartDistance),
  46. o => o.ScrollStartDistance,
  47. (o, v) => o.ScrollStartDistance = v);
  48. /// <summary>
  49. /// Gets or sets a value indicating whether the content can be scrolled horizontally.
  50. /// </summary>
  51. public bool CanHorizontallyScroll
  52. {
  53. get => _canHorizontallyScroll;
  54. set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value);
  55. }
  56. /// <summary>
  57. /// Gets or sets a value indicating whether the content can be scrolled horizontally.
  58. /// </summary>
  59. public bool CanVerticallyScroll
  60. {
  61. get => _canVerticallyScroll;
  62. set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
  63. }
  64. /// <summary>
  65. /// Gets or sets a value indicating the distance the pointer moves before scrolling is started
  66. /// </summary>
  67. public int ScrollStartDistance
  68. {
  69. get => _scrollStartDistance;
  70. set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value);
  71. }
  72. public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
  73. {
  74. _target = target;
  75. _actions = actions;
  76. }
  77. public void PointerPressed(PointerPressedEventArgs e)
  78. {
  79. if (e.Pointer.IsPrimary &&
  80. (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
  81. {
  82. EndGesture();
  83. _tracking = e.Pointer;
  84. _gestureId = ScrollGestureEventArgs.GetNextFreeId();
  85. _trackedRootPoint = _pointerPressedPoint = e.GetPosition((Visual?)_target);
  86. }
  87. }
  88. // Pixels per second speed that is considered to be the stop of inertial scroll
  89. private const double InertialScrollSpeedEnd = 5;
  90. public void PointerMoved(PointerEventArgs e)
  91. {
  92. if (e.Pointer == _tracking)
  93. {
  94. var rootPoint = e.GetPosition((Visual?)_target);
  95. if (!_scrolling)
  96. {
  97. if (CanHorizontallyScroll && Math.Abs(_trackedRootPoint.X - rootPoint.X) > ScrollStartDistance)
  98. _scrolling = true;
  99. if (CanVerticallyScroll && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > ScrollStartDistance)
  100. _scrolling = true;
  101. if (_scrolling)
  102. {
  103. _velocityTracker = new VelocityTracker();
  104. // Correct _trackedRootPoint with ScrollStartDistance, so scrolling does not start with a skip of ScrollStartDistance
  105. _trackedRootPoint = new Point(
  106. _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? _scrollStartDistance : -_scrollStartDistance),
  107. _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? _scrollStartDistance : -_scrollStartDistance));
  108. _actions!.Capture(e.Pointer, this);
  109. }
  110. }
  111. if (_scrolling)
  112. {
  113. var vector = _trackedRootPoint - rootPoint;
  114. _velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), _pointerPressedPoint - rootPoint);
  115. _lastMoveTimestamp = e.Timestamp;
  116. _trackedRootPoint = rootPoint;
  117. _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
  118. e.Handled = true;
  119. }
  120. }
  121. }
  122. public void PointerCaptureLost(IPointer pointer)
  123. {
  124. if (pointer == _tracking) EndGesture();
  125. }
  126. void EndGesture()
  127. {
  128. _tracking = null;
  129. if (_scrolling)
  130. {
  131. _inertia = default;
  132. _scrolling = false;
  133. _target!.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId));
  134. _gestureId = 0;
  135. _lastMoveTimestamp = null;
  136. }
  137. }
  138. public void PointerReleased(PointerReleasedEventArgs e)
  139. {
  140. if (e.Pointer == _tracking && _scrolling)
  141. {
  142. _inertia = _velocityTracker?.GetFlingVelocity().PixelsPerSecond ?? Vector.Zero;
  143. e.Handled = true;
  144. if (_inertia == default
  145. || e.Timestamp == 0
  146. || _lastMoveTimestamp == 0
  147. || e.Timestamp - _lastMoveTimestamp > 200)
  148. EndGesture();
  149. else
  150. {
  151. _tracking = null;
  152. var savedGestureId = _gestureId;
  153. var st = Stopwatch.StartNew();
  154. var lastTime = TimeSpan.Zero;
  155. DispatcherTimer.Run(() =>
  156. {
  157. // Another gesture has started, finish the current one
  158. if (_gestureId != savedGestureId)
  159. {
  160. return false;
  161. }
  162. var elapsedSinceLastTick = st.Elapsed - lastTime;
  163. lastTime = st.Elapsed;
  164. var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds);
  165. var distance = speed * elapsedSinceLastTick.TotalSeconds;
  166. var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance);
  167. _target!.RaiseEvent(scrollGestureEventArgs);
  168. if (!scrollGestureEventArgs.Handled || scrollGestureEventArgs.ShouldEndScrollGesture)
  169. {
  170. EndGesture();
  171. return false;
  172. }
  173. // EndGesture using InertialScrollSpeedEnd only in the direction of scrolling
  174. if (CanVerticallyScroll && CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd && Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
  175. {
  176. EndGesture();
  177. return false;
  178. }
  179. else if (CanVerticallyScroll && Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
  180. {
  181. EndGesture();
  182. return false;
  183. }
  184. else if (CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd)
  185. {
  186. EndGesture();
  187. return false;
  188. }
  189. return true;
  190. }, TimeSpan.FromMilliseconds(16), DispatcherPriority.Background);
  191. }
  192. }
  193. }
  194. }
  195. }