Browse Source

Add more gesture recognizer sample pages to ControlCatalog (#20983)

* Added Gestures Samples

* More changes
pull/20994/head
Javier Suárez 6 months ago
committed by GitHub
parent
commit
ffa6406b64
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 215
      samples/ControlCatalog/Pages/GesturePage.cs
  2. 173
      samples/ControlCatalog/Pages/GesturePage.xaml
  3. 59
      samples/ControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml
  4. 33
      samples/ControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml.cs
  5. 51
      samples/ControlCatalog/Pages/Gestures/GesturePinchZoomPage.xaml
  6. 134
      samples/ControlCatalog/Pages/Gestures/GesturePinchZoomPage.xaml.cs
  7. 113
      samples/ControlCatalog/Pages/Gestures/GesturePullPage.xaml
  8. 100
      samples/ControlCatalog/Pages/Gestures/GesturePullPage.xaml.cs
  9. 123
      samples/ControlCatalog/Pages/Gestures/GestureSwipePage.xaml
  10. 125
      samples/ControlCatalog/Pages/Gestures/GestureSwipePage.xaml.cs

215
samples/ControlCatalog/Pages/GesturePage.cs

@ -1,214 +1,39 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Rendering.Composition;
using Avalonia.Utilities;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public partial class GesturePage : UserControl
{
private bool _isInit;
private double _currentScale;
public GesturePage()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
private static readonly (string Group, string Title, string Description, Func<UserControl> Factory)[] Demos =
{
base.OnAttachedToVisualTree(e);
if (_isInit)
{
return;
}
_isInit = true;
SetPullHandlers(TopPullZone, false);
SetPullHandlers(BottomPullZone, true);
SetPullHandlers(RightPullZone, true);
SetPullHandlers(LeftPullZone, false);
var image = PinchImage;
SetPinchHandlers(image);
var reset = ResetButton;
("Touch / Pen", "Pull Gesture",
"Press and drag from colored border zones. A green ball tracks the pull delta and springs back on release.",
() => new GesturePullPage()),
reset.Click += (_, _) =>
{
var compositionVisual = ElementComposition.GetElementVisual(image);
("Multi Touch", "Pinch / Zoom",
"Pinch to scale an image using composition visuals. Scroll to pan when zoomed in.",
() => new GesturePinchZoomPage()),
if (compositionVisual != null)
{
_currentScale = 1;
compositionVisual.Scale = new(1, 1, 1);
compositionVisual.Offset = default;
image.InvalidateMeasure();
}
};
("Multi Touch", "Pinch / Rotation",
"Pinch to rotate a rectangle. The Angle property from the pinch event drives a RotateTransform.",
() => new GesturePinchRotationPage()),
RotationGesture.AddHandler(InputElement.PinchEvent, (s, e) =>
{
AngleSlider.Value = e.Angle;
});
}
("Touch / Pen / Mouse", "Swipe Gesture",
"Swipe horizontally or vertically. Configure direction, threshold, and mouse support. Shows live delta, velocity, and direction.",
() => new GestureSwipePage()),
};
private void SetPinchHandlers(Control? control)
public GesturePage()
{
if (control == null)
{
return;
}
_currentScale = 1;
Vector3D currentOffset = default;
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(InputElement.PinchEvent, (s, e) =>
{
InitComposition(control!);
if (compositionVisual != null)
{
var scale = _currentScale * (float)e.Scale;
if (scale <= 1)
{
scale = 1;
compositionVisual.Offset = default;
}
compositionVisual.Scale = new(scale, scale, 1);
e.Handled = true;
}
});
control.AddHandler(InputElement.PinchEndedEvent, (s, e) =>
{
InitComposition(control!);
if (compositionVisual != null)
{
_currentScale = compositionVisual.Scale.X;
}
});
control.AddHandler(InputElement.ScrollGestureEvent, (s, e) =>
{
InitComposition(control!);
if (compositionVisual != null && _currentScale != 1)
{
currentOffset += new Vector3D(e.Delta.X, e.Delta.Y, 0);
var currentSize = control.Bounds.Size * _currentScale;
currentOffset = new Vector3D(Math.Clamp(currentOffset.X, 0, currentSize.Width - control.Bounds.Width),
(float)Math.Clamp(currentOffset.Y, 0, currentSize.Height - control.Bounds.Height),
0);
compositionVisual.Offset = currentOffset * -1;
e.Handled = true;
}
});
InitializeComponent();
Loaded += OnLoaded;
}
private void SetPullHandlers(Control control, bool inverse)
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
var ball = control.FindLogicalDescendantOfType<Border>();
Vector3D 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(InputElement.PullGestureEvent, (s, e) =>
{
Vector3D center = new((float)control.Bounds.Center.X, (float)control.Bounds.Center.Y, 0);
InitComposition(ball!);
if (ballCompositionVisual != null)
{
ballCompositionVisual.Offset = defaultOffset + new Vector3D(e.Delta.X * 0.4f, e.Delta.Y * 0.4f, 0) * (inverse ? -1 : 1);
e.Handled = true;
}
});
control.AddHandler(InputElement.PullGestureEndedEvent, (s, e) =>
{
InitComposition(ball!);
if (ballCompositionVisual != null)
{
ballCompositionVisual.Offset = defaultOffset;
}
});
void InitComposition(Control control)
{
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;
}
}
await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null);
}
}
}

173
samples/ControlCatalog/Pages/GesturePage.xaml

@ -1,170 +1,11 @@
<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">
<TabControl>
<TabItem>
<TabItem.Header>
<TextBlock FontWeight="Bold"
FontSize="18"
Margin="5">Pull Gexture (Touch / Pen)</TextBlock>
</TabItem.Header>
<StackPanel>
<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>
</StackPanel>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock FontWeight="Bold"
FontSize="18"
Margin="5">Pinch/Zoom Gexture (Multi Touch)</TextBlock>
</TabItem.Header>
<StackPanel>
<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>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock FontWeight="Bold"
FontSize="18"
Margin="5">Pinch/Rotation Gexture (Multi Touch)</TextBlock>
</TabItem.Header>
<DockPanel>
<Slider Minimum="0"
Maximum="360"
DockPanel.Dock="Bottom"
x:Name="AngleSlider"
/>
<Panel x:Name="RotationGesture">
<Panel.GestureRecognizers>
<PinchGestureRecognizer/>
</Panel.GestureRecognizers>
<Border BorderThickness="1.5" BorderBrush="LawnGreen"/>
<Panel HorizontalAlignment="Center"
Width="100"
Height="100">
<Panel.RenderTransform>
<RotateTransform Angle="{Binding #AngleSlider.Value}"/>
</Panel.RenderTransform>
<Rectangle Fill="SkyBlue"/>
<Rectangle HorizontalAlignment="Center"
VerticalAlignment="Top"
Fill="Yellow"
Width="5"
Height="35"/>
</Panel>
<TextBlock Text="{Binding #AngleSlider.Value, StringFormat=0°}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
FontWeight="DemiBold"
FontSize="20"
/>
</Panel>
</DockPanel>
</TabItem>
</TabControl>
<NavigationPage x:Name="SampleNav">
<NavigationPage.Styles>
<Style Selector="NavigationPage#SampleNav /template/ Border#PART_NavigationBar">
<Setter Property="Background" Value="Transparent" />
</Style>
</NavigationPage.Styles>
</NavigationPage>
</UserControl>

59
samples/ControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml

@ -0,0 +1,59 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.GesturePinchRotationPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="260">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="How It Works" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="A PinchGestureRecognizer raises Pinch events that include an Angle property. The angle is applied to a RotateTransform on the inner panel." />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="Use the slider below to manually adjust the rotation angle, or use a multi-touch pinch/twist gesture." />
<Separator />
<TextBlock Text="API" FontWeight="SemiBold" FontSize="13" />
<StackPanel Spacing="4">
<TextBlock FontSize="13" FontFamily="Cascadia Code, Consolas, Monospace"
Text="PinchEventArgs.Angle" />
<TextBlock FontSize="12" TextWrapping="Wrap" Opacity="0.7"
Text="The rotation angle in degrees from the pinch gesture." />
</StackPanel>
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1" Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<DockPanel Margin="12">
<Slider Minimum="0"
Maximum="360"
DockPanel.Dock="Bottom"
x:Name="AngleSlider" />
<Panel x:Name="RotationGesture">
<Panel.GestureRecognizers>
<PinchGestureRecognizer/>
</Panel.GestureRecognizers>
<Border BorderThickness="1.5" BorderBrush="LawnGreen"/>
<Panel HorizontalAlignment="Center"
Width="100"
Height="100">
<Panel.RenderTransform>
<RotateTransform Angle="{Binding #AngleSlider.Value}"/>
</Panel.RenderTransform>
<Rectangle Fill="SkyBlue"/>
<Rectangle HorizontalAlignment="Center"
VerticalAlignment="Top"
Fill="Yellow"
Width="5"
Height="35"/>
</Panel>
<TextBlock Text="{Binding #AngleSlider.Value, StringFormat={}{0:F0}\°}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
FontWeight="DemiBold"
FontSize="20" />
</Panel>
</DockPanel>
</DockPanel>
</UserControl>

33
samples/ControlCatalog/Pages/Gestures/GesturePinchRotationPage.xaml.cs

@ -0,0 +1,33 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
namespace ControlCatalog.Pages
{
public partial class GesturePinchRotationPage : UserControl
{
private bool _isInit;
public GesturePinchRotationPage()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (_isInit)
{
return;
}
_isInit = true;
RotationGesture.AddHandler(InputElement.PinchEvent, (s, e) =>
{
AngleSlider.Value = e.Angle;
});
}
}
}

51
samples/ControlCatalog/Pages/Gestures/GesturePinchZoomPage.xaml

@ -0,0 +1,51 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.GesturePinchZoomPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="260">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="How It Works" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="Attach a PinchGestureRecognizer and a ScrollGestureRecognizer to an image. Pinch to zoom in and out, then scroll to pan when zoomed." />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="The composition visual's Scale and Offset are updated directly for smooth performance." />
<Separator />
<TextBlock Text="API" FontWeight="SemiBold" FontSize="13" />
<StackPanel Spacing="4">
<TextBlock FontSize="13" FontFamily="Cascadia Code, Consolas, Monospace"
Text="PinchGestureRecognizer" />
<TextBlock FontSize="12" TextWrapping="Wrap" Opacity="0.7"
Text="Raises Pinch (with Scale) and PinchEnded events." />
</StackPanel>
<StackPanel Spacing="4">
<TextBlock FontSize="13" FontFamily="Cascadia Code, Consolas, Monospace"
Text="ScrollGestureRecognizer" />
<TextBlock FontSize="12" TextWrapping="Wrap" Opacity="0.7"
Text="CanHorizontallyScroll / CanVerticallyScroll enable panning." />
</StackPanel>
<Separator />
<Button x:Name="ResetButton"
Content="Reset"
HorizontalAlignment="Stretch" />
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1" Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Border Margin="12" ClipToBounds="True">
<Image Stretch="UniformToFill"
Name="PinchImage"
Source="/Assets/delicate-arch-896885_640.jpg">
<Image.GestureRecognizers>
<PinchGestureRecognizer/>
<ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True"/>
</Image.GestureRecognizers>
</Image>
</Border>
</DockPanel>
</UserControl>

134
samples/ControlCatalog/Pages/Gestures/GesturePinchZoomPage.xaml.cs

@ -0,0 +1,134 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Rendering.Composition;
namespace ControlCatalog.Pages
{
public partial class GesturePinchZoomPage : UserControl
{
private bool _isInit;
private double _currentScale;
public GesturePinchZoomPage()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (_isInit)
{
return;
}
_isInit = true;
var image = PinchImage;
SetPinchHandlers(image);
ResetButton.Click += (_, _) =>
{
var compositionVisual = ElementComposition.GetElementVisual(image);
if (compositionVisual != null)
{
_currentScale = 1;
compositionVisual.Scale = new(1, 1, 1);
compositionVisual.Offset = default;
image.InvalidateMeasure();
}
};
}
private void SetPinchHandlers(Control? control)
{
if (control == null)
{
return;
}
_currentScale = 1;
Vector3D currentOffset = default;
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(InputElement.PinchEvent, (s, e) =>
{
InitComposition(control!);
if (compositionVisual != null)
{
var scale = _currentScale * (float)e.Scale;
if (scale <= 1)
{
scale = 1;
compositionVisual.Offset = default;
}
compositionVisual.Scale = new(scale, scale, 1);
e.Handled = true;
}
});
control.AddHandler(InputElement.PinchEndedEvent, (s, e) =>
{
InitComposition(control!);
if (compositionVisual != null)
{
_currentScale = compositionVisual.Scale.X;
}
});
control.AddHandler(InputElement.ScrollGestureEvent, (s, e) =>
{
InitComposition(control!);
if (compositionVisual != null && _currentScale != 1)
{
currentOffset += new Vector3D(e.Delta.X, e.Delta.Y, 0);
var currentSize = control.Bounds.Size * _currentScale;
currentOffset = new Vector3D(Math.Clamp(currentOffset.X, 0, currentSize.Width - control.Bounds.Width),
(float)Math.Clamp(currentOffset.Y, 0, currentSize.Height - control.Bounds.Height),
0);
compositionVisual.Offset = currentOffset * -1;
e.Handled = true;
}
});
}
}
}

113
samples/ControlCatalog/Pages/Gestures/GesturePullPage.xaml

@ -0,0 +1,113 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.GesturePullPage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="260">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="How It Works" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="Attach a PullGestureRecognizer to any control. Press and drag in the specified pull direction to fire PullGesture events with a delta vector." />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="When the pointer is released, PullGestureEnded fires so the control can spring back." />
<Separator />
<TextBlock Text="API" FontWeight="SemiBold" FontSize="13" />
<StackPanel Spacing="4">
<TextBlock FontSize="13" FontFamily="Cascadia Code, Consolas, Monospace"
Text="PullDirection" />
<TextBlock FontSize="12" TextWrapping="Wrap" Opacity="0.7"
Text="TopToBottom, BottomToTop, LeftToRight, or RightToLeft." />
</StackPanel>
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1" Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Border Margin="12">
<DockPanel HorizontalAlignment="Stretch"
ClipToBounds="True"
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>
</DockPanel>
</UserControl>

100
samples/ControlCatalog/Pages/Gestures/GesturePullPage.xaml.cs

@ -0,0 +1,100 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Rendering.Composition;
namespace ControlCatalog.Pages
{
public partial class GesturePullPage : UserControl
{
private bool _isInit;
public GesturePullPage()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (_isInit)
{
return;
}
_isInit = true;
SetPullHandlers(TopPullZone, false);
SetPullHandlers(BottomPullZone, true);
SetPullHandlers(RightPullZone, true);
SetPullHandlers(LeftPullZone, false);
}
private void SetPullHandlers(Control control, bool inverse)
{
var ball = control.FindLogicalDescendantOfType<Border>();
Vector3D 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(InputElement.PullGestureEvent, (s, e) =>
{
InitComposition(ball!);
if (ballCompositionVisual != null)
{
ballCompositionVisual.Offset = defaultOffset + new Vector3D(e.Delta.X * 0.4f, e.Delta.Y * 0.4f, 0) * (inverse ? -1 : 1);
e.Handled = true;
}
});
control.AddHandler(InputElement.PullGestureEndedEvent, (s, e) =>
{
InitComposition(ball!);
if (ballCompositionVisual != null)
{
ballCompositionVisual.Offset = defaultOffset;
}
});
void InitComposition(Control control)
{
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;
}
}
}
}
}

123
samples/ControlCatalog/Pages/Gestures/GestureSwipePage.xaml

@ -0,0 +1,123 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.GestureSwipePage">
<DockPanel>
<ScrollViewer DockPanel.Dock="Right" Width="260">
<StackPanel Margin="12" Spacing="8">
<TextBlock Text="How It Works" FontSize="16" FontWeight="SemiBold"
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="Attach a SwipeGestureRecognizer to any control. Drag horizontally or vertically to fire SwipeGesture events with a delta vector and velocity." />
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7"
Text="When the pointer is released, SwipeGestureEnded fires with the final velocity so you can apply fling logic." />
<Separator />
<TextBlock Text="Options" FontWeight="SemiBold" FontSize="14" />
<CheckBox x:Name="HorizontalCheck"
Content="CanHorizontallySwipe"
IsChecked="True"
IsCheckedChanged="OnDirectionChanged" />
<CheckBox x:Name="VerticalCheck"
Content="CanVerticallySwipe"
IsChecked="True"
IsCheckedChanged="OnDirectionChanged" />
<CheckBox x:Name="MouseCheck"
Content="IsMouseEnabled"
IsChecked="True"
IsCheckedChanged="OnMouseEnabledChanged" />
<Separator />
<TextBlock Text="Threshold" FontWeight="SemiBold" FontSize="13" />
<DockPanel>
<TextBlock x:Name="ThresholdText"
DockPanel.Dock="Right"
Text="0"
Width="30"
VerticalAlignment="Center" />
<Slider x:Name="ThresholdSlider"
Minimum="0"
Maximum="50"
Value="0"
ValueChanged="OnThresholdChanged" />
</DockPanel>
<TextBlock FontSize="11" TextWrapping="Wrap" Opacity="0.5"
Text="0 = platform default. Higher values require more movement before a swipe starts." />
<Separator />
<TextBlock Text="Status" FontWeight="SemiBold" FontSize="14" />
<TextBlock x:Name="DirectionText"
Text="Direction: (none)"
Opacity="0.7" />
<TextBlock x:Name="DeltaText"
Text="Delta: 0, 0"
Opacity="0.7" />
<TextBlock x:Name="VelocityText"
Text="Velocity: 0, 0"
Opacity="0.7" />
<Separator />
<TextBlock Text="API" FontWeight="SemiBold" FontSize="13" />
<StackPanel Spacing="4">
<TextBlock FontSize="13" FontFamily="Cascadia Code, Consolas, Monospace"
Text="CanHorizontallySwipe" />
<TextBlock FontSize="12" TextWrapping="Wrap" Opacity="0.7"
Text="Enable horizontal swipe tracking." />
</StackPanel>
<StackPanel Spacing="4">
<TextBlock FontSize="13" FontFamily="Cascadia Code, Consolas, Monospace"
Text="CanVerticallySwipe" />
<TextBlock FontSize="12" TextWrapping="Wrap" Opacity="0.7"
Text="Enable vertical swipe tracking." />
</StackPanel>
<StackPanel Spacing="4">
<TextBlock FontSize="13" FontFamily="Cascadia Code, Consolas, Monospace"
Text="Threshold" />
<TextBlock FontSize="12" TextWrapping="Wrap" Opacity="0.7"
Text="Minimum pixel distance before a swipe is recognized. 0 uses platform default." />
</StackPanel>
<StackPanel Spacing="4">
<TextBlock FontSize="13" FontFamily="Cascadia Code, Consolas, Monospace"
Text="IsMouseEnabled" />
<TextBlock FontSize="12" TextWrapping="Wrap" Opacity="0.7"
Text="Allow mouse pointer to trigger swipes (touch/pen always enabled)." />
</StackPanel>
</StackPanel>
</ScrollViewer>
<Border DockPanel.Dock="Right" Width="1" Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Border Margin="12"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1"
CornerRadius="6"
ClipToBounds="True">
<Panel x:Name="SwipePanel" Background="Transparent">
<Panel.GestureRecognizers>
<SwipeGestureRecognizer x:Name="SwipeRecognizer"
CanHorizontallySwipe="True"
CanVerticallySwipe="True"
IsMouseEnabled="True" />
</Panel.GestureRecognizers>
<Border x:Name="SwipeIndicator"
Width="80" Height="80"
CornerRadius="40"
Background="{DynamicResource SystemControlHighlightAccentBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock x:Name="SwipeDirectionLabel"
Text="Swipe here"
FontSize="11"
FontWeight="SemiBold"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</Panel>
</Border>
</DockPanel>
</UserControl>

125
samples/ControlCatalog/Pages/Gestures/GestureSwipePage.xaml.cs

@ -0,0 +1,125 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Controls.Primitives;
using Avalonia.Rendering.Composition;
namespace ControlCatalog.Pages
{
public partial class GestureSwipePage : UserControl
{
private bool _isInit;
private CompositionVisual? _indicatorVisual;
private Vector3D _swipeDelta;
public GestureSwipePage()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (_isInit)
{
return;
}
_isInit = true;
SwipePanel.AddHandler(InputElement.SwipeGestureEvent, OnSwipeGesture);
SwipePanel.AddHandler(InputElement.SwipeGestureEndedEvent, OnSwipeGestureEnded);
}
private void EnsureVisual()
{
if (_indicatorVisual != null)
{
return;
}
_indicatorVisual = ElementComposition.GetElementVisual(SwipeIndicator);
if (_indicatorVisual != null)
{
var offsetAnimation = _indicatorVisual.Compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150);
var implicitAnimations = _indicatorVisual.Compositor.CreateImplicitAnimationCollection();
implicitAnimations["Offset"] = offsetAnimation;
_indicatorVisual.ImplicitAnimations = implicitAnimations;
}
}
private void OnSwipeGesture(object? sender, SwipeGestureEventArgs e)
{
EnsureVisual();
if (_indicatorVisual != null)
{
_swipeDelta += new Vector3D(-e.Delta.X, -e.Delta.Y, 0);
_indicatorVisual.Offset += new Vector3D(-e.Delta.X, -e.Delta.Y, 0);
}
DirectionText.Text = $"Direction: {e.SwipeDirection}";
DeltaText.Text = $"Delta: {e.Delta.X:F1}, {e.Delta.Y:F1}";
VelocityText.Text = $"Velocity: {e.Velocity.X:F0}, {e.Velocity.Y:F0}";
SwipeDirectionLabel.Text = e.SwipeDirection.ToString();
SwipeDirectionLabel.Opacity = 1;
e.Handled = true;
}
private void OnSwipeGestureEnded(object? sender, SwipeGestureEndedEventArgs e)
{
if (_indicatorVisual != null)
{
_indicatorVisual.Offset -= _swipeDelta;
}
_swipeDelta = default;
VelocityText.Text = $"Velocity: {e.Velocity.X:F0}, {e.Velocity.Y:F0} (ended)";
SwipeDirectionLabel.Text = "Swipe here";
SwipeDirectionLabel.Opacity = 0.5;
}
private void OnDirectionChanged(object? sender, RoutedEventArgs e)
{
if (SwipeRecognizer == null)
{
return;
}
SwipeRecognizer.CanHorizontallySwipe = HorizontalCheck.IsChecked == true;
SwipeRecognizer.CanVerticallySwipe = VerticalCheck.IsChecked == true;
}
private void OnMouseEnabledChanged(object? sender, RoutedEventArgs e)
{
if (SwipeRecognizer == null)
{
return;
}
SwipeRecognizer.IsMouseEnabled = MouseCheck.IsChecked == true;
}
private void OnThresholdChanged(object? sender, RangeBaseValueChangedEventArgs e)
{
if (SwipeRecognizer == null || ThresholdText == null)
{
return;
}
SwipeRecognizer.Threshold = e.NewValue;
ThresholdText.Text = $"{e.NewValue:F0}";
}
}
}
Loading…
Cancel
Save