diff --git a/samples/SafeAreaDemo.iOS/Info.plist b/samples/SafeAreaDemo.iOS/Info.plist
index ec04bd5a87..7ba449d525 100644
--- a/samples/SafeAreaDemo.iOS/Info.plist
+++ b/samples/SafeAreaDemo.iOS/Info.plist
@@ -39,9 +39,5 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
- UIStatusBarHidden
-
- UIViewControllerBasedStatusBarAppearance
-
diff --git a/samples/SafeAreaDemo/App.xaml b/samples/SafeAreaDemo/App.xaml
index f5ffbdb32a..f406bfe710 100644
--- a/samples/SafeAreaDemo/App.xaml
+++ b/samples/SafeAreaDemo/App.xaml
@@ -2,9 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:SafeAreaDemo"
x:Class="SafeAreaDemo.App"
- RequestedThemeVariant="Default">
-
-
+ RequestedThemeVariant="Light">
@@ -12,4 +10,4 @@
-
\ No newline at end of file
+
diff --git a/samples/SafeAreaDemo/App.xaml.cs b/samples/SafeAreaDemo/App.xaml.cs
index e23cb0e04a..536c850026 100644
--- a/samples/SafeAreaDemo/App.xaml.cs
+++ b/samples/SafeAreaDemo/App.xaml.cs
@@ -17,20 +17,14 @@ namespace SafeAreaDemo
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
- desktop.MainWindow = new MainWindow
- {
- DataContext = new MainViewModel()
- };
+ desktop.MainWindow = new MainWindow();
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
- singleViewPlatform.MainView = new MainView
- {
- DataContext = new MainViewModel()
- };
+ singleViewPlatform.MainView = new MainView();
}
base.OnFrameworkInitializationCompleted();
}
}
-}
\ No newline at end of file
+}
diff --git a/samples/SafeAreaDemo/ViewModels/MainViewModel.cs b/samples/SafeAreaDemo/ViewModels/MainViewModel.cs
index fe58567171..3d826d8a9c 100644
--- a/samples/SafeAreaDemo/ViewModels/MainViewModel.cs
+++ b/samples/SafeAreaDemo/ViewModels/MainViewModel.cs
@@ -1,4 +1,5 @@
using Avalonia;
+using Avalonia.Controls;
using Avalonia.Controls.Platform;
using MiniMvvm;
@@ -7,15 +8,16 @@ namespace SafeAreaDemo.ViewModels
public class MainViewModel : ViewModelBase
{
private bool _useSafeArea = true;
- private bool _fullscreen;
+ private bool _displayEdgeToEdge;
private IInsetsManager? _insetsManager;
private bool _hideSystemBars;
+ private bool _autoSafeAreaPadding;
public Thickness SafeAreaPadding
{
get
{
- return _insetsManager?.SafeAreaPadding ?? default;
+ return !_autoSafeAreaPadding ? _insetsManager?.SafeAreaPadding ?? default : default;
}
}
@@ -40,12 +42,12 @@ namespace SafeAreaDemo.ViewModels
}
}
- public bool Fullscreen
+ public bool DisplayEdgeToEdge
{
- get => _fullscreen;
+ get => _displayEdgeToEdge;
set
{
- _fullscreen = value;
+ _displayEdgeToEdge = value;
if (_insetsManager != null)
{
@@ -76,25 +78,34 @@ namespace SafeAreaDemo.ViewModels
}
}
- internal IInsetsManager? InsetsManager
+ public bool AutoSafeAreaPadding
{
- get => _insetsManager;
+ get => _autoSafeAreaPadding;
set
{
- if (_insetsManager != null)
- {
- _insetsManager.SafeAreaChanged -= InsetsManager_SafeAreaChanged;
- }
+ _autoSafeAreaPadding = value;
+
+ RaisePropertyChanged();
+ RaiseSafeAreaChanged();
+ }
+ }
+
+ internal void Initialize(Control mainView, IInsetsManager? InsetsManager)
+ {
+ if (_insetsManager != null)
+ {
+ _insetsManager.SafeAreaChanged -= InsetsManager_SafeAreaChanged;
+ }
- _insetsManager = value;
+ _autoSafeAreaPadding = mainView.GetValue(TopLevel.AutoSafeAreaPaddingProperty);
+ _insetsManager = InsetsManager;
- if (_insetsManager != null)
- {
- _insetsManager.SafeAreaChanged += InsetsManager_SafeAreaChanged;
+ if (_insetsManager != null)
+ {
+ _insetsManager.SafeAreaChanged += InsetsManager_SafeAreaChanged;
- _insetsManager.DisplayEdgeToEdge = _fullscreen;
- _insetsManager.IsSystemBarVisible = !_hideSystemBars;
- }
+ _displayEdgeToEdge = _insetsManager.DisplayEdgeToEdge;
+ _hideSystemBars = !(_insetsManager.IsSystemBarVisible ?? false);
}
}
diff --git a/samples/SafeAreaDemo/Views/MainView.xaml b/samples/SafeAreaDemo/Views/MainView.xaml
index a8f7c2e735..966b0a02ea 100644
--- a/samples/SafeAreaDemo/Views/MainView.xaml
+++ b/samples/SafeAreaDemo/Views/MainView.xaml
@@ -7,7 +7,9 @@
d:DesignWidth="800"
d:DesignHeight="450"
x:Class="SafeAreaDemo.Views.MainView"
- x:DataType="vm:MainViewModel">
+ x:DataType="vm:MainViewModel"
+ Background="#ccc"
+ TopLevel.AutoSafeAreaPadding="{Binding AutoSafeAreaPadding, Mode=TwoWay}">
- Fullscreen
- Use Safe Area
+ Display Edge To Edge
+ Use Safe Area
+ Automatic Paddings
Hide System Bars
diff --git a/samples/SafeAreaDemo/Views/MainView.xaml.cs b/samples/SafeAreaDemo/Views/MainView.xaml.cs
index 4b8c5e5f15..bacb721d27 100644
--- a/samples/SafeAreaDemo/Views/MainView.xaml.cs
+++ b/samples/SafeAreaDemo/Views/MainView.xaml.cs
@@ -18,10 +18,9 @@ namespace SafeAreaDemo.Views
base.OnLoaded(e);
var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager;
- if (insetsManager != null && DataContext is MainViewModel viewModel)
- {
- viewModel.InsetsManager = insetsManager;
- }
+ var viewModel = new MainViewModel();
+ viewModel.Initialize(this, insetsManager);
+ DataContext = viewModel;
}
}
}
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index 8e59f66b0b..6898879b32 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -101,6 +101,14 @@ namespace Avalonia.Controls
"SystemBarColor",
inherits: true);
+ ///
+ /// Defines the AutoSafeAreaPadding attached property.
+ ///
+ public static readonly AttachedProperty AutoSafeAreaPaddingProperty =
+ AvaloniaProperty.RegisterAttached(
+ "AutoSafeAreaPadding",
+ defaultValue: true);
+
///
/// Defines the event.
///
@@ -155,6 +163,12 @@ namespace Avalonia.Controls
}
});
+ AutoSafeAreaPaddingProperty.Changed.AddClassHandler((view, e) =>
+ {
+ var topLevel = view as TopLevel ?? view.Parent as TopLevel;
+ topLevel?.InvalidateChildInsetsPadding();
+ });
+
PointerOverElementProperty.Changed.AddClassHandler((topLevel, e) =>
{
if (e.OldValue is InputElement oldInputElement)
@@ -478,25 +492,44 @@ namespace Avalonia.Controls
}
///
- /// Helper for setting the color of the platform's system bars
+ /// Helper for setting the color of the platform's system bars.
///
- /// The main view attached to the toplevel, or the toplevel
- /// The color to set
+ /// The main view attached to the toplevel, or the toplevel.
+ /// The color to set.
public static void SetSystemBarColor(Control control, SolidColorBrush? color)
{
control.SetValue(SystemBarColorProperty, color);
}
///
- /// Helper for getting the color of the platform's system bars
+ /// Helper for getting the color of the platform's system bars.
///
- /// The main view attached to the toplevel, or the toplevel
- /// The current color of the platform's system bars
+ /// The main view attached to the toplevel, or the toplevel.
+ /// The current color of the platform's system bars.
public static SolidColorBrush? GetSystemBarColor(Control control)
{
return control.GetValue(SystemBarColorProperty);
}
+ ///
+ /// Enabled or disables whenever TopLevel should automatically adjust paddings depending on the safe area.
+ ///
+ /// The main view attached to the toplevel, or the toplevel.
+ /// Value to be set.
+ public static void SetAutoSafeAreaPadding(Control control, bool value)
+ {
+ control.SetValue(AutoSafeAreaPaddingProperty, value);
+ }
+
+ ///
+ /// Gets if auto safe area padding is enabled.
+ ///
+ /// The main view attached to the toplevel, or the toplevel.
+ public static bool GetAutoSafeAreaPadding(Control control)
+ {
+ return control.GetValue(AutoSafeAreaPaddingProperty);
+ }
+
///
double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1;
@@ -585,12 +618,41 @@ namespace Avalonia.Controls
{
base.OnPropertyChanged(change);
- if (_platformImplBindings.TryGetValue(change.Property, out var bindingAction))
+ if (change.Property == ContentProperty)
+ {
+ InvalidateChildInsetsPadding();
+ }
+ else if (_platformImplBindings.TryGetValue(change.Property, out var bindingAction))
{
bindingAction();
}
}
-
+
+ private IDisposable? _insetsPaddings;
+ private void InvalidateChildInsetsPadding()
+ {
+ if (Content is Control child
+ && InsetsManager is {} insetsManager)
+ {
+ insetsManager.SafeAreaChanged -= InsetsManagerOnSafeAreaChanged;
+ _insetsPaddings?.Dispose();
+
+ if (child.GetValue(AutoSafeAreaPaddingProperty))
+ {
+ insetsManager.SafeAreaChanged += InsetsManagerOnSafeAreaChanged;
+ _insetsPaddings = child.SetValue(
+ PaddingProperty,
+ insetsManager.SafeAreaPadding,
+ BindingPriority.Style); // lower priority, so it can be redefined by user
+ }
+
+ void InsetsManagerOnSafeAreaChanged(object? sender, SafeAreaChangedArgs e)
+ {
+ InvalidateChildInsetsPadding();
+ }
+ }
+ }
+
///
/// Creates the layout manager for this .
///
diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs
index 939df1a3a0..61b268bac9 100644
--- a/src/iOS/Avalonia.iOS/AvaloniaView.cs
+++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
using Avalonia.Controls.Platform;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
@@ -89,6 +91,7 @@ namespace Avalonia.iOS
private readonly IStorageProvider _storageProvider;
internal readonly InsetsManager _insetsManager;
private readonly ClipboardImpl _clipboard;
+ private IDisposable _paddingInsets;
public AvaloniaView View => _view;
@@ -98,9 +101,19 @@ namespace Avalonia.iOS
_nativeControlHost = new NativeControlHostImpl(view);
_storageProvider = new IOSStorageProvider(view);
_insetsManager = new InsetsManager(view);
- _insetsManager.DisplayEdgeToEdgeChanged += (sender, b) =>
+ _insetsManager.DisplayEdgeToEdgeChanged += (sender, edgeToEdge) =>
{
- view._topLevel.Padding = b ? default : _insetsManager.SafeAreaPadding;
+ // iOS doesn't add any paddings/margins to the application by itself.
+ // Application is fully responsible for safe area paddings.
+ // So, unlikely to android, we need to "fake" safe area insets when edge to edge is disabled.
+ _paddingInsets?.Dispose();
+ if (!edgeToEdge)
+ {
+ _paddingInsets = view._topLevel.SetValue(
+ TemplatedControl.PaddingProperty,
+ view._controller.SafeAreaPadding,
+ BindingPriority.Style); // lower priority, so it can be redefined by user
+ }
};
_clipboard = new ClipboardImpl();
}
diff --git a/src/iOS/Avalonia.iOS/InsetsManager.cs b/src/iOS/Avalonia.iOS/InsetsManager.cs
index bd6f989dbd..54b769c567 100644
--- a/src/iOS/Avalonia.iOS/InsetsManager.cs
+++ b/src/iOS/Avalonia.iOS/InsetsManager.cs
@@ -10,7 +10,7 @@ internal class InsetsManager : IInsetsManager
{
private readonly AvaloniaView _view;
private IAvaloniaViewController? _controller;
- private bool _displayEdgeToEdge;
+ private bool _displayEdgeToEdge = true;
public InsetsManager(AvaloniaView view)
{
@@ -30,29 +30,6 @@ internal class InsetsManager : IInsetsManager
}
}
- 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;
@@ -76,11 +53,12 @@ internal class InsetsManager : IInsetsManager
{
_displayEdgeToEdge = value;
DisplayEdgeToEdgeChanged?.Invoke(this, value);
+ SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding));
}
}
}
- public Thickness SafeAreaPadding => _controller?.SafeAreaPadding ?? default;
+ public Thickness SafeAreaPadding => _displayEdgeToEdge ? _controller?.SafeAreaPadding ?? default : default;
public Color? SystemBarColor { get; set; }
}