From 746b53b388c007b23846d493ccc27cedd871f070 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 31 Jan 2023 20:26:09 +0000 Subject: [PATCH 01/11] add support for getting safe insets --- samples/ControlCatalog.Browser/app.css | 9 +- .../Avalonia.Android/AvaloniaMainActivity.cs | 12 + src/Android/Avalonia.Android/AvaloniaView.cs | 2 + .../Platform/AndroidInsetsManager.cs | 215 ++++++++++++++++++ .../Platform/SkiaPlatform/TopLevelImpl.cs | 40 ++-- .../Platform/IInsetsManager.cs | 34 +++ src/Avalonia.Controls/TopLevel.cs | 5 +- .../Avalonia.Browser/BrowserInsetsManager.cs | 43 ++++ .../Avalonia.Browser/BrowserTopLevelImpl.cs | 11 + .../Avalonia.Browser/Interop/DomHelper.cs | 9 + .../webapp/modules/avalonia/dom.ts | 22 ++ 11 files changed, 382 insertions(+), 20 deletions(-) create mode 100644 src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs create mode 100644 src/Avalonia.Controls/Platform/IInsetsManager.cs create mode 100644 src/Browser/Avalonia.Browser/BrowserInsetsManager.cs diff --git a/samples/ControlCatalog.Browser/app.css b/samples/ControlCatalog.Browser/app.css index 27680f6e1a..0e6ab12461 100644 --- a/samples/ControlCatalog.Browser/app.css +++ b/samples/ControlCatalog.Browser/app.css @@ -1,4 +1,11 @@ -#out { +:root { + --sat: env(safe-area-inset-top); + --sar: env(safe-area-inset-right); + --sab: env(safe-area-inset-bottom); + --sal: env(safe-area-inset-left); +} + +#out { height: 100vh; width: 100vw } diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index 247008c503..5b22d2c270 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Android.App; using Android.Content; using Android.Content.PM; @@ -55,6 +56,17 @@ namespace Avalonia.Android } } + protected override void OnResume() + { + base.OnResume(); + + // Android only respects LayoutInDisplayCutoutMode value if it has been set once before window becomes visible. + if (Build.VERSION.SdkInt >= BuildVersionCodes.P) + { + Window.Attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges; + } + } + public event EventHandler BackRequested; public override void OnBackPressed() diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 2a345a857c..50de0b49de 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -67,6 +67,8 @@ namespace Avalonia.Android } _root.Renderer.Start(); + + (_view._insetsManager as AndroidInsetsManager)?.ApplyStatusBarState(); } else { diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs new file mode 100644 index 0000000000..ea8e0c0490 --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using Android.OS; +using Android.Views; +using AndroidX.Core.View; +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Controls.Platform; +using static Avalonia.Controls.Platform.IInsetsManager; + +namespace Avalonia.Android.Platform +{ + internal class AndroidInsetsManager : Java.Lang.Object, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener + { + private readonly AvaloniaMainActivity _activity; + private readonly TopLevelImpl _topLevel; + private readonly InsetsAnimationCallback _callback; + private bool _displayEdgeToEdge; + private bool _usesLegacyLayouts; + private bool? _systemUiVisibility; + private SystemBarTheme? _statusBarTheme; + private bool? _isDefaultSystemBarLightTheme; + + public event EventHandler SafeAreaChanged; + + public bool DisplayEdgeToEdge + { + get => _displayEdgeToEdge; + set + { + _displayEdgeToEdge = value; + + if(Build.VERSION.SdkInt >= BuildVersionCodes.P) + { + _activity.Window.Attributes.LayoutInDisplayCutoutMode = value ? LayoutInDisplayCutoutMode.ShortEdges : LayoutInDisplayCutoutMode.Default; + } + + WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value); + } + } + + public AndroidInsetsManager(AvaloniaMainActivity activity, TopLevelImpl topLevel) + { + _activity = activity; + _topLevel = topLevel; + _callback = new InsetsAnimationCallback(WindowInsetsAnimationCompat.Callback.DispatchModeStop); + + _callback.InsetsManager = this; + + ViewCompat.SetOnApplyWindowInsetsListener(_activity.Window.DecorView, this); + + ViewCompat.SetWindowInsetsAnimationCallback(_activity.Window.DecorView, _callback); + + if(Build.VERSION.SdkInt < BuildVersionCodes.R) + { + _usesLegacyLayouts = true; + _activity.Window.DecorView.ViewTreeObserver.AddOnGlobalLayoutListener(this); + } + } + + public Thickness GetSafeAreaPadding() + { + var insets = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); + + if (insets != null) + { + var renderScaling = _topLevel.RenderScaling; + + var inset = insets.GetInsets((DisplayEdgeToEdge ? WindowInsetsCompat.Type.SystemBars() | 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 default; + } + + public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets) + { + NotifySafeAreaChanged(GetSafeAreaPadding()); + return insets; + } + + private void NotifySafeAreaChanged(Thickness safeAreaPadding) + { + SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(safeAreaPadding)); + } + + public void OnGlobalLayout() + { + NotifySafeAreaChanged(GetSafeAreaPadding()); + } + + public SystemBarTheme? SystemBarTheme + { + get + { + try + { + var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View); + + return compat.AppearanceLightStatusBars ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark; + } + catch (Exception _) + { + return Controls.Platform.SystemBarTheme.Light; + } + } + set + { + _statusBarTheme = value; + + if (!_topLevel.View.IsShown) + { + return; + } + + var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View); + + if (_isDefaultSystemBarLightTheme == null) + { + _isDefaultSystemBarLightTheme = compat.AppearanceLightStatusBars; + } + + if (value == null && _isDefaultSystemBarLightTheme != null) + { + value = (bool)_isDefaultSystemBarLightTheme ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark; + } + + compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light; + compat.AppearanceLightNavigationBars = value == Controls.Platform.SystemBarTheme.Light; + } + } + + public bool? IsSystemBarVisible + { + get + { + var compat = ViewCompat.GetRootWindowInsets(_topLevel.View); + + return compat?.IsVisible(WindowInsetsCompat.Type.SystemBars()); + } + set + { + _systemUiVisibility = value; + + if (!_topLevel.View.IsShown) + { + return; + } + + var compat = WindowCompat.GetInsetsController(_activity.Window, _topLevel.View); + + if (value == null || value.Value) + { + compat?.Show(WindowInsetsCompat.Type.SystemBars()); + } + else + { + compat?.Hide(WindowInsetsCompat.Type.SystemBars()); + + if (compat != null) + { + compat.SystemBarsBehavior = WindowInsetsControllerCompat.BehaviorShowTransientBarsBySwipe; + } + } + } + } + + internal void ApplyStatusBarState() + { + IsSystemBarVisible = _systemUiVisibility; + SystemBarTheme = _statusBarTheme; + } + + private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback + { + public InsetsAnimationCallback(int dispatchMode) : base(dispatchMode) + { + } + + public AndroidInsetsManager InsetsManager { get; set; } + + public override WindowInsetsCompat OnProgress(WindowInsetsCompat insets, IList runningAnimations) + { + foreach (var anim in runningAnimations) + { + if ((anim.TypeMask & WindowInsetsCompat.Type.Ime()) != 0) + { + var renderScaling = InsetsManager._topLevel.RenderScaling; + + var inset = insets.GetInsets((InsetsManager.DisplayEdgeToEdge ? WindowInsetsCompat.Type.SystemBars() | WindowInsetsCompat.Type.DisplayCutout() : 0) | WindowInsetsCompat.Type.Ime()); + var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()); + var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime()); + + + var bottomPadding = (imeInset.Bottom > 0 && !InsetsManager.DisplayEdgeToEdge ? imeInset.Bottom - navBarInset.Bottom : inset.Bottom); + bottomPadding = (int)(bottomPadding * anim.InterpolatedFraction); + + var padding = new Thickness(inset.Left / renderScaling, + inset.Top / renderScaling, + inset.Right / renderScaling, + bottomPadding / renderScaling); + InsetsManager?.NotifySafeAreaChanged(padding); + break; + } + } + return insets; + } + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 693a26f3bd..3e2cf12801 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -3,9 +3,7 @@ using System.Collections.Generic; using Android.App; using Android.Content; using Android.Graphics; -using Android.OS; using Android.Runtime; -using Android.Text; using Android.Views; using Android.Views.InputMethods; using Avalonia.Android.Platform.Specific; @@ -28,6 +26,7 @@ using Math = System.Math; using AndroidRect = Android.Graphics.Rect; using Window = Android.Views.Window; using Android.Graphics.Drawables; +using Android.OS; namespace Avalonia.Android.Platform.SkiaPlatform { @@ -42,6 +41,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; private readonly ISystemNavigationManagerImpl _systemNavigationManager; + private readonly IInsetsManager _insetsManager; private ViewImpl _view; public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false) @@ -58,6 +58,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); + if (avaloniaView.Context is AvaloniaMainActivity mainActivity) + { + _insetsManager = new AndroidInsetsManager(mainActivity, this); + } + _nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView); _storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context); @@ -69,21 +74,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IInputRoot InputRoot { get; private set; } - public virtual Size ClientSize - { - get - { - AndroidRect rect = new AndroidRect(); - AndroidRect intersection = new AndroidRect(); - - _view.GetWindowVisibleDisplayFrame(intersection); - _view.GetGlobalVisibleRect(rect); - - intersection.Intersect(rect); - - return new PixelSize(intersection.Right - intersection.Left, intersection.Bottom - intersection.Top).ToSize(RenderScaling); - } - } + public virtual Size ClientSize => _view.Size.ToSize(RenderScaling); public Size? FrameSize => null; @@ -284,7 +275,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { - // TODO adjust status bar depending on full screen mode. + if(_insetsManager != null) + { + _insetsManager.SystemBarTheme = themeVariant switch + { + PlatformThemeVariant.Light => SystemBarTheme.Light, + PlatformThemeVariant.Dark => SystemBarTheme.Dark, + _ => null, + }; + } } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); @@ -402,6 +401,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform return _nativeControlHost; } + if (featureType == typeof(IInsetsManager)) + { + return _insetsManager; + } + return null; } } diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs new file mode 100644 index 0000000000..15df684a0e --- /dev/null +++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs @@ -0,0 +1,34 @@ +using System; + +namespace Avalonia.Controls.Platform +{ + [Avalonia.Metadata.Unstable] + public interface IInsetsManager + { + SystemBarTheme? SystemBarTheme { get; set; } + + bool? IsSystemBarVisible { get; set; } + + event EventHandler SafeAreaChanged; + + bool DisplayEdgeToEdge { get; set; } + + Thickness GetSafeAreaPadding(); + + public class SafeAreaChangedArgs : EventArgs + { + public SafeAreaChangedArgs(Thickness safeArePadding) + { + SafeAreaPadding = safeArePadding; + } + + public Thickness SafeAreaPadding { get; } + } + } + + public enum SystemBarTheme + { + Light, + Dark + } +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index cb2b8cfd1c..83655db7ae 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -14,6 +14,7 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Platform.Storage; +using Avalonia.Reactive; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; @@ -393,7 +394,9 @@ namespace Avalonia.Controls ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this) ?? PlatformImpl?.TryGetFeature() ?? throw new InvalidOperationException("StorageProvider platform implementation is not available."); - + + public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature(); + /// Point IRenderRoot.PointToClient(PixelPoint p) { diff --git a/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs new file mode 100644 index 0000000000..d751152a53 --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserInsetsManager.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Browser.Interop; +using Avalonia.Controls.Platform; +using static Avalonia.Controls.Platform.IInsetsManager; + +namespace Avalonia.Browser +{ + internal class BrowserInsetsManager : IInsetsManager + { + public SystemBarTheme? SystemBarTheme { get; set; } + public bool? IsSystemBarVisible + { + get + { + return DomHelper.IsFullscreen(); + } + set + { + DomHelper.SetFullscreen(value != null ? !value.Value : false); + } + } + + public bool DisplayEdgeToEdge { get; set; } + + public event EventHandler? SafeAreaChanged; + + public Thickness GetSafeAreaPadding() + { + var padding = DomHelper.GetSafeAreaPadding(); + + return new Thickness(padding[0], padding[1], padding[2], padding[3]); + } + + public void NotifySafeAreaPaddingChanged() + { + SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(GetSafeAreaPadding())); + } + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index f1cd441f45..c00fe690b0 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -31,6 +31,7 @@ namespace Avalonia.Browser private readonly INativeControlHostImpl _nativeControlHost; private readonly IStorageProvider _storageProvider; private readonly ISystemNavigationManagerImpl _systemNavigationManager; + private readonly IInsetsManager? _insetsManager; public BrowserTopLevelImpl(AvaloniaView avaloniaView) { @@ -40,9 +41,12 @@ namespace Avalonia.Browser AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); _touchDevice = new TouchDevice(); _penDevice = new PenDevice(); + + _insetsManager = new BrowserInsetsManager(); _nativeControlHost = _avaloniaView.GetNativeControlHostImpl(); _storageProvider = new BrowserStorageProvider(); _systemNavigationManager = new BrowserSystemNavigationManagerImpl(); + } public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; @@ -69,6 +73,8 @@ namespace Avalonia.Browser } Resized?.Invoke(newSize, PlatformResizeReason.User); + + (_insetsManager as BrowserInsetsManager)?.NotifySafeAreaPaddingChanged(); } } @@ -262,6 +268,11 @@ namespace Avalonia.Browser return _nativeControlHost; } + if (featureType == typeof(IInsetsManager)) + { + return _insetsManager; + } + return null; } } diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs index d1133b8916..c1811bb117 100644 --- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs @@ -11,6 +11,15 @@ internal static partial class DomHelper [JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)] public static partial JSObject CreateAvaloniaHost(JSObject element); + [JSImport("AvaloniaDOM.isFullscreen", AvaloniaModule.MainModuleName)] + public static partial bool IsFullscreen(); + + [JSImport("AvaloniaDOM.setFullscreen", AvaloniaModule.MainModuleName)] + public static partial JSObject SetFullscreen(bool isFullscreen); + + [JSImport("AvaloniaDOM.getSafeAreaPadding", AvaloniaModule.MainModuleName)] + public static partial byte[] GetSafeAreaPadding(); + [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)] public static partial void AddCssClass(JSObject element, string className); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts index b99f8e7907..d9790f69e9 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts @@ -84,4 +84,26 @@ export class AvaloniaDOM { inputElement }; } + + public static isFullscreen(): boolean { + return document.fullscreenElement != null; + } + + public static async setFullscreen(isFullscreen: boolean) { + if (isFullscreen) { + const doc = document.documentElement; + await doc.requestFullscreen(); + } else { + await document.exitFullscreen(); + } + } + + public static getSafeAreaPadding(): number[] { + const top = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sat")); + const bottom = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sab")); + const left = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sal")); + const right = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--sar")); + + return [left, top, bottom, right]; + } } From 63b931253a92881198cc189f22124b2e698a89bb Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 31 Jan 2023 20:45:47 +0000 Subject: [PATCH 02/11] add comment docs --- .../Platform/IInsetsManager.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Platform/IInsetsManager.cs b/src/Avalonia.Controls/Platform/IInsetsManager.cs index 15df684a0e..028e434104 100644 --- a/src/Avalonia.Controls/Platform/IInsetsManager.cs +++ b/src/Avalonia.Controls/Platform/IInsetsManager.cs @@ -1,21 +1,40 @@ using System; +using Avalonia.Interactivity; namespace Avalonia.Controls.Platform { [Avalonia.Metadata.Unstable] 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. + /// bool DisplayEdgeToEdge { get; set; } + /// + /// Gets the current safe area padding. + /// + /// Thickness GetSafeAreaPadding(); - public class SafeAreaChangedArgs : EventArgs + public class SafeAreaChangedArgs : RoutedEventArgs { public SafeAreaChangedArgs(Thickness safeArePadding) { @@ -28,7 +47,14 @@ namespace Avalonia.Controls.Platform public enum SystemBarTheme { + /// + /// Light system bar theme, with light background and a dark foreground + /// Light, + + /// + /// Bark system bar theme, with dark background and a light foreground + /// Dark } } From 5389d88b0d82e756b10a9b958fb880389a11716a Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 1 Feb 2023 08:55:16 +0000 Subject: [PATCH 03/11] fix android build --- src/Android/Avalonia.Android/AvaloniaView.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 50de0b49de..27481a598e 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -8,6 +8,7 @@ using Avalonia.Android.Platform; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; using Avalonia.Controls.Embedding; +using Avalonia.Controls.Platform; using Avalonia.Platform; using Avalonia.Rendering; @@ -68,7 +69,10 @@ namespace Avalonia.Android _root.Renderer.Start(); - (_view._insetsManager as AndroidInsetsManager)?.ApplyStatusBarState(); + if (_view.TryGetFeature(out var insetsManager) == true) + { + (insetsManager as AndroidInsetsManager)?.ApplyStatusBarState(); + } } else { From b07e5a50ba1de79dfb2123af0943bb2813efe1cd Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 23 Feb 2023 07:23:37 +0000 Subject: [PATCH 04/11] fix build error in android project --- .../Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 2d547201e9..f0fc13c86f 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -28,6 +28,7 @@ using AndroidRect = Android.Graphics.Rect; using Window = Android.Views.Window; using Android.Graphics.Drawables; using Android.OS; +using Android.Text; namespace Avalonia.Android.Platform.SkiaPlatform { From d24b3a9fbfbc866b42e11e0a0c532ef3b8fbac4b Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 23 Feb 2023 16:50:03 +0000 Subject: [PATCH 05/11] set initial value of displayEdgeToEdge on android --- src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index ea8e0c0490..2dcd3e2c4c 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -55,6 +55,8 @@ namespace Avalonia.Android.Platform _usesLegacyLayouts = true; _activity.Window.DecorView.ViewTreeObserver.AddOnGlobalLayoutListener(this); } + + _displayEdgeToEdge = !activity.Window.DecorView.FitsSystemWindows; } public Thickness GetSafeAreaPadding() From dcf8afa1a6201ee16ce61ed6e367766394af1782 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 24 Feb 2023 13:46:39 +0000 Subject: [PATCH 06/11] set initial edge to edge sate to false. --- src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index 2dcd3e2c4c..10f06c726b 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -56,7 +56,7 @@ namespace Avalonia.Android.Platform _activity.Window.DecorView.ViewTreeObserver.AddOnGlobalLayoutListener(this); } - _displayEdgeToEdge = !activity.Window.DecorView.FitsSystemWindows; + DisplayEdgeToEdge = false; } public Thickness GetSafeAreaPadding() From f6ced0811d57246a0ea758237df0f383cde40067 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 24 Feb 2023 14:00:23 +0000 Subject: [PATCH 07/11] specify the system bars to show or hide --- .../Avalonia.Android/Platform/AndroidInsetsManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index 10f06c726b..7bc3a02736 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -67,7 +67,7 @@ namespace Avalonia.Android.Platform { var renderScaling = _topLevel.RenderScaling; - var inset = insets.GetInsets((DisplayEdgeToEdge ? WindowInsetsCompat.Type.SystemBars() | WindowInsetsCompat.Type.DisplayCutout() : 0 ) | WindowInsetsCompat.Type.Ime()); + 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()); @@ -143,7 +143,7 @@ namespace Avalonia.Android.Platform { var compat = ViewCompat.GetRootWindowInsets(_topLevel.View); - return compat?.IsVisible(WindowInsetsCompat.Type.SystemBars()); + return compat?.IsVisible(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); } set { @@ -158,11 +158,11 @@ namespace Avalonia.Android.Platform if (value == null || value.Value) { - compat?.Show(WindowInsetsCompat.Type.SystemBars()); + compat?.Show(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); } else { - compat?.Hide(WindowInsetsCompat.Type.SystemBars()); + compat?.Hide(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); if (compat != null) { @@ -194,7 +194,7 @@ namespace Avalonia.Android.Platform { var renderScaling = InsetsManager._topLevel.RenderScaling; - var inset = insets.GetInsets((InsetsManager.DisplayEdgeToEdge ? WindowInsetsCompat.Type.SystemBars() | WindowInsetsCompat.Type.DisplayCutout() : 0) | WindowInsetsCompat.Type.Ime()); + var inset = insets.GetInsets((InsetsManager.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()); From 87402e5b94774938c3b9bb2a3f1f5702b8a9437f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 2 Mar 2023 07:58:28 -1000 Subject: [PATCH 08/11] 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; +} From a28aa6e04ada88853865d0900805d58bf1cff27d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 2 Mar 2023 07:58:57 -1000 Subject: [PATCH 09/11] Update control catalog to support insets samples --- .../ControlCatalog.iOS.csproj | 4 +- samples/ControlCatalog.iOS/Info.plist | 6 +-- samples/ControlCatalog/App.xaml.cs | 4 +- samples/ControlCatalog/MainView.xaml.cs | 38 ++++++++++++++++++- samples/ControlCatalog/MainWindow.xaml.cs | 1 - .../Pages/WindowCustomizationsPage.xaml | 22 ++++++++--- .../ViewModels/MainWindowViewModel.cs | 30 +++++++++++++-- 7 files changed, 84 insertions(+), 21 deletions(-) diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index 74d5b2fd8c..b4dac5399c 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -3,7 +3,7 @@ Exe manual net6.0-ios - 10.0 + 13.0 True iossimulator-x64 @@ -16,4 +16,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog.iOS/Info.plist b/samples/ControlCatalog.iOS/Info.plist index 6ffe3ba662..1dd4416c28 100644 --- a/samples/ControlCatalog.iOS/Info.plist +++ b/samples/ControlCatalog.iOS/Info.plist @@ -13,7 +13,7 @@ LSRequiresIPhoneOS MinimumOSVersion - 10.0 + 13.0 UIDeviceFamily 1 @@ -39,9 +39,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIStatusBarHidden - - UIViewControllerBasedStatusBarAppearance - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index d71d51f068..246fe4385f 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -44,11 +44,11 @@ namespace ControlCatalog { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) { - desktopLifetime.MainWindow = new MainWindow(); + desktopLifetime.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() }; } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) { - singleViewLifetime.MainView = new MainView(); + singleViewLifetime.MainView = new MainView { DataContext = new MainWindowViewModel() }; } base.OnFrameworkInitializationCompleted(); diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 6f31d22677..f29da3126f 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -12,6 +13,7 @@ using Avalonia.VisualTree; using Avalonia.Styling; using ControlCatalog.Models; using ControlCatalog.Pages; +using ControlCatalog.ViewModels; namespace ControlCatalog { @@ -99,13 +101,47 @@ namespace ControlCatalog }; } + internal MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!; + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); + var decorations = this.Get("Decorations"); if (VisualRoot is Window window) decorations.SelectedIndex = (int)window.SystemDecorations; - + + var insets = TopLevel.GetTopLevel(this)!.InsetsManager; + if (insets != null) + { + // In real life application these events should be unsubscribed to avoid memory leaks. + ViewModel.SafeAreaPadding = insets.SafeAreaPadding; + insets.SafeAreaChanged += (sender, args) => + { + ViewModel.SafeAreaPadding = insets.SafeAreaPadding; + }; + + ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? false; + + ViewModel.PropertyChanged += async (sender, args) => + { + if (args.PropertyName == nameof(ViewModel.DisplayEdgeToEdge)) + { + insets.DisplayEdgeToEdge = ViewModel.DisplayEdgeToEdge; + } + else if (args.PropertyName == nameof(ViewModel.IsSystemBarVisible)) + { + insets.IsSystemBarVisible = ViewModel.IsSystemBarVisible; + } + + // Give the OS some time to apply new values and refresh the view model. + await Task.Delay(100); + ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? false; + }; + } + _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues()); } diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index c589f41442..10ff94d25c 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -17,7 +17,6 @@ namespace ControlCatalog { this.InitializeComponent(); - DataContext = new MainWindowViewModel(); _recentMenu = ((NativeMenu.GetMenu(this)?.Items[0] as NativeMenuItem)?.Menu?.Items[2] as NativeMenuItem)?.Menu; } diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml index d690058b27..bcc1a71243 100644 --- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -5,11 +5,21 @@ xmlns:viewModels="using:ControlCatalog.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ControlCatalog.Pages.WindowCustomizationsPage" - x:DataType="viewModels:MainWindowViewModel"> - - - - - + x:DataType="viewModels:MainWindowViewModel" + x:CompileBindings="True"> + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 3628a9b8a7..8c6f0a2bd6 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -6,6 +6,7 @@ using Avalonia.Platform; using Avalonia.Reactive; using System; using System.ComponentModel.DataAnnotations; +using Avalonia; using MiniMvvm; namespace ControlCatalog.ViewModels @@ -20,6 +21,9 @@ namespace ControlCatalog.ViewModels private bool _systemTitleBarEnabled; private bool _preferSystemChromeEnabled; private double _titleBarHeight; + private bool _isSystemBarVisible; + private bool _displayEdgeToEdge; + private Thickness _safeAreaPadding; public MainWindowViewModel() { @@ -78,25 +82,25 @@ namespace ControlCatalog.ViewModels { get { return _chromeHints; } set { this.RaiseAndSetIfChanged(ref _chromeHints, value); } - } + } public bool ExtendClientAreaEnabled { get { return _extendClientAreaEnabled; } set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); } - } + } public bool SystemTitleBarEnabled { get { return _systemTitleBarEnabled; } set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); } - } + } public bool PreferSystemChromeEnabled { get { return _preferSystemChromeEnabled; } set { this.RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); } - } + } public double TitleBarHeight { @@ -122,6 +126,24 @@ namespace ControlCatalog.ViewModels set { this.RaiseAndSetIfChanged(ref _isMenuItemChecked, value); } } + public bool IsSystemBarVisible + { + get { return _isSystemBarVisible; } + set { this.RaiseAndSetIfChanged(ref _isSystemBarVisible, value); } + } + + public bool DisplayEdgeToEdge + { + get { return _displayEdgeToEdge; } + set { this.RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); } + } + + public Thickness SafeAreaPadding + { + get { return _safeAreaPadding; } + set { this.RaiseAndSetIfChanged(ref _safeAreaPadding, value); } + } + public MiniCommand AboutCommand { get; } public MiniCommand ExitCommand { get; } From 34c1b46b30fad19f043a1f46a5e887685fd2aba6 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 3 Mar 2023 15:00:40 +0000 Subject: [PATCH 10/11] Force app theme change when status bar theme changes on android. --- .../Resources/values/styles.xml | 2 +- samples/ControlCatalog/MainView.xaml.cs | 4 ++-- src/Android/Avalonia.Android/AvaloniaMainActivity.cs | 3 +++ .../Avalonia.Android/Platform/AndroidInsetsManager.cs | 11 ++++++++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog.Android/Resources/values/styles.xml b/samples/ControlCatalog.Android/Resources/values/styles.xml index 49e079a719..3e1270256d 100644 --- a/samples/ControlCatalog.Android/Resources/values/styles.xml +++ b/samples/ControlCatalog.Android/Resources/values/styles.xml @@ -4,7 +4,7 @@ - diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index f29da3126f..9c439c874f 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -122,7 +122,7 @@ namespace ControlCatalog }; ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; - ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? false; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; ViewModel.PropertyChanged += async (sender, args) => { @@ -138,7 +138,7 @@ namespace ControlCatalog // Give the OS some time to apply new values and refresh the view model. await Task.Delay(100); ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge; - ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? false; + ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; }; } diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index 5b22d2c270..eb4b6bf6a0 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -33,6 +33,9 @@ namespace Avalonia.Android lifetime.View = View; } + Window?.ClearFlags(WindowManagerFlags.TranslucentStatus); + Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds); + base.OnCreate(savedInstanceState); SetContentView(View); diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs index 3850199226..35d1b06e6a 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Android.OS; using Android.Views; +using AndroidX.AppCompat.App; using AndroidX.Core.View; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls.Platform; @@ -124,6 +125,8 @@ namespace Avalonia.Android.Platform { _statusBarTheme = value; + var isDefault = _statusBarTheme == null; + if (!_topLevel.View.IsShown) { return; @@ -143,6 +146,8 @@ namespace Avalonia.Android.Platform compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light; compat.AppearanceLightNavigationBars = value == Controls.Platform.SystemBarTheme.Light; + + AppCompatDelegate.DefaultNightMode = isDefault ? AppCompatDelegate.ModeNightFollowSystem : compat.AppearanceLightStatusBars ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes; } } @@ -150,7 +155,11 @@ namespace Avalonia.Android.Platform { get { - var compat = ViewCompat.GetRootWindowInsets(_topLevel.View); + if(_activity.Window == null) + { + return true; + } + var compat = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); return compat?.IsVisible(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); } From 16efee95caef4632a573a860ca7f25e07ce28c8c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Mar 2023 16:11:22 +0100 Subject: [PATCH 11/11] Disambiguate ShowWindowTest controls. Under the appium-mac2-driver, `FindElementByAccessibilityId` doesn't actually find elements by their accessibility ID, it also finds them by their accessibility name. Since #10531 added an automation peer for `Label` which sets the accessibility name, the wrong control was being located (e.g. it located the "Position" label instead of the `Name="Position"` text box). Disambiguate these controls by adding a `Current` prefix to make integration tests pass again. --- .../IntegrationTestApp/ShowWindowTest.axaml | 16 +++++++-------- .../ShowWindowTest.axaml.cs | 12 +++++------ .../WindowTests.cs | 20 +++++++++---------- .../WindowTests_MacOS.cs | 2 +- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index 00987429d0..5162eeee92 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -6,27 +6,27 @@ Title="Show Window Test"> - - - + - + - + - + - + Normal Minimized Maximized @@ -34,7 +34,7 @@ - + diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs index 43875dd990..1a267ea20b 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -16,11 +16,11 @@ namespace IntegrationTestApp { InitializeComponent(); DataContext = this; - PositionChanged += (s, e) => this.GetControl("Position").Text = $"{Position}"; + PositionChanged += (s, e) => this.GetControl("CurrentPosition").Text = $"{Position}"; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - _orderTextBox = this.GetControl("Order"); + _orderTextBox = this.GetControl("CurrentOrder"); _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) }; _timer.Tick += TimerOnTick; _timer.Start(); @@ -36,13 +36,13 @@ namespace IntegrationTestApp { base.OnOpened(e); var scaling = PlatformImpl!.DesktopScaling; - this.GetControl("Position").Text = $"{Position}"; - this.GetControl("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; - this.GetControl("Scaling").Text = $"{scaling}"; + this.GetControl("CurrentPosition").Text = $"{Position}"; + this.GetControl("CurrentScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}"; + this.GetControl("CurrentScaling").Text = $"{scaling}"; if (Owner is not null) { - var ownerRect = this.GetControl("OwnerRect"); + var ownerRect = this.GetControl("CurrentOwnerRect"); var owner = (Window)Owner; ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}"; } diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index a2bfb618d6..79eea09e01 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -92,7 +92,7 @@ namespace Avalonia.IntegrationTests.Appium { try { - _session.FindElementByAccessibilityId("WindowState").SendClick(); + _session.FindElementByAccessibilityId("CurrentWindowState").SendClick(); _session.FindElementByAccessibilityId("WindowStateNormal").SendClick(); // Wait for animations to run. @@ -112,7 +112,7 @@ namespace Avalonia.IntegrationTests.Appium { using (OpenWindow(new Size(400, 400), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) { - var windowState = _session.FindElementByAccessibilityId("WindowState"); + var windowState = _session.FindElementByAccessibilityId("CurrentWindowState"); Assert.Equal("Normal", windowState.GetComboBoxValue()); @@ -151,7 +151,7 @@ namespace Avalonia.IntegrationTests.Appium public void ShowMode(ShowWindowMode mode) { using var window = OpenWindow(null, mode, WindowStartupLocation.Manual); - var windowState = _session.FindElementByAccessibilityId("WindowState"); + var windowState = _session.FindElementByAccessibilityId("CurrentWindowState"); var original = GetWindowInfo(); Assert.Equal("Normal", windowState.GetComboBoxValue()); @@ -354,7 +354,7 @@ namespace Avalonia.IntegrationTests.Appium { PixelRect? ReadOwnerRect() { - var text = _session.FindElementByAccessibilityId("OwnerRect").Text; + var text = _session.FindElementByAccessibilityId("CurrentOwnerRect").Text; return !string.IsNullOrWhiteSpace(text) ? PixelRect.Parse(text) : null; } @@ -365,13 +365,13 @@ namespace Avalonia.IntegrationTests.Appium try { return new( - Size.Parse(_session.FindElementByAccessibilityId("ClientSize").Text), - Size.Parse(_session.FindElementByAccessibilityId("FrameSize").Text), - PixelPoint.Parse(_session.FindElementByAccessibilityId("Position").Text), + Size.Parse(_session.FindElementByAccessibilityId("CurrentClientSize").Text), + Size.Parse(_session.FindElementByAccessibilityId("CurrentFrameSize").Text), + PixelPoint.Parse(_session.FindElementByAccessibilityId("CurrentPosition").Text), ReadOwnerRect(), - PixelRect.Parse(_session.FindElementByAccessibilityId("ScreenRect").Text), - double.Parse(_session.FindElementByAccessibilityId("Scaling").Text), - Enum.Parse(_session.FindElementByAccessibilityId("WindowState").Text)); + PixelRect.Parse(_session.FindElementByAccessibilityId("CurrentScreenRect").Text), + double.Parse(_session.FindElementByAccessibilityId("CurrentScaling").Text), + Enum.Parse(_session.FindElementByAccessibilityId("CurrentWindowState").Text)); } catch (OpenQA.Selenium.NoSuchElementException) when (retry++ < 3) { diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 90fdc2511f..039d30bbc1 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -393,7 +393,7 @@ namespace Avalonia.IntegrationTests.Appium private int GetWindowOrder(string identifier) { var window = GetWindow(identifier); - var order = window.FindElementByXPath("//*[@identifier='Order']"); + var order = window.FindElementByXPath("//*[@identifier='CurrentOrder']"); return int.Parse(order.Text); }