diff --git a/src/Core/src/Platform/Android/ActivityIndicatorExtensions.cs b/src/Core/src/Platform/Android/ActivityIndicatorExtensions.cs index d3dc568c6202..09f8bacd31e0 100644 --- a/src/Core/src/Platform/Android/ActivityIndicatorExtensions.cs +++ b/src/Core/src/Platform/Android/ActivityIndicatorExtensions.cs @@ -7,28 +7,28 @@ public static class ActivityIndicatorExtensions { public static void UpdateIsRunning(this ProgressBar progressBar, IActivityIndicator activityIndicator) { - // Guard: check IsDisposed() before accessing any view properties to avoid ObjectDisposedException - // on a ProgressBar whose native handle has already been released (see ViewExtensions pattern). + // Guard: check IsDisposed() before touching any view properties to avoid an + // ObjectDisposedException on a ProgressBar whose native handle was already released. if (progressBar.IsDisposed()) { return; } - // Pre-compute the desired visibility from the current activityIndicator state before deferring, - // so the lambda performs a simple write with no risk of reading stale handler state. - // Defer via Post() only when a layout traversal is in progress OR the view is not yet attached - // (RecyclerView header collapse scenario, see https://github.com/dotnet/maui/issues/33780). - // On the common path (attached, idle), apply synchronously so that existing tests and callers - // that read state immediately after update continue to work correctly. + // Defer via Post() only when a layout traversal is in progress OR the view is not yet + // attached (RecyclerView header collapse scenario). On the common path (attached, idle), + // apply synchronously so existing tests and callers that read state immediately after the + // update continue to work correctly. if (progressBar.IsInLayout || !progressBar.IsAttachedToWindow) { - var targetVisibility = GetActivityIndicatorVisibility(activityIndicator); progressBar.Post(() => { - // Guard: skip if the view was recycled/disposed or detached before the runnable fired. + // Guard: skip if the view was disposed before the runnable fired. if (!progressBar.IsDisposed()) { - progressBar.Visibility = targetVisibility; + // Re-read the LIVE visibility here instead of capturing a snapshot before the + // Post(). Capturing a stale "show" value caused this deferred runnable to + // resurrect an indicator that was hidden after it was queued. + progressBar.Visibility = GetActivityIndicatorVisibility(activityIndicator); } }); } diff --git a/src/Core/tests/DeviceTests/Handlers/ActivityIndicator/ActivityIndicatorHandlerTests.Android.cs b/src/Core/tests/DeviceTests/Handlers/ActivityIndicator/ActivityIndicatorHandlerTests.Android.cs index 479adb8a95d6..3b968411e8a4 100644 --- a/src/Core/tests/DeviceTests/Handlers/ActivityIndicator/ActivityIndicatorHandlerTests.Android.cs +++ b/src/Core/tests/DeviceTests/Handlers/ActivityIndicator/ActivityIndicatorHandlerTests.Android.cs @@ -32,5 +32,35 @@ public override async Task SetVisibility(Visibility visibility) var id = await GetValueAsync(view, handler => GetVisibility(handler)); Assert.Equal(view.Visibility, id); } + + [Fact(DisplayName = "Deferred Show Does Not Resurrect A Hidden Indicator")] + public async Task DeferredShowDoesNotResurrectHiddenIndicator() + { + var activityIndicator = new ActivityIndicatorStub + { + IsRunning = true, + Visibility = Visibility.Visible + }; + + var visibility = await InvokeOnMainThreadAsync(async () => + { + var handler = CreateHandler(activityIndicator); + var progressBar = handler.PlatformView; + + activityIndicator.IsRunning = false; + activityIndicator.Visibility = Visibility.Collapsed; + + ViewStates result = ViewStates.Visible; + await progressBar.AttachAndRun(async () => + { + await Task.Delay(100); + result = progressBar.Visibility; + }); + + return result; + }); + + Assert.Equal(ViewStates.Gone, visibility); + } } -} \ No newline at end of file +}