Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix statusBar color changes on modal pages #2413

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
45 changes: 43 additions & 2 deletions src/CommunityToolkit.Maui.Core/AppBuilderExtensions.shared.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
namespace CommunityToolkit.Maui.Core;
using System.Diagnostics;
using CommunityToolkit.Maui.Core.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.LifecycleEvents;
using Microsoft.Maui.Platform;

namespace CommunityToolkit.Maui.Core;

/// <summary>
/// <see cref="MauiAppBuilder"/> Extensions
Expand All @@ -11,9 +17,44 @@ public static class AppBuilderExtensions
/// <param name="builder"><see cref="MauiAppBuilder"/> generated by <see cref="MauiApp"/> </param>
/// <param name="options"><see cref="Options"/></param>
/// <returns><see cref="MauiAppBuilder"/> initialized for <see cref="CommunityToolkit.Maui.Core"/></returns>
public static MauiAppBuilder UseMauiCommunityToolkitCore(this MauiAppBuilder builder, Action<Options>? options = default)
public static MauiAppBuilder UseMauiCommunityToolkitCore(this MauiAppBuilder builder, Action<Options>? options = null)
{
options?.Invoke(new Options());

#if ANDROID
if (Options.ShouldUseStatusBarBehaviorOnAndroidModalPage)
{
builder.Services.AddSingleton<IDialogFragmentService, DialogFragmentService>();

builder.ConfigureLifecycleEvents(static lifecycleBuilder =>
{
lifecycleBuilder.AddAndroid(static androidBuilder =>
{
androidBuilder.OnCreate(static (activity, _) =>
{
if (activity is not AndroidX.AppCompat.App.AppCompatActivity componentActivity)
{
Trace.WriteLine($"Unable to Modify Android StatusBar On ModalPage: Activity {activity.LocalClassName} must be an {nameof(AndroidX.AppCompat.App.AppCompatActivity)}");
return;
}

if (componentActivity.GetFragmentManager() is not AndroidX.Fragment.App.FragmentManager fragmentManager)
{
Trace.WriteLine($"Unable to Modify Android StatusBar On ModalPage: Unable to retrieve fragment manager from {nameof(AndroidX.AppCompat.App.AppCompatActivity)}");
return;
}

var dialogFragmentService = IPlatformApplication.Current?.Services.GetRequiredService<IDialogFragmentService>()
?? throw new InvalidOperationException($"Unable to retrieve {nameof(IDialogFragmentService)}");


fragmentManager.RegisterFragmentLifecycleCallbacks(new FragmentLifecycleManager(dialogFragmentService), false);
});
});
});
}
#endif

return builder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Android.Content;
using Android.Views;

namespace CommunityToolkit.Maui.Core;

public interface IDialogFragmentService
{
void OnFragmentAttached(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment f, Context context);
void OnFragmentCreated(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment f, Bundle? savedInstanceState);
void OnFragmentDestroyed(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment f);
void OnFragmentDetached(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment f);
void OnFragmentPaused(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment f);
void OnFragmentPreAttached(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment f, Context context);
void OnFragmentPreCreated(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment f, Bundle? savedInstanceState);
void OnFragmentResumed(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment f);
void OnFragmentSaveInstanceState(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment f, Bundle outState);
void OnFragmentStarted(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment f);
void OnFragmentStopped(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment f);
void OnFragmentViewCreated(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment f, View v, Bundle? savedInstanceState);
void OnFragmentViewDestroyed(AndroidX.Fragment.App.FragmentManager fm, AndroidX.Fragment.App.Fragment f);
}
10 changes: 10 additions & 0 deletions src/CommunityToolkit.Maui.Core/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,14 @@ namespace CommunityToolkit.Maui.Core;
/// </summary>
public class Options
{
internal static bool ShouldUseStatusBarBehaviorOnAndroidModalPage { get; private set; } = true;

/// <summary>
/// Enables the use of the DialogFragment Lifecycle service for Android.
/// </summary>
/// <param name="value">true if yes or false if you want to implement your own.</param>
/// <remarks>
/// Default value is true.
/// </remarks>
public void SetShouldUseStatusBarBehaviorOnAndroidModalPage(bool value) => ShouldUseStatusBarBehaviorOnAndroidModalPage = value;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using System.Diagnostics.CodeAnalysis;
using Android.Content;
using Android.Views;
using AndroidX.AppCompat.App;
using DialogFragment = AndroidX.Fragment.App.DialogFragment;
using Fragment = AndroidX.Fragment.App.Fragment;
using FragmentManager = AndroidX.Fragment.App.FragmentManager;

namespace CommunityToolkit.Maui.Core.Services;

sealed partial class DialogFragmentService : IDialogFragmentService
{
public void OnFragmentAttached(FragmentManager fm, Fragment f, Context context)
{
}

public void OnFragmentCreated(FragmentManager fm, Fragment f, Bundle? savedInstanceState)
{
}

public void OnFragmentDestroyed(FragmentManager fm, Fragment f)
{
}

public void OnFragmentDetached(FragmentManager fm, Fragment f)
{
}

public void OnFragmentPaused(FragmentManager fm, Fragment f)
{
}

public void OnFragmentPreAttached(FragmentManager fm, Fragment f, Context context)
{
}

public void OnFragmentPreCreated(FragmentManager fm, Fragment f, Bundle? savedInstanceState)
{
}

public void OnFragmentResumed(FragmentManager fm, Fragment f)
{
}

public void OnFragmentSaveInstanceState(FragmentManager fm, Fragment f, Bundle outState)
{
}

public void OnFragmentStarted(FragmentManager fm, Fragment f)
{
if (!TryConvertToDialogFragment(f, out var dialogFragment) || Microsoft.Maui.ApplicationModel.Platform.CurrentActivity is not AppCompatActivity activity)
{
return;
}

HandleStatusBarColor(dialogFragment, activity);
}

static void HandleStatusBarColor(DialogFragment dialogFragment, AppCompatActivity activity)
{
if (activity.Window is null)
{
return;
}

var statusBarColor = activity.Window.StatusBarColor;
var platformColor = new Android.Graphics.Color(statusBarColor);
if (dialogFragment.Dialog?.Window is not Window dialogWindow)
{
throw new InvalidOperationException("Dialog window cannot be null");
Copy link
Member Author

Choose a reason for hiding this comment

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

@brminnick we shouldn't throw here, since there's no way for devs to catch this exception. I used Debug.Assert because it's what the runtime/sdk uses to safe check for null . But here you're checking against a type which can be false and throw this exception.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Using Debug.Assert() is a bit of an anti-pattern because it only works when building in Debug configuration:

Debug.Assert method works only in debug builds.

This means that when users consume our library and the value of dialog.Window is null, the following code in Release builds is guaranteed to throw an unhelpful NullReferenceException:

Debug.Assert(dialog is not null);
Debug.Assert(dialog.Window is not null);

var window = dialog.Window;

In what scenario would this code be executed called and dialog.Window also be null? If this scenario exists, I agree that we should implement a solution.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think just returning is enough, the only scenario I can imagine that will happening is when the app is backgrounded or is moving into background.

}

var isColorTransparent = platformColor == Android.Graphics.Color.Transparent;

if (OperatingSystem.IsAndroidVersionAtLeast(30))
{
var windowInsetsController = dialogWindow.InsetsController;
var appearance = activity.Window.InsetsController?.SystemBarsAppearance;

if (windowInsetsController is null)
{
System.Diagnostics.Trace.WriteLine("WindowInsetsController is null, cannot set system bars appearance.");
return;
}

if (appearance.HasValue)
{
windowInsetsController.SetSystemBarsAppearance(appearance.Value, appearance.Value);
}
else
{
windowInsetsController.SetSystemBarsAppearance(
isColorTransparent ? 0 : (int)WindowInsetsControllerAppearance.LightStatusBars,
(int)WindowInsetsControllerAppearance.LightStatusBars);
}

dialogWindow.SetStatusBarColor(platformColor);

if (!OperatingSystem.IsAndroidVersionAtLeast(35))
{
dialogWindow.SetDecorFitsSystemWindows(!isColorTransparent);
}
else
{
AndroidX.Core.View.WindowCompat.SetDecorFitsSystemWindows(dialogWindow, !isColorTransparent);
}
}
else
{
dialogWindow.SetStatusBarColor(platformColor);

if (isColorTransparent)
{
dialogWindow.ClearFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
dialogWindow.SetFlags(WindowManagerFlags.LayoutNoLimits, WindowManagerFlags.LayoutNoLimits);
}
else
{
dialogWindow.ClearFlags(WindowManagerFlags.LayoutNoLimits);
dialogWindow.SetFlags(WindowManagerFlags.DrawsSystemBarBackgrounds, WindowManagerFlags.DrawsSystemBarBackgrounds);
}
}
}

public void OnFragmentStopped(FragmentManager fm, Fragment f)
{
}

public void OnFragmentViewCreated(FragmentManager fm, Fragment f, Android.Views.View v, Bundle? savedInstanceState)
{
}

public void OnFragmentViewDestroyed(FragmentManager fm, Fragment f)
{
}

static bool TryConvertToDialogFragment(Fragment fragment, [NotNullWhen(true)] out DialogFragment? dialogFragment)
{
dialogFragment = null;

if (fragment is not DialogFragment dialog)
{
return false;
}

dialogFragment = dialog;
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace CommunityToolkit.Maui.Core.Services;

sealed partial class DialogFragmentService
{

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Android.Content;
using FragmentManager = AndroidX.Fragment.App.FragmentManager;

namespace CommunityToolkit.Maui.Core.Services;

sealed class FragmentLifecycleManager(IDialogFragmentService dialogFragmentService) : FragmentManager.FragmentLifecycleCallbacks
{
readonly IDialogFragmentService dialogFragmentService = dialogFragmentService;

public override void OnFragmentAttached(FragmentManager fm, AndroidX.Fragment.App.Fragment f, Context context)
{
base.OnFragmentAttached(fm, f, context);
dialogFragmentService.OnFragmentAttached(fm, f, context);
}

public override void OnFragmentCreated(FragmentManager fm, AndroidX.Fragment.App.Fragment f, Bundle? savedInstanceState)
{
base.OnFragmentCreated(fm, f, savedInstanceState);
dialogFragmentService.OnFragmentCreated(fm, f, savedInstanceState);
}

public override void OnFragmentDestroyed(FragmentManager fm, AndroidX.Fragment.App.Fragment f)
{
base.OnFragmentDestroyed(fm, f);
dialogFragmentService.OnFragmentDestroyed(fm, f);
}

public override void OnFragmentDetached(FragmentManager fm, AndroidX.Fragment.App.Fragment f)
{
base.OnFragmentDetached(fm, f);
dialogFragmentService.OnFragmentDetached(fm, f);
}

public override void OnFragmentPaused(FragmentManager fm, AndroidX.Fragment.App.Fragment f)
{
base.OnFragmentPaused(fm, f);
dialogFragmentService.OnFragmentPaused(fm, f);
}

public override void OnFragmentPreAttached(FragmentManager fm, AndroidX.Fragment.App.Fragment f, Context context)
{
base.OnFragmentPreAttached(fm, f, context);
dialogFragmentService.OnFragmentPreAttached(fm, f, context);
}

public override void OnFragmentPreCreated(FragmentManager fm, AndroidX.Fragment.App.Fragment f, Bundle? savedInstanceState)
{
base.OnFragmentPreCreated(fm, f, savedInstanceState);
dialogFragmentService.OnFragmentPreCreated(fm, f, savedInstanceState);
}

public override void OnFragmentResumed(FragmentManager fm, AndroidX.Fragment.App.Fragment f)
{
base.OnFragmentResumed(fm, f);
dialogFragmentService.OnFragmentResumed(fm, f);
}

public override void OnFragmentSaveInstanceState(FragmentManager fm, AndroidX.Fragment.App.Fragment f, Bundle outState)
{
base.OnFragmentSaveInstanceState(fm, f, outState);
dialogFragmentService.OnFragmentSaveInstanceState(fm, f, outState);
}

public override void OnFragmentStarted(FragmentManager fm, AndroidX.Fragment.App.Fragment f)
{
base.OnFragmentStarted(fm, f);
dialogFragmentService.OnFragmentStarted(fm, f);
}

public override void OnFragmentStopped(FragmentManager fm, AndroidX.Fragment.App.Fragment f)
{
base.OnFragmentStopped(fm, f);
dialogFragmentService.OnFragmentStopped(fm, f);
}

public override void OnFragmentViewCreated(FragmentManager fm, AndroidX.Fragment.App.Fragment f, Android.Views.View v, Bundle? savedInstanceState)
{
base.OnFragmentViewCreated(fm, f, v, savedInstanceState);
dialogFragmentService.OnFragmentViewCreated(fm, f, v, savedInstanceState);
}

public override void OnFragmentViewDestroyed(FragmentManager fm, AndroidX.Fragment.App.Fragment f)
{
base.OnFragmentViewDestroyed(fm, f);
dialogFragmentService.OnFragmentViewDestroyed(fm, f);
}
}
8 changes: 4 additions & 4 deletions src/CommunityToolkit.Maui/AppBuilderExtensions.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ public static class AppBuilderExtensions
/// <param name="builder"><see cref="MauiAppBuilder"/> generated by <see cref="MauiApp"/> </param>
/// <param name="options"><see cref="Options"/></param>
/// <returns><see cref="MauiAppBuilder"/> initialized for <see cref="CommunityToolkit.Maui"/></returns>
public static MauiAppBuilder UseMauiCommunityToolkit(this MauiAppBuilder builder, Action<Options>? options = default)
public static MauiAppBuilder UseMauiCommunityToolkit(this MauiAppBuilder builder, Action<Options>? options = null)
{
// Pass `null` because `options?.Invoke()` will set options on both `CommunityToolkit.Maui` and `CommunityToolkit.Maui.Core`
builder.UseMauiCommunityToolkitCore(null);

builder.Services.AddSingleton<IPopupService, PopupService>();

// Invokes options for both `CommunityToolkit.Maui` and `CommunityToolkit.Maui.Core`
options?.Invoke(new Options(builder));

builder.ConfigureMauiHandlers(h =>
builder.Services.AddSingleton<IPopupService, PopupService>();

builder.ConfigureMauiHandlers(static h =>
{
h.AddHandler<DrawingView, DrawingViewHandler>();
h.AddHandler<Popup, PopupHandler>();
Expand Down