27 changed files with 732 additions and 76 deletions
@ -0,0 +1,235 @@ |
|||
using System; |
|||
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; |
|||
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); |
|||
} |
|||
|
|||
DisplayEdgeToEdge = false; |
|||
} |
|||
|
|||
public Thickness SafeAreaPadding |
|||
{ |
|||
get |
|||
{ |
|||
var insets = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); |
|||
|
|||
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 default; |
|||
} |
|||
} |
|||
|
|||
public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets) |
|||
{ |
|||
NotifySafeAreaChanged(SafeAreaPadding); |
|||
return insets; |
|||
} |
|||
|
|||
private void NotifySafeAreaChanged(Thickness safeAreaPadding) |
|||
{ |
|||
SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(safeAreaPadding)); |
|||
} |
|||
|
|||
public void OnGlobalLayout() |
|||
{ |
|||
NotifySafeAreaChanged(SafeAreaPadding); |
|||
} |
|||
|
|||
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; |
|||
|
|||
var isDefault = _statusBarTheme == null; |
|||
|
|||
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; |
|||
|
|||
AppCompatDelegate.DefaultNightMode = isDefault ? AppCompatDelegate.ModeNightFollowSystem : compat.AppearanceLightStatusBars ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes; |
|||
} |
|||
} |
|||
|
|||
public bool? IsSystemBarVisible |
|||
{ |
|||
get |
|||
{ |
|||
if(_activity.Window == null) |
|||
{ |
|||
return true; |
|||
} |
|||
var compat = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView); |
|||
|
|||
return compat?.IsVisible(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); |
|||
} |
|||
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.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); |
|||
} |
|||
else |
|||
{ |
|||
compat?.Hide(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars()); |
|||
|
|||
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.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()); |
|||
|
|||
|
|||
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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
using System; |
|||
using Avalonia.Metadata; |
|||
|
|||
#nullable enable |
|||
namespace Avalonia.Controls.Platform |
|||
{ |
|||
[Unstable] |
|||
[NotClientImplementable] |
|||
public interface IInsetsManager |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets whether the system bars are visible.
|
|||
/// </summary>
|
|||
bool? IsSystemBarVisible { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets whether the window draws edge to edge. behind any visibile system bars.
|
|||
/// </summary>
|
|||
bool DisplayEdgeToEdge { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the current safe area padding.
|
|||
/// </summary>
|
|||
Thickness SafeAreaPadding { get; } |
|||
|
|||
/// <summary>
|
|||
/// Occurs when safe area for the current window changes.
|
|||
/// </summary>
|
|||
event EventHandler<SafeAreaChangedArgs>? SafeAreaChanged; |
|||
} |
|||
|
|||
public class SafeAreaChangedArgs : EventArgs |
|||
{ |
|||
public SafeAreaChangedArgs(Thickness safeArePadding) |
|||
{ |
|||
SafeAreaPadding = safeArePadding; |
|||
} |
|||
|
|||
/// <inheritdoc cref="IInsetsManager.GetSafeAreaPadding"/>
|
|||
public Thickness SafeAreaPadding { get; } |
|||
} |
|||
|
|||
public enum SystemBarTheme |
|||
{ |
|||
/// <summary>
|
|||
/// Light system bar theme, with light background and a dark foreground
|
|||
/// </summary>
|
|||
Light, |
|||
|
|||
/// <summary>
|
|||
/// Bark system bar theme, with dark background and a light foreground
|
|||
/// </summary>
|
|||
Dark |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
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 bool? IsSystemBarVisible |
|||
{ |
|||
get |
|||
{ |
|||
return DomHelper.IsFullscreen(); |
|||
} |
|||
set |
|||
{ |
|||
DomHelper.SetFullscreen(!value ?? false); |
|||
} |
|||
} |
|||
|
|||
public bool DisplayEdgeToEdge { get; set; } |
|||
|
|||
public event EventHandler<SafeAreaChangedArgs>? SafeAreaChanged; |
|||
|
|||
public Thickness SafeAreaPadding |
|||
{ |
|||
get |
|||
{ |
|||
var padding = DomHelper.GetSafeAreaPadding(); |
|||
|
|||
return new Thickness(padding[0], padding[1], padding[2], padding[3]); |
|||
} |
|||
} |
|||
|
|||
public void NotifySafeAreaPaddingChanged() |
|||
{ |
|||
SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
using System; |
|||
using Avalonia.Controls.Platform; |
|||
using UIKit; |
|||
|
|||
namespace Avalonia.iOS; |
|||
#nullable enable |
|||
|
|||
internal class InsetsManager : IInsetsManager |
|||
{ |
|||
private readonly AvaloniaView _view; |
|||
private IAvaloniaViewController? _controller; |
|||
private bool _displayEdgeToEdge; |
|||
|
|||
public InsetsManager(AvaloniaView view) |
|||
{ |
|||
_view = view; |
|||
} |
|||
|
|||
internal void InitWithController(IAvaloniaViewController controller) |
|||
{ |
|||
_controller = controller; |
|||
if (_controller is not null) |
|||
{ |
|||
_controller.SafeAreaPaddingChanged += (_, _) => |
|||
{ |
|||
SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(SafeAreaPadding)); |
|||
DisplayEdgeToEdgeChanged?.Invoke(this, _displayEdgeToEdge); |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public SystemBarTheme? SystemBarTheme |
|||
{ |
|||
get => _controller?.PreferredStatusBarStyle switch |
|||
{ |
|||
UIStatusBarStyle.LightContent => Controls.Platform.SystemBarTheme.Dark, |
|||
UIStatusBarStyle.DarkContent => Controls.Platform.SystemBarTheme.Light, |
|||
_ => null |
|||
}; |
|||
set |
|||
{ |
|||
if (_controller != null) |
|||
{ |
|||
_controller.PreferredStatusBarStyle = value switch |
|||
{ |
|||
Controls.Platform.SystemBarTheme.Light => UIStatusBarStyle.DarkContent, |
|||
Controls.Platform.SystemBarTheme.Dark => UIStatusBarStyle.LightContent, |
|||
null => UIStatusBarStyle.Default, |
|||
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null) |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool? IsSystemBarVisible |
|||
{ |
|||
get => _controller?.PrefersStatusBarHidden == false; |
|||
set |
|||
{ |
|||
if (_controller is not null) |
|||
{ |
|||
_controller.PrefersStatusBarHidden = value == false; |
|||
} |
|||
} |
|||
} |
|||
public event EventHandler<SafeAreaChangedArgs>? SafeAreaChanged; |
|||
public event EventHandler<bool>? DisplayEdgeToEdgeChanged; |
|||
|
|||
public bool DisplayEdgeToEdge |
|||
{ |
|||
get => _displayEdgeToEdge; |
|||
set |
|||
{ |
|||
if (_displayEdgeToEdge != value) |
|||
{ |
|||
_displayEdgeToEdge = value; |
|||
DisplayEdgeToEdgeChanged?.Invoke(this, value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public Thickness SafeAreaPadding => _controller?.SafeAreaPadding ?? default; |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
using System; |
|||
using Avalonia.Metadata; |
|||
using UIKit; |
|||
|
|||
namespace Avalonia.iOS; |
|||
|
|||
[Unstable] |
|||
public interface IAvaloniaViewController |
|||
{ |
|||
UIStatusBarStyle PreferredStatusBarStyle { get; set; } |
|||
bool PrefersStatusBarHidden { get; set; } |
|||
Thickness SafeAreaPadding { get; } |
|||
event EventHandler SafeAreaPaddingChanged; |
|||
} |
|||
|
|||
/// <inheritdoc cref="IAvaloniaViewController" />
|
|||
public class DefaultAvaloniaViewController : UIViewController, IAvaloniaViewController |
|||
{ |
|||
private UIStatusBarStyle? _preferredStatusBarStyle; |
|||
private bool? _prefersStatusBarHidden; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void ViewDidLayoutSubviews() |
|||
{ |
|||
base.ViewDidLayoutSubviews(); |
|||
var size = View?.Frame.Size ?? default; |
|||
var frame = View?.SafeAreaLayoutGuide.LayoutFrame ?? default; |
|||
var safeArea = new Thickness(frame.Left, frame.Top, size.Width - frame.Right, size.Height - frame.Bottom); |
|||
if (SafeAreaPadding != safeArea) |
|||
{ |
|||
SafeAreaPadding = safeArea; |
|||
SafeAreaPaddingChanged?.Invoke(this, EventArgs.Empty); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool PrefersStatusBarHidden() |
|||
{ |
|||
return _prefersStatusBarHidden ??= base.PrefersStatusBarHidden(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override UIStatusBarStyle PreferredStatusBarStyle() |
|||
{ |
|||
// don't set _preferredStatusBarStyle value if it's null, so we can keep "default" there instead of actual app style.
|
|||
return _preferredStatusBarStyle ?? base.PreferredStatusBarStyle(); |
|||
} |
|||
|
|||
UIStatusBarStyle IAvaloniaViewController.PreferredStatusBarStyle |
|||
{ |
|||
get => _preferredStatusBarStyle ?? UIStatusBarStyle.Default; |
|||
set |
|||
{ |
|||
_preferredStatusBarStyle = value; |
|||
SetNeedsStatusBarAppearanceUpdate(); |
|||
} |
|||
} |
|||
|
|||
bool IAvaloniaViewController.PrefersStatusBarHidden |
|||
{ |
|||
get => _prefersStatusBarHidden ?? false; // false is default on ios/ipados
|
|||
set |
|||
{ |
|||
_prefersStatusBarHidden = value; |
|||
SetNeedsStatusBarAppearanceUpdate(); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public Thickness SafeAreaPadding { get; private set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public event EventHandler SafeAreaPaddingChanged; |
|||
} |
|||
Loading…
Reference in new issue