From ddb71c648dbfc713eac4e507091a971409795f8b Mon Sep 17 00:00:00 2001 From: Dhivya-SF4094 <127717131+Dhivya-SF4094@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:04:49 +0530 Subject: [PATCH 1/7] Fixed CarouselView behaves strangely when swiping vertically in view --- .../Items/Android/MauiCarouselRecyclerView.cs | 37 ++++++ .../TestCases.HostApp/Issues/Issue22507.cs | 114 ++++++++++++++++++ .../Tests/Issues/Issue22507.cs | 34 ++++++ 3 files changed, 185 insertions(+) create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue22507.cs create mode 100644 src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs index 0ca5e53aa278..70afa60ae71b 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs @@ -23,6 +23,9 @@ public class MauiCarouselRecyclerView : MauiRecyclerView _oldViews; CarouselViewOnGlobalLayoutListener _carouselViewLayoutListener; + float _initialTouchX; + float _initialTouchY; + protected CarouselView Carousel => ItemsView as CarouselView; public MauiCarouselRecyclerView(Context context, Func getItemsLayout, Func> getAdapter) : base(context, getItemsLayout, getAdapter) @@ -40,9 +43,43 @@ public override bool OnInterceptTouchEvent(MotionEvent ev) return false; } + switch (ev.Action) + { + case MotionEventActions.Down: + _initialTouchX = ev.GetX(); + _initialTouchY = ev.GetY(); + break; + + case MotionEventActions.Move: + float deltaX = ev.GetX() - _initialTouchX; + float deltaY = ev.GetY() - _initialTouchY; + + if (ShouldDelegateToChild(deltaX, deltaY)) + { + return false; + } + break; + + case MotionEventActions.Cancel: + case MotionEventActions.Up: + // Reset initial touch values at the end of the gesture + // to prevent old values from being used if we don't get a Down event + _initialTouchX = 0; + _initialTouchY = 0; + break; + } + return base.OnInterceptTouchEvent(ev); } + bool ShouldDelegateToChild(float deltaX, float deltaY) + { + float absDeltaX = Math.Abs(deltaX); + float absDeltaY = Math.Abs(deltaY); + + return IsHorizontal ? absDeltaY > absDeltaX : absDeltaX > absDeltaY; + } + protected virtual bool IsHorizontal => (Carousel?.ItemsLayout)?.Orientation == ItemsLayoutOrientation.Horizontal; protected override int DetermineTargetPosition(ScrollToRequestEventArgs args) diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue22507.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue22507.cs new file mode 100644 index 000000000000..db5c36d6ffbd --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue22507.cs @@ -0,0 +1,114 @@ +using System.Collections.ObjectModel; + +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 22507, "CarouselView behaves strangely when swiping vertically in view", PlatformAffected.Android)] +public class Issue22507 : ContentPage +{ + public ObservableCollection ItemsList { get; set; } + + public Issue22507() + { + ItemsList = new ObservableCollection + { + new Issue22507Model( + "Page 1", + Enumerable.Range('A', 20) + .Select(c => $"Item {(char)c}") + .ToArray()), + + new Issue22507Model( + "Page 2", + Enumerable.Range(1, 20) + .Select(i => $"Item {i}") + .ToArray()) + }; + Grid mainGrid = new Grid + { + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Star } + } + }; + // Create CarouselView + var mainCarousel = new CarouselView + { + BackgroundColor = Colors.Yellow, + Margin = new Thickness(10), + IsBounceEnabled = true, + IsSwipeEnabled = true, + Loop = false, + ItemsSource = ItemsList, + ItemTemplate = new DataTemplate(() => + { + // Grid for each carousel item + var grid = new Grid + { + BackgroundColor = Colors.White, + Padding = 10, + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Star } + } + }; + + // Label for Title + var titleLabel = new Label + { + FontSize = 18, + TextColor = Colors.Black, + Margin = new Thickness(0, 0, 0, 10) + }; + titleLabel.SetBinding(Label.TextProperty, "Title"); + + // CollectionView for nested items + var collectionView = new CollectionView + { + BackgroundColor = Colors.LightBlue, + AutomationId = "Issue22507CollectionView", + ItemTemplate = new DataTemplate(() => + { + var itemLabel = new Label + { + FontSize = 16, + Padding = 10 + }; + itemLabel.SetBinding(Label.TextProperty, "."); + return itemLabel; + }) + }; + collectionView.SetBinding(CollectionView.ItemsSourceProperty, "Items"); + + grid.Add(titleLabel); + grid.Add(collectionView, 0, 1); + + return grid; + }) + }; + Label label = new Label + { + Text = "Swipe vertically on the items below. CarouselView should not interfere with vertical scrolling.", + AutomationId = "Issue22507Label" + }; + mainGrid.Add(label); + Grid.SetRow(label, 0); + mainGrid.Add(mainCarousel); + Grid.SetRow(mainCarousel, 1); + + Content = mainGrid; + } +} + +public class Issue22507Model +{ + public string Title { get; set; } + public ObservableCollection Items { get; set; } + + public Issue22507Model(string title, string[] items) + { + Title = title; + Items = new ObservableCollection(items); + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs new file mode 100644 index 000000000000..27416654c2f6 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs @@ -0,0 +1,34 @@ +#if TEST_FAILS_ON_CATALYST // App.SwipeRightToLeft does not scroll to next item in Mac catalyst. +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue22507 : _IssuesUITest +{ + public Issue22507(TestDevice testDevice) : base(testDevice) + { + } + public override string Issue => "CarouselView behaves strangely when swiping vertically in view"; + + [Test] + [Category(UITestCategories.CarouselView)] + public void HandleCarouselVerticalScroll() + { + // Wait for the test page to load + App.WaitForElement("Issue22507Label"); + + App.ScrollDown("Issue22507CollectionView"); + // Swipe horizontally to navigate to the next CarouselView item (Page 2) + App.SwipeRightToLeft(); + // wait for page 2 + App.WaitForElement("Item 1"); + for (int i = 0; i < 2; i++) + { + App.ScrollDown("Issue22507CollectionView", ScrollStrategy.Gesture, 0.99, swipeSpeed: 900); + } + App.WaitForElement("Item 20"); + } +} +#endif \ No newline at end of file From 728b65bce85923406ab03b43cb156f360ae944b2 Mon Sep 17 00:00:00 2001 From: Dhivya-SF4094 <127717131+Dhivya-SF4094@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:05:49 +0530 Subject: [PATCH 2/7] Updated fix and test sample --- .../Items/Android/MauiCarouselRecyclerView.cs | 11 +++++++--- .../Tests/Issues/Issue22507.cs | 20 +++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs index 70afa60ae71b..ddd3050c5cc3 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs @@ -22,7 +22,7 @@ public class MauiCarouselRecyclerView : MauiRecyclerView _oldViews; CarouselViewOnGlobalLayoutListener _carouselViewLayoutListener; - + float _touchSlop; float _initialTouchX; float _initialTouchY; @@ -32,6 +32,7 @@ public MauiCarouselRecyclerView(Context context, Func getItemsLayo { _oldViews = new List(); _carouselViewLoopManager = new CarouselViewLoopManager(); + _touchSlop = ViewConfiguration.Get(context).ScaledTouchSlop; } public bool IsSwipeEnabled { get; set; } @@ -54,9 +55,13 @@ public override bool OnInterceptTouchEvent(MotionEvent ev) float deltaX = ev.GetX() - _initialTouchX; float deltaY = ev.GetY() - _initialTouchY; - if (ShouldDelegateToChild(deltaX, deltaY)) + // Check if movement exceeds touch slop before evaluating direction + if (Math.Abs(deltaX) > _touchSlop || Math.Abs(deltaY) > _touchSlop) { - return false; + if (ShouldDelegateToChild(deltaX, deltaY)) + { + return false; + } } break; diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs index 27416654c2f6..9eefc67c5a3c 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs @@ -14,7 +14,7 @@ public Issue22507(TestDevice testDevice) : base(testDevice) [Test] [Category(UITestCategories.CarouselView)] - public void HandleCarouselVerticalScroll() + public void HandleCarouselVerticalToHorizontalScroll() { // Wait for the test page to load App.WaitForElement("Issue22507Label"); @@ -22,7 +22,23 @@ public void HandleCarouselVerticalScroll() App.ScrollDown("Issue22507CollectionView"); // Swipe horizontally to navigate to the next CarouselView item (Page 2) App.SwipeRightToLeft(); - // wait for page 2 + App.WaitForElement("Item 1"); + for (int i = 0; i < 2; i++) + { + App.ScrollDown("Issue22507CollectionView", ScrollStrategy.Gesture, 0.99, swipeSpeed: 900); + } + App.WaitForElement("Item 20"); + } + + [Test] + [Category(UITestCategories.CarouselView)] + public void HandleCarouselHorizontalToVerticalScroll() + { + // Wait for the test page to load + App.WaitForElement("Issue22507Label"); + + // Swipe horizontally to navigate to the next CarouselView item (Page 2) + App.SwipeRightToLeft(); App.WaitForElement("Item 1"); for (int i = 0; i < 2; i++) { From 9f5b86dc3230f12e0b05736a0e1380281986715d Mon Sep 17 00:00:00 2001 From: Dhivya-SF4094 <127717131+Dhivya-SF4094@users.noreply.github.com> Date: Tue, 4 Nov 2025 10:12:40 +0530 Subject: [PATCH 3/7] Addressed review comment --- .../Handlers/Items/Android/MauiCarouselRecyclerView.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs index ddd3050c5cc3..b30d3c0ce758 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs @@ -19,12 +19,11 @@ public class MauiCarouselRecyclerView : MauiRecyclerView _oldViews; CarouselViewOnGlobalLayoutListener _carouselViewLayoutListener; - float _touchSlop; - float _initialTouchX; - float _initialTouchY; protected CarouselView Carousel => ItemsView as CarouselView; @@ -35,6 +34,7 @@ public MauiCarouselRecyclerView(Context context, Func getItemsLayo _touchSlop = ViewConfiguration.Get(context).ScaledTouchSlop; } + // Gets or sets a value indicating whether swipe gestures are enabled for the carousel. public bool IsSwipeEnabled { get; set; } public override bool OnInterceptTouchEvent(MotionEvent ev) From 9b8a2956a66183e95a61ead3ee9dffa63fec69d5 Mon Sep 17 00:00:00 2001 From: Dhivya-SF4094 <127717131+Dhivya-SF4094@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:58:29 +0530 Subject: [PATCH 4/7] Added comment for ShouldDelegateToChild() --- .../Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs index b30d3c0ce758..3ea86baa2525 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs @@ -77,6 +77,9 @@ public override bool OnInterceptTouchEvent(MotionEvent ev) return base.OnInterceptTouchEvent(ev); } + // Determines whether the touch event should be handled by a child view rather than the carousel. + // Delegates when the dominant axis of movement does not match the + // carousel's scroll orientation. bool ShouldDelegateToChild(float deltaX, float deltaY) { float absDeltaX = Math.Abs(deltaX); From 430e73f2e4695ba20a1199474d62b0e96fb0dd8c Mon Sep 17 00:00:00 2001 From: Dhivya-SF4094 <127717131+Dhivya-SF4094@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:48:02 +0530 Subject: [PATCH 5/7] Addressed AI summary --- .../TestCases.HostApp/Issues/Issue22507.cs | 8 ++++++-- .../Tests/Issues/Issue22507.cs | 19 +++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue22507.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue22507.cs index db5c36d6ffbd..19ec5411d71f 100644 --- a/src/Controls/tests/TestCases.HostApp/Issues/Issue22507.cs +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue22507.cs @@ -13,12 +13,14 @@ public Issue22507() { new Issue22507Model( "Page 1", + "Issue22507CV_Page1", Enumerable.Range('A', 20) .Select(c => $"Item {(char)c}") .ToArray()), new Issue22507Model( "Page 2", + "Issue22507CV_Page2", Enumerable.Range(1, 20) .Select(i => $"Item {i}") .ToArray()) @@ -67,7 +69,6 @@ public Issue22507() var collectionView = new CollectionView { BackgroundColor = Colors.LightBlue, - AutomationId = "Issue22507CollectionView", ItemTemplate = new DataTemplate(() => { var itemLabel = new Label @@ -80,6 +81,7 @@ public Issue22507() }) }; collectionView.SetBinding(CollectionView.ItemsSourceProperty, "Items"); + collectionView.SetBinding(VisualElement.AutomationIdProperty, "CollectionAutomationId"); grid.Add(titleLabel); grid.Add(collectionView, 0, 1); @@ -104,11 +106,13 @@ public Issue22507() public class Issue22507Model { public string Title { get; set; } + public string CollectionAutomationId { get; set; } public ObservableCollection Items { get; set; } - public Issue22507Model(string title, string[] items) + public Issue22507Model(string title, string collectionAutomationId, string[] items) { Title = title; + CollectionAutomationId = collectionAutomationId; Items = new ObservableCollection(items); } } \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs index 9eefc67c5a3c..848089b1c4be 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs @@ -19,15 +19,16 @@ public void HandleCarouselVerticalToHorizontalScroll() // Wait for the test page to load App.WaitForElement("Issue22507Label"); - App.ScrollDown("Issue22507CollectionView"); + // Scroll down in Page 1's CollectionView + for (int i = 0; i < 5; i++) + { + App.ScrollDown("Issue22507CV_Page1", ScrollStrategy.Gesture, 0.99, swipeSpeed: 900); + } + App.WaitForElement("Item T"); + // Swipe horizontally to navigate to the next CarouselView item (Page 2) App.SwipeRightToLeft(); App.WaitForElement("Item 1"); - for (int i = 0; i < 2; i++) - { - App.ScrollDown("Issue22507CollectionView", ScrollStrategy.Gesture, 0.99, swipeSpeed: 900); - } - App.WaitForElement("Item 20"); } [Test] @@ -40,9 +41,11 @@ public void HandleCarouselHorizontalToVerticalScroll() // Swipe horizontally to navigate to the next CarouselView item (Page 2) App.SwipeRightToLeft(); App.WaitForElement("Item 1"); - for (int i = 0; i < 2; i++) + + // Scroll down in Page 2's CollectionView + for (int i = 0; i < 5; i++) { - App.ScrollDown("Issue22507CollectionView", ScrollStrategy.Gesture, 0.99, swipeSpeed: 900); + App.ScrollDown("Issue22507CV_Page2", ScrollStrategy.Gesture, 0.99, swipeSpeed: 900); } App.WaitForElement("Item 20"); } From 4767c1c6aaa639bee7df88cebeda9f5c7dbc3f22 Mon Sep 17 00:00:00 2001 From: Dhivya-SF4094 <127717131+Dhivya-SF4094@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:24:24 +0530 Subject: [PATCH 6/7] Updated Test --- .../Tests/Issues/Issue22507.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs index 848089b1c4be..6d80f602dd31 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs @@ -12,6 +12,34 @@ public Issue22507(TestDevice testDevice) : base(testDevice) } public override string Issue => "CarouselView behaves strangely when swiping vertically in view"; + // NUnit reuses the same fixture (and the same loaded page) for all tests in this class, + // and runs them in alphabetical order. That means HandleCarouselHorizontalToVerticalScroll + // runs first and leaves the carousel on Page 2 with Page 2's CollectionView scrolled down. + // Reset to Page 1 *and* scroll both inner CollectionViews back to the top so the tests are + // independent regardless of execution order. + [SetUp] + public void ResetCarouselState() + { + App.WaitForElement("Issue22507Label"); + + // Navigate to Page 2 (no-op if we're already there because Loop = false) + // and scroll Page 2's CollectionView back to the top. + App.SwipeRightToLeft(); + App.WaitForElement("Issue22507CV_Page2"); + for (int i = 0; i < 5; i++) + { + App.ScrollUp("Issue22507CV_Page2", ScrollStrategy.Gesture, 0.99, swipeSpeed: 900); + } + + // Navigate back to Page 1 and scroll its CollectionView back to the top. + App.SwipeLeftToRight(); + App.WaitForElement("Issue22507CV_Page1"); + for (int i = 0; i < 5; i++) + { + App.ScrollUp("Issue22507CV_Page1", ScrollStrategy.Gesture, 0.99, swipeSpeed: 900); + } + } + [Test] [Category(UITestCategories.CarouselView)] public void HandleCarouselVerticalToHorizontalScroll() From cd199d81c71b80a088414b87035997faa0f6f34b Mon Sep 17 00:00:00 2001 From: Dhivya-SF4094 <127717131+Dhivya-SF4094@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:08:16 +0530 Subject: [PATCH 7/7] Updated fix and Testcase --- .../Items/Android/MauiCarouselRecyclerView.cs | 61 ++++++--- .../CarouselView/CarouselViewTests.Android.cs | 55 ++++++++ .../TestCases.HostApp/Issues/Issue22507.cs | 118 ------------------ .../Tests/Issues/Issue22507.cs | 81 ------------ 4 files changed, 102 insertions(+), 213 deletions(-) delete mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue22507.cs delete mode 100644 src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs index 3ea86baa2525..df60e994eaa6 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs @@ -20,8 +20,10 @@ public class MauiCarouselRecyclerView : MauiRecyclerView _oldViews; CarouselViewOnGlobalLayoutListener _carouselViewLayoutListener; @@ -39,6 +41,15 @@ public MauiCarouselRecyclerView(Context context, Func getItemsLayo public override bool OnInterceptTouchEvent(MotionEvent ev) { + // If ItemsView is explicitly disabled, defer to the base implementation so it can + // intercept all touch events and block interaction. Returning false here (for either + // the swipe-disabled or off-axis delegation paths) would bypass that guard and allow + // a disabled CarouselView to delegate gestures to a nested child. + if (ItemsView?.IsEnabled == false && !ItemsView.IsExplicitlyEnabled) + { + return base.OnInterceptTouchEvent(ev); + } + if (!IsSwipeEnabled) { return false; @@ -49,43 +60,65 @@ public override bool OnInterceptTouchEvent(MotionEvent ev) case MotionEventActions.Down: _initialTouchX = ev.GetX(); _initialTouchY = ev.GetY(); + _directionLocked = false; + _delegatingToChild = false; break; case MotionEventActions.Move: - float deltaX = ev.GetX() - _initialTouchX; - float deltaY = ev.GetY() - _initialTouchY; + // Once a gesture has been delegated to a nested child, keep delegating for the + // rest of the gesture. This prevents a later ambiguous move - or the child + // reaching its scroll boundary - from letting the carousel hijack the swipe and + // transition to the next item. + if (_delegatingToChild) + { + return false; + } - // Check if movement exceeds touch slop before evaluating direction - if (Math.Abs(deltaX) > _touchSlop || Math.Abs(deltaY) > _touchSlop) + if (!_directionLocked) { - if (ShouldDelegateToChild(deltaX, deltaY)) + float deltaX = ev.GetX() - _initialTouchX; + float deltaY = ev.GetY() - _initialTouchY; + + // Lock the gesture direction the first time movement exceeds touch slop. + if (Math.Abs(deltaX) > _touchSlop || Math.Abs(deltaY) > _touchSlop) { - return false; + _directionLocked = true; + + if (IsOffAxisGesture(deltaX, deltaY)) + { + // Perpendicular gesture (e.g. a vertical swipe on a horizontal carousel): + // it belongs to nested scrollable content, never the carousel. + _delegatingToChild = true; + return false; + } } } break; case MotionEventActions.Cancel: case MotionEventActions.Up: - // Reset initial touch values at the end of the gesture + // Reset gesture state at the end of the gesture // to prevent old values from being used if we don't get a Down event _initialTouchX = 0; _initialTouchY = 0; + _directionLocked = false; + _delegatingToChild = false; break; } return base.OnInterceptTouchEvent(ev); } - // Determines whether the touch event should be handled by a child view rather than the carousel. - // Delegates when the dominant axis of movement does not match the - // carousel's scroll orientation. - bool ShouldDelegateToChild(float deltaX, float deltaY) + // Determines whether the gesture's dominant axis is the opposite of the carousel's scroll + // orientation (e.g. a vertical swipe on a horizontal carousel). Off-axis gestures belong to + // nested scrollable content, so the carousel must not intercept them. + bool IsOffAxisGesture(float deltaX, float deltaY) { float absDeltaX = Math.Abs(deltaX); float absDeltaY = Math.Abs(deltaY); + bool isVerticalGesture = absDeltaY > absDeltaX; - return IsHorizontal ? absDeltaY > absDeltaX : absDeltaX > absDeltaY; + return IsHorizontal ? isVerticalGesture : !isVerticalGesture; } protected virtual bool IsHorizontal => (Carousel?.ItemsLayout)?.Orientation == ItemsLayoutOrientation.Horizontal; diff --git a/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewTests.Android.cs index 4e3e7eae604b..8f4910d439fa 100644 --- a/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewTests.Android.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using System.Threading.Tasks; +using Android.Views; using Android.Widget; using AndroidX.RecyclerView.Widget; using Microsoft.Maui.Controls; @@ -55,6 +56,60 @@ await CreateHandlerAndAddToWindow(carouselView, async (hand }); } + [Fact(DisplayName = "Vertical Drag On Horizontal CarouselView Is Not Intercepted")] + public async Task VerticalDragOnHorizontalCarouselIsNotIntercepted() + { + SetupBuilder(); + + var data = new ObservableCollection { "Item 1", "Item 2", "Item 3" }; + + var template = new DataTemplate(() => new Grid { new Label() }); + + var carouselView = new CarouselView + { + ItemTemplate = template, + ItemsSource = data, + IsSwipeEnabled = true, + }; + + await CreateHandlerAndAddToWindow(carouselView, async (handler) => + { + var recyclerView = handler.PlatformView; + await recyclerView.WaitForLayoutOrNonZeroSize(); + + // A vertical-dominant drag with a horizontal component large enough that the base + // RecyclerView would otherwise treat it as a horizontal page swipe. The fix must + // detect the off-axis gesture and delegate it to nested scrollable content, so + // OnInterceptTouchEvent returns false. Without the fix, the carousel intercepts it. + bool intercepted = SimulateDragIntercept(recyclerView, deltaX: 80, deltaY: 400); + + Assert.False(intercepted); + }); + } + + // Dispatches a synthetic down/move/up gesture to the RecyclerView's touch-interception + // pipeline and reports whether the move was intercepted by the carousel. + static bool SimulateDragIntercept(RecyclerView recyclerView, float deltaX, float deltaY) + { + const float startX = 200f; + const float startY = 200f; + long downTime = global::Android.OS.SystemClock.UptimeMillis(); + + var down = MotionEvent.Obtain(downTime, downTime, MotionEventActions.Down, startX, startY, 0); + recyclerView.OnInterceptTouchEvent(down); + down.Recycle(); + + var move = MotionEvent.Obtain(downTime, downTime + 16, MotionEventActions.Move, startX + deltaX, startY + deltaY, 0); + bool intercepted = recyclerView.OnInterceptTouchEvent(move); + move.Recycle(); + + var up = MotionEvent.Obtain(downTime, downTime + 32, MotionEventActions.Up, startX + deltaX, startY + deltaY, 0); + recyclerView.OnInterceptTouchEvent(up); + up.Recycle(); + + return intercepted; + } + RecyclerView GetPlatformCarouselView(CarouselViewHandler carouselViewHandler) => carouselViewHandler.PlatformView; diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue22507.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue22507.cs deleted file mode 100644 index 19ec5411d71f..000000000000 --- a/src/Controls/tests/TestCases.HostApp/Issues/Issue22507.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.Collections.ObjectModel; - -namespace Maui.Controls.Sample.Issues; - -[Issue(IssueTracker.Github, 22507, "CarouselView behaves strangely when swiping vertically in view", PlatformAffected.Android)] -public class Issue22507 : ContentPage -{ - public ObservableCollection ItemsList { get; set; } - - public Issue22507() - { - ItemsList = new ObservableCollection - { - new Issue22507Model( - "Page 1", - "Issue22507CV_Page1", - Enumerable.Range('A', 20) - .Select(c => $"Item {(char)c}") - .ToArray()), - - new Issue22507Model( - "Page 2", - "Issue22507CV_Page2", - Enumerable.Range(1, 20) - .Select(i => $"Item {i}") - .ToArray()) - }; - Grid mainGrid = new Grid - { - RowDefinitions = - { - new RowDefinition { Height = GridLength.Auto }, - new RowDefinition { Height = GridLength.Star } - } - }; - // Create CarouselView - var mainCarousel = new CarouselView - { - BackgroundColor = Colors.Yellow, - Margin = new Thickness(10), - IsBounceEnabled = true, - IsSwipeEnabled = true, - Loop = false, - ItemsSource = ItemsList, - ItemTemplate = new DataTemplate(() => - { - // Grid for each carousel item - var grid = new Grid - { - BackgroundColor = Colors.White, - Padding = 10, - RowDefinitions = - { - new RowDefinition { Height = GridLength.Auto }, - new RowDefinition { Height = GridLength.Star } - } - }; - - // Label for Title - var titleLabel = new Label - { - FontSize = 18, - TextColor = Colors.Black, - Margin = new Thickness(0, 0, 0, 10) - }; - titleLabel.SetBinding(Label.TextProperty, "Title"); - - // CollectionView for nested items - var collectionView = new CollectionView - { - BackgroundColor = Colors.LightBlue, - ItemTemplate = new DataTemplate(() => - { - var itemLabel = new Label - { - FontSize = 16, - Padding = 10 - }; - itemLabel.SetBinding(Label.TextProperty, "."); - return itemLabel; - }) - }; - collectionView.SetBinding(CollectionView.ItemsSourceProperty, "Items"); - collectionView.SetBinding(VisualElement.AutomationIdProperty, "CollectionAutomationId"); - - grid.Add(titleLabel); - grid.Add(collectionView, 0, 1); - - return grid; - }) - }; - Label label = new Label - { - Text = "Swipe vertically on the items below. CarouselView should not interfere with vertical scrolling.", - AutomationId = "Issue22507Label" - }; - mainGrid.Add(label); - Grid.SetRow(label, 0); - mainGrid.Add(mainCarousel); - Grid.SetRow(mainCarousel, 1); - - Content = mainGrid; - } -} - -public class Issue22507Model -{ - public string Title { get; set; } - public string CollectionAutomationId { get; set; } - public ObservableCollection Items { get; set; } - - public Issue22507Model(string title, string collectionAutomationId, string[] items) - { - Title = title; - CollectionAutomationId = collectionAutomationId; - Items = new ObservableCollection(items); - } -} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs deleted file mode 100644 index 6d80f602dd31..000000000000 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22507.cs +++ /dev/null @@ -1,81 +0,0 @@ -#if TEST_FAILS_ON_CATALYST // App.SwipeRightToLeft does not scroll to next item in Mac catalyst. -using NUnit.Framework; -using UITest.Appium; -using UITest.Core; - -namespace Microsoft.Maui.TestCases.Tests.Issues; - -public class Issue22507 : _IssuesUITest -{ - public Issue22507(TestDevice testDevice) : base(testDevice) - { - } - public override string Issue => "CarouselView behaves strangely when swiping vertically in view"; - - // NUnit reuses the same fixture (and the same loaded page) for all tests in this class, - // and runs them in alphabetical order. That means HandleCarouselHorizontalToVerticalScroll - // runs first and leaves the carousel on Page 2 with Page 2's CollectionView scrolled down. - // Reset to Page 1 *and* scroll both inner CollectionViews back to the top so the tests are - // independent regardless of execution order. - [SetUp] - public void ResetCarouselState() - { - App.WaitForElement("Issue22507Label"); - - // Navigate to Page 2 (no-op if we're already there because Loop = false) - // and scroll Page 2's CollectionView back to the top. - App.SwipeRightToLeft(); - App.WaitForElement("Issue22507CV_Page2"); - for (int i = 0; i < 5; i++) - { - App.ScrollUp("Issue22507CV_Page2", ScrollStrategy.Gesture, 0.99, swipeSpeed: 900); - } - - // Navigate back to Page 1 and scroll its CollectionView back to the top. - App.SwipeLeftToRight(); - App.WaitForElement("Issue22507CV_Page1"); - for (int i = 0; i < 5; i++) - { - App.ScrollUp("Issue22507CV_Page1", ScrollStrategy.Gesture, 0.99, swipeSpeed: 900); - } - } - - [Test] - [Category(UITestCategories.CarouselView)] - public void HandleCarouselVerticalToHorizontalScroll() - { - // Wait for the test page to load - App.WaitForElement("Issue22507Label"); - - // Scroll down in Page 1's CollectionView - for (int i = 0; i < 5; i++) - { - App.ScrollDown("Issue22507CV_Page1", ScrollStrategy.Gesture, 0.99, swipeSpeed: 900); - } - App.WaitForElement("Item T"); - - // Swipe horizontally to navigate to the next CarouselView item (Page 2) - App.SwipeRightToLeft(); - App.WaitForElement("Item 1"); - } - - [Test] - [Category(UITestCategories.CarouselView)] - public void HandleCarouselHorizontalToVerticalScroll() - { - // Wait for the test page to load - App.WaitForElement("Issue22507Label"); - - // Swipe horizontally to navigate to the next CarouselView item (Page 2) - App.SwipeRightToLeft(); - App.WaitForElement("Item 1"); - - // Scroll down in Page 2's CollectionView - for (int i = 0; i < 5; i++) - { - App.ScrollDown("Issue22507CV_Page2", ScrollStrategy.Gesture, 0.99, swipeSpeed: 900); - } - App.WaitForElement("Item 20"); - } -} -#endif \ No newline at end of file