committed by
GitHub
10 changed files with 765 additions and 361 deletions
@ -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); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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> |
|||
|
|||
@ -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> |
|||
@ -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; |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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…
Reference in new issue