diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs index 4a29b8484fc1..26e91b8b618c 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs @@ -680,6 +680,34 @@ void ClearLayoutListener() _carouselViewLayoutListener = null; } + // https://github.com/dotnet/maui/issues/13323 + // CarouselView is a full-page pager; child-initiated rectangle scroll requests + // (e.g. EditText cursor positioning) must not scroll the carousel. + public override bool RequestChildRectangleOnScreen( + global::Android.Views.View child, + global::Android.Graphics.Rect rect, + bool immediate) + { + return false; + } + + // https://github.com/dotnet/maui/issues/13323 + // base.RequestChildFocus preserves normal focus propagation, but it may + // start a focus-driven scroll from an otherwise idle CarouselView. + public override void RequestChildFocus( + global::Android.Views.View child, + global::Android.Views.View focused) + { + var wasIdleBeforeFocus = ScrollState == RecyclerView.ScrollStateIdle; + + base.RequestChildFocus(child, focused); + + if (wasIdleBeforeFocus && ScrollState != RecyclerView.ScrollStateIdle) + { + StopScroll(); + } + } + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) { // If the height or width are unbounded and the user is set to diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt index ed768b9e0ee1..974d78fabfef 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -9,6 +9,8 @@ override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void ~static Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer.DefaultTitleColor.get -> Microsoft.Maui.Graphics.Color ~static Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer.DefaultUnselectedColor.get -> Microsoft.Maui.Graphics.Color override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void +~override Microsoft.Maui.Controls.Handlers.Items.MauiCarouselRecyclerView.RequestChildFocus(Android.Views.View child, Android.Views.View focused) -> void +~override Microsoft.Maui.Controls.Handlers.Items.MauiCarouselRecyclerView.RequestChildRectangleOnScreen(Android.Views.View child, Android.Graphics.Rect rect, bool immediate) -> bool ~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.DispatchTouchEvent(Android.Views.MotionEvent e) -> bool ~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnInterceptTouchEvent(Android.Views.MotionEvent e) -> bool ~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnTouchEvent(Android.Views.MotionEvent e) -> bool diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue13323.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue13323.cs new file mode 100644 index 000000000000..1a7ed667a07a --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue13323.cs @@ -0,0 +1,126 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 13323, + "CarouselView on Android does not work if HorizontalTextAlignment in Entry is not Start", + PlatformAffected.Android)] +public class Issue13323 : ContentPage +{ + public Issue13323() + { + var items = Enumerable.Range(0, 5).ToArray(); + + var positionLabel = new Label + { + AutomationId = "PositionLabel", + Text = "Position:0", + HorizontalOptions = LayoutOptions.Center, + }; + + var carousel = new CarouselView + { + AutomationId = "CarouselView13323", + ItemsSource = items, + Loop = false, + HeightRequest = 250, + ItemTemplate = new DataTemplate(() => + { + var label = new Label + { + HeightRequest = 100, + HorizontalOptions = LayoutOptions.Fill, + VerticalOptions = LayoutOptions.Start, + BackgroundColor = Colors.LightGray, + HorizontalTextAlignment = TextAlignment.Center, + VerticalTextAlignment = TextAlignment.Center, + }; + label.SetBinding(Label.TextProperty, ".", stringFormat: "Item {0}"); + + var entry = new Entry + { + Placeholder = "Tap me", + HorizontalTextAlignment = TextAlignment.Center, + HorizontalOptions = LayoutOptions.FillAndExpand, + }; + entry.SetBinding(AutomationIdProperty, ".", stringFormat: "CenterEntry_{0}"); + + return new VerticalStackLayout + { + Children = { label, entry } + }; + }) + }; + + var goToItem2Button = new Button + { + AutomationId = "GoToItem2", + Text = "Go to Item 2", + }; + goToItem2Button.Clicked += (s, e) => carousel.ScrollTo(2, animate: false); + + carousel.PositionChanged += (s, e) => + { + positionLabel.Text = $"Position:{carousel.Position}"; + }; + + var loopPositionLabel = new Label + { + AutomationId = "LoopPositionLabel", + Text = "LoopPosition:0", + HorizontalOptions = LayoutOptions.Center, + }; + + var loopCarousel = new CarouselView + { + AutomationId = "LoopCarouselView13323", + ItemsSource = items, + Loop = true, + HeightRequest = 250, + ItemTemplate = new DataTemplate(() => + { + var label = new Label + { + HeightRequest = 100, + HorizontalOptions = LayoutOptions.Fill, + VerticalOptions = LayoutOptions.Start, + BackgroundColor = Colors.LightGray, + HorizontalTextAlignment = TextAlignment.Center, + VerticalTextAlignment = TextAlignment.Center, + }; + label.SetBinding(Label.TextProperty, ".", stringFormat: "Loop Item {0}"); + + var entry = new Entry + { + Placeholder = "Tap me", + HorizontalTextAlignment = TextAlignment.Center, + HorizontalOptions = LayoutOptions.FillAndExpand, + }; + entry.SetBinding(AutomationIdProperty, ".", stringFormat: "LoopCenterEntry_{0}"); + + return new VerticalStackLayout + { + Children = { label, entry } + }; + }) + }; + + var loopGoToItem2Button = new Button + { + AutomationId = "LoopGoToItem2", + Text = "Go to Loop Item 2", + }; + loopGoToItem2Button.Clicked += (s, e) => loopCarousel.ScrollTo(2, animate: false); + + loopCarousel.PositionChanged += (s, e) => + { + loopPositionLabel.Text = $"LoopPosition:{loopCarousel.Position}"; + }; + + Content = new ScrollView + { + Content = new VerticalStackLayout + { + Children = { positionLabel, goToItem2Button, carousel, loopPositionLabel, loopGoToItem2Button, loopCarousel } + } + }; + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue13323.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue13323.cs new file mode 100644 index 000000000000..2f3c8340efb3 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue13323.cs @@ -0,0 +1,61 @@ +using Microsoft.Maui.TestCases.Tests; +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.AppUITests.Issues; + +public class Issue13323 : _IssuesUITest +{ + public Issue13323(TestDevice device) : base(device) { } + + public override string Issue => "CarouselView on Android does not work if HorizontalTextAlignment in Entry is not Start"; + + [Test] + [Category(UITestCategories.CarouselView)] + public void CarouselView_EntryTap_DoesNotChangePosition() + { + App.WaitForElement("CarouselView13323", timeout: TimeSpan.FromSeconds(30)); + App.WaitForElement("PositionLabel", timeout: TimeSpan.FromSeconds(15)); + + App.Tap("GoToItem2"); + App.WaitForElement("CenterEntry_2", timeout: TimeSpan.FromSeconds(10)); + + var positionBefore = App.FindElement("PositionLabel").GetText(); + Assert.That(positionBefore, Is.EqualTo("Position:2"), $"CarouselView did not reach Position:2 before tapping Entry. Actual: {positionBefore}"); + + App.Tap("CenterEntry_2"); + + var positionAfter = App.FindElement("PositionLabel").GetText(); + Assert.That(positionAfter, Is.EqualTo("Position:2"), $"CarouselView jumped after tapping Center-aligned Entry. Before: {positionBefore}, After: {positionAfter}"); + + App.DismissKeyboard(); +#if ANDROID + App.WaitForKeyboardToHide(); +#endif + } + + [Test] + [Category(UITestCategories.CarouselView)] + public void CarouselView_Loop_EntryTap_DoesNotChangePosition() + { + App.WaitForElement("LoopCarouselView13323", timeout: TimeSpan.FromSeconds(30)); + App.WaitForElement("LoopPositionLabel", timeout: TimeSpan.FromSeconds(15)); + + App.Tap("LoopGoToItem2"); + App.WaitForElement("LoopCenterEntry_2", timeout: TimeSpan.FromSeconds(10)); + + var positionBefore = App.FindElement("LoopPositionLabel").GetText(); + Assert.That(positionBefore, Is.EqualTo("LoopPosition:2"), $"Loop CarouselView did not reach LoopPosition:2 before tapping Entry. Actual: {positionBefore}"); + + App.Tap("LoopCenterEntry_2"); + + var positionAfter = App.FindElement("LoopPositionLabel").GetText(); + Assert.That(positionAfter, Is.EqualTo("LoopPosition:2"), $"CarouselView (Loop=true) jumped after tapping Center-aligned Entry. Before: {positionBefore}, After: {positionAfter}"); + + App.DismissKeyboard(); +#if ANDROID + App.WaitForKeyboardToHide(); +#endif + } +}