From 87402e5b94774938c3b9bb2a3f1f5702b8a9437f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 2 Mar 2023 07:58:28 -1000 Subject: [PATCH] Update insets API and implement insets for iOS with IAvaloniaViewController --- .../Platform/AndroidInsetsManager.cs | 41 +++++---- .../Platform/SkiaPlatform/TopLevelImpl.cs | 2 +- .../Platform/IInsetsManager.cs | 43 +++++----- .../Avalonia.Browser/BrowserInsetsManager.cs | 14 ++-- src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs | 4 +- src/iOS/Avalonia.iOS/AvaloniaView.cs | 35 ++++++-- src/iOS/Avalonia.iOS/InsetsManager.cs | 83 +++++++++++++++++++ src/iOS/Avalonia.iOS/ViewController.cs | 74 +++++++++++++++++ 8 files changed, 241 insertions(+), 55 deletions(-) create mode 100644 src/iOS/Avalonia.iOS/InsetsManager.cs create mode 100644 src/iOS/Avalonia.iOS/ViewController.cs 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; +}