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..305937bb2313 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33510.cs @@ -0,0 +1,128 @@ +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 +{ + RefreshView _refreshView; + Label _statusLabel; + + protected override void Init() + { + _statusLabel = new Label + { + AutomationId = "StatusLabel", + Text = "Loading..." + }; + + var webView = new WebView + { + AutomationId = "TestWebView", + Source = new HtmlWebViewSource { Html = ScrollableHtml } + }; + + webView.Navigated += (_, _) => _statusLabel.Text = "WebView ready"; + + _refreshView = new RefreshView + { + AutomationId = "TestRefreshView", + Content = webView + }; + + _refreshView.Command = new Command(async () => + { + _statusLabel.Text = "Refresh triggered"; + await Task.Delay(150); + _refreshView.IsRefreshing = false; + }); + + var grid = new Grid + { + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Star } + } + }; + + grid.Add(_statusLabel, 0, 0); + grid.Add(_refreshView, 0, 1); + + Content = grid; + } + + const string ScrollableHtml = """ + + + + + + + + +
+ +
+
Content 01
+
Content 02
+
Content 03
+
Content 04
+
Content 05
+
Content 06
+
Content 07
+
Content 08
+
Content 09
+
Content 10
+
Content 11
+
Content 12
+
Content 13
+
Content 14
+
Content 15
+
Content 16
+
Content 17
+
Content 18
+
Content 19
+
Content 20
+
+
+ + + """; +} 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..f33deebba16d --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33510.cs @@ -0,0 +1,77 @@ +#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 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 TestRefreshView = "TestRefreshView"; + + public Issue33510(TestDevice device) : base(device) + { + } + + public override string Issue => "[Android] RefreshView triggers pull-to-refresh immediately when scrolling up inside a WebView"; + + [Test] + public void PullToRefreshShouldNotTriggerWhenWebViewIsScrolledDown() + { + var androidApp = WaitForAndroidApp(); + var refreshViewRect = App.WaitForElement(TestRefreshView).GetRect(); + var x = refreshViewRect.CenterX(); + + // Scroll content down by swiping up inside the WebView + for (var i = 0; i < 3; i++) + { + DragInsideWebView(androidApp, x, + refreshViewRect.Y + (refreshViewRect.Height * 70 / 100), + x, + refreshViewRect.Y + (refreshViewRect.Height * 25 / 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)); + + 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() + { + 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; + } + + 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/MauiHybridWebView.cs b/src/Core/src/Platform/Android/MauiHybridWebView.cs index 6354bdf7abea..74518c09cd1e 100644 --- a/src/Core/src/Platform/Android/MauiHybridWebView.cs +++ b/src/Core/src/Platform/Android/MauiHybridWebView.cs @@ -36,12 +36,31 @@ 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 (RefreshViewWebViewScrollCapture.IsInsideMauiSwipeRefreshLayout(this)) + { + 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(); } void UpdateClipBounds(int width, int height) @@ -77,5 +96,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 1d2553dde335..23cc46bd77d5 100644 --- a/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs +++ b/src/Core/src/Platform/Android/MauiSwipeRefreshLayout.cs @@ -19,6 +19,10 @@ public class MauiSwipeRefreshLayout : SwipeRefreshLayout readonly Context _context; AView? _contentView; bool _refreshEnabled = true; + AWebView? _activeTouchWebView; + RefreshViewWebViewScrollCapture.ScrollCaptureState? _activeTouchScrollState; + bool _webViewOwnsGesture; + bool _touchStartedInWebView; public MauiSwipeRefreshLayout(Context context) : base(context) { @@ -135,6 +139,66 @@ 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; + // ACTION_DOWN — caches ScrollCaptureState via GetAttachedState(). + _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: + // 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 (!_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; + } + + return base.OnInterceptTouchEvent(ev); + } + bool CanScrollUp(AView? view) { if (!(view is ViewGroup viewGroup)) @@ -182,9 +246,44 @@ 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; } + + // 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 2d571d50030a..9b439e7c66d7 100644 --- a/src/Core/src/Platform/Android/MauiWebView.cs +++ b/src/Core/src/Platform/Android/MauiWebView.cs @@ -36,6 +36,25 @@ protected override void OnAttachedToWindow() // Re-evaluate ClipBounds when re-parented (e.g., wrapped in WrapperView for shadow) UpdateClipBounds(Width, Height); + + if (RefreshViewWebViewScrollCapture.IsInsideMauiSwipeRefreshLayout(this)) + { + 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(); } void UpdateClipBounds(int width, int height) @@ -108,5 +127,15 @@ 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..89e53e36c47b 100644 --- a/src/Core/src/Platform/Android/MauiWebViewClient.cs +++ b/src/Core/src/Platform/Android/MauiWebViewClient.cs @@ -22,8 +22,12 @@ 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; + } if (!string.IsNullOrWhiteSpace(url)) { @@ -59,12 +63,21 @@ 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); 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); } @@ -76,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); @@ -102,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); @@ -111,7 +128,9 @@ static bool IsBlankNavigation(string? url) static string GetValidUrl(string? url) { if (string.IsNullOrEmpty(url)) + { return string.Empty; + } return url; } @@ -119,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 new file mode 100644 index 000000000000..ac2ab4b54da7 --- /dev/null +++ b/src/Core/src/Platform/Android/RefreshViewWebViewScrollCapture.cs @@ -0,0 +1,228 @@ +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; + + // 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 () { + 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 host = window.mauiRefreshViewHost; + if (!host || typeof host.setCanScrollUp !== 'function') { + return; + } + var scrollable = getScrollableElement(target); + host.setCanScrollUp(getScrollTopForElement(scrollable) > 0); + } catch (e) { + } + } + + // 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); + } + + 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); + })(); + """; + + 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; + } + + // 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); + // 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); + } + + 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) + { + 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; + + // 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. + 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; + + internal bool HasReportedState => _hasReportedState; + + [JavascriptInterface] + [RequiresUnreferencedCode("Java.Interop.Export uses dynamic features.")] + [Export("setCanScrollUp")] + public void SetCanScrollUp(bool canScrollUp) + { + if (_detached) + { + return; + } + _canScrollUp = canScrollUp; + _hasReportedState = true; + } + + internal void MarkDetached() => _detached = 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 eebc770d253a..b3c4372f7b7d 100644 --- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -13,7 +13,14 @@ 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 override Microsoft.Maui.Platform.MauiWebView.OnTouchEvent(Android.Views.MotionEvent? e) -> bool