Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions src/Core/src/Platform/Android/ActivityIndicatorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Comment on lines +39 to +61

Assert.Equal(ViewStates.Gone, visibility);
}
}
}
}
Loading