Date: Tue, 24 Mar 2026 16:08:36 +0530
Subject: [PATCH 06/12] fix-33510: Made code changes to prevent RefreshView
from intercepting WebView drag gestures when internal DOM content can still
scroll up.
---
.../TestCases.HostApp/Issues/Issue33510.cs | 225 ++++++++++++++++++
.../Tests/Issues/Issue33510.cs | 128 ++++++++++
.../Android/MauiSwipeRefreshLayout.cs | 62 ++++-
src/Core/src/Platform/Android/MauiWebView.cs | 29 +++
.../src/Platform/Android/MauiWebViewClient.cs | 3 +
.../RefreshViewWebViewScrollCapture.cs | 166 +++++++++++++
.../net-android/PublicAPI.Unshipped.txt | 3 +
7 files changed, 615 insertions(+), 1 deletion(-)
create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs
create mode 100644 src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs
create mode 100644 src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs
new file mode 100644
index 000000000000..8e575d8ae768
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs
@@ -0,0 +1,225 @@
+using System.Text;
+
+namespace Maui.Controls.Sample.Issues;
+
+[Issue(IssueTracker.Github, 33510, "[Android] RefreshView triggers pull-to-refresh immediately when scrolling up inside a WebView", PlatformAffected.Android)]
+public class Issue33510 : TestContentPage
+{
+ WebView _webView;
+ RefreshView _refreshView;
+ Label _statusLabel;
+ Label _scrollTopLabel;
+ bool _isWebViewLoaded;
+
+ protected override void Init()
+ {
+ Title = "RefreshView + WebView";
+
+ _statusLabel = new Label
+ {
+ AutomationId = "StatusLabel",
+ Text = "Loading WebView..."
+ };
+
+ _scrollTopLabel = new Label
+ {
+ AutomationId = "ScrollTopLabel",
+ Text = "ScrollTop: unavailable"
+ };
+
+ var scrollWebViewButton = new Button
+ {
+ AutomationId = "ScrollWebViewButton",
+ Text = "Scroll down in WebView"
+ };
+
+ scrollWebViewButton.Clicked += async (_, _) =>
+ {
+ if (!_isWebViewLoaded)
+ {
+ return;
+ }
+
+ var result = await _webView.EvaluateJavaScriptAsync("window.scrollInnerContainerTo(900);");
+ _scrollTopLabel.Text = $"ScrollTop: {NormalizeJavaScriptNumber(result)}";
+ };
+
+ var readScrollTopButton = new Button
+ {
+ AutomationId = "ReadScrollTopButton",
+ Text = "Read WebView scroll position"
+ };
+
+ readScrollTopButton.Clicked += async (_, _) => await UpdateScrollStatusAsync();
+
+ var scrollWebViewToTopButton = new Button
+ {
+ AutomationId = "ScrollWebViewToTopButton",
+ Text = "Scroll WebView to top"
+ };
+
+ scrollWebViewToTopButton.Clicked += async (_, _) =>
+ {
+ if (!_isWebViewLoaded)
+ {
+ return;
+ }
+
+ var result = await _webView.EvaluateJavaScriptAsync("window.scrollInnerContainerTo(0);");
+ _scrollTopLabel.Text = $"ScrollTop: {NormalizeJavaScriptNumber(result)}";
+ };
+
+ _webView = new WebView
+ {
+ AutomationId = "TestWebView",
+ };
+
+ _webView.Navigated += async (_, _) =>
+ {
+ _isWebViewLoaded = true;
+ _statusLabel.Text = "WebView ready. Scroll down, then drag downward inside the WebView.";
+ await UpdateScrollStatusAsync();
+ };
+
+ _webView.Source = new HtmlWebViewSource
+ {
+ Html = CreateHtml()
+ };
+
+ var webViewContainer = new ContentView
+ {
+ AutomationId = "TestWebViewContainer",
+ HorizontalOptions = LayoutOptions.Fill,
+ VerticalOptions = LayoutOptions.Fill,
+ Content = _webView
+ };
+
+ _refreshView = new RefreshView
+ {
+ AutomationId = "TestRefreshView",
+ Content = webViewContainer
+ };
+
+ _refreshView.Command = new Command(async () =>
+ {
+ _statusLabel.Text = "Refresh triggered";
+
+ await Task.Delay(150);
+ _refreshView.IsRefreshing = false;
+
+ await UpdateScrollStatusAsync();
+ });
+
+ var controlsLayout = new VerticalStackLayout
+ {
+ Padding = new Thickness(12),
+ Spacing = 8,
+ Children =
+ {
+ new Label
+ {
+ Text = "This page reproduces issue #33510. On Android, RefreshView should not refresh while the WebView can still scroll upward internally."
+ },
+ _statusLabel,
+ _scrollTopLabel,
+ scrollWebViewButton,
+ scrollWebViewToTopButton,
+ readScrollTopButton
+ }
+ };
+
+ var separator = new BoxView
+ {
+ HeightRequest = 1,
+ Color = Colors.LightGray
+ };
+
+ var grid = new Grid
+ {
+ RowDefinitions =
+ {
+ new RowDefinition { Height = GridLength.Auto },
+ new RowDefinition { Height = GridLength.Auto },
+ new RowDefinition { Height = GridLength.Star }
+ }
+ };
+
+ grid.Add(controlsLayout);
+ Grid.SetRow(controlsLayout, 0);
+
+ grid.Add(separator);
+ Grid.SetRow(separator, 1);
+
+ grid.Add(_refreshView);
+ Grid.SetRow(_refreshView, 2);
+
+ Content = grid;
+ }
+
+ async Task UpdateScrollStatusAsync()
+ {
+ if (!_isWebViewLoaded)
+ {
+ _scrollTopLabel.Text = "ScrollTop: unavailable";
+ return;
+ }
+
+ var result = await _webView.EvaluateJavaScriptAsync("window.getInnerScrollTop();");
+ _scrollTopLabel.Text = $"ScrollTop: {NormalizeJavaScriptNumber(result)}";
+ }
+
+ static string NormalizeJavaScriptNumber(string result)
+ {
+ if (string.IsNullOrWhiteSpace(result))
+ return "unknown";
+
+ return result.Trim().Trim('"');
+ }
+
+ static string CreateHtml()
+ {
+ var rows = string.Join(Environment.NewLine, Enumerable.Range(1, 30).Select(index =>
+ $"Scrollable row {index}
"));
+
+ var html = new StringBuilder();
+ html.AppendLine("");
+ html.AppendLine("");
+ html.AppendLine("");
+ html.AppendLine("");
+ html.AppendLine("");
+ html.AppendLine("");
+ html.AppendLine("");
+ html.AppendLine("");
+ html.AppendLine("");
+ html.AppendLine("");
+ html.AppendLine("");
+ html.AppendLine("");
+
+ return html.ToString();
+ }
+}
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs
new file mode 100644
index 000000000000..8aa2e6ea261c
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs
@@ -0,0 +1,128 @@
+#if ANDROID // This test is Android-only because Issue #33510 (RefreshView triggering pull-to-refresh when scrolling inside a WebView) only reproduces on Android, and the test uses Android-specific Appium touch gesture APIs.
+using System.Globalization;
+using System.Text.RegularExpressions;
+using NUnit.Framework;
+using OpenQA.Selenium.Appium.Interactions;
+using OpenQA.Selenium.Interactions;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+
+[Category(UITestCategories.RefreshView)]
+public class Issue33510 : _IssuesUITest
+{
+ const string StatusLabel = "StatusLabel";
+ const string ScrollWebViewButton = "ScrollWebViewButton";
+ const string ScrollWebViewToTopButton = "ScrollWebViewToTopButton";
+ const string ReadScrollTopButton = "ReadScrollTopButton";
+ const string ScrollTopLabel = "ScrollTopLabel";
+ const string TestWebViewContainer = "TestWebViewContainer";
+
+ public Issue33510(TestDevice device) : base(device)
+ {
+ }
+
+ public override string Issue => "[Android] RefreshView triggers pull-to-refresh immediately when scrolling up inside a WebView";
+
+ protected override bool ResetAfterEachTest => true;
+
+ [Test]
+ public void PullToRefreshWaitsUntilInternalWebViewContainerReachesTop()
+ {
+ var androidApp = WaitForAndroidApp();
+ var webViewRect = App.WaitForElement(TestWebViewContainer).GetRect();
+ var x = webViewRect.CenterX();
+ var upwardFromY = webViewRect.Y + (webViewRect.Height * 75 / 100);
+ var upwardToY = webViewRect.Y + (webViewRect.Height * 30 / 100);
+
+ for (var attempt = 0; attempt < 3 && GetScrollTop() <= 200; attempt++)
+ {
+ DragInsideWebView(androidApp, x, upwardFromY, x, upwardToY);
+ }
+
+ var initialScrollTop = GetScrollTop();
+ Assert.That(initialScrollTop, Is.GreaterThan(200), "The inner HTML container must start away from the top.");
+
+ Assert.That(GetStatus(), Does.Not.Contain("Refresh triggered"));
+
+ var fromY = webViewRect.Y + (webViewRect.Height * 35 / 100);
+ var toY = webViewRect.Y + (webViewRect.Height * 70 / 100);
+
+ DragInsideWebView(androidApp, x, fromY, x, toY);
+
+ var scrollTopAfterGesture = GetScrollTop();
+ Assert.That(scrollTopAfterGesture, Is.LessThan(initialScrollTop), "Dragging down inside the WebView should scroll the inner container upward.");
+ Assert.That(scrollTopAfterGesture, Is.GreaterThan(0), "The inner container should still be away from the top after a partial upward scroll.");
+ Assert.That(GetStatus(), Does.Not.Contain("Refresh triggered"));
+ }
+
+ [Test]
+ public void PullToRefreshStillWorksWhenInternalWebViewContainerStartsAtTop()
+ {
+ var androidApp = WaitForAndroidApp();
+
+ Assert.That(GetScrollTop(), Is.LessThan(1), "The inner HTML container should start at the top before pull-to-refresh begins.");
+
+ var webViewRect = App.WaitForElement(TestWebViewContainer).GetRect();
+ var x = webViewRect.CenterX();
+ var fromY = webViewRect.Y + (webViewRect.Height * 30 / 100);
+ var toY = webViewRect.Y + (webViewRect.Height * 85 / 100);
+
+ DragInsideWebView(androidApp, x, fromY, x, toY);
+
+ Assert.That(
+ App.WaitForTextToBePresentInElement(StatusLabel, "Refresh triggered", timeout: TimeSpan.FromSeconds(10)),
+ Is.True,
+ "Pulling down inside the WebView at scroll top should still trigger RefreshView.");
+ }
+
+ AppiumAndroidApp WaitForAndroidApp()
+ {
+ if (App is not AppiumAndroidApp androidApp)
+ {
+ Assert.Ignore("Issue #33510 is Android-specific.");
+ return null!;
+ }
+
+ Assert.That(
+ App.WaitForTextToBePresentInElement(StatusLabel, "WebView ready", timeout: TimeSpan.FromSeconds(30)),
+ Is.True,
+ "WebView never finished loading.");
+
+ return androidApp;
+ }
+
+ double GetScrollTop()
+ {
+ App.Tap(ReadScrollTopButton);
+ Assert.That(
+ App.WaitForTextToBePresentInElement(ScrollTopLabel, "ScrollTop:", timeout: TimeSpan.FromSeconds(5)),
+ Is.True,
+ "Scroll position was not reported.");
+
+ var status = App.FindElement(ScrollTopLabel).GetText() ?? string.Empty;
+ var match = Regex.Match(status, @"(\d+(\.\d+)?)");
+
+ Assert.That(match.Success, Is.True, $"Could not parse scroll position from '{status}'.");
+
+ return double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
+ }
+
+ string GetStatus() =>
+ App.FindElement(StatusLabel).GetText() ?? string.Empty;
+
+ static void DragInsideWebView(AppiumAndroidApp androidApp, int fromX, int fromY, int toX, int toY)
+ {
+ var touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch);
+ var dragSequence = new ActionSequence(touchDevice, 0);
+
+ dragSequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, fromX, fromY, TimeSpan.Zero));
+ dragSequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact));
+ dragSequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, toX, toY, TimeSpan.FromMilliseconds(450)));
+ dragSequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact));
+
+ androidApp.Driver.PerformActions([dragSequence]);
+ }
+}
+#endif
diff --git a/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs b/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
index 1d2553dde335..284ecb5270de 100644
--- a/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
+++ b/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
@@ -19,6 +19,9 @@ public class MauiSwipeRefreshLayout : SwipeRefreshLayout
readonly Context _context;
AView? _contentView;
bool _refreshEnabled = true;
+ AWebView? _activeTouchWebView;
+ bool _webViewOwnsGesture;
+ bool _touchStartedInWebView;
public MauiSwipeRefreshLayout(Context context) : base(context)
{
@@ -135,6 +138,36 @@ public override bool CanChildScrollUp()
return CanScrollUp(_contentView);
}
+ public override bool OnInterceptTouchEvent(MotionEvent? ev)
+ {
+ if (ev is null)
+ return false;
+
+ switch (ev.ActionMasked)
+ {
+ case MotionEventActions.Down:
+ _activeTouchWebView = FindWebView(_contentView, ev.GetX(), ev.GetY());
+ _touchStartedInWebView = _activeTouchWebView is not null;
+ _webViewOwnsGesture = _touchStartedInWebView &&
+ RefreshViewWebViewScrollCapture.TryGetCanScrollUp(_activeTouchWebView, out var canScrollUpAtStart) &&
+ canScrollUpAtStart;
+ break;
+ case MotionEventActions.Cancel:
+ case MotionEventActions.Up:
+ _activeTouchWebView = null;
+ _touchStartedInWebView = false;
+ _webViewOwnsGesture = false;
+ break;
+ }
+
+ if (_touchStartedInWebView && _webViewOwnsGesture)
+ {
+ return false;
+ }
+
+ return base.OnInterceptTouchEvent(ev);
+ }
+
bool CanScrollUp(AView? view)
{
if (!(view is ViewGroup viewGroup))
@@ -182,9 +215,36 @@ static bool CanScrollUpViewByType(AView? view)
#pragma warning restore XAOBS001 // Obsolete
if (view is AWebView webView)
- return webView.ScrollY > 0;
+ return RefreshViewWebViewScrollCapture.TryGetCanScrollUp(webView, out var canScrollUp) && canScrollUp;
return true;
}
+
+ static AWebView? FindWebView(AView? view, float x, float y)
+ {
+ if (view is null || view.Visibility != ViewStates.Visible)
+ return null;
+
+ if (x < view.Left || x > view.Right || y < view.Top || y > view.Bottom)
+ return null;
+
+ if (view is AWebView)
+ return (AWebView)view;
+
+ if (view is not ViewGroup viewGroup)
+ return null;
+
+ var localX = x - view.Left;
+ var localY = y - view.Top;
+
+ for (int i = viewGroup.ChildCount - 1; i >= 0; i--)
+ {
+ var webView = FindWebView(viewGroup.GetChildAt(i), localX, localY);
+ if (webView is not null)
+ return webView;
+ }
+
+ return null;
+ }
}
}
diff --git a/src/Core/src/Platform/Android/MauiWebView.cs b/src/Core/src/Platform/Android/MauiWebView.cs
index 7e9019100caf..13b2c3621d8a 100644
--- a/src/Core/src/Platform/Android/MauiWebView.cs
+++ b/src/Core/src/Platform/Android/MauiWebView.cs
@@ -35,6 +35,27 @@ protected override void OnAttachedToWindow()
// Re-evaluate ClipBounds when re-parented (e.g., wrapped in WrapperView for shadow)
UpdateClipBounds(Width, Height);
+
+ if (IsInsideSwipeRefreshLayout())
+ RefreshViewWebViewScrollCapture.Attach(this);
+ }
+
+ protected override void OnDetachedFromWindow()
+ {
+ RefreshViewWebViewScrollCapture.Detach(this);
+ base.OnDetachedFromWindow();
+ }
+
+ bool IsInsideSwipeRefreshLayout()
+ {
+ var parent = Parent;
+ while (parent is not null)
+ {
+ if (parent is MauiSwipeRefreshLayout)
+ return true;
+ parent = parent.Parent;
+ }
+ return false;
}
void UpdateClipBounds(int width, int height)
@@ -86,5 +107,13 @@ void IWebViewDelegate.LoadUrl(string? url)
LoadUrl(url ?? string.Empty);
}
}
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ RefreshViewWebViewScrollCapture.Detach(this);
+
+ base.Dispose(disposing);
+ }
}
}
\ No newline at end of file
diff --git a/src/Core/src/Platform/Android/MauiWebViewClient.cs b/src/Core/src/Platform/Android/MauiWebViewClient.cs
index b24f3ab6aa13..abaac0f770ab 100644
--- a/src/Core/src/Platform/Android/MauiWebViewClient.cs
+++ b/src/Core/src/Platform/Android/MauiWebViewClient.cs
@@ -22,6 +22,8 @@ public override bool ShouldOverrideUrlLoading(WebView? view, IWebResourceRequest
public override void OnPageStarted(WebView? view, string? url, Bitmap? favicon)
{
+ RefreshViewWebViewScrollCapture.Reset(view);
+
if (!_handler.TryGetTarget(out var handler) || handler.VirtualView == null)
return;
@@ -64,6 +66,7 @@ public override void OnPageFinished(WebView? view, string? url)
handler.SyncPlatformCookiesToVirtualView(url);
handler?.PlatformView.UpdateCanGoBackForward(handler.VirtualView);
+ RefreshViewWebViewScrollCapture.InjectObserver(view);
base.OnPageFinished(view, url);
}
diff --git a/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
new file mode 100644
index 000000000000..83497c730377
--- /dev/null
+++ b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
@@ -0,0 +1,166 @@
+using Android.Webkit;
+using Java.Interop;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Maui.Platform;
+
+internal static class RefreshViewWebViewScrollCapture
+{
+ const string JavaScriptInterfaceName = "mauiRefreshViewHost";
+ const int ScrollCaptureStateKey = 0x4D415549;
+
+ const string ObserverScript =
+ """
+ (function () {
+ if (window.__mauiRefreshViewObserverInstalled) {
+ return;
+ }
+
+ var host = window.mauiRefreshViewHost;
+ if (!host || typeof host.setCanScrollUp !== 'function') {
+ return;
+ }
+
+ window.__mauiRefreshViewObserverInstalled = true;
+
+ function isScrollableElement(node) {
+ if (!node || node.nodeType !== Node.ELEMENT_NODE) {
+ return false;
+ }
+
+ var style = window.getComputedStyle(node);
+ var overflowY = style ? style.overflowY : '';
+ return (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') &&
+ node.scrollHeight > node.clientHeight + 1;
+ }
+
+ function getScrollableElement(startNode) {
+ for (var node = startNode; node && node.nodeType === Node.ELEMENT_NODE; node = node.parentElement) {
+ if (isScrollableElement(node)) {
+ return node;
+ }
+ }
+
+ return document.scrollingElement || document.documentElement || document.body;
+ }
+
+ function getScrollTopForElement(element) {
+ if (!element) {
+ return 0;
+ }
+
+ if (element === document.body || element === document.documentElement || element === document.scrollingElement) {
+ return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
+ }
+
+ return element.scrollTop || 0;
+ }
+
+ function report(target) {
+ try {
+ var scrollable = getScrollableElement(target);
+ host.setCanScrollUp(getScrollTopForElement(scrollable) > 0);
+ } catch (e) {
+ }
+ }
+
+ document.addEventListener('touchstart', function (event) {
+ report(event.target);
+ }, true);
+
+ document.addEventListener('touchmove', function (event) {
+ report(event.target);
+ }, true);
+
+ report(document.body);
+ })();
+ """;
+
+ internal static void Attach(WebView webView)
+ {
+ if (GetState(webView) is not null)
+ return;
+
+ var state = new ScrollCaptureState();
+ webView.SetTag(ScrollCaptureStateKey, state);
+ webView.AddJavascriptInterface(state, JavaScriptInterfaceName);
+ }
+
+ internal static void Detach(WebView? webView)
+ {
+ if (webView is null)
+ return;
+
+ if (GetState(webView) is not ScrollCaptureState state)
+ return;
+
+ webView.RemoveJavascriptInterface(JavaScriptInterfaceName);
+ webView.SetTag(ScrollCaptureStateKey, null);
+ state.Dispose();
+ }
+
+ internal static void Reset(WebView? webView)
+ {
+ if (GetState(webView) is ScrollCaptureState state)
+ state.Reset();
+ }
+
+ internal static void InjectObserver(WebView? webView)
+ {
+ if (webView is null)
+ return;
+
+ webView.EvaluateJavascript(ObserverScript, null);
+ }
+
+ internal static bool TryGetCanScrollUp(WebView? webView, out bool canScrollUp)
+ {
+ if (webView is null)
+ {
+ canScrollUp = false;
+ return false;
+ }
+
+ var nativeCanScrollUp = webView.CanScrollVertically(-1) || webView.ScrollY > 0;
+
+ if (GetState(webView) is ScrollCaptureState state && state.HasReportedState)
+ {
+ canScrollUp = state.CanScrollUp || nativeCanScrollUp;
+ return true;
+ }
+
+ if (nativeCanScrollUp)
+ {
+ canScrollUp = true;
+ return true;
+ }
+
+ canScrollUp = false;
+ return false;
+ }
+
+ static ScrollCaptureState? GetState(WebView? webView) =>
+ webView?.GetTag(ScrollCaptureStateKey) as ScrollCaptureState;
+
+ sealed class ScrollCaptureState : Java.Lang.Object
+ {
+ internal bool CanScrollUp { get; private set; }
+
+ internal bool HasReportedState { get; private set; }
+
+ [JavascriptInterface]
+ [RequiresUnreferencedCode("Java.Interop.Export uses dynamic features.")]
+ [Export("setCanScrollUp")]
+ public void SetCanScrollUp(bool canScrollUp)
+ {
+ CanScrollUp = canScrollUp;
+ HasReportedState = true;
+ }
+
+ internal void Reset()
+ {
+ CanScrollUp = false;
+ HasReportedState = false;
+ }
+ }
+}
diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
index 59fed41b6da4..44cc4b54cd5f 100644
--- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
@@ -16,3 +16,6 @@ override Microsoft.Maui.Platform.MauiHybridWebView.OnAttachedToWindow() -> void
override Microsoft.Maui.Platform.MauiHybridWebView.OnSizeChanged(int width, int height, int oldWidth, int oldHeight) -> void
override Microsoft.Maui.Platform.MauiWebView.OnAttachedToWindow() -> void
override Microsoft.Maui.Platform.MauiWebView.OnSizeChanged(int width, int height, int oldWidth, int oldHeight) -> void
+override Microsoft.Maui.Platform.MauiSwipeRefreshLayout.OnInterceptTouchEvent(Android.Views.MotionEvent? ev) -> bool
+override Microsoft.Maui.Platform.MauiWebView.Dispose(bool disposing) -> void
+override Microsoft.Maui.Platform.MauiWebView.OnDetachedFromWindow() -> void
From ae07f1e9790021c805ad5c9750b84fb4deebca84 Mon Sep 17 00:00:00 2001
From: BagavathiPerumal
Date: Fri, 27 Mar 2026 18:25:44 +0530
Subject: [PATCH 07/12] fix-33510-Changes updated.
---
.../Android/MauiSwipeRefreshLayout.cs | 26 +++++++++++++++----
.../src/Platform/Android/MauiWebViewClient.cs | 6 ++++-
.../RefreshViewWebViewScrollCapture.cs | 2 ++
3 files changed, 28 insertions(+), 6 deletions(-)
diff --git a/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs b/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
index 284ecb5270de..3e3edac88e88 100644
--- a/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
+++ b/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
@@ -151,6 +151,27 @@ public override bool OnInterceptTouchEvent(MotionEvent? ev)
_webViewOwnsGesture = _touchStartedInWebView &&
RefreshViewWebViewScrollCapture.TryGetCanScrollUp(_activeTouchWebView, out var canScrollUpAtStart) &&
canScrollUpAtStart;
+ if (_webViewOwnsGesture)
+ {
+ // Forward to base so SwipeRefreshLayout records the initial pointer ID
+ // and Y position – required for correct mid-gesture intercept if the
+ // web content scrolls to the top during the same drag.
+ base.OnInterceptTouchEvent(ev);
+ return false;
+ }
+ break;
+ case MotionEventActions.Move:
+ // Re-evaluate scrollability so that once the WebView reaches the top,
+ // RefreshLayout can start intercepting mid-gesture.
+ if (_touchStartedInWebView && _webViewOwnsGesture && _activeTouchWebView is not null)
+ {
+ if (!RefreshViewWebViewScrollCapture.TryGetCanScrollUp(_activeTouchWebView, out var canStillScrollUp) || !canStillScrollUp)
+ {
+ _webViewOwnsGesture = false;
+ }
+ }
+ if (_touchStartedInWebView && _webViewOwnsGesture)
+ return false;
break;
case MotionEventActions.Cancel:
case MotionEventActions.Up:
@@ -160,11 +181,6 @@ public override bool OnInterceptTouchEvent(MotionEvent? ev)
break;
}
- if (_touchStartedInWebView && _webViewOwnsGesture)
- {
- return false;
- }
-
return base.OnInterceptTouchEvent(ev);
}
diff --git a/src/Core/src/Platform/Android/MauiWebViewClient.cs b/src/Core/src/Platform/Android/MauiWebViewClient.cs
index abaac0f770ab..a5b42375e31f 100644
--- a/src/Core/src/Platform/Android/MauiWebViewClient.cs
+++ b/src/Core/src/Platform/Android/MauiWebViewClient.cs
@@ -66,7 +66,11 @@ public override void OnPageFinished(WebView? view, string? url)
handler.SyncPlatformCookiesToVirtualView(url);
handler?.PlatformView.UpdateCanGoBackForward(handler.VirtualView);
- RefreshViewWebViewScrollCapture.InjectObserver(view);
+
+ // Only inject the scroll-capture observer when the WebView is hosted inside
+ // a RefreshView – avoids unnecessary JS overhead for standalone WebViews.
+ if (RefreshViewWebViewScrollCapture.IsAttached(view))
+ RefreshViewWebViewScrollCapture.InjectObserver(view);
base.OnPageFinished(view, url);
}
diff --git a/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
index 83497c730377..4cfa7b92f5fd 100644
--- a/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
+++ b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
@@ -113,6 +113,8 @@ internal static void InjectObserver(WebView? webView)
webView.EvaluateJavascript(ObserverScript, null);
}
+ internal static bool IsAttached(WebView? webView) => GetState(webView) is not null;
+
internal static bool TryGetCanScrollUp(WebView? webView, out bool canScrollUp)
{
if (webView is null)
From 3c51cf5ec7f2f85f308687297a0cbe7132c6d8a3 Mon Sep 17 00:00:00 2001
From: BagavathiPerumal
Date: Tue, 21 Apr 2026 18:29:29 +0530
Subject: [PATCH 08/12] fix-33510-Simplied Testcase and test script.
Additionally, updated the code changes based on AI summary.
---
.../TestCases.HostApp/Issues/Issue33510.cs | 189 +-----------------
.../Tests/Issues/Issue33510.cs | 86 ++------
.../RefreshViewWebViewScrollCapture.cs | 17 +-
3 files changed, 37 insertions(+), 255 deletions(-)
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs
index 8e575d8ae768..16a54137b17d 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs
@@ -1,225 +1,52 @@
-using System.Text;
-
namespace Maui.Controls.Sample.Issues;
-[Issue(IssueTracker.Github, 33510, "[Android] RefreshView triggers pull-to-refresh immediately when scrolling up inside a WebView", PlatformAffected.Android)]
+[Issue(IssueTracker.Github, 33510, "[Android] RefreshView triggers pull-to-refresh immediately when scrolling up inside a WebView", PlatformAffected.Android, isInternetRequired: true)]
public class Issue33510 : TestContentPage
{
- WebView _webView;
RefreshView _refreshView;
Label _statusLabel;
- Label _scrollTopLabel;
- bool _isWebViewLoaded;
protected override void Init()
{
- Title = "RefreshView + WebView";
-
_statusLabel = new Label
{
AutomationId = "StatusLabel",
- Text = "Loading WebView..."
- };
-
- _scrollTopLabel = new Label
- {
- AutomationId = "ScrollTopLabel",
- Text = "ScrollTop: unavailable"
- };
-
- var scrollWebViewButton = new Button
- {
- AutomationId = "ScrollWebViewButton",
- Text = "Scroll down in WebView"
+ Text = "Loading..."
};
- scrollWebViewButton.Clicked += async (_, _) =>
- {
- if (!_isWebViewLoaded)
- {
- return;
- }
-
- var result = await _webView.EvaluateJavaScriptAsync("window.scrollInnerContainerTo(900);");
- _scrollTopLabel.Text = $"ScrollTop: {NormalizeJavaScriptNumber(result)}";
- };
-
- var readScrollTopButton = new Button
- {
- AutomationId = "ReadScrollTopButton",
- Text = "Read WebView scroll position"
- };
-
- readScrollTopButton.Clicked += async (_, _) => await UpdateScrollStatusAsync();
-
- var scrollWebViewToTopButton = new Button
- {
- AutomationId = "ScrollWebViewToTopButton",
- Text = "Scroll WebView to top"
- };
-
- scrollWebViewToTopButton.Clicked += async (_, _) =>
- {
- if (!_isWebViewLoaded)
- {
- return;
- }
-
- var result = await _webView.EvaluateJavaScriptAsync("window.scrollInnerContainerTo(0);");
- _scrollTopLabel.Text = $"ScrollTop: {NormalizeJavaScriptNumber(result)}";
- };
-
- _webView = new WebView
+ var webView = new WebView
{
AutomationId = "TestWebView",
+ Source = new UrlWebViewSource { Url = "https://material.angular.io/components/sidenav/overview" }
};
- _webView.Navigated += async (_, _) =>
- {
- _isWebViewLoaded = true;
- _statusLabel.Text = "WebView ready. Scroll down, then drag downward inside the WebView.";
- await UpdateScrollStatusAsync();
- };
-
- _webView.Source = new HtmlWebViewSource
- {
- Html = CreateHtml()
- };
-
- var webViewContainer = new ContentView
- {
- AutomationId = "TestWebViewContainer",
- HorizontalOptions = LayoutOptions.Fill,
- VerticalOptions = LayoutOptions.Fill,
- Content = _webView
- };
+ webView.Navigated += (_, _) => _statusLabel.Text = "WebView ready";
_refreshView = new RefreshView
{
AutomationId = "TestRefreshView",
- Content = webViewContainer
+ Content = webView
};
_refreshView.Command = new Command(async () =>
{
_statusLabel.Text = "Refresh triggered";
-
await Task.Delay(150);
_refreshView.IsRefreshing = false;
-
- await UpdateScrollStatusAsync();
});
- var controlsLayout = new VerticalStackLayout
- {
- Padding = new Thickness(12),
- Spacing = 8,
- Children =
- {
- new Label
- {
- Text = "This page reproduces issue #33510. On Android, RefreshView should not refresh while the WebView can still scroll upward internally."
- },
- _statusLabel,
- _scrollTopLabel,
- scrollWebViewButton,
- scrollWebViewToTopButton,
- readScrollTopButton
- }
- };
-
- var separator = new BoxView
- {
- HeightRequest = 1,
- Color = Colors.LightGray
- };
-
var grid = new Grid
{
RowDefinitions =
{
- new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Star }
}
};
- grid.Add(controlsLayout);
- Grid.SetRow(controlsLayout, 0);
-
- grid.Add(separator);
- Grid.SetRow(separator, 1);
-
- grid.Add(_refreshView);
- Grid.SetRow(_refreshView, 2);
+ grid.Add(_statusLabel, 0, 0);
+ grid.Add(_refreshView, 0, 1);
Content = grid;
}
-
- async Task UpdateScrollStatusAsync()
- {
- if (!_isWebViewLoaded)
- {
- _scrollTopLabel.Text = "ScrollTop: unavailable";
- return;
- }
-
- var result = await _webView.EvaluateJavaScriptAsync("window.getInnerScrollTop();");
- _scrollTopLabel.Text = $"ScrollTop: {NormalizeJavaScriptNumber(result)}";
- }
-
- static string NormalizeJavaScriptNumber(string result)
- {
- if (string.IsNullOrWhiteSpace(result))
- return "unknown";
-
- return result.Trim().Trim('"');
- }
-
- static string CreateHtml()
- {
- var rows = string.Join(Environment.NewLine, Enumerable.Range(1, 30).Select(index =>
- $"Scrollable row {index}
"));
-
- var html = new StringBuilder();
- html.AppendLine("");
- html.AppendLine("");
- html.AppendLine("");
- html.AppendLine("");
- html.AppendLine("");
- html.AppendLine("");
- html.AppendLine("");
- html.AppendLine("");
- html.AppendLine("");
- html.AppendLine("");
- html.AppendLine("");
- html.AppendLine("");
-
- return html.ToString();
- }
}
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs
index 8aa2e6ea261c..6f6622188791 100644
--- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs
@@ -1,6 +1,4 @@
#if ANDROID // This test is Android-only because Issue #33510 (RefreshView triggering pull-to-refresh when scrolling inside a WebView) only reproduces on Android, and the test uses Android-specific Appium touch gesture APIs.
-using System.Globalization;
-using System.Text.RegularExpressions;
using NUnit.Framework;
using OpenQA.Selenium.Appium.Interactions;
using OpenQA.Selenium.Interactions;
@@ -13,11 +11,7 @@ namespace Microsoft.Maui.TestCases.Tests.Issues;
public class Issue33510 : _IssuesUITest
{
const string StatusLabel = "StatusLabel";
- const string ScrollWebViewButton = "ScrollWebViewButton";
- const string ScrollWebViewToTopButton = "ScrollWebViewToTopButton";
- const string ReadScrollTopButton = "ReadScrollTopButton";
- const string ScrollTopLabel = "ScrollTopLabel";
- const string TestWebViewContainer = "TestWebViewContainer";
+ const string TestRefreshView = "TestRefreshView";
public Issue33510(TestDevice device) : base(device)
{
@@ -25,56 +19,31 @@ public Issue33510(TestDevice device) : base(device)
public override string Issue => "[Android] RefreshView triggers pull-to-refresh immediately when scrolling up inside a WebView";
- protected override bool ResetAfterEachTest => true;
-
[Test]
- public void PullToRefreshWaitsUntilInternalWebViewContainerReachesTop()
+ public void PullToRefreshShouldNotTriggerWhenWebViewIsScrolledDown()
{
+ VerifyInternetConnectivity();
var androidApp = WaitForAndroidApp();
- var webViewRect = App.WaitForElement(TestWebViewContainer).GetRect();
- var x = webViewRect.CenterX();
- var upwardFromY = webViewRect.Y + (webViewRect.Height * 75 / 100);
- var upwardToY = webViewRect.Y + (webViewRect.Height * 30 / 100);
+ var refreshViewRect = App.WaitForElement(TestRefreshView).GetRect();
+ var x = refreshViewRect.CenterX();
- for (var attempt = 0; attempt < 3 && GetScrollTop() <= 200; attempt++)
+ // Scroll content down by swiping up inside the WebView
+ for (var i = 0; i < 3; i++)
{
- DragInsideWebView(androidApp, x, upwardFromY, x, upwardToY);
+ DragInsideWebView(androidApp, x,
+ refreshViewRect.Y + (refreshViewRect.Height * 70 / 100),
+ x,
+ refreshViewRect.Y + (refreshViewRect.Height * 25 / 100));
}
- var initialScrollTop = GetScrollTop();
- Assert.That(initialScrollTop, Is.GreaterThan(200), "The inner HTML container must start away from the top.");
-
- Assert.That(GetStatus(), Does.Not.Contain("Refresh triggered"));
-
- var fromY = webViewRect.Y + (webViewRect.Height * 35 / 100);
- var toY = webViewRect.Y + (webViewRect.Height * 70 / 100);
-
- DragInsideWebView(androidApp, x, fromY, x, toY);
-
- var scrollTopAfterGesture = GetScrollTop();
- Assert.That(scrollTopAfterGesture, Is.LessThan(initialScrollTop), "Dragging down inside the WebView should scroll the inner container upward.");
- Assert.That(scrollTopAfterGesture, Is.GreaterThan(0), "The inner container should still be away from the top after a partial upward scroll.");
- Assert.That(GetStatus(), Does.Not.Contain("Refresh triggered"));
- }
-
- [Test]
- public void PullToRefreshStillWorksWhenInternalWebViewContainerStartsAtTop()
- {
- var androidApp = WaitForAndroidApp();
-
- Assert.That(GetScrollTop(), Is.LessThan(1), "The inner HTML container should start at the top before pull-to-refresh begins.");
-
- var webViewRect = App.WaitForElement(TestWebViewContainer).GetRect();
- var x = webViewRect.CenterX();
- var fromY = webViewRect.Y + (webViewRect.Height * 30 / 100);
- var toY = webViewRect.Y + (webViewRect.Height * 85 / 100);
+ // Attempt pull-to-refresh (drag down) while content is still scrolled down
+ DragInsideWebView(androidApp, x,
+ refreshViewRect.Y + (refreshViewRect.Height * 30 / 100),
+ x,
+ refreshViewRect.Y + (refreshViewRect.Height * 80 / 100));
- DragInsideWebView(androidApp, x, fromY, x, toY);
-
- Assert.That(
- App.WaitForTextToBePresentInElement(StatusLabel, "Refresh triggered", timeout: TimeSpan.FromSeconds(10)),
- Is.True,
- "Pulling down inside the WebView at scroll top should still trigger RefreshView.");
+ Assert.That(App.FindElement(StatusLabel).GetText(), Does.Not.Contain("Refresh triggered"),
+ "RefreshView should not trigger while WebView content is not at the top.");
}
AppiumAndroidApp WaitForAndroidApp()
@@ -93,25 +62,6 @@ AppiumAndroidApp WaitForAndroidApp()
return androidApp;
}
- double GetScrollTop()
- {
- App.Tap(ReadScrollTopButton);
- Assert.That(
- App.WaitForTextToBePresentInElement(ScrollTopLabel, "ScrollTop:", timeout: TimeSpan.FromSeconds(5)),
- Is.True,
- "Scroll position was not reported.");
-
- var status = App.FindElement(ScrollTopLabel).GetText() ?? string.Empty;
- var match = Regex.Match(status, @"(\d+(\.\d+)?)");
-
- Assert.That(match.Success, Is.True, $"Could not parse scroll position from '{status}'.");
-
- return double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
- }
-
- string GetStatus() =>
- App.FindElement(StatusLabel).GetText() ?? string.Empty;
-
static void DragInsideWebView(AppiumAndroidApp androidApp, int fromX, int fromY, int toX, int toY)
{
var touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch);
diff --git a/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
index 4cfa7b92f5fd..402eb8d49a0b 100644
--- a/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
+++ b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
@@ -146,23 +146,28 @@ internal static bool TryGetCanScrollUp(WebView? webView, out bool canScrollUp)
sealed class ScrollCaptureState : Java.Lang.Object
{
- internal bool CanScrollUp { get; private set; }
+ // These fields are written from the JavaBridge thread (via [JavascriptInterface])
+ // and read from the UI thread, so they must be volatile to ensure visibility on ARM.
+ volatile bool _canScrollUp;
+ volatile bool _hasReportedState;
- internal bool HasReportedState { get; private set; }
+ internal bool CanScrollUp => _canScrollUp;
+
+ internal bool HasReportedState => _hasReportedState;
[JavascriptInterface]
[RequiresUnreferencedCode("Java.Interop.Export uses dynamic features.")]
[Export("setCanScrollUp")]
public void SetCanScrollUp(bool canScrollUp)
{
- CanScrollUp = canScrollUp;
- HasReportedState = true;
+ _canScrollUp = canScrollUp;
+ _hasReportedState = true;
}
internal void Reset()
{
- CanScrollUp = false;
- HasReportedState = false;
+ _canScrollUp = false;
+ _hasReportedState = false;
}
}
}
From 62ecbf58d939c197d430c69c8953ded7c4c6499d Mon Sep 17 00:00:00 2001
From: BagavathiPerumal
Date: Mon, 27 Apr 2026 18:31:32 +0530
Subject: [PATCH 09/12] fix-33510-Testcase updated.
---
.../TestCases.HostApp/Issues/Issue33510.cs | 80 ++++++++++++++++++-
.../Tests/Issues/Issue33510.cs | 1 -
2 files changed, 78 insertions(+), 3 deletions(-)
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs
index 16a54137b17d..305937bb2313 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs
@@ -1,6 +1,6 @@
namespace Maui.Controls.Sample.Issues;
-[Issue(IssueTracker.Github, 33510, "[Android] RefreshView triggers pull-to-refresh immediately when scrolling up inside a WebView", PlatformAffected.Android, isInternetRequired: true)]
+[Issue(IssueTracker.Github, 33510, "[Android] RefreshView triggers pull-to-refresh immediately when scrolling up inside a WebView", PlatformAffected.Android)]
public class Issue33510 : TestContentPage
{
RefreshView _refreshView;
@@ -17,7 +17,7 @@ protected override void Init()
var webView = new WebView
{
AutomationId = "TestWebView",
- Source = new UrlWebViewSource { Url = "https://material.angular.io/components/sidenav/overview" }
+ Source = new HtmlWebViewSource { Html = ScrollableHtml }
};
webView.Navigated += (_, _) => _statusLabel.Text = "WebView ready";
@@ -49,4 +49,80 @@ protected override void Init()
Content = grid;
}
+
+ const string ScrollableHtml = """
+
+
+
+
+
+
+
+
+
+
+
+ """;
}
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs
index 6f6622188791..f33deebba16d 100644
--- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs
@@ -22,7 +22,6 @@ public Issue33510(TestDevice device) : base(device)
[Test]
public void PullToRefreshShouldNotTriggerWhenWebViewIsScrolledDown()
{
- VerifyInternetConnectivity();
var androidApp = WaitForAndroidApp();
var refreshViewRect = App.WaitForElement(TestRefreshView).GetRect();
var x = refreshViewRect.CenterX();
From 3ed45d7833dc1256344e9d39c8f970995a6a852a Mon Sep 17 00:00:00 2001
From: BagavathiPerumal
Date: Thu, 30 Apr 2026 13:00:23 +0530
Subject: [PATCH 10/12] fix-33510-Code changes updated.
---
.../Android/MauiSwipeRefreshLayout.cs | 12 ++++++--
src/Core/src/Platform/Android/MauiWebView.cs | 8 ++++++
.../RefreshViewWebViewScrollCapture.cs | 28 ++++++++++++++++++-
3 files changed, 45 insertions(+), 3 deletions(-)
diff --git a/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs b/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
index 3e3edac88e88..14749d2d2a37 100644
--- a/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
+++ b/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
@@ -236,6 +236,13 @@ static bool CanScrollUpViewByType(AView? view)
return true;
}
+ // Recursively hit-tests the view tree to find a WebView at the given
+ // coordinates (in the parent's coordinate space).
+ // ScrollX/ScrollY are added when converting to a child's local coordinate
+ // space so that scrolled containers (HorizontalScrollView, NestedScrollView,
+ // etc.) are handled correctly. Without this adjustment, any ViewGroup that
+ // has been scrolled would cause the hit-test to miss the WebView or match
+ // the wrong region.
static AWebView? FindWebView(AView? view, float x, float y)
{
if (view is null || view.Visibility != ViewStates.Visible)
@@ -250,8 +257,8 @@ static bool CanScrollUpViewByType(AView? view)
if (view is not ViewGroup viewGroup)
return null;
- var localX = x - view.Left;
- var localY = y - view.Top;
+ var localX = x - view.Left + view.ScrollX;
+ var localY = y - view.Top + view.ScrollY;
for (int i = viewGroup.ChildCount - 1; i >= 0; i--)
{
@@ -262,5 +269,6 @@ static bool CanScrollUpViewByType(AView? view)
return null;
}
+
}
}
diff --git a/src/Core/src/Platform/Android/MauiWebView.cs b/src/Core/src/Platform/Android/MauiWebView.cs
index 13b2c3621d8a..3f21af4e6ea1 100644
--- a/src/Core/src/Platform/Android/MauiWebView.cs
+++ b/src/Core/src/Platform/Android/MauiWebView.cs
@@ -37,7 +37,15 @@ protected override void OnAttachedToWindow()
UpdateClipBounds(Width, Height);
if (IsInsideSwipeRefreshLayout())
+ {
RefreshViewWebViewScrollCapture.Attach(this);
+ // If a page has already loaded before this WebView was placed inside a
+ // RefreshView (late-attach), OnPageFinished already fired with IsAttached=false
+ // and the observer was never injected. Re-inject it now so inner-scroll can
+ // correctly prevent pull-to-refresh.
+ if (!string.IsNullOrEmpty(Url))
+ RefreshViewWebViewScrollCapture.InjectObserver(this);
+ }
}
protected override void OnDetachedFromWindow()
diff --git a/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
index 402eb8d49a0b..872f3312a286 100644
--- a/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
+++ b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
@@ -79,7 +79,9 @@ function report(target) {
internal static void Attach(WebView webView)
{
if (GetState(webView) is not null)
+ {
return;
+ }
var state = new ScrollCaptureState();
webView.SetTag(ScrollCaptureStateKey, state);
@@ -89,26 +91,41 @@ internal static void Attach(WebView webView)
internal static void Detach(WebView? webView)
{
if (webView is null)
+ {
return;
+ }
if (GetState(webView) is not ScrollCaptureState state)
+ {
return;
+ }
+ // Mark detached BEFORE removing the interface so any in-flight JNI
+ // callbacks to SetCanScrollUp become no-ops instead of accessing a
+ // disposed object. RemoveJavascriptInterface is async from V8's
+ // perspective — the JS bridge can still fire after this call returns.
+ state.MarkDetached();
webView.RemoveJavascriptInterface(JavaScriptInterfaceName);
webView.SetTag(ScrollCaptureStateKey, null);
- state.Dispose();
+ // Do NOT call state.Dispose() here — V8 may still hold a reference to
+ // the state object via the JS bridge. The GC will collect it once V8
+ // releases its last reference.
}
internal static void Reset(WebView? webView)
{
if (GetState(webView) is ScrollCaptureState state)
+ {
state.Reset();
+ }
}
internal static void InjectObserver(WebView? webView)
{
if (webView is null)
+ {
return;
+ }
webView.EvaluateJavascript(ObserverScript, null);
}
@@ -150,6 +167,9 @@ sealed class ScrollCaptureState : Java.Lang.Object
// and read from the UI thread, so they must be volatile to ensure visibility on ARM.
volatile bool _canScrollUp;
volatile bool _hasReportedState;
+ // Set before RemoveJavascriptInterface so any in-flight JNI callbacks become
+ // no-ops rather than accessing a disposed object.
+ volatile bool _detached;
internal bool CanScrollUp => _canScrollUp;
@@ -160,10 +180,16 @@ sealed class ScrollCaptureState : Java.Lang.Object
[Export("setCanScrollUp")]
public void SetCanScrollUp(bool canScrollUp)
{
+ if (_detached)
+ {
+ return;
+ }
_canScrollUp = canScrollUp;
_hasReportedState = true;
}
+ internal void MarkDetached() => _detached = true;
+
internal void Reset()
{
_canScrollUp = false;
From 746249131d8189a1ece1dcc7f72aef261deed157 Mon Sep 17 00:00:00 2001
From: BagavathiPerumal
Date: Wed, 6 May 2026 15:05:17 +0530
Subject: [PATCH 11/12] fix-33510-Updated the code changes, addressed the issue
in MauiHybridWebView, optimized the implementation, and resolved the related
issues as well.
---
.../src/Platform/Android/MauiHybridWebView.cs | 45 ++++++++++++++++
.../Android/MauiHybridWebViewClient.cs | 27 ++++++++++
.../Android/MauiSwipeRefreshLayout.cs | 23 +++++++--
src/Core/src/Platform/Android/MauiWebView.cs | 6 +++
.../src/Platform/Android/MauiWebViewClient.cs | 14 +++++
.../RefreshViewWebViewScrollCapture.cs | 51 ++++++++++++-------
.../net-android/PublicAPI.Unshipped.txt | 4 ++
7 files changed, 148 insertions(+), 22 deletions(-)
diff --git a/src/Core/src/Platform/Android/MauiHybridWebView.cs b/src/Core/src/Platform/Android/MauiHybridWebView.cs
index 6354bdf7abea..2a26aea9f4c4 100644
--- a/src/Core/src/Platform/Android/MauiHybridWebView.cs
+++ b/src/Core/src/Platform/Android/MauiHybridWebView.cs
@@ -36,12 +36,46 @@ protected override void OnSizeChanged(int width, int height, int oldWidth, int o
UpdateClipBounds(width, height);
}
+ // OnAttachedToWindow — calls Attach(this) when inside a SwipeRefreshLayout.
protected override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
// Re-evaluate ClipBounds when re-parented (e.g., wrapped in WrapperView for shadow)
UpdateClipBounds(Width, Height);
+
+ if (IsInsideSwipeRefreshLayout())
+ {
+ RefreshViewWebViewScrollCapture.Attach(this);
+ // If a page has already loaded before this HybridWebView was placed inside a
+ // RefreshView (late-attach), the observer was never injected. Re-inject now.
+ if (!string.IsNullOrEmpty(Url))
+ {
+ RefreshViewWebViewScrollCapture.InjectObserver(this);
+ }
+ }
+ }
+
+ // OnDetachedFromWindow — calls Detach().
+ protected override void OnDetachedFromWindow()
+ {
+ RefreshViewWebViewScrollCapture.Detach(this);
+ base.OnDetachedFromWindow();
+ }
+
+ // IsInsideSwipeRefreshLayout — walks parent view tree.
+ bool IsInsideSwipeRefreshLayout()
+ {
+ var parent = Parent;
+ while (parent is not null)
+ {
+ if (parent is MauiSwipeRefreshLayout)
+ {
+ return true;
+ }
+ parent = parent.Parent;
+ }
+ return false;
}
void UpdateClipBounds(int width, int height)
@@ -77,5 +111,16 @@ public void SendRawMessage(string rawMessage)
PostWebMessage(new WebMessage(rawMessage), AndroidAppOriginUri);
#pragma warning restore CA1416 // Validate platform compatibility
}
+
+ // Dispose(bool) — calls Detach() on cleanup.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ RefreshViewWebViewScrollCapture.Detach(this);
+ }
+
+ base.Dispose(disposing);
+ }
}
}
diff --git a/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs b/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs
index 1c8b1bd3ce1b..c202d14e2366 100644
--- a/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs
+++ b/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs
@@ -7,6 +7,7 @@
using System.IO;
using System.Text;
using System.Web;
+using Android.Graphics;
using Android.Webkit;
using Java.Net;
using Microsoft.Extensions.Logging;
@@ -30,6 +31,32 @@ public MauiHybridWebViewClient(HybridWebViewHandler handler)
private HybridWebViewHandler? Handler => _handler is not null && _handler.TryGetTarget(out var h) ? h : null;
+ // OnPageStarted — calls Reset() to clear stale scroll state.
+ public override void OnPageStarted(AWebView? view, string? url, Bitmap? favicon)
+ {
+ RefreshViewWebViewScrollCapture.Reset(view);
+ base.OnPageStarted(view, url, favicon);
+ }
+
+ // OnPageFinished — calls InjectObserver() to inject JS bridge when page loads.
+ public override void OnPageFinished(AWebView? view, string? url)
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ {
+ base.OnPageFinished(view, url);
+ return;
+ }
+
+ // Only inject the scroll-capture observer when the WebView is hosted inside
+ // a RefreshView – avoids unnecessary JS overhead for standalone HybridWebViews.
+ if (RefreshViewWebViewScrollCapture.IsAttached(view))
+ {
+ RefreshViewWebViewScrollCapture.InjectObserver(view);
+ }
+
+ base.OnPageFinished(view, url);
+ }
+
public override WebResourceResponse? ShouldInterceptRequest(AWebView? view, IWebResourceRequest? request)
{
var url = request?.Url?.ToString();
diff --git a/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs b/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
index 14749d2d2a37..23cc46bd77d5 100644
--- a/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
+++ b/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
@@ -20,6 +20,7 @@ public class MauiSwipeRefreshLayout : SwipeRefreshLayout
AView? _contentView;
bool _refreshEnabled = true;
AWebView? _activeTouchWebView;
+ RefreshViewWebViewScrollCapture.ScrollCaptureState? _activeTouchScrollState;
bool _webViewOwnsGesture;
bool _touchStartedInWebView;
@@ -148,6 +149,8 @@ public override bool OnInterceptTouchEvent(MotionEvent? ev)
case MotionEventActions.Down:
_activeTouchWebView = FindWebView(_contentView, ev.GetX(), ev.GetY());
_touchStartedInWebView = _activeTouchWebView is not null;
+ // ACTION_DOWN — caches ScrollCaptureState via GetAttachedState().
+ _activeTouchScrollState = RefreshViewWebViewScrollCapture.GetAttachedState(_activeTouchWebView);
_webViewOwnsGesture = _touchStartedInWebView &&
RefreshViewWebViewScrollCapture.TryGetCanScrollUp(_activeTouchWebView, out var canScrollUpAtStart) &&
canScrollUpAtStart;
@@ -160,22 +163,34 @@ public override bool OnInterceptTouchEvent(MotionEvent? ev)
return false;
}
break;
+ case MotionEventActions.PointerDown:
+ // Reset WebView gesture ownership when a second finger is placed –
+ // multi-touch cancels the pending single-finger pull-to-refresh guard.
+ _activeTouchWebView = null;
+ _activeTouchScrollState = null;
+ _touchStartedInWebView = false;
+ _webViewOwnsGesture = false;
+ break;
case MotionEventActions.Move:
- // Re-evaluate scrollability so that once the WebView reaches the top,
- // RefreshLayout can start intercepting mid-gesture.
- if (_touchStartedInWebView && _webViewOwnsGesture && _activeTouchWebView is not null)
+ // ACTION_MOVE — reads CanScrollUp (volatile bool, zero JNI) from cached state
+ // instead of calling TryGetCanScrollUp every frame.
+ if (_touchStartedInWebView && _webViewOwnsGesture && _activeTouchScrollState is not null)
{
- if (!RefreshViewWebViewScrollCapture.TryGetCanScrollUp(_activeTouchWebView, out var canStillScrollUp) || !canStillScrollUp)
+ if (!_activeTouchScrollState.CanScrollUp)
{
_webViewOwnsGesture = false;
}
}
if (_touchStartedInWebView && _webViewOwnsGesture)
+ {
return false;
+ }
break;
case MotionEventActions.Cancel:
case MotionEventActions.Up:
+ // ACTION_UP/CANCEL — clears cached state.
_activeTouchWebView = null;
+ _activeTouchScrollState = null;
_touchStartedInWebView = false;
_webViewOwnsGesture = false;
break;
diff --git a/src/Core/src/Platform/Android/MauiWebView.cs b/src/Core/src/Platform/Android/MauiWebView.cs
index 3f21af4e6ea1..bd91d4622564 100644
--- a/src/Core/src/Platform/Android/MauiWebView.cs
+++ b/src/Core/src/Platform/Android/MauiWebView.cs
@@ -44,7 +44,9 @@ protected override void OnAttachedToWindow()
// and the observer was never injected. Re-inject it now so inner-scroll can
// correctly prevent pull-to-refresh.
if (!string.IsNullOrEmpty(Url))
+ {
RefreshViewWebViewScrollCapture.InjectObserver(this);
+ }
}
}
@@ -60,7 +62,9 @@ bool IsInsideSwipeRefreshLayout()
while (parent is not null)
{
if (parent is MauiSwipeRefreshLayout)
+ {
return true;
+ }
parent = parent.Parent;
}
return false;
@@ -119,7 +123,9 @@ void IWebViewDelegate.LoadUrl(string? url)
protected override void Dispose(bool disposing)
{
if (disposing)
+ {
RefreshViewWebViewScrollCapture.Detach(this);
+ }
base.Dispose(disposing);
}
diff --git a/src/Core/src/Platform/Android/MauiWebViewClient.cs b/src/Core/src/Platform/Android/MauiWebViewClient.cs
index a5b42375e31f..89e53e36c47b 100644
--- a/src/Core/src/Platform/Android/MauiWebViewClient.cs
+++ b/src/Core/src/Platform/Android/MauiWebViewClient.cs
@@ -25,7 +25,9 @@ public override void OnPageStarted(WebView? view, string? url, Bitmap? favicon)
RefreshViewWebViewScrollCapture.Reset(view);
if (!_handler.TryGetTarget(out var handler) || handler.VirtualView == null)
+ {
return;
+ }
if (!string.IsNullOrWhiteSpace(url))
{
@@ -61,7 +63,9 @@ public override void OnPageFinished(WebView? view, string? url)
// Skip Navigated event for about:blank to prevent unwanted events when Source is null
if (navigate && !IsBlankNavigation(url))
+ {
handler.VirtualView.Navigated(handler.CurrentNavigationEvent, GetValidUrl(url), _navigationResult);
+ }
handler.SyncPlatformCookiesToVirtualView(url);
@@ -70,7 +74,9 @@ public override void OnPageFinished(WebView? view, string? url)
// Only inject the scroll-capture observer when the WebView is hosted inside
// a RefreshView – avoids unnecessary JS overhead for standalone WebViews.
if (RefreshViewWebViewScrollCapture.IsAttached(view))
+ {
RefreshViewWebViewScrollCapture.InjectObserver(view);
+ }
base.OnPageFinished(view, url);
}
@@ -83,7 +89,9 @@ public override void OnReceivedError(WebView? view, IWebResourceRequest? request
_navigationResult = WebNavigationResult.Failure;
if (error?.ErrorCode == ClientError.Timeout)
+ {
_navigationResult = WebNavigationResult.Timeout;
+ }
}
base.OnReceivedError(view, request, error);
@@ -109,7 +117,9 @@ static bool IsBlankNavigation(string? url)
// Null/empty URLs are handled by the early return in OnPageFinished,
// so we only need to check for the explicit "about:blank" URL
if (string.IsNullOrWhiteSpace(url))
+ {
return false;
+ }
// Check if URL is about:blank (case insensitive)
return string.Equals(url.Trim(), "about:blank", StringComparison.OrdinalIgnoreCase);
@@ -118,7 +128,9 @@ static bool IsBlankNavigation(string? url)
static string GetValidUrl(string? url)
{
if (string.IsNullOrEmpty(url))
+ {
return string.Empty;
+ }
return url;
}
@@ -126,7 +138,9 @@ static string GetValidUrl(string? url)
protected override void Dispose(bool disposing)
{
if (disposing)
+ {
Disconnect();
+ }
base.Dispose(disposing);
}
diff --git a/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
index 872f3312a286..f9b03a4dfade 100644
--- a/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
+++ b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
@@ -9,20 +9,19 @@ internal static class RefreshViewWebViewScrollCapture
const string JavaScriptInterfaceName = "mauiRefreshViewHost";
const int ScrollCaptureStateKey = 0x4D415549;
+ // Observer JS script — replaces static boolean guard (__mauiRefreshViewObserverInstalled) with
+ // dynamic window.mauiRefreshViewHost lookup inside report(). Fixes scroll tracking after Shell tab-switch.
+ //
+ // After a Shell tab-switch the WebView is detached (Detach removes the old bridge) then re-attached
+ // (Attach adds a fresh ScrollCaptureState, InjectObserver re-runs this script). A static guard would
+ // prevent re-injection, so the new bridge object would never receive callbacks, silently breaking
+ // pull-to-refresh protection until the next full page reload.
+ //
+ // Named global JS listener vars (window.__mauiTouchStartHandler/MoveHandler) so removeEventListener
+ // works on re-inject, preventing listener stacking across Detach/re-Attach cycles.
const string ObserverScript =
"""
(function () {
- if (window.__mauiRefreshViewObserverInstalled) {
- return;
- }
-
- var host = window.mauiRefreshViewHost;
- if (!host || typeof host.setCanScrollUp !== 'function') {
- return;
- }
-
- window.__mauiRefreshViewObserverInstalled = true;
-
function isScrollableElement(node) {
if (!node || node.nodeType !== Node.ELEMENT_NODE) {
return false;
@@ -58,19 +57,30 @@ function getScrollTopForElement(element) {
function report(target) {
try {
+ var host = window.mauiRefreshViewHost;
+ if (!host || typeof host.setCanScrollUp !== 'function') {
+ return;
+ }
var scrollable = getScrollableElement(target);
host.setCanScrollUp(getScrollTopForElement(scrollable) > 0);
} catch (e) {
}
}
- document.addEventListener('touchstart', function (event) {
- report(event.target);
- }, true);
+ // Remove any previously installed listeners to prevent accumulation
+ // after Shell tab-switch (detach + re-attach without page reload).
+ if (window.__mauiTouchStartHandler) {
+ document.removeEventListener('touchstart', window.__mauiTouchStartHandler, true);
+ document.removeEventListener('touchmove', window.__mauiTouchMoveHandler, true);
+ }
- document.addEventListener('touchmove', function (event) {
- report(event.target);
- }, true);
+ var touchStartHandler = function (event) { report(event.target); };
+ var touchMoveHandler = function (event) { report(event.target); };
+ window.__mauiTouchStartHandler = touchStartHandler;
+ window.__mauiTouchMoveHandler = touchMoveHandler;
+
+ document.addEventListener('touchstart', touchStartHandler, true);
+ document.addEventListener('touchmove', touchMoveHandler, true);
report(document.body);
})();
@@ -161,7 +171,12 @@ internal static bool TryGetCanScrollUp(WebView? webView, out bool canScrollUp)
static ScrollCaptureState? GetState(WebView? webView) =>
webView?.GetTag(ScrollCaptureStateKey) as ScrollCaptureState;
- sealed class ScrollCaptureState : Java.Lang.Object
+ // Returns the cached ScrollCaptureState for the given WebView so callers on the
+ // UI thread can read CanScrollUp (a volatile bool) directly without any JNI overhead.
+ // Returns null when the WebView is not inside a RefreshView.
+ internal static ScrollCaptureState? GetAttachedState(WebView? webView) => GetState(webView);
+
+ internal sealed class ScrollCaptureState : Java.Lang.Object
{
// These fields are written from the JavaBridge thread (via [JavascriptInterface])
// and read from the UI thread, so they must be volatile to ensure visibility on ARM.
diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
index 44cc4b54cd5f..db676dbda3ad 100644
--- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
@@ -13,7 +13,11 @@ override Microsoft.Maui.Platform.ContentViewGroup.HasOverlappingRendering.get ->
override Microsoft.Maui.Platform.LayoutViewGroup.HasOverlappingRendering.get -> bool
override Microsoft.Maui.Platform.WrapperView.HasOverlappingRendering.get -> bool
override Microsoft.Maui.Platform.MauiHybridWebView.OnAttachedToWindow() -> void
+override Microsoft.Maui.Platform.MauiHybridWebView.OnDetachedFromWindow() -> void
+override Microsoft.Maui.Platform.MauiHybridWebView.Dispose(bool disposing) -> void
override Microsoft.Maui.Platform.MauiHybridWebView.OnSizeChanged(int width, int height, int oldWidth, int oldHeight) -> void
+override Microsoft.Maui.Platform.MauiHybridWebViewClient.OnPageFinished(Android.Webkit.WebView? view, string? url) -> void
+override Microsoft.Maui.Platform.MauiHybridWebViewClient.OnPageStarted(Android.Webkit.WebView? view, string? url, Android.Graphics.Bitmap? favicon) -> void
override Microsoft.Maui.Platform.MauiWebView.OnAttachedToWindow() -> void
override Microsoft.Maui.Platform.MauiWebView.OnSizeChanged(int width, int height, int oldWidth, int oldHeight) -> void
override Microsoft.Maui.Platform.MauiSwipeRefreshLayout.OnInterceptTouchEvent(Android.Views.MotionEvent? ev) -> bool
From 3c98c02d4db5e0ed3f5fd89e0db787cd8de83094 Mon Sep 17 00:00:00 2001
From: BagavathiPerumal
Date: Tue, 12 May 2026 17:11:30 +0530
Subject: [PATCH 12/12] fix-33510-Optimized code implementation
---
.../src/Platform/Android/MauiHybridWebView.cs | 17 +----------------
src/Core/src/Platform/Android/MauiWebView.cs | 16 +---------------
.../Android/RefreshViewWebViewScrollCapture.cs | 14 ++++++++++++++
3 files changed, 16 insertions(+), 31 deletions(-)
diff --git a/src/Core/src/Platform/Android/MauiHybridWebView.cs b/src/Core/src/Platform/Android/MauiHybridWebView.cs
index 2a26aea9f4c4..74518c09cd1e 100644
--- a/src/Core/src/Platform/Android/MauiHybridWebView.cs
+++ b/src/Core/src/Platform/Android/MauiHybridWebView.cs
@@ -44,7 +44,7 @@ protected override void OnAttachedToWindow()
// Re-evaluate ClipBounds when re-parented (e.g., wrapped in WrapperView for shadow)
UpdateClipBounds(Width, Height);
- if (IsInsideSwipeRefreshLayout())
+ if (RefreshViewWebViewScrollCapture.IsInsideMauiSwipeRefreshLayout(this))
{
RefreshViewWebViewScrollCapture.Attach(this);
// If a page has already loaded before this HybridWebView was placed inside a
@@ -63,21 +63,6 @@ protected override void OnDetachedFromWindow()
base.OnDetachedFromWindow();
}
- // IsInsideSwipeRefreshLayout — walks parent view tree.
- bool IsInsideSwipeRefreshLayout()
- {
- var parent = Parent;
- while (parent is not null)
- {
- if (parent is MauiSwipeRefreshLayout)
- {
- return true;
- }
- parent = parent.Parent;
- }
- return false;
- }
-
void UpdateClipBounds(int width, int height)
{
if (width > 0 && height > 0)
diff --git a/src/Core/src/Platform/Android/MauiWebView.cs b/src/Core/src/Platform/Android/MauiWebView.cs
index bd91d4622564..bd3f41402f31 100644
--- a/src/Core/src/Platform/Android/MauiWebView.cs
+++ b/src/Core/src/Platform/Android/MauiWebView.cs
@@ -36,7 +36,7 @@ protected override void OnAttachedToWindow()
// Re-evaluate ClipBounds when re-parented (e.g., wrapped in WrapperView for shadow)
UpdateClipBounds(Width, Height);
- if (IsInsideSwipeRefreshLayout())
+ if (RefreshViewWebViewScrollCapture.IsInsideMauiSwipeRefreshLayout(this))
{
RefreshViewWebViewScrollCapture.Attach(this);
// If a page has already loaded before this WebView was placed inside a
@@ -56,20 +56,6 @@ protected override void OnDetachedFromWindow()
base.OnDetachedFromWindow();
}
- bool IsInsideSwipeRefreshLayout()
- {
- var parent = Parent;
- while (parent is not null)
- {
- if (parent is MauiSwipeRefreshLayout)
- {
- return true;
- }
- parent = parent.Parent;
- }
- return false;
- }
-
void UpdateClipBounds(int width, int height)
{
if (width > 0 && height > 0)
diff --git a/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
index f9b03a4dfade..ac2ab4b54da7 100644
--- a/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
+++ b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
@@ -142,6 +142,20 @@ internal static void InjectObserver(WebView? webView)
internal static bool IsAttached(WebView? webView) => GetState(webView) is not null;
+ internal static bool IsInsideMauiSwipeRefreshLayout(WebView webView)
+ {
+ var parent = webView.Parent;
+ while (parent is not null)
+ {
+ if (parent is MauiSwipeRefreshLayout)
+ {
+ return true;
+ }
+ parent = parent.Parent;
+ }
+ return false;
+ }
+
internal static bool TryGetCanScrollUp(WebView? webView, out bool canScrollUp)
{
if (webView is null)