Browse Source

feat(PinchGestureRecognizer): Report Pinch angle in degrees (#13244)

* feat(PinchGestureRecognizer): Report Pinch angle in degrees

* fix: Address review

* fix: Angle calculation

* fix: Prevent Gesture Recognition from other Recognition

* feat: Add Sample

* fix: make GetDistance static

* feat(PinchGestureRecognizer): AngleDelta
workgroupengineering 1 year ago
parent
commit
2852c149e8

+ 12 - 1
samples/ControlCatalog/Pages/GesturePage.cs

@@ -58,7 +58,18 @@ namespace ControlCatalog.Pages
                     image.InvalidateMeasure();
                 }
             };
-                
+
+
+
+            if (this.Find<Slider>("AngleSlider") is { } slider &&
+                this.Find<Panel>("RotationGesture") is { } rotationGesture
+               )
+            {
+                rotationGesture.AddHandler(Gestures.PinchEvent, (s, e) =>
+                {
+                    slider.Value = e.Angle;
+                });
+            }
         }
 
         private void SetPinchHandlers(Control? control)

+ 158 - 105
samples/ControlCatalog/Pages/GesturePage.xaml

@@ -4,114 +4,167 @@
              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"/>
+  <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>
-        <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"/>
+      </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>
-      </DockPanel>
-    </Border>
+        <Button HorizontalAlignment="Center" Name="ResetButton">Reset</Button>
+      </StackPanel>
+    </TabItem>
 
-    <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>
+    <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>
 </UserControl>

+ 32 - 7
src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs

@@ -10,6 +10,7 @@ namespace Avalonia.Input
         private IPointer? _secondContact;
         private Point _secondPoint;
         private Point _origin;
+        private double _previousAngle;
 
         protected override void PointerCaptureLost(IPointer pointer)
         {
@@ -20,7 +21,7 @@ namespace Avalonia.Input
         {
             if (Target is Visual visual)
             {
-                if(_firstContact == e.Pointer)
+                if (_firstContact == e.Pointer)
                 {
                     _firstPoint = e.GetPosition(visual);
                 }
@@ -39,10 +40,13 @@ namespace Avalonia.Input
 
                     var scale = distance / _initialDistance;
 
-                    var pinchEventArgs = new PinchEventArgs(scale, _origin);
-                    Target?.RaiseEvent(pinchEventArgs);
+                    var degree = GetAngleDegreeFromPoints(_firstPoint, _secondPoint);
 
+                    var pinchEventArgs = new PinchEventArgs(scale, _origin, degree, _previousAngle - degree);
+                    _previousAngle = degree;
+                    Target?.RaiseEvent(pinchEventArgs);
                     e.Handled = pinchEventArgs.Handled;
+                    e.PreventGestureRecognition();
                 }
             }
         }
@@ -74,18 +78,24 @@ namespace Avalonia.Input
 
                     _origin = new Point((_firstPoint.X + _secondPoint.X) / 2.0f, (_firstPoint.Y + _secondPoint.Y) / 2.0f);
 
+                    _previousAngle = GetAngleDegreeFromPoints(_firstPoint, _secondPoint);
+
                     Capture(_firstContact);
                     Capture(_secondContact);
+                    e.PreventGestureRecognition();
                 }
             }
         }
 
         protected override void PointerReleased(PointerReleasedEventArgs e)
         {
-            RemoveContact(e.Pointer);
+            if(RemoveContact(e.Pointer))
+            {
+                e.PreventGestureRecognition();
+            }
         }
 
-        private void RemoveContact(IPointer pointer)
+        private bool RemoveContact(IPointer pointer)
         {
             if (_firstContact == pointer || _secondContact == pointer)
             {
@@ -102,13 +112,28 @@ namespace Avalonia.Input
                 }
 
                 Target?.RaiseEvent(new PinchEndedEventArgs());
+                return true;
             }
+            return false;
         }
 
-        private float GetDistance(Point a, Point b)
+        private static float GetDistance(Point a, Point b)
         {
-            var length = _secondPoint - _firstPoint;
+            var length = b - a;
             return (float)new Vector(length.X, length.Y).Length;
         }
+
+        private static double GetAngleDegreeFromPoints(Point a, Point b)
+        {
+            // https://stackoverflow.com/a/15994225/20894223
+
+            var deltaX = a.X - b.X;
+            var deltaY = -(a.Y - b.Y);                           // I reverse the sign, because on the screen the Y axes
+                                                                 // are reversed with respect to the Cartesian plane.
+            var rad = System.Math.Atan2(deltaX, deltaY);         // radians from -π to +π
+            var degree = ((rad * (180 / System.Math.PI))) + 180; // Atan2 returns a radian value between -π to +π, in degrees -180 to +180.
+                                                                 // To get the angle between 0 and 360 degrees you need to add 180 degrees.
+            return degree;
+        }
     }
 }

+ 26 - 2
src/Avalonia.Base/Input/PinchEventArgs.cs

@@ -4,20 +4,44 @@ namespace Avalonia.Input
 {
     public class PinchEventArgs : RoutedEventArgs
     {
-        public PinchEventArgs(double scale, Point scaleOrigin) :  base(Gestures.PinchEvent)
+        public PinchEventArgs(double scale, Point scaleOrigin) : base(Gestures.PinchEvent)
         {
             Scale = scale;
             ScaleOrigin = scaleOrigin;
         }
 
+        public PinchEventArgs(double scale, Point scaleOrigin, double angle, double angleDelta) : base(Gestures.PinchEvent)
+        {
+            Scale = scale;
+            ScaleOrigin = scaleOrigin;
+            Angle = angle;
+            AngleDelta = angleDelta;
+        }
+
         public double Scale { get; } = 1;
 
         public Point ScaleOrigin { get; }
+
+        /// <summary>
+        /// Gets the angle of the pinch gesture, in degrees.
+        /// <summary>
+        /// <remarks>
+        /// A pinch gesture is the movement of two pressed points closer together. This property is the measured angle of the line between those two points. Remember zero degrees is a line pointing up.
+        /// </remarks>
+        public double Angle { get; }
+
+        /// <summary>
+        /// Gets the difference from the previous and current pinch angle.
+        /// </summary>
+        /// <remarks>
+        /// The AngleDelta value includes the sign of rotation. Positive for clockwise, negative counterclockwise.
+        /// </remarks>
+        public double AngleDelta { get; }
     }
 
     public class PinchEndedEventArgs : RoutedEventArgs
     {
-        public PinchEndedEventArgs() :  base(Gestures.PinchEndedEvent)
+        public PinchEndedEventArgs() : base(Gestures.PinchEndedEvent)
         {
         }
     }