Browse Source

Implemented simple inertial scroll

pull/2595/head
Nikita Tsukanov 7 years ago
parent
commit
b387c38c84
  1. 4
      src/Avalonia.Controls/MenuItem.cs
  2. 62
      src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
  3. 57
      src/Avalonia.Input/MouseDevice.cs
  4. 20
      src/Avalonia.Input/PointerEventArgs.cs
  5. 5
      src/Avalonia.Input/PointerWheelEventArgs.cs
  6. 6
      src/Avalonia.Input/TouchDevice.cs
  7. 14
      tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs
  8. 6
      tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs

4
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>

62
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);
}
}
}
}

57
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);

20
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;
}

5
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;
}

6
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));
}
}

14
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));
}
}

6
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
{

Loading…
Cancel
Save