Explorar o código

avalonia: image editor added panning and zooming for mobile.

Kenric Nugteren hai 3 semanas
pai
achega
a05df3aa2c

+ 4 - 0
InABox.Avalonia/Components/ImageEditor/ImageEditor.axaml

@@ -4,6 +4,7 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
 			 xmlns:components="using:InABox.Avalonia.Components"
 			 xmlns:converters="using:InABox.Avalonia.Converters"
+			 xmlns:image="using:InABox.Avalonia.Components.ImageEditing"
 			 xmlns:system="using:System"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="InABox.Avalonia.Components.ImageEditor">
@@ -69,6 +70,9 @@
 				<RowDefinition Height="Auto"/>
 			</Grid.RowDefinitions>
 			<Canvas Name="OuterCanvas" Grid.Row="1" Background="White">
+				<Canvas.GestureRecognizers>
+					<image:PanAndZoomGestureRecognizer/>
+				</Canvas.GestureRecognizers>
 				<Canvas.Styles>
 					<Style Selector="Thumb">
 						<Setter Property="Template">

+ 44 - 5
InABox.Avalonia/Components/ImageEditor/ImageEditor.axaml.cs

@@ -200,6 +200,10 @@ public partial class ImageEditor : UserControl
     private Stack<IImageEditorObject> RedoStack = new();
 
     private double ScaleFactor = 1.0;
+    private double _originalScaleFactor = 1.0;
+
+    // Center of the image.
+    private Point ImageCenter = new();
 
     #endregion
 
@@ -229,13 +233,38 @@ public partial class ImageEditor : UserControl
         SetMode(Mode);
 
         OuterCanvas.LayoutUpdated += OuterCanvas_LayoutUpdated;
+        OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEndedEvent, OuterCanvas_PinchEnded);
+        OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEvent, OuterCanvas_Pinch);
+    }
+
+    private void OuterCanvas_PinchEnded(object? sender, PanAndZoomEndedEventArgs e)
+    {
+        _originalScaleFactor = ScaleFactor;
+    }
+
+    private void OuterCanvas_Pinch(object? sender, PanAndZoomEventArgs e)
+    {
+        // Convert Scale Origin to image coordinates (relative to center).
+        // Work out where this position will move to under the new scaling.
+        // Adjust so that these are the same.
+
+        var pos = (e.ScaleOrigin - e.Pan) - ImageCenter;
+        var contentMPos = pos / ScaleFactor;
+
+        ScaleFactor = _originalScaleFactor * e.Scale;
+
+        var scaledPos = ImageCenter + contentMPos * ScaleFactor;
+        var offset = scaledPos - e.ScaleOrigin;
+
+        ImageCenter -= offset;
+        UpdateCanvasPosition();
     }
 
     #region Layout
 
     private void OuterCanvas_LayoutUpdated(object? sender, EventArgs e)
     {
-        PositionImage();
+        // PositionImage();
     }
 
     protected override void OnLoaded(RoutedEventArgs e)
@@ -251,15 +280,25 @@ public partial class ImageEditor : UserControl
 
         var scaleFactor = Math.Min(canvasWidth / ImageWidth, canvasHeight / ImageHeight);
         ScaleFactor = scaleFactor;
+        _originalScaleFactor = ScaleFactor;
 
-        var imageWidth = ImageWidth * scaleFactor;
-        var imageHeight = ImageHeight * scaleFactor;
+        ImageCenter = new Point(
+            OuterCanvas.Bounds.Width / 2,
+            OuterCanvas.Bounds.Height / 2);
+
+        UpdateCanvasPosition();
+    }
+
+    private void UpdateCanvasPosition()
+    {
+        var imageWidth = ImageWidth * ScaleFactor;
+        var imageHeight = ImageHeight * ScaleFactor;
 
         ImageBorder.Width = imageWidth;
         ImageBorder.Height = imageHeight;
 
-        Canvas.SetLeft(ImageBorder, OuterCanvas.Bounds.Width / 2 - imageWidth / 2);
-        Canvas.SetTop(ImageBorder, OuterCanvas.Bounds.Height / 2 - imageHeight / 2);
+        Canvas.SetLeft(ImageBorder, ImageCenter.X - imageWidth / 2);
+        Canvas.SetTop(ImageBorder, ImageCenter.Y - imageHeight / 2);
 
         Canvas.RenderTransform = new ScaleTransform(ScaleFactor, ScaleFactor);
         Canvas.Width = ImageWidth;

+ 158 - 0
InABox.Avalonia/Components/ImageEditor/PanAndZoomGestureRecognizer.cs

@@ -0,0 +1,158 @@
+using Avalonia;
+using Avalonia.Input;
+using Avalonia.Input.GestureRecognizers;
+using Avalonia.Interactivity;
+
+namespace InABox.Avalonia.Components.ImageEditing;
+
+public class PanAndZoomEventArgs : RoutedEventArgs
+{
+    public PanAndZoomEventArgs(double scale, Point scaleOrigin, Vector pan) : base(PanAndZoomGestureRecognizer.PanAndZoomEvent)
+    {
+        Scale = scale;
+        ScaleOrigin = scaleOrigin;
+        Pan = pan;
+    }
+
+    public double Scale { get; } = 1;
+
+    public Point ScaleOrigin { get; }
+
+    public Vector Pan { get; set; }
+}
+
+public class PanAndZoomEndedEventArgs : RoutedEventArgs
+{
+    public PanAndZoomEndedEventArgs() : base(PanAndZoomGestureRecognizer.PanAndZoomEndedEvent)
+    {
+    }
+}
+
+// Adapted from the PinchGestureRecognizer built in to Avalonia, but with panning added.
+public class PanAndZoomGestureRecognizer : GestureRecognizer
+{
+    public static readonly RoutedEvent<PanAndZoomEventArgs> PanAndZoomEvent =
+        RoutedEvent.Register<PanAndZoomEventArgs>(
+            "PanAndZoom", RoutingStrategies.Bubble, typeof(PanAndZoomGestureRecognizer));
+
+    public static readonly RoutedEvent<PanAndZoomEndedEventArgs> PanAndZoomEndedEvent =
+        RoutedEvent.Register<PanAndZoomEndedEventArgs>(
+            "PanAndZoomEnded", RoutingStrategies.Bubble, typeof(PanAndZoomGestureRecognizer));
+
+    private float _initialDistance;
+    private IPointer? _firstContact;
+    private Point _firstPoint;
+    private IPointer? _secondContact;
+    private Point _secondPoint;
+    private Point _previousOrigin;
+
+    protected override void PointerCaptureLost(IPointer pointer)
+    {
+        RemoveContact(pointer);
+    }
+
+    protected override void PointerMoved(PointerEventArgs e)
+    {
+        if (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;
+
+                var origin = (_firstPoint + _secondPoint) / 2;
+
+                var pinchEventArgs = new PanAndZoomEventArgs(scale, origin, origin - _previousOrigin);
+                _previousOrigin = origin;
+
+                Target?.RaiseEvent(pinchEventArgs);
+                e.Handled = pinchEventArgs.Handled;
+                e.PreventGestureRecognition();
+            }
+        }
+    }
+
+    protected override void PointerPressed(PointerPressedEventArgs e)
+    {
+        if (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);
+
+                _previousOrigin = new Point((_firstPoint.X + _secondPoint.X) / 2.0f, (_firstPoint.Y + _secondPoint.Y) / 2.0f);
+
+                Capture(_firstContact);
+                Capture(_secondContact);
+                e.PreventGestureRecognition();
+            }
+        }
+    }
+
+    protected override void PointerReleased(PointerReleasedEventArgs e)
+    {
+        if(RemoveContact(e.Pointer))
+        {
+            e.PreventGestureRecognition();
+        }
+    }
+
+    private bool RemoveContact(IPointer pointer)
+    {
+        if (_firstContact == pointer || _secondContact == pointer)
+        {
+            if (_secondContact == pointer)
+            {
+                _secondContact = null;
+            }
+
+            if (_firstContact == pointer)
+            {
+                _firstContact = _secondContact;
+
+                _secondContact = null;
+            }
+
+            Target?.RaiseEvent(new PanAndZoomEndedEventArgs());
+            return true;
+        }
+        return false;
+    }
+
+    private static float GetDistance(Point a, Point b)
+    {
+        var length = b - a;
+        return (float)new Vector(length.X, length.Y).Length;
+    }
+}