Skip to content

[Android] Fix for RefreshView triggering pull-to-refresh when scrolling inside a WebView with internal scrollable content#34614

Open
BagavathiPerumal wants to merge 6 commits intodotnet:mainfrom
BagavathiPerumal:fix-33510
Open

[Android] Fix for RefreshView triggering pull-to-refresh when scrolling inside a WebView with internal scrollable content#34614
BagavathiPerumal wants to merge 6 commits intodotnet:mainfrom
BagavathiPerumal:fix-33510

Conversation

@BagavathiPerumal
Copy link
Copy Markdown
Contributor

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:

  • If the internal web content is not yet at the top, the gesture remains with the WebView
  • Once the internal web content reaches the top, RefreshView is allowed to intercept and trigger refresh

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

  • Android
  • Windows
  • iOS
  • Mac

Issues Fixed

Fixes #33510

Output ScreenShot

Before After
BeforeFix-33510.mov
AfterFix-33510.mov

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 24, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34614

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34614"

@dotnet-policy-service dotnet-policy-service Bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Mar 24, 2026
@sheiksyedm sheiksyedm marked this pull request as ready for review March 26, 2026 10:58
Copilot AI review requested due to automatic review settings March 26, 2026 10:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 WebView JavaScript bridge + injected observer script to report whether touched DOM content can still scroll up.
  • Update MauiSwipeRefreshLayout to consult the reported “can scroll up” state for WebView (and add intercept logic for gestures starting inside a WebView).
  • 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.

Comment thread src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
Comment thread src/Core/src/Platform/Android/MauiWebViewClient.cs
Comment thread src/Core/src/Platform/Android/MauiWebView.cs
@MauiBot MauiBot added s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) s/agent-changes-requested AI agent recommends changes - found a better alternative or issues labels Mar 27, 2026
kubaflo
kubaflo previously approved these changes Mar 28, 2026
@sheiksyedm
Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests , maui-pr-devicetests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 2 pipeline(s).

@sheiksyedm
Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

@dotnet dotnet deleted a comment from MauiBot May 1, 2026
@kubaflo kubaflo dismissed MauiBot’s stale review May 1, 2026 09:39

Resetting for re-review

Copy link
Copy Markdown
Collaborator

@MauiBot MauiBot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expert Review — 7 findings

See inline comments for details.

@MauiBot MauiBot added s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues labels May 1, 2026
@dotnet dotnet deleted a comment from MauiBot May 3, 2026
@dotnet dotnet deleted a comment from MauiBot May 3, 2026
@dotnet dotnet deleted a comment from MauiBot May 3, 2026
@dotnet dotnet deleted a comment from MauiBot May 3, 2026
@dotnet dotnet deleted a comment from MauiBot May 3, 2026
@dotnet dotnet deleted a comment from MauiBot May 3, 2026
@dotnet dotnet deleted a comment from MauiBot May 3, 2026
@dotnet dotnet deleted a comment from MauiBot May 3, 2026
MauiBot
MauiBot previously requested changes May 3, 2026
Copy link
Copy Markdown
Collaborator

@MauiBot MauiBot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against #2 automatically generated candidates and selected try-fix-2 as the strongest fix.

Why: try-fix-2 preserves the PR's well-designed JS observer architecture while fixing two functional gaps identified in code review: (1) the __mauiRefreshViewObserverInstalled guard that silently breaks JS re-registration after Shell tab-switch (detach+re-attach without page reload), replaced with named JS event handles that are properly removed/re-added on each injection; (2) MauiHybridWebView inside a RefreshView was unprotected since Attach() was never called for it, now fixed via OnAttachedToWindow/OnDetachedFromWindow/Dispose overrides mirroring MauiWebView.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (`try-fix-2`)
diff --git a/src/Core/src/Platform/Android/MauiHybridWebView.cs b/src/Core/src/Platform/Android/MauiHybridWebView.cs
index 6354bdf7ab..2bff13e668 100644
--- a/src/Core/src/Platform/Android/MauiHybridWebView.cs
+++ b/src/Core/src/Platform/Android/MauiHybridWebView.cs
@@ -42,6 +42,33 @@ namespace Microsoft.Maui.Platform
 
 			// 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);
+			}
+		}
+
+		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)
@@ -77,5 +104,13 @@ namespace Microsoft.Maui.Platform
 			PostWebMessage(new WebMessage(rawMessage), AndroidAppOriginUri);
 #pragma warning restore CA1416 // Validate platform compatibility
 		}
+
+		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 1c8b1bd3ce..37d3942749 100644
--- a/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs
+++ b/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs
@@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis;
 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,26 @@ namespace Microsoft.Maui.Platform
 
 		private HybridWebViewHandler? Handler => _handler is not null && _handler.TryGetTarget(out var h) ? h : null;
 
+		public override void OnPageStarted(AWebView? view, string? url, Bitmap? favicon)
+		{
+			RefreshViewWebViewScrollCapture.Reset(view);
+			base.OnPageStarted(view, url, favicon);
+		}
+
+		public override void OnPageFinished(AWebView? view, string? url)
+		{
+			if (string.IsNullOrWhiteSpace(url))
+			{
+				base.OnPageFinished(view, url);
+				return;
+			}
+
+			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 1d2553dde3..a0b681bc97 100644
--- a/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
+++ b/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs
@@ -19,6 +19,10 @@ namespace Microsoft.Maui.Platform
 		readonly Context _context;
 		AView? _contentView;
 		bool _refreshEnabled = true;
+		AWebView? _activeTouchWebView;
+		RefreshViewWebViewScrollCapture.ScrollCaptureState? _activeTouchScrollState;
+		bool _webViewOwnsGesture;
+		bool _touchStartedInWebView;
 
 		public MauiSwipeRefreshLayout(Context context) : base(context)
 		{
@@ -182,9 +186,100 @@ namespace Microsoft.Maui.Platform
 #pragma warning restore XAOBS001 // Obsolete
 
 			if (view is AWebView webView)
-				return webView.ScrollY > 0;
+				return RefreshViewWebViewScrollCapture.TryGetCanScrollUp(webView, out var canScrollUp) && canScrollUp;
 
 			return true;
 		}
+
+		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;
+					// Cache the ScrollCaptureState object at DOWN time so the MOVE hot-path
+					// can check CanScrollUp (a volatile bool read) without any JNI calls.
+					_activeTouchScrollState = RefreshViewWebViewScrollCapture.GetAttachedState(_activeTouchWebView);
+					_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.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. Use the cached
+					// ScrollCaptureState to read a volatile bool with zero JNI overhead.
+					if (_touchStartedInWebView && _webViewOwnsGesture && _activeTouchScrollState is not null)
+					{
+						if (!_activeTouchScrollState.CanScrollUp)
+							_webViewOwnsGesture = false;
+					}
+					if (_touchStartedInWebView && _webViewOwnsGesture)
+						return false;
+					break;
+				case MotionEventActions.Cancel:
+				case MotionEventActions.Up:
+					_activeTouchWebView = null;
+					_activeTouchScrollState = null;
+					_touchStartedInWebView = false;
+					_webViewOwnsGesture = false;
+					break;
+			}
+
+			return base.OnInterceptTouchEvent(ev);
+		}
+
+		// 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)
+				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 + view.ScrollX;
+			var localY = y - view.Top  + view.ScrollY;
+
+			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 7e9019100c..377705ec64 100644
--- a/src/Core/src/Platform/Android/MauiWebView.cs
+++ b/src/Core/src/Platform/Android/MauiWebView.cs
@@ -35,6 +35,35 @@ namespace Microsoft.Maui.Platform
 
 			// 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 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()
+		{
+			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 +115,12 @@ namespace Microsoft.Maui.Platform
 				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 b24f3ab6aa..a5b42375e3 100644
--- a/src/Core/src/Platform/Android/MauiWebViewClient.cs
+++ b/src/Core/src/Platform/Android/MauiWebViewClient.cs
@@ -22,6 +22,8 @@ namespace Microsoft.Maui.Platform
 
 		public override void OnPageStarted(WebView? view, string? url, Bitmap? favicon)
 		{
+			RefreshViewWebViewScrollCapture.Reset(view);
+
 			if (!_handler.TryGetTarget(out var handler) || handler.VirtualView == null)
 				return;
 
@@ -65,6 +67,11 @@ namespace Microsoft.Maui.Platform
 
 			handler?.PlatformView.UpdateCanGoBackForward(handler.VirtualView);
 
+			// 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 872f3312a2..527afa1624 100644
--- a/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
+++ b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs
@@ -9,20 +9,24 @@ internal static class RefreshViewWebViewScrollCapture
 	const string JavaScriptInterfaceName = "mauiRefreshViewHost";
 	const int ScrollCaptureStateKey = 0x4D415549;
 
+	// The observer script intentionally omits the __mauiRefreshViewObserverInstalled guard
+	// and looks up window.mauiRefreshViewHost dynamically on every event.
+	//
+	// Why no install guard?
+	// 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).
+	// If a guard prevented re-injection, the new bridge object would never receive callbacks,
+	// silently breaking pull-to-refresh protection until the next full page reload.
+	//
+	// Why dynamic lookup?
+	// Capturing `var host = window.mauiRefreshViewHost` at install time would hold a reference
+	// to the *old* Java ScrollCaptureState even after Detach removed it.  Dynamically resolving
+	// window.mauiRefreshViewHost on each event ensures callbacks always reach the current bridge.
+	// Duplicate listeners from repeated injections are harmless – each call reads the same host
+	// and sets the same value; there is no observable side-effect from the extra invocations.
 	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 +62,30 @@ internal static class RefreshViewWebViewScrollCapture
 
 			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);
 		})();
@@ -158,10 +173,13 @@ internal static class RefreshViewWebViewScrollCapture
 		return false;
 	}
 
+	internal static ScrollCaptureState? GetAttachedState(WebView? webView) =>
+GetState(webView);
+
 	static ScrollCaptureState? GetState(WebView? webView) =>
 		webView?.GetTag(ScrollCaptureStateKey) as ScrollCaptureState;
 
-	sealed class ScrollCaptureState : Java.Lang.Object
+	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 59fed41b6d..db676dbda3 100644
--- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
+++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt
@@ -13,6 +13,13 @@ 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
+override Microsoft.Maui.Platform.MauiWebView.Dispose(bool disposing) -> void
+override Microsoft.Maui.Platform.MauiWebView.OnDetachedFromWindow() -> void

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR and removed s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates labels May 3, 2026
…WebView, optimized the implementation, and resolved the related issues as well.
@BagavathiPerumal
Copy link
Copy Markdown
Contributor Author

🤖 Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against #2 automatically generated candidates and selected try-fix-2 as the strongest fix.

Why: try-fix-2 preserves the PR's well-designed JS observer architecture while fixing two functional gaps identified in code review: (1) the __mauiRefreshViewObserverInstalled guard that silently breaks JS re-registration after Shell tab-switch (detach+re-attach without page reload), replaced with named JS event handles that are properly removed/re-added on each injection; (2) MauiHybridWebView inside a RefreshView was unprotected since Attach() was never called for it, now fixed via OnAttachedToWindow/OnDetachedFromWindow/Dispose overrides mirroring MauiWebView.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (try-fix-2)

I've updated the changes based on the Copilot review suggestions to extend the same fixes to MauiHybridWebView.

Specifically:

MauiHybridWebView.cs

  • Added OnAttachedToWindow override calls Attach(this) when the view is inside a SwipeRefreshLayout.
  • Added OnDetachedFromWindow override calls Detach() on removal.
  • Added IsInsideSwipeRefreshLayout helper walks the parent view tree to detect nesting.
  • Added Dispose(bool) override calls Detach() during cleanup to prevent leaks.

MauiHybridWebViewClient.cs

  • Added OnPageStarted override calls Reset() to clear stale scroll state on navigation.
  • Added OnPageFinished override calls InjectObserver() to inject the JS bridge after page load.
  • Added using Android.Graphics; to resolve the CS0234 build error.

PublicAPI.Unshipped.txt

  • Added five new entries for the new public overrides in MauiHybridWebView and MauiHybridWebViewClient.

These changes mirror the existing RefreshView/WebView fix pattern and resolve the same scroll-capture issue for HybridWebView inside SwipeRefreshLayout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/android s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] RefreshView triggers pull-to-refresh immediately when scrolling up inside a WebView

8 participants