[Android] Fix for RefreshView triggering pull-to-refresh when scrolling inside a WebView with internal scrollable content#34614
Conversation
… WebView drag gestures when internal DOM content can still scroll up.
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34614Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34614" |
There was a problem hiding this comment.
Pull request overview
This PR addresses Android-specific RefreshView behavior when wrapping a WebView whose visible scrolling happens inside an internal HTML overflow container (so native WebView scroll state can incorrectly indicate “at top”), causing pull-to-refresh to trigger too early.
Changes:
- Add an Android
WebViewJavaScript bridge + injected observer script to report whether touched DOM content can still scroll up. - Update
MauiSwipeRefreshLayoutto consult the reported “can scroll up” state forWebView(and add intercept logic for gestures starting inside aWebView). - Add a HostApp repro page and an Android UI test for issue #33510.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt | Records the new/changed Android public surface from overrides added in this PR. |
| src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs | Introduces the JS bridge + observer script and exposes TryGetCanScrollUp for RefreshView decisions. |
| src/Core/src/Platform/Android/MauiWebViewClient.cs | Resets scroll capture state on navigation start and injects the observer script on navigation finish. |
| src/Core/src/Platform/Android/MauiWebView.cs | Attaches/detaches the JS interface lifecycle to the native MauiWebView. |
| src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs | Uses the bridge-reported scrollability for WebView and adds intercept logic for gestures that start in a WebView. |
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs | Adds an Android UI test that validates pull-to-refresh doesn’t trigger until internal web scrolling reaches top. |
| src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs | Adds a HostApp issue page with a RefreshView + WebView using internal overflow scrolling to reproduce the bug. |
| _webViewOwnsGesture = _touchStartedInWebView && | ||
| RefreshViewWebViewScrollCapture.TryGetCanScrollUp(_activeTouchWebView, out var canScrollUpAtStart) && | ||
| canScrollUpAtStart; | ||
| break; |
There was a problem hiding this comment.
OnInterceptTouchEvent decides WebView vs RefreshView ownership only on MotionEventActions.Down (via _webViewOwnsGesture). If the inner web content scrolls to the top during the same drag, RefreshView will never get a chance to intercept until the user lifts and starts a new gesture, which doesn’t match SwipeRefreshLayout’s typical behavior and the PR description (“once ... reaches the top, RefreshView is allowed to intercept”). Consider re-evaluating TryGetCanScrollUp on Move events and clearing _webViewOwnsGesture when canScrollUp becomes false so the base implementation can start intercepting mid-gesture.
| break; | |
| break; | |
| case MotionEventActions.Move: | |
| // Re-evaluate scrollability while moving so that once the WebView | |
| // reaches the top, the RefreshLayout can start intercepting. | |
| if (_touchStartedInWebView && _webViewOwnsGesture && _activeTouchWebView is not null) | |
| { | |
| if (!RefreshViewWebViewScrollCapture.TryGetCanScrollUp(_activeTouchWebView, out var canStillScrollUp) || !canStillScrollUp) | |
| { | |
| _webViewOwnsGesture = false; | |
| } | |
| } | |
| break; |
| handler.SyncPlatformCookiesToVirtualView(url); | ||
|
|
||
| handler?.PlatformView.UpdateCanGoBackForward(handler.VirtualView); | ||
| RefreshViewWebViewScrollCapture.InjectObserver(view); | ||
|
|
||
| base.OnPageFinished(view, url); |
There was a problem hiding this comment.
RefreshViewWebViewScrollCapture.InjectObserver(view) is now executed for every OnPageFinished on every MauiWebView. This injects a script which installs capture-phase touchstart/touchmove listeners and runs DOM/style queries on user input, which is a cross-cutting behavioral/perf change even for WebViews not inside a RefreshView. Please gate this injection so it only runs when the WebView is actually hosted inside a RefreshView/SwipeRefreshLayout (e.g., only attach/inject when RefreshView content contains a WebView, or mark such WebViews via a tag and check it here).
| public MauiWebView(WebViewHandler handler, Context context) : base(context) | ||
| { | ||
| _handler = handler ?? throw new ArgumentNullException(nameof(handler)); | ||
| RefreshViewWebViewScrollCapture.Attach(this); | ||
| } |
There was a problem hiding this comment.
RefreshViewWebViewScrollCapture.Attach(this) runs for every MauiWebView instance, meaning every WebView gets a new JavaScript interface (mauiRefreshViewHost) regardless of whether it’s used inside a RefreshView. Even though the exposed surface is small, this still changes global JS environment/interop for all WebViews and pairs with the unconditional observer injection. Consider only attaching this interface when a WebView is detected inside a RefreshView (e.g., from RefreshViewHandler/MauiSwipeRefreshLayout.UpdateContent) so normal WebView usage remains unaffected.
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Issue Details
When using a RefreshView that wraps a WebView in a .NET MAUI app on Android, the pull-to-refresh gesture is triggered as soon as the user scrolls up, even if the WebView content has not reached the top. This prevents normal upward scrolling through web content without accidentally refreshing the page.
Root Cause
The issue occurs because of how Android RefreshView determines whether to intercept a downward drag gesture. For a WebView, this decision is based on the native scroll state (ScrollY / CanScrollVertically(-1)).
In this scenario, the visible content is not scrolled by the native WebView itself, but by an internal HTML container (overflow-y: auto). While this internal DOM element is still mid-scroll, the native WebView may incorrectly report that it is already at the top.
As a result, RefreshView intercepts the gesture too early, triggering pull-to-refresh instead of allowing the web content to continue scrolling.
Description of Change
The fix involves adding Android-specific handling for the WebView + RefreshView interaction.
A lightweight WebView bridge is introduced to determine whether the touched DOM content can still scroll upward. MauiSwipeRefreshLayout uses this information when a gesture starts inside a WebView:
This approach preserves existing RefreshView behavior for other controls, while correctly handling the WebView scenario that native Android scroll checks cannot accurately detect.
Validated the behavior in the following platforms
Issues Fixed
Fixes #33510
Output ScreenShot
BeforeFix-33510.mov
AfterFix-33510.mov