diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue28064.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue28064.cs new file mode 100644 index 000000000000..b713719ec465 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue28064.cs @@ -0,0 +1,78 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 28064, "TapGestureRecognizer on ScrollView background does not fire on Android", PlatformAffected.Android)] +public class Issue28064 : ContentPage +{ + public Issue28064() + { + var statusLabel = new Label + { + Text = "Tap the brown background", + AutomationId = "StatusLabel" + }; + + var childStatusLabel = new Label + { + Text = "Tap a child label", + AutomationId = "ChildStatusLabel" + }; + + var child1 = new Label + { + BackgroundColor = Colors.LightPink, + WidthRequest = 100, + HeightRequest = 80, + Text = "Child1", + AutomationId = "Child1Label" + }; + var childTapRecognizer = new TapGestureRecognizer(); + childTapRecognizer.Tapped += (s, e) => + { + childStatusLabel.Text = "Child Tapped"; + }; + child1.GestureRecognizers.Add(childTapRecognizer); + + // ScrollView with Brown background — the area NOT covered by children is the tap target + var scrollView = new ScrollView + { + HeightRequest = 80, + BackgroundColor = Colors.Brown, + Orientation = ScrollOrientation.Horizontal, + AutomationId = "TheScrollView", + Content = new HorizontalStackLayout + { + Children = + { + child1, + new Label + { + BackgroundColor = Colors.LightGreen, + WidthRequest = 100, + HeightRequest = 80, + Text = "Child2", + AutomationId = "Child2Label" + } + } + } + }; + + var tapRecognizer = new TapGestureRecognizer(); + tapRecognizer.Tapped += (s, e) => + { + statusLabel.Text = "ScrollView Tapped"; + }; + scrollView.GestureRecognizers.Add(tapRecognizer); + + Content = new VerticalStackLayout + { + Spacing = 10, + Padding = 10, + Children = + { + statusLabel, + childStatusLabel, + scrollView + } + }; + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28064.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28064.cs new file mode 100644 index 000000000000..1c6452585800 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28064.cs @@ -0,0 +1,34 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue28064 : _IssuesUITest +{ + public Issue28064(TestDevice device) : base(device) + { + } + + public override string Issue => "TapGestureRecognizer on ScrollView background does not fire on Android"; + + [Test] + [Category(UITestCategories.ScrollView)] + public void ScrollViewBackgroundTapGestureShouldFire() + { + App.WaitForElement("StatusLabel"); + App.Tap("TheScrollView"); + var labelText = App.WaitForElement("StatusLabel").GetText(); + Assert.That(labelText, Is.EqualTo("ScrollView Tapped")); + } + + [Test] + [Category(UITestCategories.ScrollView)] + public void ScrollViewChildTapGestureShouldFire() + { + App.WaitForElement("ChildStatusLabel"); + App.Tap("Child1Label"); + var labelText = App.WaitForElement("ChildStatusLabel").GetText(); + Assert.That(labelText, Is.EqualTo("Child Tapped")); + } +} diff --git a/src/Core/src/Platform/Android/MauiScrollView.cs b/src/Core/src/Platform/Android/MauiScrollView.cs index 49c36ee11de2..c2cb3096acf0 100644 --- a/src/Core/src/Platform/Android/MauiScrollView.cs +++ b/src/Core/src/Platform/Android/MauiScrollView.cs @@ -33,6 +33,15 @@ public class MauiScrollView : NestedScrollView, IScrollBarView, NestedScrollView internal bool ShouldSkipOnTouch; internal int HorizontalScrollOffset => _hScrollView?.ScrollX ?? 0; + // Stores the parent touch listener so horizontal ScrollView taps can be forwarded to it directly. + internal IOnTouchListener? _touchListener; + + public override void SetOnTouchListener(IOnTouchListener? touchListener) + { + _touchListener = touchListener; + base.SetOnTouchListener(touchListener); + } + public MauiScrollView(Context context) : base(context) { _context = context; @@ -556,6 +565,12 @@ public override bool OnTouchEvent(MotionEvent? ev) // This allows ScrollView to work properly inside containers like DrawerLayout (Shell Flyout) ScrollViewExtensions.HandleTouchEvent(ev, Parent); + // OnTouchEvent is only called when no child has claimed the touch event, which mirrors + // exactly when a vertical ScrollView's touch listener fires. We invoke the parent's + // stored touch listener here so TapGestureRecognizers on a horizontal/both ScrollView + // fire correctly. + _parentScrollView._touchListener?.OnTouch(_parentScrollView, ev); + // If the touch is caught by the horizontal scrollview, forward it to the parent _parentScrollView.ShouldSkipOnTouch = true; _parentScrollView.OnTouchEvent(ev); diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index 01d355e5ff68..e1cd65345d57 100644 --- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -10,6 +10,7 @@ override Microsoft.Maui.PlatformDrawable.ThresholdType.get -> System.Type! *REMOVED*override Microsoft.Maui.Graphics.MauiDrawable.OnDraw(Android.Graphics.Drawables.Shapes.Shape? shape, Android.Graphics.Canvas? canvas, Android.Graphics.Paint? paint) -> void override Microsoft.Maui.Handlers.LabelHandler.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size override Microsoft.Maui.Platform.MauiScrollView.OnVisibilityChanged(Android.Views.View! changedView, Android.Views.ViewStates visibility) -> void +override Microsoft.Maui.Platform.MauiScrollView.SetOnTouchListener(Android.Views.View.IOnTouchListener? touchListener) -> void override Microsoft.Maui.Platform.ContentViewGroup.HasOverlappingRendering.get -> bool override Microsoft.Maui.Platform.LayoutViewGroup.HasOverlappingRendering.get -> bool override Microsoft.Maui.Platform.WrapperView.HasOverlappingRendering.get -> bool