committed by
GitHub
119 changed files with 2636 additions and 1390 deletions
@ -1,5 +0,0 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup> |
|||
<PackageReference Include="JetBrains.Annotations" Version="10.3.0" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -1,5 +1,5 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup> |
|||
<ItemGroup Condition="'$(TargetFramework)' != 'net6'"> |
|||
<PackageReference Include="System.Memory" Version="4.5.3" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
|
|||
@ -0,0 +1,212 @@ |
|||
using System; |
|||
using System.Numerics; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Input; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.Rendering.Composition; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class GesturePage : UserControl |
|||
{ |
|||
private bool _isInit; |
|||
private float _currentScale; |
|||
|
|||
public GesturePage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
base.OnAttachedToVisualTree(e); |
|||
|
|||
if(_isInit) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_isInit = true; |
|||
|
|||
SetPullHandlers(this.Find<Border>("TopPullZone"), false); |
|||
SetPullHandlers(this.Find<Border>("BottomPullZone"), true); |
|||
SetPullHandlers(this.Find<Border>("RightPullZone"), true); |
|||
SetPullHandlers(this.Find<Border>("LeftPullZone"), false); |
|||
|
|||
var image = this.Find<Image>("PinchImage"); |
|||
SetPinchHandlers(image); |
|||
|
|||
var reset = this.Find<Button>("ResetButton"); |
|||
|
|||
reset!.Click += (s, e) => |
|||
{ |
|||
var compositionVisual = ElementComposition.GetElementVisual(image); |
|||
|
|||
if(compositionVisual!= null) |
|||
{ |
|||
_currentScale = 1; |
|||
compositionVisual.Scale = new Vector3(1,1,1); |
|||
image.InvalidateMeasure(); |
|||
} |
|||
}; |
|||
|
|||
} |
|||
|
|||
private void SetPinchHandlers(Control? control) |
|||
{ |
|||
if (control == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_currentScale = 1; |
|||
Vector3 currentOffset = default; |
|||
bool isZooming = false; |
|||
|
|||
CompositionVisual? compositionVisual = null; |
|||
|
|||
void InitComposition(Control visual) |
|||
{ |
|||
if (compositionVisual != null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
compositionVisual = ElementComposition.GetElementVisual(visual); |
|||
} |
|||
|
|||
control.LayoutUpdated += (s, e) => |
|||
{ |
|||
InitComposition(control!); |
|||
if (compositionVisual != null) |
|||
{ |
|||
compositionVisual.Scale = new(_currentScale, _currentScale, 1); |
|||
|
|||
if(currentOffset == default) |
|||
{ |
|||
currentOffset = compositionVisual.Offset; |
|||
} |
|||
} |
|||
}; |
|||
|
|||
control.AddHandler(Gestures.PinchEvent, (s, e) => |
|||
{ |
|||
InitComposition(control!); |
|||
|
|||
isZooming = true; |
|||
|
|||
if(compositionVisual != null) |
|||
{ |
|||
var scale = _currentScale * (float)e.Scale; |
|||
|
|||
compositionVisual.Scale = new(scale, scale, 1); |
|||
} |
|||
}); |
|||
|
|||
control.AddHandler(Gestures.PinchEndedEvent, (s, e) => |
|||
{ |
|||
InitComposition(control!); |
|||
|
|||
isZooming = false; |
|||
|
|||
if (compositionVisual != null) |
|||
{ |
|||
_currentScale = compositionVisual.Scale.X; |
|||
} |
|||
}); |
|||
|
|||
control.AddHandler(Gestures.ScrollGestureEvent, (s, e) => |
|||
{ |
|||
InitComposition(control!); |
|||
|
|||
if (compositionVisual != null && !isZooming) |
|||
{ |
|||
currentOffset -= new Vector3((float)e.Delta.X, (float)e.Delta.Y, 0); |
|||
|
|||
compositionVisual.Offset = currentOffset; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private void SetPullHandlers(Control? control, bool inverse) |
|||
{ |
|||
if (control == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var ball = control.FindLogicalDescendantOfType<Border>(); |
|||
|
|||
Vector3 defaultOffset = default; |
|||
|
|||
CompositionVisual? ballCompositionVisual = null; |
|||
|
|||
if (ball != null) |
|||
{ |
|||
InitComposition(ball); |
|||
} |
|||
else |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
control.LayoutUpdated += (s, e) => |
|||
{ |
|||
InitComposition(ball!); |
|||
if (ballCompositionVisual != null) |
|||
{ |
|||
defaultOffset = ballCompositionVisual.Offset; |
|||
} |
|||
}; |
|||
|
|||
control.AddHandler(Gestures.PullGestureEvent, (s, e) => |
|||
{ |
|||
Vector3 center = new((float)control.Bounds.Center.X, (float)control.Bounds.Center.Y, 0); |
|||
InitComposition(ball!); |
|||
if (ballCompositionVisual != null) |
|||
{ |
|||
ballCompositionVisual.Offset = defaultOffset + new System.Numerics.Vector3((float)e.Delta.X * 0.4f, (float)e.Delta.Y * 0.4f, 0) * (inverse ? -1 : 1); |
|||
} |
|||
}); |
|||
|
|||
control.AddHandler(Gestures.PullGestureEndedEvent, (s, e) => |
|||
{ |
|||
InitComposition(ball!); |
|||
if (ballCompositionVisual != null) |
|||
{ |
|||
ballCompositionVisual.Offset = defaultOffset; |
|||
} |
|||
}); |
|||
|
|||
void InitComposition(Control control) |
|||
{ |
|||
if (ballCompositionVisual != null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
ballCompositionVisual = ElementComposition.GetElementVisual(ball); |
|||
|
|||
if (ballCompositionVisual != null) |
|||
{ |
|||
var offsetAnimation = ballCompositionVisual.Compositor.CreateVector3KeyFrameAnimation(); |
|||
offsetAnimation.Target = "Offset"; |
|||
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); |
|||
offsetAnimation.Duration = TimeSpan.FromMilliseconds(100); |
|||
|
|||
var implicitAnimations = ballCompositionVisual.Compositor.CreateImplicitAnimationCollection(); |
|||
implicitAnimations["Offset"] = offsetAnimation; |
|||
|
|||
ballCompositionVisual.ImplicitAnimations = implicitAnimations; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,117 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
d:DesignHeight="800" |
|||
d:DesignWidth="400" |
|||
x:Class="ControlCatalog.Pages.GesturePage"> |
|||
<StackPanel Orientation="Vertical" |
|||
Spacing="4"> |
|||
<TextBlock FontWeight="Bold" |
|||
FontSize="18" |
|||
Margin="5">Pull Gexture (Touch / Pen)</TextBlock> |
|||
<TextBlock Margin="5">Pull from colored rectangles</TextBlock> |
|||
<Border> |
|||
<DockPanel HorizontalAlignment="Stretch" |
|||
ClipToBounds="True" |
|||
Margin="5" |
|||
Height="200"> |
|||
<Border DockPanel.Dock="Top" |
|||
Margin="2" |
|||
Name="TopPullZone" |
|||
Background="Transparent" |
|||
BorderBrush="Red" |
|||
HorizontalAlignment="Stretch" |
|||
Height="50" |
|||
BorderThickness="1"> |
|||
<Border.GestureRecognizers> |
|||
<PullGestureRecognizer PullDirection="TopToBottom"/> |
|||
</Border.GestureRecognizers> |
|||
<Border Width="10" |
|||
Height="10" |
|||
HorizontalAlignment="Center" |
|||
VerticalAlignment="Center" |
|||
CornerRadius="5" |
|||
Name="TopBall" |
|||
Background="Green"/> |
|||
</Border> |
|||
<Border DockPanel.Dock="Bottom" |
|||
BorderBrush="Green" |
|||
Margin="2" |
|||
Background="Transparent" |
|||
Name="BottomPullZone" |
|||
HorizontalAlignment="Stretch" |
|||
Height="50" |
|||
BorderThickness="1"> |
|||
<Border.GestureRecognizers> |
|||
<PullGestureRecognizer PullDirection="BottomToTop"/> |
|||
</Border.GestureRecognizers> |
|||
<Border Width="10" |
|||
Name="BottomBall" |
|||
HorizontalAlignment="Center" |
|||
VerticalAlignment="Center" |
|||
Height="10" |
|||
CornerRadius="5" |
|||
Background="Green"/> |
|||
</Border> |
|||
<Border DockPanel.Dock="Right" |
|||
Margin="2" |
|||
Background="Transparent" |
|||
Name="RightPullZone" |
|||
BorderBrush="Blue" |
|||
HorizontalAlignment="Right" |
|||
VerticalAlignment="Stretch" |
|||
Width="50" |
|||
BorderThickness="1"> |
|||
<Border.GestureRecognizers> |
|||
<PullGestureRecognizer PullDirection="RightToLeft"/> |
|||
</Border.GestureRecognizers> |
|||
<Border Width="10" |
|||
Height="10" |
|||
Name="RightBall" |
|||
HorizontalAlignment="Center" |
|||
VerticalAlignment="Center" |
|||
CornerRadius="5" |
|||
Background="Green"/> |
|||
|
|||
</Border> |
|||
<Border DockPanel.Dock="Left" |
|||
Margin="2" |
|||
Background="Transparent" |
|||
Name="LeftPullZone" |
|||
BorderBrush="Orange" |
|||
HorizontalAlignment="Left" |
|||
VerticalAlignment="Stretch" |
|||
Width="50" |
|||
BorderThickness="1"> |
|||
<Border.GestureRecognizers> |
|||
<PullGestureRecognizer PullDirection="LeftToRight"/> |
|||
</Border.GestureRecognizers> |
|||
<Border Width="10" |
|||
Height="10" |
|||
Name="LeftBall" |
|||
HorizontalAlignment="Center" |
|||
VerticalAlignment="Center" |
|||
CornerRadius="5" |
|||
Background="Green"/> |
|||
|
|||
</Border> |
|||
</DockPanel> |
|||
</Border> |
|||
|
|||
<TextBlock FontWeight="Bold" |
|||
FontSize="18" |
|||
Margin="5">Pinch/Zoom Gexture (Multi Touch)</TextBlock> |
|||
<Border ClipToBounds="True"> |
|||
<Image Stretch="UniformToFill" |
|||
Margin="5" |
|||
Name="PinchImage" |
|||
Source="/Assets/delicate-arch-896885_640.jpg"> |
|||
<Image.GestureRecognizers> |
|||
<PinchGestureRecognizer/> |
|||
<ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True"/> |
|||
</Image.GestureRecognizers> |
|||
</Image> |
|||
</Border> |
|||
<Button HorizontalAlignment="Center" Name="ResetButton">Reset</Button> |
|||
</StackPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,26 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace Avalonia.Compatibility |
|||
{ |
|||
internal sealed class OperatingSystemEx |
|||
{ |
|||
#if NET6_0_OR_GREATER
|
|||
public static bool IsWindows() => OperatingSystem.IsWindows(); |
|||
public static bool IsMacOS() => OperatingSystem.IsMacOS(); |
|||
public static bool IsLinux() => OperatingSystem.IsLinux(); |
|||
public static bool IsAndroid() => OperatingSystem.IsAndroid(); |
|||
public static bool IsIOS() => OperatingSystem.IsIOS(); |
|||
public static bool IsBrowser() => OperatingSystem.IsBrowser(); |
|||
public static bool IsOSPlatform(string platform) => OperatingSystem.IsOSPlatform(platform); |
|||
#else
|
|||
public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); |
|||
public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); |
|||
public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); |
|||
public static bool IsAndroid() => IsOSPlatform("ANDROID"); |
|||
public static bool IsIOS() => IsOSPlatform("IOS"); |
|||
public static bool IsBrowser() => IsOSPlatform("BROWSER"); |
|||
public static bool IsOSPlatform(string platform) => RuntimeInformation.IsOSPlatform(OSPlatform.Create(platform)); |
|||
#endif
|
|||
} |
|||
} |
|||
@ -1,34 +0,0 @@ |
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// A stub of Code Contract's Contract class.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// It would be nice to use Code Contracts on Avalonia but last time I tried it slowed things
|
|||
/// to a crawl and often crashed. Instead use the same signature for checking preconditions
|
|||
/// in the hope that it might become usable at some point.
|
|||
/// </remarks>
|
|||
public static class Contract |
|||
{ |
|||
/// <summary>
|
|||
/// Specifies a precondition.
|
|||
/// </summary>
|
|||
/// <typeparam name="TException">
|
|||
/// The exception to throw if <paramref name="condition"/> is false.
|
|||
/// </typeparam>
|
|||
/// <param name="condition">The precondition.</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
[ContractAnnotation("condition:false=>stop")] |
|||
public static void Requires<TException>(bool condition) where TException : Exception, new() |
|||
{ |
|||
if (!condition) |
|||
{ |
|||
throw new TException(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,128 @@ |
|||
using Avalonia.Input.GestureRecognizers; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public class PinchGestureRecognizer : StyledElement, IGestureRecognizer |
|||
{ |
|||
private IInputElement? _target; |
|||
private IGestureRecognizerActionsDispatcher? _actions; |
|||
private float _initialDistance; |
|||
private IPointer? _firstContact; |
|||
private Point _firstPoint; |
|||
private IPointer? _secondContact; |
|||
private Point _secondPoint; |
|||
private Point _origin; |
|||
|
|||
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) |
|||
{ |
|||
_target = target; |
|||
_actions = actions; |
|||
} |
|||
|
|||
private void OnPointerPressed(object? sender, PointerPressedEventArgs e) |
|||
{ |
|||
PointerPressed(e); |
|||
} |
|||
|
|||
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) |
|||
{ |
|||
PointerReleased(e); |
|||
} |
|||
|
|||
public void PointerCaptureLost(IPointer pointer) |
|||
{ |
|||
RemoveContact(pointer); |
|||
} |
|||
|
|||
public void PointerMoved(PointerEventArgs e) |
|||
{ |
|||
if (_target != null && _target is Visual visual) |
|||
{ |
|||
if(_firstContact == e.Pointer) |
|||
{ |
|||
_firstPoint = e.GetPosition(visual); |
|||
} |
|||
else if (_secondContact == e.Pointer) |
|||
{ |
|||
_secondPoint = e.GetPosition(visual); |
|||
} |
|||
else |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (_firstContact != null && _secondContact != null) |
|||
{ |
|||
var distance = GetDistance(_firstPoint, _secondPoint); |
|||
|
|||
var scale = distance / _initialDistance; |
|||
|
|||
_target?.RaiseEvent(new PinchEventArgs(scale, _origin)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void PointerPressed(PointerPressedEventArgs e) |
|||
{ |
|||
if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) |
|||
{ |
|||
if (_firstContact == null) |
|||
{ |
|||
_firstContact = e.Pointer; |
|||
_firstPoint = e.GetPosition(visual); |
|||
|
|||
return; |
|||
} |
|||
else if (_secondContact == null && _firstContact != e.Pointer) |
|||
{ |
|||
_secondContact = e.Pointer; |
|||
_secondPoint = e.GetPosition(visual); |
|||
} |
|||
else |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (_firstContact != null && _secondContact != null) |
|||
{ |
|||
_initialDistance = GetDistance(_firstPoint, _secondPoint); |
|||
|
|||
_origin = new Point((_firstPoint.X + _secondPoint.X) / 2.0f, (_firstPoint.Y + _secondPoint.Y) / 2.0f); |
|||
|
|||
_actions!.Capture(_firstContact, this); |
|||
_actions!.Capture(_secondContact, this); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void PointerReleased(PointerReleasedEventArgs e) |
|||
{ |
|||
RemoveContact(e.Pointer); |
|||
} |
|||
|
|||
private void RemoveContact(IPointer pointer) |
|||
{ |
|||
if (_firstContact == pointer || _secondContact == pointer) |
|||
{ |
|||
if (_secondContact == pointer) |
|||
{ |
|||
_secondContact = null; |
|||
} |
|||
|
|||
if (_firstContact == pointer) |
|||
{ |
|||
_firstContact = _secondContact; |
|||
|
|||
_secondContact = null; |
|||
} |
|||
_target?.RaiseEvent(new PinchEndedEventArgs()); |
|||
} |
|||
} |
|||
|
|||
private float GetDistance(Point a, Point b) |
|||
{ |
|||
var length = _secondPoint - _firstPoint; |
|||
return (float)new Vector(length.X, length.Y).Length; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,375 @@ |
|||
// Code in this file is derived from
|
|||
// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/velocity_tracker.dart
|
|||
|
|||
using System; |
|||
using System.Diagnostics; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Input.GestureRecognizers |
|||
{ |
|||
// Possible enhancement: add Flutter's 'IOSScrollViewFlingVelocityTracker' and 'MacOSScrollViewFlingVelocityTracker'?
|
|||
|
|||
internal readonly record struct Velocity(Vector PixelsPerSecond) |
|||
{ |
|||
public Velocity ClampMagnitude(double minValue, double maxValue) |
|||
{ |
|||
Debug.Assert(minValue >= 0.0); |
|||
Debug.Assert(maxValue >= 0.0 && maxValue >= minValue); |
|||
double valueSquared = PixelsPerSecond.SquaredLength; |
|||
if (valueSquared > maxValue * maxValue) |
|||
{ |
|||
double length = PixelsPerSecond.Length; |
|||
return new Velocity(length != 0.0 ? (PixelsPerSecond / length) * maxValue : Vector.Zero); |
|||
// preventing double.NaN in Vector PixelsPerSecond is important -- if a NaN eventually gets into a
|
|||
// ScrollGestureEventArgs it results in runtime errors.
|
|||
} |
|||
if (valueSquared < minValue * minValue) |
|||
{ |
|||
double length = PixelsPerSecond.Length; |
|||
return new Velocity(length != 0.0 ? (PixelsPerSecond / length) * minValue : Vector.Zero); |
|||
} |
|||
return this; |
|||
} |
|||
} |
|||
|
|||
/// A two dimensional velocity estimate.
|
|||
///
|
|||
/// VelocityEstimates are computed by [VelocityTracker.getVelocityEstimate]. An
|
|||
/// estimate's [confidence] measures how well the velocity tracker's position
|
|||
/// data fit a straight line, [duration] is the time that elapsed between the
|
|||
/// first and last position sample used to compute the velocity, and [offset]
|
|||
/// is similarly the difference between the first and last positions.
|
|||
///
|
|||
/// See also:
|
|||
///
|
|||
/// * [VelocityTracker], which computes [VelocityEstimate]s.
|
|||
/// * [Velocity], which encapsulates (just) a velocity vector and provides some
|
|||
/// useful velocity operations.
|
|||
internal record VelocityEstimate(Vector PixelsPerSecond, double Confidence, TimeSpan Duration, Vector Offset); |
|||
|
|||
internal record struct PointAtTime(bool Valid, Vector Point, TimeSpan Time); |
|||
|
|||
/// Computes a pointer's velocity based on data from [PointerMoveEvent]s.
|
|||
///
|
|||
/// The input data is provided by calling [addPosition]. Adding data is cheap.
|
|||
///
|
|||
/// To obtain a velocity, call [getVelocity] or [getVelocityEstimate]. This will
|
|||
/// compute the velocity based on the data added so far. Only call these when
|
|||
/// you need to use the velocity, as they are comparatively expensive.
|
|||
///
|
|||
/// The quality of the velocity estimation will be better if more data points
|
|||
/// have been received.
|
|||
internal class VelocityTracker |
|||
{ |
|||
private const int AssumePointerMoveStoppedMilliseconds = 40; |
|||
private const int HistorySize = 20; |
|||
private const int HorizonMilliseconds = 100; |
|||
private const int MinSampleSize = 3; |
|||
private const double MinFlingVelocity = 50.0; // Logical pixels / second
|
|||
private const double MaxFlingVelocity = 8000.0; |
|||
|
|||
private readonly PointAtTime[] _samples = new PointAtTime[HistorySize]; |
|||
private int _index = 0; |
|||
|
|||
/// <summary>
|
|||
/// Adds a position as the given time to the tracker.
|
|||
/// </summary>
|
|||
/// <param name="time"></param>
|
|||
/// <param name="position"></param>
|
|||
public void AddPosition(TimeSpan time, Vector position) |
|||
{ |
|||
_index++; |
|||
if (_index == HistorySize) |
|||
{ |
|||
_index = 0; |
|||
} |
|||
_samples[_index] = new PointAtTime(true, position, time); |
|||
} |
|||
|
|||
/// Returns an estimate of the velocity of the object being tracked by the
|
|||
/// tracker given the current information available to the tracker.
|
|||
///
|
|||
/// Information is added using [addPosition].
|
|||
///
|
|||
/// Returns null if there is no data on which to base an estimate.
|
|||
protected virtual VelocityEstimate? GetVelocityEstimate() |
|||
{ |
|||
Span<double> x = stackalloc double[HistorySize]; |
|||
Span<double> y = stackalloc double[HistorySize]; |
|||
Span<double> w = stackalloc double[HistorySize]; |
|||
Span<double> time = stackalloc double[HistorySize]; |
|||
int sampleCount = 0; |
|||
int index = _index; |
|||
|
|||
var newestSample = _samples[index]; |
|||
if (!newestSample.Valid) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var previousSample = newestSample; |
|||
var oldestSample = newestSample; |
|||
|
|||
// Starting with the most recent PointAtTime sample, iterate backwards while
|
|||
// the samples represent continuous motion.
|
|||
do |
|||
{ |
|||
var sample = _samples[index]; |
|||
if (!sample.Valid) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
double age = (newestSample.Time - sample.Time).TotalMilliseconds; |
|||
double delta = Math.Abs((sample.Time - previousSample.Time).TotalMilliseconds); |
|||
previousSample = sample; |
|||
if (age > HorizonMilliseconds || delta > AssumePointerMoveStoppedMilliseconds) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
oldestSample = sample; |
|||
var position = sample.Point; |
|||
x[sampleCount] = position.X; |
|||
y[sampleCount] = position.Y; |
|||
w[sampleCount] = 1.0; |
|||
time[sampleCount] = -age; |
|||
index = (index == 0 ? HistorySize : index) - 1; |
|||
|
|||
sampleCount++; |
|||
} while (sampleCount < HistorySize); |
|||
|
|||
if (sampleCount >= MinSampleSize) |
|||
{ |
|||
var xFit = LeastSquaresSolver.Solve(2, time.Slice(0, sampleCount), x.Slice(0, sampleCount), w.Slice(0, sampleCount)); |
|||
if (xFit != null) |
|||
{ |
|||
var yFit = LeastSquaresSolver.Solve(2, time.Slice(0, sampleCount), y.Slice(0, sampleCount), w.Slice(0, sampleCount)); |
|||
if (yFit != null) |
|||
{ |
|||
return new VelocityEstimate( // convert from pixels/ms to pixels/s
|
|||
PixelsPerSecond: new Vector(xFit.Coefficients[1] * 1000, yFit.Coefficients[1] * 1000), |
|||
Confidence: xFit.Confidence * yFit.Confidence, |
|||
Duration: newestSample.Time - oldestSample.Time, |
|||
Offset: newestSample.Point - oldestSample.Point |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// We're unable to make a velocity estimate but we did have at least one
|
|||
// valid pointer position.
|
|||
return new VelocityEstimate( |
|||
PixelsPerSecond: Vector.Zero, |
|||
Confidence: 1.0, |
|||
Duration: newestSample.Time - oldestSample.Time, |
|||
Offset: newestSample.Point - oldestSample.Point |
|||
); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Computes the velocity of the pointer at the time of the last
|
|||
/// provided data point.
|
|||
///
|
|||
/// This can be expensive. Only call this when you need the velocity.
|
|||
///
|
|||
/// Returns [Velocity.zero] if there is no data from which to compute an
|
|||
/// estimate or if the estimated velocity is zero.///
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
internal Velocity GetVelocity() |
|||
{ |
|||
var estimate = GetVelocityEstimate(); |
|||
if (estimate == null || estimate.PixelsPerSecond.IsDefault) |
|||
{ |
|||
return new Velocity(Vector.Zero); |
|||
} |
|||
return new Velocity(estimate.PixelsPerSecond); |
|||
} |
|||
|
|||
internal virtual Velocity GetFlingVelocity() |
|||
{ |
|||
return GetVelocity().ClampMagnitude(MinFlingVelocity, MaxFlingVelocity); |
|||
} |
|||
} |
|||
|
|||
/// An nth degree polynomial fit to a dataset.
|
|||
internal class PolynomialFit |
|||
{ |
|||
/// Creates a polynomial fit of the given degree.
|
|||
///
|
|||
/// There are n + 1 coefficients in a fit of degree n.
|
|||
internal PolynomialFit(int degree) |
|||
{ |
|||
Coefficients = new double[degree + 1]; |
|||
} |
|||
|
|||
/// The polynomial coefficients of the fit.
|
|||
public double[] Coefficients { get; } |
|||
|
|||
/// An indicator of the quality of the fit.
|
|||
///
|
|||
/// Larger values indicate greater quality.
|
|||
public double Confidence { get; set; } |
|||
} |
|||
|
|||
internal class LeastSquaresSolver |
|||
{ |
|||
private const double PrecisionErrorTolerance = 1e-10; |
|||
|
|||
/// <summary>
|
|||
/// Fits a polynomial of the given degree to the data points.
|
|||
/// When there is not enough data to fit a curve null is returned.
|
|||
/// </summary>
|
|||
public static PolynomialFit? Solve(int degree, ReadOnlySpan<double> x, ReadOnlySpan<double> y, ReadOnlySpan<double> w) |
|||
{ |
|||
if (degree > x.Length) |
|||
{ |
|||
// Not enough data to fit a curve.
|
|||
return null; |
|||
} |
|||
|
|||
PolynomialFit result = new PolynomialFit(degree); |
|||
|
|||
// Shorthands for the purpose of notation equivalence to original C++ code.
|
|||
int m = x.Length; |
|||
int n = degree + 1; |
|||
|
|||
// Expand the X vector to a matrix A, pre-multiplied by the weights.
|
|||
_Matrix a = new _Matrix(m, stackalloc double[n * m]); |
|||
for (int h = 0; h < m; h += 1) |
|||
{ |
|||
a[0, h] = w[h]; |
|||
for (int i = 1; i < n; i += 1) |
|||
{ |
|||
a[i, h] = a[i - 1, h] * x[h]; |
|||
} |
|||
} |
|||
|
|||
// Apply the Gram-Schmidt process to A to obtain its QR decomposition.
|
|||
|
|||
// Orthonormal basis, column-major order Vector.
|
|||
_Matrix q = new _Matrix(m, stackalloc double[n * m]); |
|||
// Upper triangular matrix, row-major order.
|
|||
_Matrix r = new _Matrix(n, stackalloc double[n * n]); |
|||
for (int j = 0; j < n; j += 1) |
|||
{ |
|||
for (int h = 0; h < m; h += 1) |
|||
{ |
|||
q[j, h] = a[j, h]; |
|||
} |
|||
for (int i = 0; i < j; i += 1) |
|||
{ |
|||
double dot = Multiply(q.GetRow(j), q.GetRow(i)); |
|||
for (int h = 0; h < m; h += 1) |
|||
{ |
|||
q[j, h] = q[j, h] - dot * q[i, h]; |
|||
} |
|||
} |
|||
|
|||
double norm = Norm(q.GetRow(j)); |
|||
if (norm < PrecisionErrorTolerance) |
|||
{ |
|||
// Vectors are linearly dependent or zero so no solution.
|
|||
return null; |
|||
} |
|||
|
|||
double inverseNorm = 1.0 / norm; |
|||
for (int h = 0; h < m; h += 1) |
|||
{ |
|||
q[j, h] = q[j, h] * inverseNorm; |
|||
} |
|||
for (int i = 0; i < n; i += 1) |
|||
{ |
|||
r[j, i] = i < j ? 0.0 : Multiply(q.GetRow(j), a.GetRow(i)); |
|||
} |
|||
} |
|||
|
|||
// Solve R B = Qt W Y to find B. This is easy because R is upper triangular.
|
|||
// We just work from bottom-right to top-left calculating B's coefficients.
|
|||
// "m" isn't expected to be bigger than HistorySize=20, so allocation on stack is safe.
|
|||
Span<double> wy = stackalloc double[m]; |
|||
for (int h = 0; h < m; h += 1) |
|||
{ |
|||
wy[h] = y[h] * w[h]; |
|||
} |
|||
for (int i = n - 1; i >= 0; i -= 1) |
|||
{ |
|||
result.Coefficients[i] = Multiply(q.GetRow(i), wy); |
|||
for (int j = n - 1; j > i; j -= 1) |
|||
{ |
|||
result.Coefficients[i] -= r[i, j] * result.Coefficients[j]; |
|||
} |
|||
result.Coefficients[i] /= r[i, i]; |
|||
} |
|||
|
|||
// Calculate the coefficient of determination (confidence) as:
|
|||
// 1 - (sumSquaredError / sumSquaredTotal)
|
|||
// ...where sumSquaredError is the residual sum of squares (variance of the
|
|||
// error), and sumSquaredTotal is the total sum of squares (variance of the
|
|||
// data) where each has been weighted.
|
|||
double yMean = 0.0; |
|||
for (int h = 0; h < m; h += 1) |
|||
{ |
|||
yMean += y[h]; |
|||
} |
|||
yMean /= m; |
|||
|
|||
double sumSquaredError = 0.0; |
|||
double sumSquaredTotal = 0.0; |
|||
for (int h = 0; h < m; h += 1) |
|||
{ |
|||
double term = 1.0; |
|||
double err = y[h] - result.Coefficients[0]; |
|||
for (int i = 1; i < n; i += 1) |
|||
{ |
|||
term *= x[h]; |
|||
err -= term * result.Coefficients[i]; |
|||
} |
|||
sumSquaredError += w[h] * w[h] * err * err; |
|||
double v = y[h] - yMean; |
|||
sumSquaredTotal += w[h] * w[h] * v * v; |
|||
} |
|||
|
|||
result.Confidence = sumSquaredTotal <= PrecisionErrorTolerance ? 1.0 : |
|||
1.0 - (sumSquaredError / sumSquaredTotal); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private static double Multiply(Span<double> v1, Span<double> v2) |
|||
{ |
|||
double result = 0.0; |
|||
for (int i = 0; i < v1.Length; i += 1) |
|||
{ |
|||
result += v1[i] * v2[i]; |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
private static double Norm(Span<double> v) |
|||
{ |
|||
return Math.Sqrt(Multiply(v, v)); |
|||
} |
|||
|
|||
private readonly ref struct _Matrix |
|||
{ |
|||
private readonly int _columns; |
|||
private readonly Span<double> _elements; |
|||
|
|||
internal _Matrix(int cols, Span<double> elements) |
|||
{ |
|||
_columns = cols; |
|||
_elements = elements; |
|||
} |
|||
|
|||
public double this[int row, int col] |
|||
{ |
|||
get => _elements[row * _columns + col]; |
|||
set => _elements[row * _columns + col] = value; |
|||
} |
|||
|
|||
public Span<double> GetRow(int row) => _elements.Slice(row * _columns, _columns); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
using System; |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public class HoldingRoutedEventArgs : RoutedEventArgs |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the state of the <see cref="Gestures.HoldingEvent"/> event.
|
|||
/// </summary>
|
|||
public HoldingState HoldingState { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the location of the touch, mouse, or pen/stylus contact.
|
|||
/// </summary>
|
|||
public Point Position { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the pointer type of the input source.
|
|||
/// </summary>
|
|||
public PointerType PointerType { get; } |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="HoldingRoutedEventArgs"/> class.
|
|||
/// </summary>
|
|||
public HoldingRoutedEventArgs(HoldingState holdingState, Point position, PointerType pointerType) : base(Gestures.HoldingEvent) |
|||
{ |
|||
HoldingState = holdingState; |
|||
Position = position; |
|||
PointerType = pointerType; |
|||
} |
|||
} |
|||
|
|||
public enum HoldingState |
|||
{ |
|||
/// <summary>
|
|||
/// A single contact has been detected and a time threshold is crossed without the contact being lifted, another contact detected, or another gesture started.
|
|||
/// </summary>
|
|||
Started, |
|||
|
|||
/// <summary>
|
|||
/// The single contact is lifted.
|
|||
/// </summary>
|
|||
Completed, |
|||
|
|||
/// <summary>
|
|||
/// An additional contact is detected or a subsequent gesture (such as a slide) is detected.
|
|||
/// </summary>
|
|||
Cancelled, |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
public class PinchEventArgs : RoutedEventArgs |
|||
{ |
|||
public PinchEventArgs(double scale, Point scaleOrigin) : base(Gestures.PinchEvent) |
|||
{ |
|||
Scale = scale; |
|||
ScaleOrigin = scaleOrigin; |
|||
} |
|||
|
|||
public double Scale { get; } = 1; |
|||
|
|||
public Point ScaleOrigin { get; } |
|||
} |
|||
|
|||
public class PinchEndedEventArgs : RoutedEventArgs |
|||
{ |
|||
public PinchEndedEventArgs() : base(Gestures.PinchEndedEvent) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -1,244 +0,0 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Reflection; |
|||
using System.Linq; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
/// <summary>
|
|||
/// Base class for initializing platform-specific services for an <see cref="Application"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="TAppBuilder">The type of the AppBuilder class itself.</typeparam>
|
|||
public abstract class AppBuilderBase<TAppBuilder> where TAppBuilder : AppBuilderBase<TAppBuilder>, new() |
|||
{ |
|||
private static bool s_setupWasAlreadyCalled; |
|||
private Action? _optionsInitializers; |
|||
private Func<Application>? _appFactory; |
|||
private IApplicationLifetime? _lifetime; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the <see cref="IRuntimePlatform"/> instance.
|
|||
/// </summary>
|
|||
public IRuntimePlatform RuntimePlatform { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a method to call the initialize the runtime platform services (e. g. AssetLoader)
|
|||
/// </summary>
|
|||
public Action RuntimePlatformServicesInitializer { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="Application"/> instance being initialized.
|
|||
/// </summary>
|
|||
public Application? Instance { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the type of the Instance (even if it's not created yet)
|
|||
/// </summary>
|
|||
public Type? ApplicationType { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a method to call the initialize the windowing subsystem.
|
|||
/// </summary>
|
|||
public Action? WindowingSubsystemInitializer { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the name of the currently selected windowing subsystem.
|
|||
/// </summary>
|
|||
public string? WindowingSubsystemName { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a method to call the initialize the windowing subsystem.
|
|||
/// </summary>
|
|||
public Action? RenderingSubsystemInitializer { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the name of the currently selected rendering subsystem.
|
|||
/// </summary>
|
|||
public string? RenderingSubsystemName { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a method to call after the <see cref="Application"/> is setup.
|
|||
/// </summary>
|
|||
public Action<TAppBuilder> AfterSetupCallback { get; private set; } = builder => { }; |
|||
|
|||
|
|||
public Action<TAppBuilder> AfterPlatformServicesSetupCallback { get; private set; } = builder => { }; |
|||
|
|||
protected AppBuilderBase(IRuntimePlatform platform, Action<TAppBuilder> platformServices) |
|||
{ |
|||
RuntimePlatform = platform; |
|||
RuntimePlatformServicesInitializer = () => platformServices((TAppBuilder)this); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Begin configuring an <see cref="Application"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="TApp">The subclass of <see cref="Application"/> to configure.</typeparam>
|
|||
/// <returns>An <typeparamref name="TAppBuilder"/> instance.</returns>
|
|||
public static TAppBuilder Configure<TApp>() |
|||
where TApp : Application, new() |
|||
{ |
|||
return new TAppBuilder() |
|||
{ |
|||
ApplicationType = typeof(TApp), |
|||
// Needed for CoreRT compatibility
|
|||
_appFactory = () => new TApp() |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Begin configuring an <see cref="Application"/>.
|
|||
/// </summary>
|
|||
/// <param name="appFactory">Factory function for <typeparamref name="TApp"/>.</param>
|
|||
/// <typeparam name="TApp">The subclass of <see cref="Application"/> to configure.</typeparam>
|
|||
/// <remarks><paramref name="appFactory"/> is useful for passing of dependencies to <typeparamref name="TApp"/>.</remarks>
|
|||
/// <returns>An <typeparamref name="TAppBuilder"/> instance.</returns>
|
|||
public static TAppBuilder Configure<TApp>(Func<TApp> appFactory) |
|||
where TApp : Application |
|||
{ |
|||
return new TAppBuilder() |
|||
{ |
|||
ApplicationType = typeof(TApp), |
|||
_appFactory = appFactory |
|||
}; |
|||
} |
|||
|
|||
protected TAppBuilder Self => (TAppBuilder)this; |
|||
|
|||
public TAppBuilder AfterSetup(Action<TAppBuilder> callback) |
|||
{ |
|||
AfterSetupCallback = (Action<TAppBuilder>)Delegate.Combine(AfterSetupCallback, callback); |
|||
return Self; |
|||
} |
|||
|
|||
|
|||
public TAppBuilder AfterPlatformServicesSetup(Action<TAppBuilder> callback) |
|||
{ |
|||
AfterPlatformServicesSetupCallback = (Action<TAppBuilder>)Delegate.Combine(AfterPlatformServicesSetupCallback, callback); |
|||
return Self; |
|||
} |
|||
|
|||
public delegate void AppMainDelegate(Application app, string[] args); |
|||
|
|||
public void Start(AppMainDelegate main, string[] args) |
|||
{ |
|||
Setup(); |
|||
main(Instance!, args); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets up the platform-specific services for the application, but does not run it.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public TAppBuilder SetupWithoutStarting() |
|||
{ |
|||
Setup(); |
|||
return Self; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets up the platform-specific services for the application and initialized it with a particular lifetime, but does not run it.
|
|||
/// </summary>
|
|||
/// <param name="lifetime"></param>
|
|||
/// <returns></returns>
|
|||
public TAppBuilder SetupWithLifetime(IApplicationLifetime lifetime) |
|||
{ |
|||
_lifetime = lifetime; |
|||
Setup(); |
|||
return Self; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Specifies a windowing subsystem to use.
|
|||
/// </summary>
|
|||
/// <param name="initializer">The method to call to initialize the windowing subsystem.</param>
|
|||
/// <param name="name">The name of the windowing subsystem.</param>
|
|||
/// <returns>An <typeparamref name="TAppBuilder"/> instance.</returns>
|
|||
public TAppBuilder UseWindowingSubsystem(Action initializer, string name = "") |
|||
{ |
|||
WindowingSubsystemInitializer = initializer; |
|||
WindowingSubsystemName = name; |
|||
return Self; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Specifies a rendering subsystem to use.
|
|||
/// </summary>
|
|||
/// <param name="initializer">The method to call to initialize the rendering subsystem.</param>
|
|||
/// <param name="name">The name of the rendering subsystem.</param>
|
|||
/// <returns>An <typeparamref name="TAppBuilder"/> instance.</returns>
|
|||
public TAppBuilder UseRenderingSubsystem(Action initializer, string name = "") |
|||
{ |
|||
RenderingSubsystemInitializer = initializer; |
|||
RenderingSubsystemName = name; |
|||
return Self; |
|||
} |
|||
|
|||
protected virtual bool CheckSetup => true; |
|||
|
|||
/// <summary>
|
|||
/// Configures platform-specific options
|
|||
/// </summary>
|
|||
public TAppBuilder With<T>(T options) |
|||
{ |
|||
_optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToConstant(options); }; |
|||
return Self; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Configures platform-specific options
|
|||
/// </summary>
|
|||
public TAppBuilder With<T>(Func<T> options) |
|||
{ |
|||
_optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToFunc(options); }; |
|||
return Self; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets up the platform-specific services for the <see cref="Application"/>.
|
|||
/// </summary>
|
|||
private void Setup() |
|||
{ |
|||
if (RuntimePlatformServicesInitializer == null) |
|||
{ |
|||
throw new InvalidOperationException("No runtime platform services configured."); |
|||
} |
|||
|
|||
if (WindowingSubsystemInitializer == null) |
|||
{ |
|||
throw new InvalidOperationException("No windowing system configured."); |
|||
} |
|||
|
|||
if (RenderingSubsystemInitializer == null) |
|||
{ |
|||
throw new InvalidOperationException("No rendering system configured."); |
|||
} |
|||
|
|||
if (_appFactory == null) |
|||
{ |
|||
throw new InvalidOperationException("No Application factory configured."); |
|||
} |
|||
|
|||
if (s_setupWasAlreadyCalled && CheckSetup) |
|||
{ |
|||
throw new InvalidOperationException("Setup was already called on one of AppBuilder instances"); |
|||
} |
|||
|
|||
s_setupWasAlreadyCalled = true; |
|||
_optionsInitializers?.Invoke(); |
|||
RuntimePlatformServicesInitializer(); |
|||
RenderingSubsystemInitializer(); |
|||
WindowingSubsystemInitializer(); |
|||
AfterPlatformServicesSetupCallback(Self); |
|||
Instance = _appFactory(); |
|||
Instance.ApplicationLifetime = _lifetime; |
|||
AvaloniaLocator.CurrentMutable.BindToSelf(Instance); |
|||
Instance.RegisterServices(); |
|||
Instance.Initialize(); |
|||
AfterSetupCallback(Self); |
|||
Instance.OnFrameworkInitializationCompleted(); |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue