StatusBar and NavigationBar color setup

by  littlecode
Phone with NavigationBar and StatusBar

Setting StatusBar and NavigationBar color is a common requirement in mobile application design. The NavigationBar is a shared Xamarin component for iOS and Android so its color can be changed easily from a shared project. On the other hand, the StatusBar must be handled natively on both platforms.

NavigationBar color

To set the NavigationBar color we are going to use Xamarin XAML styles and dynamic resource binding. Since the NavigationBar background color is the property of the navigation page, all we need to do is define the style for the navigation page and dinamically bind its BarBackgroundColor to NavigationBarColor resource. Using the DynamicResource binding every time the NavigationBarColor is changed, all resources bound to its value will be updated.

<Application.Resources>

    <Color x:Key="NavigationBarColor">#2196F3</Color>

    <Style TargetType="NavigationPage">
        <Setter Property="BarBackgroundColor" Value="{DynamicResource Key=NavigationBarColor}" />
    </Style>

</Application.Resources>

The NavigationBar color can be set by changing the value of the NavigationBarColor property in ResourceDictionary. So, we can define the INavigationBarService that will do that for us.

public interface INavigationBarService
{
    void SetNavigationBarColor(Color color);
}

public class NavigationBarService : INavigationBarService
{
    public const string NavigationBarColorKey = "NavigationBarColor";

    public void SetNavigationBarColor(Color color)
    {
        App.Current.Resources[NavigationBarColorKey] = color;
    }
}


StatusBar color Android

Setting the status bar color in Android is relatively simple since Android has native functionality to change the StatusBar color. We only need to create the StatusBarService in Android and register it to IStatusBarService in the dependency injection container.

private readonly MainActivity _mainActivity;

public StatusBarService(MainActivity mainActivity)
{
    _mainActivity = mainActivity;
}

public void SetStatusBarColor(Color color)
    => _mainActivity.Window.SetStatusBarColor(color.ToAndroid());

Because the SetStatusBarColor method can be only accessed via MainActivity when registering IStatusBarService, we need to pass an instance of StatusBarService that has reference to MainActivitiy. Since in Android dependency injection is done in the AndroidInitalizer, we need a way to pass the MainActivity reference to the initializer. In the example, it is done via a constructor.

public class AndroidInitializer : IPlatformInitializer
{
    private readonly MainActivity _mainActivity;

    public AndroidInitializer(MainActivity mainActivity)
    {
        _mainActivity = mainActivity;
    }

    public void RegisterTypes(IContainerRegistry containerRegistry)
    {
        containerRegistry.RegisterInstance<IStatusBarService>(new StatusBarService(_mainActivity));
    }
}

Lastly, we need to update the OnCreate method to use our new AndoridInitalizer with the MainActivity reference.

protected override void OnCreate(Bundle savedInstanceState)
{
    TabLayoutResource = Resource.Layout.Tabbar;
    ToolbarResource = Resource.Layout.Toolbar;

    base.OnCreate(savedInstanceState);

    Xamarin.Essentials.Platform.Init(this, savedInstanceState);
    global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
    LoadApplication(new App(new AndroidInitializer(this)));
}

StatusBar color iOS

In iOS 13.0 and greater, the StatusBar color is set to the color of the NavigationBar color if there is a navigation page present. If not, the StatusBar color is set to system light or dark, depending on the current theme.

To get the same functionality as in Android, we need to create a new iOS frame and put it in the StatusBar location. Then we can change its background color. Hence we can have a different color for the StatusBar and the NavigationBar. Also, we can have the StatusBar color if there is no Navigation page present.

On previous iOS versions, the StatusBar is set by simply setting the background color of the StatusBar UIView. Just like in Android, we need to create the StatusBarService that will be registered to the IStatusBarService in iOS dependency injection.

public class StatusBarService : IStatusBarService
{
    private int _statusBarFrameHash = 0;

    private Color _lastStatusBarColor = Color.Default;

    public void SetStatusBarColor(Color color)
    {
        _lastStatusBarColor = color;

        var uiColor = color.ToUIColor();

        if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0))
        {
            CreateOrUpdateStatusBarBackgroundFrame(uiColor);
        }
        else if (UIApplication.SharedApplication.ValueForKey(new NSString("statusBar")) is UIView statusBar && statusBar.RespondsToSelector(new ObjCRuntime.Selector("setBackgroundColor:")))
        {
            statusBar.BackgroundColor = uiColor;
        }
    }

    private void CreateOrUpdateStatusBarBackgroundFrame(UIColor uiColor)
    {
        if (_statusBarFrameHash is 0)
        {
            var window = UIApplication.SharedApplication.Windows.OrderBy(x => x.WindowLevel).LastOrDefault();
            if (window is null)
                return;

            var statusBarView = new UIView(UIApplication.SharedApplication.StatusBarFrame)
            {
                BackgroundColor = uiColor
            };

            _statusBarFrameHash = statusBarView.GetHashCode();

            window.AddSubview(statusBarView);
        }
        else
        {
            var status = UIApplication.SharedApplication.Windows.SelectMany(x => x.Subviews).FirstOrDefault(x => x.GetHashCode() == _statusBarFrameHash);
            if (status != null)
            {
                status.BackgroundColor = uiColor;
            }
        }
    }
}

Registering status bar service:

public class iOSInitializer : IPlatformInitializer
{
    public void RegisterTypes(IContainerRegistry containerRegistry)
    {
        containerRegistry.RegisterSingleton<IStatusBarService, StatusBarService>();
    }
}

PopupPage iOS

With the current implementation using INavigationBarServie and IStatusBarService, we can independently change the background color of NavigationBar and StatusBar on iOS and Android. But on iOS, there is a problem with PopupPages.

In this example, we are using Rg.Plugins.Popup. When navigating the PopupPage, its background color tints the page behind it. On Android, when on PopupPage, StatusBar, and NavigationBar color is tinted by the PopupPage. On iOS, if on version 13.0 and up, we are creating a frame that is above PopupPage therefore it will not be tinted by the PopupPage background color.

To fix this, we need to use the current StatusBar color and PopupPage background color and calculate the new color by overlaying the latter on the former. When the new StatusBar color is calculated, we set it to StatusBar color and save the previous status bar color so that we can set it back to the original color after navigating from PopupPage.

Now we can update the StatusBarService with two new methods. The SetStatusBarModalDialogColor that has argument corresponding to PopupPage background color and RemoveStatusBarModalDialogColor that will set background color to original. Also, there is a private method AddOverlayColorOnStatusBar for calculating the overlayed color.

public class StatusBarService : IStatusBarService
{
    private int _statusBarFrameHash = 0;

    private Color _lastStatusBarColor = Color.Default;

    public void SetStatusBarColor(Color color)
    {
        _lastStatusBarColor = color;

        var uiColor = color.ToUIColor();

        if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0))
        {
            CreateOrUpdateStatusBarBackgroundFrame(uiColor);
        }
        else if (UIApplication.SharedApplication.ValueForKey(new NSString("statusBar")) is UIView statusBar && statusBar.RespondsToSelector(new ObjCRuntime.Selector("setBackgroundColor:")))
        {
            statusBar.BackgroundColor = uiColor;
        }
    }

    public void SetStatusBarModalDialogColor(Color color)
    {
        if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0))
        {
            CreateOrUpdateStatusBarBackgroundFrame(AddOverlayColorOnStatusBar(color).ToUIColor());
        }
    }

    public void RemoveStatusBarModalDialogColor()
        => SetStatusBarColor(_lastStatusBarColor);

    private void CreateOrUpdateStatusBarBackgroundFrame(UIColor uiColor)
    {
        if (_statusBarFrameHash is 0)
        {
            var window = UIApplication.SharedApplication.Windows.OrderBy(x => x.WindowLevel).LastOrDefault();
            if (window is null)
                return;

            var statusBarView = new UIView(UIApplication.SharedApplication.StatusBarFrame)
            {
                BackgroundColor = uiColor
            };

            _statusBarFrameHash = statusBarView.GetHashCode();

            window.AddSubview(statusBarView);
        }
        else
        {
            var status = UIApplication.SharedApplication.Windows.SelectMany(x => x.Subviews).FirstOrDefault(x => x.GetHashCode() == _statusBarFrameHash);
            if (status != null)
            {
                status.BackgroundColor = uiColor;
            }
        }
    }

    private Color AddOverlayColorOnStatusBar(Color overlay)
    {
        var a0 = overlay.A;
        var r0 = overlay.R;
        var g0 = overlay.G;
        var b0 = overlay.B;

        var a1 = _lastStatusBarColor.A;
        var r1 = _lastStatusBarColor.R;
        var g1 = _lastStatusBarColor.G;
        var b1 = _lastStatusBarColor.B;

        var a01 = (1 - a0) * a1 + a0;

        var r01 = ((1 - a0) * a1 * r1 + a0 * r0) / a01;

        var g01 = ((1 - a0) * a1 * g1 + a0 * g0) / a01;

        var b01 = ((1 - a0) * a1 * b1 + a0 * b0) / a01;

        return new Color(r01, g01, b01, a01);
    }
}

After fixing the PopupPage behavior on iOS, we need to update the Andoird implementation as well since it implements the same IStatusBarService as iOS service.

Final IStatusBarService:

public interface IStatusBarService
{
    void RemoveStatusBarModalDialogColor();
    void SetStatusBarColor(Color color);
    void SetStatusBarModalDialogColor(Color color);
}

Final android implementation:

public class StatusBarService : IStatusBarService
{
    private readonly MainActivity _mainActivity;

    public StatusBarService(MainActivity mainActivity)
    {
        _mainActivity = mainActivity;
    }

    public void SetStatusBarColor(Color color)
        => _mainActivity.Window.SetStatusBarColor(color.ToAndroid());

    public void RemoveStatusBarModalDialogColor()
    {
        // Not needed on Android
    }

    public void SetStatusBarModalDialogColor(Color color)
    {
        // Not needed on Android
    }
}

Conclusion

With this implementation, we can have the same StatusBar and NavigationBar functionality on both Android and iOS. For further information, you can check out the example project that fully demonstrates how to set up and implement given functionalities.