diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
index 7bc3a02736..3850199226 100644
--- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
+++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
@@ -59,30 +59,39 @@ namespace Avalonia.Android.Platform
DisplayEdgeToEdge = false;
}
- public Thickness GetSafeAreaPadding()
+ public Thickness SafeAreaPadding
{
- var insets = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView);
-
- if (insets != null)
+ get
{
- var renderScaling = _topLevel.RenderScaling;
+ var insets = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView);
- var inset = insets.GetInsets((DisplayEdgeToEdge ? WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | WindowInsetsCompat.Type.DisplayCutout() : 0 ) | WindowInsetsCompat.Type.Ime());
- var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars());
- var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime());
+ if (insets != null)
+ {
+ var renderScaling = _topLevel.RenderScaling;
+
+ var inset = insets.GetInsets(
+ (DisplayEdgeToEdge ?
+ WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() |
+ WindowInsetsCompat.Type.DisplayCutout() :
+ 0) | WindowInsetsCompat.Type.Ime());
+ var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars());
+ var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime());
+
+ return new Thickness(inset.Left / renderScaling,
+ inset.Top / renderScaling,
+ inset.Right / renderScaling,
+ (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ?
+ imeInset.Bottom - navBarInset.Bottom :
+ inset.Bottom) / renderScaling);
+ }
- return new Thickness(inset.Left / renderScaling,
- inset.Top / renderScaling,
- inset.Right / renderScaling,
- (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ? imeInset.Bottom - navBarInset.Bottom : inset.Bottom) / renderScaling);
+ return default;
}
-
- return default;
}
public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets)
{
- NotifySafeAreaChanged(GetSafeAreaPadding());
+ NotifySafeAreaChanged(SafeAreaPadding);
return insets;
}
@@ -93,7 +102,7 @@ namespace Avalonia.Android.Platform
public void OnGlobalLayout()
{
- NotifySafeAreaChanged(GetSafeAreaPadding());
+ NotifySafeAreaChanged(SafeAreaPadding);
}
public SystemBarTheme? SystemBarTheme
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
index f0fc13c86f..b8d80a50ff 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
@@ -43,7 +43,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly INativeControlHostImpl _nativeControlHost;
private readonly IStorageProvider _storageProvider;
private readonly ISystemNavigationManagerImpl _systemNavigationManager;
- private readonly IInsetsManager _insetsManager;
+ private readonly AndroidInsetsManager _insetsManager;
private ViewImpl _view;
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs
index 028e434104..6288142805 100644
--- a/src/Avalonia.Controls/Platform/IInsetsManager.cs
+++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs
@@ -1,28 +1,18 @@
using System;
-using Avalonia.Interactivity;
+using Avalonia.Metadata;
+#nullable enable
namespace Avalonia.Controls.Platform
{
- [Avalonia.Metadata.Unstable]
+ [Unstable]
+ [NotClientImplementable]
public interface IInsetsManager
{
- ///
- /// Gets or sets the theme for the system bars, if supported.
- ///
- SystemBarTheme? SystemBarTheme { get; set; }
-
///
/// Gets or sets whether the system bars are visible.
///
bool? IsSystemBarVisible { get; set; }
- ///
- /// Occurs when safe area for the current window changes.
- ///
-
- event EventHandler SafeAreaChanged;
-
-
///
/// Gets or sets whether the window draws edge to edge. behind any visibile system bars.
///
@@ -31,18 +21,23 @@ namespace Avalonia.Controls.Platform
///
/// Gets the current safe area padding.
///
- ///
- Thickness GetSafeAreaPadding();
-
- public class SafeAreaChangedArgs : RoutedEventArgs
+ Thickness SafeAreaPadding { get; }
+
+ ///
+ /// Occurs when safe area for the current window changes.
+ ///
+ event EventHandler? SafeAreaChanged;
+ }
+
+ public class SafeAreaChangedArgs : EventArgs
+ {
+ public SafeAreaChangedArgs(Thickness safeArePadding)
{
- public SafeAreaChangedArgs(Thickness safeArePadding)
- {
- SafeAreaPadding = safeArePadding;
- }
-
- public Thickness SafeAreaPadding { get; }
+ SafeAreaPadding = safeArePadding;
}
+
+ ///
+ public Thickness SafeAreaPadding { get; }
}
public enum SystemBarTheme
diff --git a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
index d751152a53..30f80ba27c 100644
--- a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
+++ b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
@@ -11,7 +11,6 @@ namespace Avalonia.Browser
{
internal class BrowserInsetsManager : IInsetsManager
{
- public SystemBarTheme? SystemBarTheme { get; set; }
public bool? IsSystemBarVisible
{
get
@@ -20,7 +19,7 @@ namespace Avalonia.Browser
}
set
{
- DomHelper.SetFullscreen(value != null ? !value.Value : false);
+ DomHelper.SetFullscreen(!value ?? false);
}
}
@@ -28,16 +27,19 @@ namespace Avalonia.Browser
public event EventHandler? SafeAreaChanged;
- public Thickness GetSafeAreaPadding()
+ public Thickness SafeAreaPadding
{
- var padding = DomHelper.GetSafeAreaPadding();
+ get
+ {
+ var padding = DomHelper.GetSafeAreaPadding();
- return new Thickness(padding[0], padding[1], padding[2], padding[3]);
+ return new Thickness(padding[0], padding[1], padding[2], padding[3]);
+ }
}
public void NotifySafeAreaPaddingChanged()
{
- SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(GetSafeAreaPadding()));
+ SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding));
}
}
}
diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs
index b605d82541..18ccbad692 100644
--- a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs
+++ b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs
@@ -40,10 +40,12 @@ namespace Avalonia.iOS
var view = new AvaloniaView();
lifetime.View = view;
- Window.RootViewController = new UIViewController
+ var controller = new DefaultAvaloniaViewController
{
View = view
};
+ Window.RootViewController = controller;
+ view.InitWithController(controller);
});
builder.SetupWithLifetime(lifetime);
diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs
index 2d6b93f818..09721ad181 100644
--- a/src/iOS/Avalonia.iOS/AvaloniaView.cs
+++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs
@@ -16,6 +16,7 @@ using Foundation;
using ObjCRuntime;
using OpenGLES;
using UIKit;
+using IInsetsManager = Avalonia.Controls.Platform.IInsetsManager;
namespace Avalonia.iOS
{
@@ -26,6 +27,7 @@ namespace Avalonia.iOS
private EmbeddableControlRoot _topLevel;
private TouchHandler _touches;
private ITextInputMethodClient _client;
+ private IAvaloniaViewController _controller;
public AvaloniaView()
{
@@ -48,10 +50,13 @@ namespace Avalonia.iOS
MultipleTouchEnabled = true;
}
+ ///
public override bool CanBecomeFirstResponder => true;
+ ///
public override bool CanResignFirstResponder => true;
+ ///
public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection)
{
base.TraitCollectionDidChange(previousTraitCollection);
@@ -60,6 +65,7 @@ namespace Avalonia.iOS
settings?.TraitCollectionDidChange();
}
+ ///
public override void TintColorDidChange()
{
base.TintColorDidChange();
@@ -68,18 +74,31 @@ namespace Avalonia.iOS
settings?.TraitCollectionDidChange();
}
+ public void InitWithController(TController controller)
+ where TController : UIViewController, IAvaloniaViewController
+ {
+ _controller = controller;
+ _topLevelImpl._insetsManager.InitWithController(controller);
+ }
+
internal class TopLevelImpl : ITopLevelImpl
{
private readonly AvaloniaView _view;
private readonly INativeControlHostImpl _nativeControlHost;
private readonly IStorageProvider _storageProvider;
+ internal readonly InsetsManager _insetsManager;
public AvaloniaView View => _view;
public TopLevelImpl(AvaloniaView view)
{
_view = view;
- _nativeControlHost = new NativeControlHostImpl(_view);
+ _nativeControlHost = new NativeControlHostImpl(view);
_storageProvider = new IOSStorageProvider(view);
+ _insetsManager = new InsetsManager(view);
+ _insetsManager.DisplayEdgeToEdgeChanged += (sender, b) =>
+ {
+ view._topLevel.Padding = b ? default : _insetsManager.SafeAreaPadding;
+ };
}
public void Dispose()
@@ -141,17 +160,14 @@ namespace Avalonia.iOS
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{
// TODO adjust status bar depending on full screen mode.
- if (OperatingSystem.IsIOSVersionAtLeast(13))
+ if (OperatingSystem.IsIOSVersionAtLeast(13) && _view._controller is not null)
{
- var uiStatusBarStyle = themeVariant switch
+ _view._controller.PreferredStatusBarStyle = themeVariant switch
{
PlatformThemeVariant.Light => UIStatusBarStyle.DarkContent,
PlatformThemeVariant.Dark => UIStatusBarStyle.LightContent,
- _ => throw new ArgumentOutOfRangeException(nameof(themeVariant), themeVariant, null)
+ _ => UIStatusBarStyle.Default
};
-
- // Consider using UIViewController.PreferredStatusBarStyle in the future.
- UIApplication.SharedApplication.SetStatusBarStyle(uiStatusBarStyle, true);
}
}
@@ -175,6 +191,11 @@ namespace Avalonia.iOS
return _nativeControlHost;
}
+ if (featureType == typeof(IInsetsManager))
+ {
+ return _insetsManager;
+ }
+
return null;
}
}
diff --git a/src/iOS/Avalonia.iOS/InsetsManager.cs b/src/iOS/Avalonia.iOS/InsetsManager.cs
new file mode 100644
index 0000000000..62e560ddf9
--- /dev/null
+++ b/src/iOS/Avalonia.iOS/InsetsManager.cs
@@ -0,0 +1,83 @@
+using System;
+using Avalonia.Controls.Platform;
+using UIKit;
+
+namespace Avalonia.iOS;
+#nullable enable
+
+internal class InsetsManager : IInsetsManager
+{
+ private readonly AvaloniaView _view;
+ private IAvaloniaViewController? _controller;
+ private bool _displayEdgeToEdge;
+
+ public InsetsManager(AvaloniaView view)
+ {
+ _view = view;
+ }
+
+ internal void InitWithController(IAvaloniaViewController controller)
+ {
+ _controller = controller;
+ if (_controller is not null)
+ {
+ _controller.SafeAreaPaddingChanged += (_, _) =>
+ {
+ SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding));
+ DisplayEdgeToEdgeChanged?.Invoke(this, _displayEdgeToEdge);
+ };
+ }
+ }
+
+ public SystemBarTheme? SystemBarTheme
+ {
+ get => _controller?.PreferredStatusBarStyle switch
+ {
+ UIStatusBarStyle.LightContent => Controls.Platform.SystemBarTheme.Dark,
+ UIStatusBarStyle.DarkContent => Controls.Platform.SystemBarTheme.Light,
+ _ => null
+ };
+ set
+ {
+ if (_controller != null)
+ {
+ _controller.PreferredStatusBarStyle = value switch
+ {
+ Controls.Platform.SystemBarTheme.Light => UIStatusBarStyle.DarkContent,
+ Controls.Platform.SystemBarTheme.Dark => UIStatusBarStyle.LightContent,
+ null => UIStatusBarStyle.Default,
+ _ => throw new ArgumentOutOfRangeException(nameof(value), value, null)
+ };
+ }
+ }
+ }
+
+ public bool? IsSystemBarVisible
+ {
+ get => _controller?.PrefersStatusBarHidden == false;
+ set
+ {
+ if (_controller is not null)
+ {
+ _controller.PrefersStatusBarHidden = value == false;
+ }
+ }
+ }
+ public event EventHandler? SafeAreaChanged;
+ public event EventHandler? DisplayEdgeToEdgeChanged;
+
+ public bool DisplayEdgeToEdge
+ {
+ get => _displayEdgeToEdge;
+ set
+ {
+ if (_displayEdgeToEdge != value)
+ {
+ _displayEdgeToEdge = value;
+ DisplayEdgeToEdgeChanged?.Invoke(this, value);
+ }
+ }
+ }
+
+ public Thickness SafeAreaPadding => _controller?.SafeAreaPadding ?? default;
+}
diff --git a/src/iOS/Avalonia.iOS/ViewController.cs b/src/iOS/Avalonia.iOS/ViewController.cs
new file mode 100644
index 0000000000..42a0949a9c
--- /dev/null
+++ b/src/iOS/Avalonia.iOS/ViewController.cs
@@ -0,0 +1,74 @@
+using System;
+using Avalonia.Metadata;
+using UIKit;
+
+namespace Avalonia.iOS;
+
+[Unstable]
+public interface IAvaloniaViewController
+{
+ UIStatusBarStyle PreferredStatusBarStyle { get; set; }
+ bool PrefersStatusBarHidden { get; set; }
+ Thickness SafeAreaPadding { get; }
+ event EventHandler SafeAreaPaddingChanged;
+}
+
+///
+public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewController
+{
+ private UIStatusBarStyle? _preferredStatusBarStyle;
+ private bool? _prefersStatusBarHidden;
+
+ ///
+ public override void ViewDidLayoutSubviews()
+ {
+ base.ViewDidLayoutSubviews();
+ var size = View?.Frame.Size ?? default;
+ var frame = View?.SafeAreaLayoutGuide.LayoutFrame ?? default;
+ var safeArea = new Thickness(frame.Left, frame.Top, size.Width - frame.Right, size.Height - frame.Bottom);
+ if (SafeAreaPadding != safeArea)
+ {
+ SafeAreaPadding = safeArea;
+ SafeAreaPaddingChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ ///
+ public override bool PrefersStatusBarHidden()
+ {
+ return _prefersStatusBarHidden ??= base.PrefersStatusBarHidden();
+ }
+
+ ///
+ public override UIStatusBarStyle PreferredStatusBarStyle()
+ {
+ // don't set _preferredStatusBarStyle value if it's null, so we can keep "default" there instead of actual app style.
+ return _preferredStatusBarStyle ?? base.PreferredStatusBarStyle();
+ }
+
+ UIStatusBarStyle IAvaloniaViewController.PreferredStatusBarStyle
+ {
+ get => _preferredStatusBarStyle ?? UIStatusBarStyle.Default;
+ set
+ {
+ _preferredStatusBarStyle = value;
+ SetNeedsStatusBarAppearanceUpdate();
+ }
+ }
+
+ bool IAvaloniaViewController.PrefersStatusBarHidden
+ {
+ get => _prefersStatusBarHidden ?? false; // false is default on ios/ipados
+ set
+ {
+ _prefersStatusBarHidden = value;
+ SetNeedsStatusBarAppearanceUpdate();
+ }
+ }
+
+ ///
+ public Thickness SafeAreaPadding { get; private set; }
+
+ ///
+ public event EventHandler SafeAreaPaddingChanged;
+}