diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ScrollToAsyncFromOnAppearingWorks.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ScrollToAsyncFromOnAppearingWorks.png new file mode 100644 index 000000000000..09f876c62ede Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ScrollToAsyncFromOnAppearingWorks.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue31177.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue31177.cs new file mode 100644 index 000000000000..131e6ccf0178 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue31177.cs @@ -0,0 +1,62 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 31177, "ScrollView ScrollToAsync does not work when called from Page OnAppearing", PlatformAffected.All)] +public class Issue31177 : ContentPage +{ + readonly ScrollView _scrollView; + readonly Label _statusLabel; + public Issue31177() + { + _statusLabel = new Label + { + Text = "The test passes if the text shown below says 'ScrollToAsync Succeeded'.", + AutomationId = "StatusLabel" + }; + + var tallSpacer = new BoxView + { + HeightRequest = 2000, + Color = Colors.LightGray + }; + + var successLabel = new Label + { + Text = "ScrollToAsync Succeeded", + AutomationId = "SuccessLabel" + }; + + _scrollView = new ScrollView + { + AutomationId = "TheScrollView", + Content = new VerticalStackLayout + { + Children = { tallSpacer, successLabel } + } + }; + + Content = new Grid + { + RowDefinitions = + { + new RowDefinition(GridLength.Auto), + new RowDefinition(GridLength.Star) + }, + Children = + { + _statusLabel, + _scrollView + } + }; + + Grid.SetRow(_statusLabel, 0); + Grid.SetRow(_scrollView, 1); + } + + protected override async void OnAppearing() + { + base.OnAppearing(); + + // This is the scenario from the issue: ScrollToAsync called from OnAppearing + await _scrollView.ScrollToAsync(0, 2000, false); + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31177.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31177.cs new file mode 100644 index 000000000000..288cf468990d --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31177.cs @@ -0,0 +1,24 @@ +#if TEST_FAILS_ON_CATALYST //In Catalyst, `ScrollDown` isn't functioning correctly with Appium. +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue31177 : _IssuesUITest +{ + public override string Issue => "ScrollView ScrollToAsync does not work when called from Page OnAppearing"; + + public Issue31177(TestDevice device) : base(device) + { + } + + [Test] + [Category(UITestCategories.ScrollView)] + public void ScrollToAsyncFromOnAppearingWorks() + { + App.WaitForElement("SuccessLabel"); + VerifyScreenshot(); + } +} +#endif diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ScrollToAsyncFromOnAppearingWorks.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ScrollToAsyncFromOnAppearingWorks.png new file mode 100644 index 000000000000..adee723e049b Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ScrollToAsyncFromOnAppearingWorks.png differ diff --git a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Android.cs b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Android.cs index f10875ee339f..8e24bc8f0daa 100644 --- a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Android.cs +++ b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Android.cs @@ -161,6 +161,16 @@ public static void MapRequestScrollTo(IScrollViewHandler handler, IScrollView sc return; } + if (!handler.PlatformView.IsLaidOut || handler.PlatformView.IsLayoutRequested) + { + handler.PlatformView.Post(() => + { + if (handler.IsConnected()) + MapRequestScrollTo(handler, scrollView, args); + }); + return; + } + var horizontalOffsetDevice = (int)context.ToPixels(request.HorizontalOffset); var verticalOffsetDevice = (int)context.ToPixels(request.VerticalOffset); diff --git a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Windows.cs b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Windows.cs index 40cb099774c4..333003de038a 100644 --- a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Windows.cs +++ b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.Windows.cs @@ -15,6 +15,9 @@ public partial class ScrollViewHandler : ViewHandler, { const string ContentPanelTag = "MAUIScrollViewContentPanel"; + // Stores a scroll request that arrived before the content was laid out. + internal ScrollToRequest? PendingScrollToRequest { get; private set; } + protected override ScrollViewer CreatePlatformView() { return new ScrollViewer(); @@ -46,6 +49,27 @@ protected override void DisconnectHandler(ScrollViewer platformView) VirtualView?.PresentedContent?.Handler?.DisconnectHandler(); base.DisconnectHandler(platformView); platformView.ViewChanged -= ViewChanged; + + if (PendingScrollToRequest is not null) + { + if (GetContentPanel(platformView) is { } contentPanel) + { + contentPanel.SizeChanged -= OnContentPanelSizeChanged; + } + + VirtualView?.ScrollFinished(); + PendingScrollToRequest = null; + } + } + + void OnContentPanelSizeChanged(object sender, SizeChangedEventArgs e) + { + ((FrameworkElement)sender).SizeChanged -= OnContentPanelSizeChanged; + if (PendingScrollToRequest is { } pending) + { + PendingScrollToRequest = null; + MapRequestScrollTo(this, VirtualView, pending); + } } public static void MapContent(IScrollViewHandler handler, IScrollView scrollView) @@ -84,13 +108,30 @@ public static void MapRequestScrollTo(IScrollViewHandler handler, IScrollView sc { if (args is ScrollToRequest request) { + if (handler is ScrollViewHandler scrollViewHandler) + { + bool notMeasuredYet = (request.VerticalOffset > 0 && handler.PlatformView.ScrollableHeight == 0 && handler.PlatformView.ExtentHeight == 0) || + (request.HorizontalOffset > 0 && handler.PlatformView.ScrollableWidth == 0 && handler.PlatformView.ExtentWidth == 0); + if (notMeasuredYet) + { + // Defer until ContentPanel.SizeChanged fires (after WinUI arrange pass). + // Only subscribe once; overwrite the pending request so the latest wins. + if (scrollViewHandler.PendingScrollToRequest is null && GetContentPanel(handler.PlatformView) is { } contentPanel) + { + contentPanel.SizeChanged += scrollViewHandler.OnContentPanelSizeChanged; + } + scrollViewHandler.PendingScrollToRequest = request; + return; + } + } + var targetHorizontalOffset = Math.Clamp(request.HorizontalOffset, 0, handler.PlatformView.ScrollableWidth); var targetVerticalOffset = Math.Clamp(request.VerticalOffset, 0, handler.PlatformView.ScrollableHeight); if (targetVerticalOffset == handler.PlatformView.VerticalOffset && targetHorizontalOffset == handler.PlatformView.HorizontalOffset) { - handler.VirtualView.ScrollFinished(); - return; + handler.VirtualView.ScrollFinished(); + return; } handler.PlatformView.ChangeView(targetHorizontalOffset, targetVerticalOffset, null, request.Instant); diff --git a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs index 290576e299c9..05daf0c17cf7 100644 --- a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs +++ b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs @@ -48,10 +48,23 @@ protected override void DisconnectHandler(UIScrollView platformView) base.DisconnectHandler(platformView); - PendingScrollToRequest = null; + if (PendingScrollToRequest is not null) + { + VirtualView?.ScrollFinished(); + PendingScrollToRequest = null; + } _eventProxy.Disconnect(platformView); } + internal void ProcessPendingScrollRequest() + { + if (PendingScrollToRequest is { } pending) + { + MapRequestScrollTo(this, VirtualView, pending); + PendingScrollToRequest = null; + } + } + public static void MapContent(IScrollViewHandler handler, IScrollView scrollView) { if (handler.PlatformView == null || handler.MauiContext == null) diff --git a/src/Core/src/Platform/iOS/MauiScrollView.cs b/src/Core/src/Platform/iOS/MauiScrollView.cs index 5362b2bf6c8d..03742e67af04 100644 --- a/src/Core/src/Platform/iOS/MauiScrollView.cs +++ b/src/Core/src/Platform/iOS/MauiScrollView.cs @@ -341,6 +341,13 @@ public override void LayoutSubviews() // in case the ScrollView is configured to grow/shrink with its content. this.InvalidateAncestorsMeasures(); } + + // Now that layout is complete and ContentSize is set, process any pending scroll request + // that was deferred because ContentSize was empty when the request arrived. + if (ContentSize != CGSize.Empty && CrossPlatformLayout is ScrollViewHandler scrollViewHandler) + { + scrollViewHandler.ProcessPendingScrollRequest(); + } } base.LayoutSubviews();