Skip to content

Commit e105772

Browse files
BurningLightsjsuarezruizPureWeen
authored
Android pan fixes (#21547)
### Description of Change Fixes jankiness with using a PanGestureRecognizer to translate a control. <!-- Enter description of the fix in this section --> ### Issues Fixed Fixes #20772 <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> --------- Co-authored-by: Javier Suárez <javiersuarezruiz@hotmail.com> Co-authored-by: Shane Neuville <shneuvil@microsoft.com>
1 parent 2b14711 commit e105772

File tree

4 files changed

+226
-5
lines changed

4 files changed

+226
-5
lines changed

src/Controls/src/Core/Platform/Android/InnerGestureListener.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,8 @@ protected override void Dispose(bool disposing)
235235

236236
void SetStartingPosition(MotionEvent e1)
237237
{
238-
_lastX = e1.GetX();
239-
_lastY = e1.GetY();
238+
_lastX = e1.RawX;
239+
_lastY = e1.RawY;
240240
}
241241

242242
bool StartScrolling(MotionEvent e2)
@@ -249,8 +249,8 @@ bool StartScrolling(MotionEvent e2)
249249

250250
_isScrolling = true;
251251

252-
float totalX = e2.GetX() - _lastX;
253-
float totalY = e2.GetY() - _lastY;
252+
float totalX = e2.RawX - _lastX;
253+
float totalY = e2.RawY - _lastY;
254254

255255
return _scrollDelegate(totalX, totalY, e2.PointerCount) || _swipeDelegate(totalX, totalY);
256256
}

src/Controls/src/Core/Platform/GestureManager/GesturePlatformManager.Android.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ void OnPlatformViewTouched(object? sender, AView.TouchEventArgs e)
282282
}
283283

284284
if (e.Event != null)
285-
OnTouchEvent(e.Event);
285+
e.Handled = OnTouchEvent(e.Event);
286286
}
287287

288288
void SetupElement(VisualElement? oldElement, VisualElement? newElement)
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
using Microsoft.Maui;
2+
using Microsoft.Maui.Controls;
3+
using Microsoft.Maui.Graphics;
4+
using Microsoft.Maui.Layouts;
5+
6+
namespace Maui.Controls.Sample.Issues
7+
{
8+
[Issue(IssueTracker.Github, 20772, "Flickering occurs while updating the width of ContentView through PanGestureRecognizer", PlatformAffected.Android)]
9+
public partial class Issue20772 : ContentPage
10+
{
11+
public Issue20772()
12+
{
13+
Content = new CustomView20772();
14+
}
15+
}
16+
17+
public abstract class ControlLayout20772 : Layout
18+
{
19+
internal abstract Size LayoutArrangeChildren(Rect bounds);
20+
21+
internal abstract Size LayoutMeasure(double widthConstraint, double heightConstraint);
22+
}
23+
24+
internal class ControlLayoutManager20772 : LayoutManager
25+
{
26+
ControlLayout20772 layout;
27+
internal ControlLayoutManager20772(ControlLayout20772 layout) : base(layout)
28+
{
29+
this.layout = layout;
30+
}
31+
32+
public override Size ArrangeChildren(Rect bounds) => this.layout.LayoutArrangeChildren(bounds);
33+
34+
public override Size Measure(double widthConstraint, double heightConstraint) => this.layout.LayoutMeasure(widthConstraint, heightConstraint);
35+
}
36+
37+
public class CustomView20772 : ControlLayout20772
38+
{
39+
CustomChild20772 _customChild;
40+
CustomContent20772 _customContent;
41+
Label _statusLabel;
42+
43+
public CustomView20772()
44+
{
45+
_customChild = new CustomChild20772();
46+
_customContent = new CustomContent20772();
47+
_statusLabel = new Label
48+
{
49+
AutomationId = "StatusLabel",
50+
Text = "Waiting for pan gesture...",
51+
HeightRequest = 50
52+
};
53+
54+
// Give status label access to CustomChild for width tracking
55+
_customContent.ChildView = _customChild;
56+
_customContent.StatusLabel = _statusLabel;
57+
58+
this.Children.Add(_customChild);
59+
this.Children.Add(_customContent);
60+
this.Children.Add(_statusLabel);
61+
}
62+
63+
protected override ILayoutManager CreateLayoutManager()
64+
{
65+
return new ControlLayoutManager20772(this);
66+
}
67+
68+
internal override Size LayoutArrangeChildren(Rect bounds)
69+
{
70+
(_customChild as IView).Arrange(new Rect(0, 0, _customChild.WidthRequest, _customChild.HeightRequest));
71+
(_customContent as IView).Arrange(new Rect(_customChild.WidthRequest, 0, _customContent.WidthRequest, _customContent.HeightRequest));
72+
(_statusLabel as IView).Arrange(new Rect(0, 120, bounds.Width, 50));
73+
return bounds.Size;
74+
}
75+
76+
internal override Size LayoutMeasure(double widthConstraint, double heightConstraint)
77+
{
78+
(_customChild as IView).Measure(_customChild.WidthRequest, _customChild.HeightRequest);
79+
(_customContent as IView).Measure(_customContent.WidthRequest, _customContent.HeightRequest);
80+
(_statusLabel as IView).Measure(widthConstraint, 50);
81+
return new Size(widthConstraint, heightConstraint);
82+
}
83+
}
84+
85+
public class CustomChild20772 : ContentView
86+
{
87+
Label _label;
88+
89+
public CustomChild20772()
90+
{
91+
AutomationId = "CustomChild";
92+
BackgroundColor = Colors.Pink;
93+
HeightRequest = 100;
94+
WidthRequest = 180;
95+
_label = new Label() { Text = "Custom Child", HeightRequest = 100, WidthRequest = 180 };
96+
Content = _label;
97+
}
98+
}
99+
100+
public class CustomContent20772 : ContentView
101+
{
102+
Label _label;
103+
double _startingChildWidth;
104+
double _totalPanDistance;
105+
106+
#nullable enable
107+
public Label? StatusLabel { get; set; }
108+
public ContentView? ChildView { get; set; }
109+
#nullable restore
110+
111+
public CustomContent20772()
112+
{
113+
AutomationId = "CustomContent";
114+
BackgroundColor = Colors.Yellow;
115+
HeightRequest = 100;
116+
WidthRequest = 180;
117+
_label = new Label() { Text = "Drag here", HeightRequest = 100, WidthRequest = 180 };
118+
Content = _label;
119+
120+
PanGestureRecognizer gestureRecognizer = new PanGestureRecognizer();
121+
gestureRecognizer.PanUpdated += OnGestureRecognizerPanUpdated;
122+
GestureRecognizers.Add(gestureRecognizer);
123+
}
124+
125+
void OnGestureRecognizerPanUpdated(object sender, PanUpdatedEventArgs e)
126+
{
127+
switch (e.StatusType)
128+
{
129+
case GestureStatus.Started:
130+
_startingChildWidth = ChildView?.WidthRequest ?? 180;
131+
_totalPanDistance = 0;
132+
break;
133+
case GestureStatus.Running:
134+
_totalPanDistance = e.TotalX;
135+
OnResizing(e);
136+
break;
137+
case GestureStatus.Completed:
138+
case GestureStatus.Canceled:
139+
// Check if final width matches expected width based on pan distance
140+
// With the bug: width changes erratically because coordinates jump
141+
// With fix: width change = pan distance (approximately)
142+
if (StatusLabel is not null && ChildView is not null)
143+
{
144+
var actualWidthChange = ChildView.WidthRequest - _startingChildWidth;
145+
var expectedWidthChange = _totalPanDistance;
146+
var difference = Math.Abs(actualWidthChange - expectedWidthChange);
147+
148+
// Allow some tolerance for timing/rounding
149+
// With the bug, the difference would be huge due to coordinate jumps
150+
StatusLabel.Text = $"WidthChange:{actualWidthChange:F0},Expected:{expectedWidthChange:F0}";
151+
}
152+
break;
153+
}
154+
}
155+
156+
void OnResizing(PanUpdatedEventArgs e)
157+
{
158+
// The key issue: when we update WidthRequest, the view moves
159+
// With relative coordinates (bug): next TotalX is wrong because view position changed
160+
// With raw coordinates (fix): TotalX stays consistent
161+
if (ChildView is not null)
162+
{
163+
// Set width directly based on starting width + total pan distance
164+
// This is the expected behavior with raw coordinates
165+
ChildView.WidthRequest = _startingChildWidth + e.TotalX;
166+
ChildView.InvalidateMeasure();
167+
}
168+
}
169+
}
170+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using NUnit.Framework;
2+
using UITest.Appium;
3+
using UITest.Core;
4+
5+
namespace Microsoft.Maui.TestCases.Tests.Issues
6+
{
7+
public class Issue20772 : _IssuesUITest
8+
{
9+
public Issue20772(TestDevice device) : base(device) { }
10+
11+
public override string Issue => "Flickering occurs while updating the width of ContentView through PanGestureRecognizer";
12+
13+
[Test]
14+
[Category(UITestCategories.Gestures)]
15+
public void PanGestureDoesNotFlickerWhenResizingView()
16+
{
17+
// Wait for the page to load
18+
App.WaitForElement("CustomContent");
19+
20+
// Get initial width of the child element
21+
var childBefore = App.WaitForElement("CustomChild").GetRect();
22+
var initialWidth = childBefore.Width;
23+
24+
// Get the element to pan on
25+
var element = App.WaitForElement("CustomContent").GetRect();
26+
var startX = element.X + (element.Width / 2);
27+
var startY = element.Y + (element.Height / 2);
28+
29+
// Perform a pan gesture that drags to the right by 100 pixels
30+
// This will trigger the view resize during the pan
31+
var dragDistance = 100;
32+
App.DragCoordinates(startX, startY, startX + dragDistance, startY);
33+
34+
// Wait for the status label to update with the result
35+
App.WaitForElement("StatusLabel");
36+
37+
// Get final width of the child element
38+
var childAfter = App.WaitForElement("CustomChild").GetRect();
39+
var finalWidth = childAfter.Width;
40+
var actualWidthChange = finalWidth - initialWidth;
41+
42+
// The test passes if the width change approximately matches the drag distance
43+
// With the bug (before fix): coordinates jump around, width change is erratic
44+
// With the fix: width change should be close to drag distance (within tolerance)
45+
// We allow 30 pixel tolerance for timing variations
46+
Assert.That(actualWidthChange, Is.EqualTo(dragDistance).Within(30),
47+
$"Width change ({actualWidthChange}) should approximately match drag distance ({dragDistance}). " +
48+
$"Initial: {initialWidth}, Final: {finalWidth}");
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)