Browse Source

add support for getting safe insets

pull/10145/head
Emmanuel Hansen 3 years ago
parent
commit
746b53b388
  1. 9
      samples/ControlCatalog.Browser/app.css
  2. 12
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  3. 2
      src/Android/Avalonia.Android/AvaloniaView.cs
  4. 215
      src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
  5. 40
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  6. 34
      src/Avalonia.Controls/Platform/IInsetsManager.cs
  7. 5
      src/Avalonia.Controls/TopLevel.cs
  8. 43
      src/Browser/Avalonia.Browser/BrowserInsetsManager.cs
  9. 11
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  10. 9
      src/Browser/Avalonia.Browser/Interop/DomHelper.cs
  11. 22
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/dom.ts

9
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
}

12
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<AndroidBackRequestedEventArgs> BackRequested;
public override void OnBackPressed()

2
src/Android/Avalonia.Android/AvaloniaView.cs

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

215
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;
}
}
}
}

40
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;
}
}

34
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
}
}

5
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<IStorageProviderFactory>()?.CreateProvider(this)
?? PlatformImpl?.TryGetFeature<IStorageProvider>()
?? throw new InvalidOperationException("StorageProvider platform implementation is not available.");
public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature<IInsetsManager>();
/// <inheritdoc/>
Point IRenderRoot.PointToClient(PixelPoint p)
{

43
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
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;
}
}

9
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);

22
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];
}
}

Loading…
Cancel
Save