1
0
Эх сурвалжийг харах

Impl auto safe area padding (#13047)

* Make iOS safe area more consistent with Android

* Implement TopLevel.AutoSafeAreaPadding (enabled by default)

* Make SafeAreaDemo more representative

* Some fixes + add comments

---------

Co-authored-by: Jumar Macato <[email protected]>
Max Katz 1 жил өмнө
parent
commit
3df092f714

+ 0 - 4
samples/SafeAreaDemo.iOS/Info.plist

@@ -39,9 +39,5 @@
 		<string>UIInterfaceOrientationLandscapeLeft</string>
 		<string>UIInterfaceOrientationLandscapeRight</string>
 	</array>
-	<key>UIStatusBarHidden</key>
-	<true/>
-	<key>UIViewControllerBasedStatusBarAppearance</key>
-	<false/>
 </dict>
 </plist>

+ 2 - 4
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">
-             <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
-
+             RequestedThemeVariant="Light">
     <Application.DataTemplates>
         <local:ViewLocator/>
     </Application.DataTemplates>
@@ -12,4 +10,4 @@
     <Application.Styles>
         <FluentTheme />
     </Application.Styles>
-</Application>
+</Application>

+ 3 - 9
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();
         }
     }
-}
+}

+ 29 - 18
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);
             }
         }
 

+ 6 - 3
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}">
     <Grid HorizontalAlignment="Stretch"
           VerticalAlignment="Stretch">
         <Border BorderBrush="Red"
@@ -40,8 +42,9 @@
                                 HorizontalAlignment="Center"
                                 VerticalAlignment="Center">
                         <Label HorizontalAlignment="Left">Options:</Label>
-                        <CheckBox IsChecked="{Binding Fullscreen}">Fullscreen</CheckBox>
-                        <CheckBox IsChecked="{Binding UseSafeArea}">Use Safe Area</CheckBox>
+                        <CheckBox IsChecked="{Binding DisplayEdgeToEdge}">Display Edge To Edge</CheckBox>
+                        <CheckBox IsChecked="{Binding UseSafeArea}" IsEnabled="{Binding !AutoSafeAreaPadding}">Use Safe Area</CheckBox>
+                        <CheckBox IsChecked="{Binding AutoSafeAreaPadding}">Automatic Paddings</CheckBox>
                         <CheckBox IsChecked="{Binding HideSystemBars}">Hide System Bars</CheckBox>
                         <TextBox Width="200" Watermark="Tap to Show Keyboard"/>
                     </StackPanel>

+ 3 - 4
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;
         }
     }
 }

+ 70 - 8
src/Avalonia.Controls/TopLevel.cs

@@ -101,6 +101,14 @@ namespace Avalonia.Controls
                 "SystemBarColor",
                 inherits: true);
 
+        /// <summary>
+        /// Defines the AutoSafeAreaPadding attached property.
+        /// </summary>
+        public static readonly AttachedProperty<bool> AutoSafeAreaPaddingProperty =
+            AvaloniaProperty.RegisterAttached<TopLevel, Control, bool>(
+                "AutoSafeAreaPadding",
+                defaultValue: true);
+
         /// <summary>
         /// Defines the <see cref="BackRequested"/> event.
         /// </summary>
@@ -155,6 +163,12 @@ namespace Avalonia.Controls
                 }
             });
 
+            AutoSafeAreaPaddingProperty.Changed.AddClassHandler<Control>((view, e) =>
+            {
+                var topLevel = view as TopLevel ?? view.Parent as TopLevel;
+                topLevel?.InvalidateChildInsetsPadding();
+            });
+
             PointerOverElementProperty.Changed.AddClassHandler<TopLevel>((topLevel, e) =>
             {
                 if (e.OldValue is InputElement oldInputElement)
@@ -478,25 +492,44 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Helper for setting the color of the platform's system bars
+        /// Helper for setting the color of the platform's system bars.
         /// </summary>
-        /// <param name="control">The main view attached to the toplevel, or the toplevel</param>
-        /// <param name="color">The color to set</param>
+        /// <param name="control">The main view attached to the toplevel, or the toplevel.</param>
+        /// <param name="color">The color to set.</param>
         public static void SetSystemBarColor(Control control, SolidColorBrush? color)
         {
             control.SetValue(SystemBarColorProperty, color);
         }
 
         /// <summary>
-        /// Helper for getting the color of the platform's system bars
+        /// Helper for getting the color of the platform's system bars.
         /// </summary>
-        /// <param name="control">The main view attached to the toplevel, or the toplevel</param>
-        /// <returns>The current color of the platform's system bars</returns>
+        /// <param name="control">The main view attached to the toplevel, or the toplevel.</param>
+        /// <returns>The current color of the platform's system bars.</returns>
         public static SolidColorBrush? GetSystemBarColor(Control control)
         {
             return control.GetValue(SystemBarColorProperty);
         }
 
+        /// <summary>
+        /// Enabled or disables whenever TopLevel should automatically adjust paddings depending on the safe area.
+        /// </summary>
+        /// <param name="control">The main view attached to the toplevel, or the toplevel.</param>
+        /// <param name="value">Value to be set.</param>
+        public static void SetAutoSafeAreaPadding(Control control, bool value)
+        {
+            control.SetValue(AutoSafeAreaPaddingProperty, value);
+        }
+
+        /// <summary>
+        /// Gets if auto safe area padding is enabled.
+        /// </summary>
+        /// <param name="control">The main view attached to the toplevel, or the toplevel.</param>
+        public static bool GetAutoSafeAreaPadding(Control control)
+        {
+            return control.GetValue(AutoSafeAreaPaddingProperty);
+        }
+
         /// <inheritdoc/>
         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();
+                }
+            }
+        }
+
         /// <summary>
         /// Creates the layout manager for this <see cref="TopLevel" />.
         /// </summary>

+ 15 - 2
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();
             }

+ 3 - 25
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; }
 }