Browse Source

Implemented simple inertial scroll

Nikita Tsukanov 6 years ago
parent
commit
b387c38c84

+ 2 - 2
src/Avalonia.Controls/MenuItem.cs

@@ -339,7 +339,7 @@ namespace Avalonia.Controls
 
             var point = e.GetPointerPoint(null);
             RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
-                point.Properties, e.InputModifiers));
+                e.Timestamp, point.Properties, e.InputModifiers));
         }
 
         /// <inheritdoc/>
@@ -349,7 +349,7 @@ namespace Avalonia.Controls
 
             var point = e.GetPointerPoint(null);
             RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
-                point.Properties, e.InputModifiers));
+                e.Timestamp, point.Properties, e.InputModifiers));
         }
 
         /// <summary>

+ 58 - 4
src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs

@@ -1,5 +1,7 @@
 using System;
+using System.Diagnostics;
 using Avalonia.Interactivity;
+using Avalonia.Threading;
 
 namespace Avalonia.Input.GestureRecognizers
 {
@@ -16,6 +18,10 @@ namespace Avalonia.Input.GestureRecognizers
         private bool _canVerticallyScroll;
         private int _gestureId;
         
+        // Movement per second
+        private Vector _inertia;
+        private ulong? _lastMoveTimestamp;
+        
         /// <summary>
         /// Defines the <see cref="CanHorizontallyScroll"/> property.
         /// </summary>
@@ -63,14 +69,19 @@ namespace Avalonia.Input.GestureRecognizers
         {
             if (e.Pointer.IsPrimary && e.Pointer.Type == PointerType.Touch)
             {
+                EndGesture();
                 _tracking = e.Pointer;
-                _scrolling = false;
+                _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)
@@ -85,14 +96,20 @@ namespace Avalonia.Input.GestureRecognizers
                     if (_scrolling)
                     {
                         _actions.Capture(e.Pointer, this);
-                        _gestureId = ScrollGestureEventArgs.GetNextFreeId();
                     }
                 }
 
                 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;
                 }
@@ -109,8 +126,11 @@ namespace Avalonia.Input.GestureRecognizers
             _tracking = null;
             if (_scrolling)
             {
+                _inertia = default;
                 _scrolling = false;
                 _target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId));
+                _gestureId = 0;
+                _lastMoveTimestamp = null;
             }
             
         }
@@ -118,11 +138,45 @@ namespace Avalonia.Input.GestureRecognizers
         
         public void PointerReleased(PointerReleasedEventArgs e)
         {
-            // TODO: handle inertia
             if (e.Pointer == _tracking && _scrolling)
             {
                 e.Handled = true;
-                EndGesture();
+                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);
+                }
             }
         }
     }

+ 29 - 28
src/Avalonia.Input/MouseDevice.cs

@@ -89,11 +89,11 @@ namespace Avalonia.Input
             {
                 if (_pointer.Captured == null)
                 {
-                    SetPointerOver(this, root, clientPoint, InputModifiers.None);
+                    SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, InputModifiers.None);
                 }
                 else
                 {
-                    SetPointerOver(this, root, _pointer.Captured, InputModifiers.None);
+                    SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, InputModifiers.None);
                 }
             }
         }
@@ -121,13 +121,13 @@ namespace Avalonia.Input
             switch (e.Type)
             {
                 case RawPointerEventType.LeaveWindow:
-                    LeaveWindow(mouse, e.Root, e.InputModifiers);
+                    LeaveWindow(mouse, e.Timestamp, e.Root, e.InputModifiers);
                     break;
                 case RawPointerEventType.LeftButtonDown:
                 case RawPointerEventType.RightButtonDown:
                 case RawPointerEventType.MiddleButtonDown:
                     if (ButtonCount(props) > 1)
-                        e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers);
+                        e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
                     else
                         e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
                             props, e.InputModifiers);
@@ -136,25 +136,25 @@ namespace Avalonia.Input
                 case RawPointerEventType.RightButtonUp:
                 case RawPointerEventType.MiddleButtonUp:
                     if (ButtonCount(props) != 0)
-                        e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers);
+                        e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
                     else
-                        e.Handled = MouseUp(mouse, e.Root, e.Position, props, e.InputModifiers);
+                        e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
                     break;
                 case RawPointerEventType.Move:
-                    e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers);
+                    e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
                     break;
                 case RawPointerEventType.Wheel:
-                    e.Handled = MouseWheel(mouse, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers);
+                    e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers);
                     break;
             }
         }
 
-        private void LeaveWindow(IMouseDevice device, IInputRoot root, InputModifiers inputModifiers)
+        private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers)
         {
             Contract.Requires<ArgumentNullException>(device != null);
             Contract.Requires<ArgumentNullException>(root != null);
 
-            ClearPointerOver(this, root, inputModifiers);
+            ClearPointerOver(this, timestamp, root, inputModifiers);
         }
 
 
@@ -206,7 +206,7 @@ namespace Avalonia.Input
                     _lastClickRect = new Rect(p, new Size())
                         .Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2));
                     _lastMouseDownButton = properties.GetObsoleteMouseButton();
-                    var e = new PointerPressedEventArgs(source, _pointer, root, p, properties, inputModifiers, _clickCount);
+                    var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount);
                     source.RaiseEvent(e);
                     return e.Handled;
                 }
@@ -215,7 +215,7 @@ namespace Avalonia.Input
             return false;
         }
 
-        private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, PointerPointProperties properties,
+        private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties,
             InputModifiers inputModifiers)
         {
             Contract.Requires<ArgumentNullException>(device != null);
@@ -225,22 +225,22 @@ namespace Avalonia.Input
 
             if (_pointer.Captured == null)
             {
-                source = SetPointerOver(this, root, p, inputModifiers);
+                source = SetPointerOver(this, timestamp, root, p, inputModifiers);
             }
             else
             {
-                SetPointerOver(this, root, _pointer.Captured, inputModifiers);
+                SetPointerOver(this, timestamp, root, _pointer.Captured, inputModifiers);
                 source = _pointer.Captured;
             }
 
             var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root,
-                p, properties, inputModifiers);
+                p, timestamp, properties, inputModifiers);
 
             source?.RaiseEvent(e);
             return e.Handled;
         }
 
-        private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, PointerPointProperties props,
+        private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props,
             InputModifiers inputModifiers)
         {
             Contract.Requires<ArgumentNullException>(device != null);
@@ -251,7 +251,8 @@ namespace Avalonia.Input
             if (hit != null)
             {
                 var source = GetSource(hit);
-                var e = new PointerReleasedEventArgs(source, _pointer, root, p, props, inputModifiers, _lastMouseDownButton);
+                var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers,
+                    _lastMouseDownButton);
 
                 source?.RaiseEvent(e);
                 _pointer.Capture(null);
@@ -261,7 +262,7 @@ namespace Avalonia.Input
             return false;
         }
 
-        private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p,
+        private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
             PointerPointProperties props,
             Vector delta, InputModifiers inputModifiers)
         {
@@ -273,7 +274,7 @@ namespace Avalonia.Input
             if (hit != null)
             {
                 var source = GetSource(hit);
-                var e = new PointerWheelEventArgs(source, _pointer, root, p, props, inputModifiers, delta);
+                var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta);
 
                 source?.RaiseEvent(e);
                 return e.Handled;
@@ -298,19 +299,19 @@ namespace Avalonia.Input
             return _pointer.Captured ?? root.InputHitTest(p);
         }
 
-        PointerEventArgs CreateSimpleEvent(RoutedEvent ev, IInteractive source, InputModifiers inputModifiers)
+        PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source, InputModifiers inputModifiers)
         {
             return new PointerEventArgs(ev, source, _pointer, null, default,
-                new PointerPointProperties(inputModifiers), inputModifiers);
+                timestamp, new PointerPointProperties(inputModifiers), inputModifiers);
         }
 
-        private void ClearPointerOver(IPointerDevice device, IInputRoot root, InputModifiers inputModifiers)
+        private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers)
         {
             Contract.Requires<ArgumentNullException>(device != null);
             Contract.Requires<ArgumentNullException>(root != null);
 
             var element = root.PointerOverElement;
-            var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, element, inputModifiers);
+            var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, inputModifiers);
 
             if (element!=null && !element.IsAttachedToVisualTree)
             {
@@ -347,7 +348,7 @@ namespace Avalonia.Input
             }
         }
 
-        private IInputElement SetPointerOver(IPointerDevice device, IInputRoot root, Point p, InputModifiers inputModifiers)
+        private IInputElement SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, InputModifiers inputModifiers)
         {
             Contract.Requires<ArgumentNullException>(device != null);
             Contract.Requires<ArgumentNullException>(root != null);
@@ -358,18 +359,18 @@ namespace Avalonia.Input
             {
                 if (element != null)
                 {
-                    SetPointerOver(device, root, element, inputModifiers);
+                    SetPointerOver(device, timestamp, root, element, inputModifiers);
                 }
                 else
                 {
-                    ClearPointerOver(device, root, inputModifiers);
+                    ClearPointerOver(device, timestamp, root, inputModifiers);
                 }
             }
 
             return element;
         }
 
-        private void SetPointerOver(IPointerDevice device, IInputRoot root, IInputElement element, InputModifiers inputModifiers)
+        private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, InputModifiers inputModifiers)
         {
             Contract.Requires<ArgumentNullException>(device != null);
             Contract.Requires<ArgumentNullException>(root != null);
@@ -391,7 +392,7 @@ namespace Avalonia.Input
 
             el = root.PointerOverElement;
 
-            var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, el, inputModifiers);
+            var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, inputModifiers);
             if (el!=null && branch!=null && !el.IsAttachedToVisualTree)
             {
                 ClearChildrenPointerOver(e,branch,false);

+ 13 - 7
src/Avalonia.Input/PointerEventArgs.cs

@@ -17,7 +17,9 @@ namespace Avalonia.Input
         public PointerEventArgs(RoutedEvent routedEvent,
             IInteractive source,
             IPointer pointer,
-            IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties,
+            IVisual rootVisual, Point rootVisualPosition,
+            ulong timestamp,
+            PointerPointProperties properties,
             InputModifiers modifiers)
            : base(routedEvent)
         {
@@ -26,6 +28,7 @@ namespace Avalonia.Input
             _rootVisualPosition = rootVisualPosition;
             _properties = properties;
             Pointer = pointer;
+            Timestamp = timestamp;
             InputModifiers = modifiers;
         }
 
@@ -50,6 +53,7 @@ namespace Avalonia.Input
         }
 
         public IPointer Pointer { get; }
+        public ulong Timestamp { get; }
 
         private IPointerDevice _device;
 
@@ -86,11 +90,13 @@ namespace Avalonia.Input
         public PointerPressedEventArgs(
             IInteractive source,
             IPointer pointer,
-            IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties,
+            IVisual rootVisual, Point rootVisualPosition,
+            ulong timestamp,
+            PointerPointProperties properties,
             InputModifiers modifiers,
             int obsoleteClickCount = 1)
-            : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, properties,
-                modifiers)
+            : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition,
+                timestamp, properties, modifiers)
         {
             _obsoleteClickCount = obsoleteClickCount;
         }
@@ -105,10 +111,10 @@ namespace Avalonia.Input
     {
         public PointerReleasedEventArgs(
             IInteractive source, IPointer pointer,
-            IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, InputModifiers modifiers,
-            MouseButton obsoleteMouseButton)
+            IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
+            PointerPointProperties properties, InputModifiers modifiers, MouseButton obsoleteMouseButton)
             : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition,
-                properties, modifiers)
+                timestamp, properties, modifiers)
         {
             MouseButton = obsoleteMouseButton;
         }

+ 3 - 2
src/Avalonia.Input/PointerWheelEventArgs.cs

@@ -11,9 +11,10 @@ namespace Avalonia.Input
         public Vector Delta { get; set; }
 
         public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual,
-            Point rootVisualPosition,
+            Point rootVisualPosition, ulong timestamp,
             PointerPointProperties properties, InputModifiers modifiers, Vector delta) 
-            : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, properties, modifiers)
+            : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition,
+                timestamp, properties, modifiers)
         {
             Delta = delta;
         }

+ 3 - 3
src/Avalonia.Input/TouchDevice.cs

@@ -44,7 +44,7 @@ namespace Avalonia.Input
             if (args.Type == RawPointerEventType.TouchBegin)
             {
                 target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
-                    args.Root, args.Position,
+                    args.Root, args.Position, ev.Timestamp,
                     new PointerPointProperties(GetModifiers(args.InputModifiers, pointer.IsPrimary)),
                     GetModifiers(args.InputModifiers, false)));
             }
@@ -55,7 +55,7 @@ namespace Avalonia.Input
                 using (pointer)
                 {
                     target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
-                        args.Root, args.Position,
+                        args.Root, args.Position, ev.Timestamp,
                         new PointerPointProperties(GetModifiers(args.InputModifiers, false)),
                         GetModifiers(args.InputModifiers, pointer.IsPrimary),
                         pointer.IsPrimary ? MouseButton.Left : MouseButton.None));
@@ -66,7 +66,7 @@ namespace Avalonia.Input
             {
                 var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
                 target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
-                    args.Position, new PointerPointProperties(modifiers), modifiers));
+                    args.Position, ev.Timestamp, new PointerPointProperties(modifiers), modifiers));
             }
         }
         

+ 9 - 5
tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs

@@ -1,3 +1,4 @@
+using System.Reactive;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.VisualTree;
@@ -22,6 +23,8 @@ namespace Avalonia.Controls.UnitTests
         }
         
         TestPointer _pointer = new TestPointer();
+        private ulong _nextStamp = 1;
+        private ulong Timestamp() => _nextStamp++;
 
         private InputModifiers _pressedButtons;
         public IInputElement Captured => _pointer.Captured;
@@ -61,7 +64,7 @@ namespace Avalonia.Controls.UnitTests
             else
             {
                 _pressedButton = mouseButton;
-                target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, props,
+                target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props,
                     GetModifiers(modifiers), clickCount));
             }
         }
@@ -70,7 +73,7 @@ namespace Avalonia.Controls.UnitTests
         public void Move(IInteractive target, IInteractive source, in Point position, InputModifiers modifiers = default)
         {
             target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (IVisual)target, position,
-                new PointerPointProperties(_pressedButtons), GetModifiers(modifiers)));
+                Timestamp(), new PointerPointProperties(_pressedButtons), GetModifiers(modifiers)));
         }
 
         public void Up(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default,
@@ -84,7 +87,8 @@ namespace Avalonia.Controls.UnitTests
             _pressedButtons = (_pressedButtons | conv) ^ conv;
             var props = new PointerPointProperties(_pressedButtons);
             if (ButtonCount(props) == 0)
-                target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, props,
+                target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position,
+                    Timestamp(), props,
                     GetModifiers(modifiers), _pressedButton));
             else
                 Move(target, source, position);
@@ -103,13 +107,13 @@ namespace Avalonia.Controls.UnitTests
         public void Enter(IInteractive target)
         {
             target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnterEvent, target, _pointer, (IVisual)target, default,
-                new PointerPointProperties(_pressedButtons), _pressedButtons));
+                Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons));
         }
 
         public void Leave(IInteractive target)
         {
             target.RaiseEvent(new PointerEventArgs(InputElement.PointerLeaveEvent, target, _pointer, (IVisual)target, default,
-                new PointerPointProperties(_pressedButtons), _pressedButtons));
+                Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons));
         }
 
     }

+ 3 - 3
tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs

@@ -11,14 +11,14 @@ namespace Avalonia.Controls.UnitTests.Platform
     public class DefaultMenuInteractionHandlerTests
     {
         static PointerEventArgs CreateArgs(RoutedEvent ev, IInteractive source) 
-            => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, new PointerPointProperties(), default);
+            => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, 0, new PointerPointProperties(), default);
 
         static PointerPressedEventArgs CreatePressed(IInteractive source) => new PointerPressedEventArgs(source,
-            new FakePointer(), (IVisual)source, default, new PointerPointProperties {IsLeftButtonPressed = true},
+            new FakePointer(), (IVisual)source, default,0, new PointerPointProperties {IsLeftButtonPressed = true},
             default);
         
         static PointerReleasedEventArgs CreateReleased(IInteractive source) => new PointerReleasedEventArgs(source,
-            new FakePointer(), (IVisual)source, default, new PointerPointProperties(), default, MouseButton.Left);
+            new FakePointer(), (IVisual)source, default,0, new PointerPointProperties(), default, MouseButton.Left);
         
         public class TopLevel
         {