Repository: AvaloniaUI/Avalonia

Issue:
[11.0.0] Android touch control breaks after swiping back
**Describe the bug**
When using the "swipe back" function on modern android devices (see here: [Image](https://image.winudf.com/v2/image1/dGhyZWVkcm9pZC5nZXN0dXJlLmNvbnRyb2xfc2NyZWVuXzBfMTU1NTk1NTYwNV8wODk/screen-0.webp?fakeurl=1&type=.webp)) the device detects a touch input at that point even after the finger was released. This results in every further drag being detected as pinch. I haven't found a way to release the imaginary finger again.

**To Reproduce**
Steps to reproduce the behavior:
1. Create an android app that is detecting PointerMoved and Pinch Events (drag an image on screen for example)
2. Swipe back on the edge of the screen (which will in most cases let you return to the home screen)
3. Reopen the app
4. Experience strange behaviour when touching the screen.


**Expected behavior**
The touch input should work the same after performing the swipe back gesture.

**Desktop (please complete the following information):**

- OS: [Android]
- Version [11.0.0]



Hints:

Patch:
diff --git a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs
index 05dce8214b1..74e8061292e 100644
--- a/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs
+++ b/src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs
@@ -1,3 +1,4 @@
+using System;
 using System.Collections;
 using System.Collections.Generic;
 using Avalonia.Controls;
@@ -59,6 +60,20 @@ internal bool HandlePointerPressed(PointerPressedEventArgs e)
             return e.Handled;
         }
 
+        internal void HandleCaptureLost(IPointer pointer)
+        {
+            if (_recognizers == null || pointer is not Pointer p)
+                return;
+
+            foreach (var r in _recognizers)
+            {
+                if (p.CapturedGestureRecognizer == r)
+                    continue;
+
+                r.PointerCaptureLostInternal(pointer);
+            }
+        }
+
         internal bool HandlePointerReleased(PointerReleasedEventArgs e)
         {
             if (_recognizers == null)
diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs
index b02c82a0660..5a22ba7a142 100644
--- a/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs
+++ b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs
@@ -1,4 +1,5 @@
-﻿using Avalonia.Input.GestureRecognizers;
+﻿using System.Diagnostics;
+using Avalonia.Input.GestureRecognizers;
 
 namespace Avalonia.Input
 {
@@ -110,6 +111,7 @@ private void RemoveContact(IPointer pointer)
 
                     _secondContact = null;
                 }
+
                 Target?.RaiseEvent(new PinchEndedEventArgs());
             }
         }
diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs
index 46f543d25bd..91dae88dbb4 100644
--- a/src/Avalonia.Base/Input/InputElement.cs
+++ b/src/Avalonia.Base/Input/InputElement.cs
@@ -230,6 +230,7 @@ static InputElement()
             PointerMovedEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerMoved(e), handledEventsToo: true);
             PointerPressedEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerPressed(e), handledEventsToo: true);
             PointerReleasedEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerReleased(e), handledEventsToo: true);
+            PointerCaptureLostEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerCaptureLost(e), handledEventsToo: true);
         }
 
         public InputElement()
@@ -615,6 +616,11 @@ private void OnGesturePointerReleased(PointerReleasedEventArgs e)
                 }
         }
 
+        private void OnGesturePointerCaptureLost(PointerCaptureLostEventArgs e)
+        {
+            _gestureRecognizers?.HandleCaptureLost(e.Pointer);
+        }
+
         private void OnGesturePointerPressed(PointerPressedEventArgs e)
         {
             if (!e.IsGestureRecognitionSkipped)
diff --git a/src/Avalonia.Base/Input/Pointer.cs b/src/Avalonia.Base/Input/Pointer.cs
index 5744ddf963a..e82432a00db 100644
--- a/src/Avalonia.Base/Input/Pointer.cs
+++ b/src/Avalonia.Base/Input/Pointer.cs
@@ -91,12 +91,16 @@ public void Dispose()
         internal void CaptureGestureRecognizer(GestureRecognizer? gestureRecognizer)
         {
             if (CapturedGestureRecognizer != gestureRecognizer)
+            {
                 CapturedGestureRecognizer?.PointerCaptureLostInternal(this);
+            }
+
+            CapturedGestureRecognizer = gestureRecognizer;
 
             if (gestureRecognizer != null)
+            {
                 Capture(null);
-
-            CapturedGestureRecognizer = gestureRecognizer;
+            }
         }
     }
 }


Test patch:
diff --git a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs
index 14fa242146f..a2afdd0af27 100644
--- a/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs
@@ -487,6 +487,40 @@ public void Pinched_Should_Be_Raised_For_Two_Pointers_Moving()
             Assert.True(raised);
         }
 
+        [Fact]
+        public void Gestures_Should_Be_Cancelled_When_Pointer_Capture_Is_Lost()
+        {
+            Border border = new Border()
+            {
+                Width = 100,
+                Height = 100,
+                Background = new SolidColorBrush(Colors.Red)
+            };
+            border.GestureRecognizers.Add(new PinchGestureRecognizer());
+            var root = new TestRoot
+            {
+                Child = border
+            };
+            var raised = false;
+
+            root.AddHandler(Gestures.PinchEvent, (_, _) => raised = true);
+
+            var firstPoint = new Point(5, 5);
+            var secondPoint = new Point(10, 10);
+
+            var firstTouch = new TouchTestHelper();
+            var secondTouch = new TouchTestHelper();
+
+            firstTouch.Down(border, position: firstPoint);
+
+            firstTouch.Cancel();
+
+            secondTouch.Down(border, position: secondPoint);
+            secondTouch.Move(border, position: new Point(20, 20));
+
+            Assert.False(raised);
+        }
+
         [Fact]
         public void Scrolling_Should_Start_After_Start_Distance_Is_Exceeded()
         {
diff --git a/tests/Avalonia.UnitTests/TouchTestHelper.cs b/tests/Avalonia.UnitTests/TouchTestHelper.cs
index 574599d1ad6..120b0c06705 100644
--- a/tests/Avalonia.UnitTests/TouchTestHelper.cs
+++ b/tests/Avalonia.UnitTests/TouchTestHelper.cs
@@ -61,5 +61,11 @@ public void Tap(Interactive target, Interactive source, Point position = default
             Down(target, source, position, modifiers);
             Up(target, source, position, modifiers);
         }
+
+        public void Cancel()
+        {
+            _pointer.Capture(null);
+            _pointer.CaptureGestureRecognizer(null);
+        }
     }
 }