Browse Source

add support for getting safe insets

Emmanuel Hansen 2 years ago
parent
commit
746b53b388

+ 8 - 1
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;
     height: 100vh;
     width: 100vw
     width: 100vw
 }
 }

+ 12 - 0
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Diagnostics;
 using Android.App;
 using Android.App;
 using Android.Content;
 using Android.Content;
 using Android.Content.PM;
 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<AndroidBackRequestedEventArgs> BackRequested;
         public event EventHandler<AndroidBackRequestedEventArgs> BackRequested;
 
 
         public override void OnBackPressed()
         public override void OnBackPressed()

+ 2 - 0
src/Android/Avalonia.Android/AvaloniaView.cs

@@ -67,6 +67,8 @@ namespace Avalonia.Android
                 }
                 }
 
 
                 _root.Renderer.Start();
                 _root.Renderer.Start();
+
+                (_view._insetsManager as AndroidInsetsManager)?.ApplyStatusBarState();
             }
             }
             else
             else
             {
             {

+ 215 - 0
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<SafeAreaChangedArgs> 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<WindowInsetsAnimationCompat> 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;
+            }
+        }
+    }
+}

+ 22 - 18
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -3,9 +3,7 @@ using System.Collections.Generic;
 using Android.App;
 using Android.App;
 using Android.Content;
 using Android.Content;
 using Android.Graphics;
 using Android.Graphics;
-using Android.OS;
 using Android.Runtime;
 using Android.Runtime;
-using Android.Text;
 using Android.Views;
 using Android.Views;
 using Android.Views.InputMethods;
 using Android.Views.InputMethods;
 using Avalonia.Android.Platform.Specific;
 using Avalonia.Android.Platform.Specific;
@@ -28,6 +26,7 @@ using Math = System.Math;
 using AndroidRect = Android.Graphics.Rect;
 using AndroidRect = Android.Graphics.Rect;
 using Window = Android.Views.Window;
 using Window = Android.Views.Window;
 using Android.Graphics.Drawables;
 using Android.Graphics.Drawables;
+using Android.OS;
 
 
 namespace Avalonia.Android.Platform.SkiaPlatform
 namespace Avalonia.Android.Platform.SkiaPlatform
 {
 {
@@ -42,6 +41,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         private readonly INativeControlHostImpl _nativeControlHost;
         private readonly INativeControlHostImpl _nativeControlHost;
         private readonly IStorageProvider _storageProvider;
         private readonly IStorageProvider _storageProvider;
         private readonly ISystemNavigationManagerImpl _systemNavigationManager;
         private readonly ISystemNavigationManagerImpl _systemNavigationManager;
+        private readonly IInsetsManager _insetsManager;
         private ViewImpl _view;
         private ViewImpl _view;
 
 
         public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
         public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@@ -58,6 +58,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
             MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
                 _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
                 _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
 
 
+            if (avaloniaView.Context is AvaloniaMainActivity mainActivity)
+            {
+                _insetsManager = new AndroidInsetsManager(mainActivity, this);
+            }
+
             _nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
             _nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
             _storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context);
             _storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context);
 
 
@@ -69,21 +74,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
 
         public IInputRoot InputRoot { get; private set; }
         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;
         public Size? FrameSize => null;
 
 
@@ -284,7 +275,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
 
         public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
         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);
         public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
@@ -402,6 +401,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
                 return _nativeControlHost;
                 return _nativeControlHost;
             }
             }
 
 
+            if (featureType == typeof(IInsetsManager))
+            {
+                return _insetsManager;
+            }
+
             return null;
             return null;
         }
         }
     }
     }

+ 34 - 0
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<SafeAreaChangedArgs> 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
+    }
+}

+ 4 - 1
src/Avalonia.Controls/TopLevel.cs

@@ -14,6 +14,7 @@ using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.Platform.Storage;
 using Avalonia.Platform.Storage;
+using Avalonia.Reactive;
 using Avalonia.Rendering;
 using Avalonia.Rendering;
 using Avalonia.Styling;
 using Avalonia.Styling;
 using Avalonia.Utilities;
 using Avalonia.Utilities;
@@ -393,7 +394,9 @@ namespace Avalonia.Controls
             ??= AvaloniaLocator.Current.GetService<IStorageProviderFactory>()?.CreateProvider(this)
             ??= AvaloniaLocator.Current.GetService<IStorageProviderFactory>()?.CreateProvider(this)
             ?? PlatformImpl?.TryGetFeature<IStorageProvider>()
             ?? PlatformImpl?.TryGetFeature<IStorageProvider>()
             ?? throw new InvalidOperationException("StorageProvider platform implementation is not available.");
             ?? throw new InvalidOperationException("StorageProvider platform implementation is not available.");
-        
+
+        public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature<IInsetsManager>();
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         Point IRenderRoot.PointToClient(PixelPoint p)
         Point IRenderRoot.PointToClient(PixelPoint p)
         {
         {

+ 43 - 0
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<SafeAreaChangedArgs>? 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()));
+        }
+    }
+}

+ 11 - 0
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@@ -31,6 +31,7 @@ namespace Avalonia.Browser
         private readonly INativeControlHostImpl _nativeControlHost;
         private readonly INativeControlHostImpl _nativeControlHost;
         private readonly IStorageProvider _storageProvider;
         private readonly IStorageProvider _storageProvider;
         private readonly ISystemNavigationManagerImpl _systemNavigationManager;
         private readonly ISystemNavigationManagerImpl _systemNavigationManager;
+        private readonly IInsetsManager? _insetsManager;
 
 
         public BrowserTopLevelImpl(AvaloniaView avaloniaView)
         public BrowserTopLevelImpl(AvaloniaView avaloniaView)
         {
         {
@@ -40,9 +41,12 @@ namespace Avalonia.Browser
             AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
             AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
             _touchDevice = new TouchDevice();
             _touchDevice = new TouchDevice();
             _penDevice = new PenDevice();
             _penDevice = new PenDevice();
+
+            _insetsManager = new BrowserInsetsManager();
             _nativeControlHost = _avaloniaView.GetNativeControlHostImpl();
             _nativeControlHost = _avaloniaView.GetNativeControlHostImpl();
             _storageProvider = new BrowserStorageProvider();
             _storageProvider = new BrowserStorageProvider();
             _systemNavigationManager = new BrowserSystemNavigationManagerImpl();
             _systemNavigationManager = new BrowserSystemNavigationManagerImpl();
+
         }
         }
 
 
         public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
         public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
@@ -69,6 +73,8 @@ namespace Avalonia.Browser
                 }
                 }
 
 
                 Resized?.Invoke(newSize, PlatformResizeReason.User);
                 Resized?.Invoke(newSize, PlatformResizeReason.User);
+
+                (_insetsManager as BrowserInsetsManager)?.NotifySafeAreaPaddingChanged();
             }
             }
         }
         }
 
 
@@ -262,6 +268,11 @@ namespace Avalonia.Browser
                 return _nativeControlHost;
                 return _nativeControlHost;
             }
             }
 
 
+            if (featureType == typeof(IInsetsManager))
+            {
+                return _insetsManager;
+            }
+
             return null;
             return null;
         }
         }
     }
     }

+ 9 - 0
src/Browser/Avalonia.Browser/Interop/DomHelper.cs

@@ -11,6 +11,15 @@ internal static partial class DomHelper
     [JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)]
     [JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)]
     public static partial JSObject CreateAvaloniaHost(JSObject element);
     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)]
     [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)]
     public static partial void AddCssClass(JSObject element, string className);
     public static partial void AddCssClass(JSObject element, string className);
 
 

+ 22 - 0
src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts

@@ -84,4 +84,26 @@ export class AvaloniaDOM {
             inputElement
             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];
+    }
 }
 }