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; } }