Преглед изворни кода

Update insets API and implement insets for iOS with IAvaloniaViewController

Max Katz пре 2 година
родитељ
комит
87402e5b94

+ 25 - 16
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

+ 1 - 1
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)

+ 19 - 24
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
     {
-        /// <summary>
-        /// Gets or sets the theme for the system bars, if supported.
-        /// </summary>
-        SystemBarTheme? SystemBarTheme { get; set; }
-
         /// <summary>
         /// Gets or sets whether the system bars are visible.
         /// </summary>
         bool? IsSystemBarVisible { get; set; }
 
-        /// <summary>
-        /// Occurs when safe area for the current window changes.
-        /// </summary>
-
-        event EventHandler<SafeAreaChangedArgs> SafeAreaChanged;
-
-
         /// <summary>
         /// Gets or sets whether the window draws edge to edge. behind any visibile system bars.
         /// </summary>
@@ -31,18 +21,23 @@ namespace Avalonia.Controls.Platform
         /// <summary>
         /// Gets the current safe area padding.
         /// </summary>
-        /// <returns></returns>
-        Thickness GetSafeAreaPadding();
-
-        public class SafeAreaChangedArgs : RoutedEventArgs
+        Thickness SafeAreaPadding { get; }
+        
+        /// <summary>
+        /// Occurs when safe area for the current window changes.
+        /// </summary>
+        event EventHandler<SafeAreaChangedArgs>? SafeAreaChanged;
+    }
+    
+    public class SafeAreaChangedArgs : EventArgs
+    {
+        public SafeAreaChangedArgs(Thickness safeArePadding)
         {
-            public SafeAreaChangedArgs(Thickness safeArePadding)
-            {
-                SafeAreaPadding = safeArePadding;
-            }
-
-            public Thickness SafeAreaPadding { get; }
+            SafeAreaPadding = safeArePadding;
         }
+
+        /// <inheritdoc cref="IInsetsManager.GetSafeAreaPadding"/>
+        public Thickness SafeAreaPadding { get; }
     }
 
     public enum SystemBarTheme

+ 8 - 6
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<SafeAreaChangedArgs>? 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));
         }
     }
 }

+ 3 - 1
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);

+ 28 - 7
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;
         }
 
+        /// <inheritdoc />
         public override bool CanBecomeFirstResponder => true;
 
+        /// <inheritdoc />
         public override bool CanResignFirstResponder => true;
 
+        /// <inheritdoc />
         public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection)
         {
             base.TraitCollectionDidChange(previousTraitCollection);
@@ -60,6 +65,7 @@ namespace Avalonia.iOS
             settings?.TraitCollectionDidChange();
         }
 
+        /// <inheritdoc />
         public override void TintColorDidChange()
         {
             base.TintColorDidChange();
@@ -68,18 +74,31 @@ namespace Avalonia.iOS
             settings?.TraitCollectionDidChange();
         }
 
+        public void InitWithController<TController>(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;
             }
         }

+ 83 - 0
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<SafeAreaChangedArgs>? SafeAreaChanged;
+    public event EventHandler<bool>? DisplayEdgeToEdgeChanged;
+
+    public bool DisplayEdgeToEdge
+    {
+        get => _displayEdgeToEdge;
+        set
+        {
+            if (_displayEdgeToEdge != value)
+            {
+                _displayEdgeToEdge = value;
+                DisplayEdgeToEdgeChanged?.Invoke(this, value);
+            }
+        }
+    }
+
+    public Thickness SafeAreaPadding => _controller?.SafeAreaPadding ?? default;
+}

+ 74 - 0
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;
+}
+
+/// <inheritdoc cref="IAvaloniaViewController" />
+public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewController
+{
+    private UIStatusBarStyle? _preferredStatusBarStyle;
+    private bool? _prefersStatusBarHidden;
+    
+    /// <inheritdoc/>
+    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);
+        }
+    }
+
+    /// <inheritdoc/>
+    public override bool PrefersStatusBarHidden()
+    {
+        return _prefersStatusBarHidden ??= base.PrefersStatusBarHidden();
+    }
+
+    /// <inheritdoc/>
+    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();
+        }
+    }
+
+    /// <inheritdoc/>
+    public Thickness SafeAreaPadding { get; private set; }
+
+    /// <inheritdoc/>
+    public event EventHandler SafeAreaPaddingChanged;
+}