|
@@ -2,53 +2,68 @@
|
|
|
using System.Collections.Generic;
|
|
|
using Android.OS;
|
|
|
using Android.Views;
|
|
|
+using Android.Views.Animations;
|
|
|
using AndroidX.Core.View;
|
|
|
using Avalonia.Android.Platform.SkiaPlatform;
|
|
|
+using Avalonia.Animation.Easings;
|
|
|
using Avalonia.Controls.Platform;
|
|
|
using Avalonia.Media;
|
|
|
+using AndroidWindow = Android.Views.Window;
|
|
|
|
|
|
namespace Avalonia.Android.Platform
|
|
|
{
|
|
|
- internal class AndroidInsetsManager : Java.Lang.Object, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener
|
|
|
+ internal sealed class AndroidInsetsManager : WindowInsetsAnimationCompat.Callback, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener, IInputPane
|
|
|
{
|
|
|
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;
|
|
|
private Color? _systemBarColor;
|
|
|
+ private InputPaneState _state;
|
|
|
+ private Rect _previousRect;
|
|
|
+ private readonly bool _usesLegacyLayouts;
|
|
|
|
|
|
+ private AndroidWindow Window => _activity.Window ?? throw new InvalidOperationException("Activity.Window must be set.");
|
|
|
+
|
|
|
public event EventHandler<SafeAreaChangedArgs> SafeAreaChanged;
|
|
|
+ public event EventHandler<InputPaneStateEventArgs> StateChanged;
|
|
|
+
|
|
|
+ public InputPaneState State
|
|
|
+ {
|
|
|
+ get => _state; set
|
|
|
+ {
|
|
|
+ var oldState = _state;
|
|
|
+ _state = value;
|
|
|
+
|
|
|
+ if (oldState != value && Build.VERSION.SdkInt <= BuildVersionCodes.Q)
|
|
|
+ {
|
|
|
+ var currentRect = OccludedRect;
|
|
|
+ StateChanged?.Invoke(this, new InputPaneStateEventArgs(value, _previousRect, currentRect, TimeSpan.Zero, null));
|
|
|
+ _previousRect = currentRect;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
public bool DisplayEdgeToEdge
|
|
|
{
|
|
|
- get => _displayEdgeToEdge;
|
|
|
+ get => _displayEdgeToEdge;
|
|
|
set
|
|
|
{
|
|
|
_displayEdgeToEdge = value;
|
|
|
|
|
|
- var window = _activity.Window;
|
|
|
-
|
|
|
- if (OperatingSystem.IsAndroidVersionAtLeast(28) && window?.Attributes is { } attributes)
|
|
|
+ if (OperatingSystem.IsAndroidVersionAtLeast(28) && Window.Attributes is { } attributes)
|
|
|
{
|
|
|
attributes.LayoutInDisplayCutoutMode = value ? LayoutInDisplayCutoutMode.ShortEdges : LayoutInDisplayCutoutMode.Default;
|
|
|
}
|
|
|
|
|
|
- if (window is not null)
|
|
|
- {
|
|
|
- WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value);
|
|
|
- }
|
|
|
+ WindowCompat.SetDecorFitsSystemWindows(Window, !value);
|
|
|
|
|
|
- if(value)
|
|
|
+ if (value)
|
|
|
{
|
|
|
- if (window is not null)
|
|
|
- {
|
|
|
- window.AddFlags(WindowManagerFlags.TranslucentStatus);
|
|
|
- window.AddFlags(WindowManagerFlags.TranslucentNavigation);
|
|
|
- }
|
|
|
+ Window.AddFlags(WindowManagerFlags.TranslucentStatus);
|
|
|
+ Window.AddFlags(WindowManagerFlags.TranslucentNavigation);
|
|
|
}
|
|
|
else
|
|
|
{
|
|
@@ -57,20 +72,12 @@ namespace Avalonia.Android.Platform
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- public AndroidInsetsManager(AvaloniaMainActivity activity, TopLevelImpl topLevel)
|
|
|
+ internal AndroidInsetsManager(AvaloniaMainActivity activity, TopLevelImpl topLevel) : base(DispatchModeStop)
|
|
|
{
|
|
|
_activity = activity;
|
|
|
_topLevel = topLevel;
|
|
|
- _callback = new InsetsAnimationCallback(WindowInsetsAnimationCompat.Callback.DispatchModeStop);
|
|
|
-
|
|
|
- _callback.InsetsManager = this;
|
|
|
-
|
|
|
- if (_activity.Window is { } window)
|
|
|
- {
|
|
|
- ViewCompat.SetOnApplyWindowInsetsListener(window.DecorView, this);
|
|
|
|
|
|
- ViewCompat.SetWindowInsetsAnimationCallback(window.DecorView, _callback);
|
|
|
- }
|
|
|
+ ViewCompat.SetOnApplyWindowInsetsListener(Window.DecorView, this);
|
|
|
|
|
|
if (Build.VERSION.SdkInt < BuildVersionCodes.R)
|
|
|
{
|
|
@@ -79,32 +86,48 @@ namespace Avalonia.Android.Platform
|
|
|
}
|
|
|
|
|
|
DisplayEdgeToEdge = false;
|
|
|
+
|
|
|
+ ViewCompat.SetWindowInsetsAnimationCallback(Window.DecorView, this);
|
|
|
}
|
|
|
|
|
|
public Thickness SafeAreaPadding
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
- var insets = _activity.Window is { } window ? ViewCompat.GetRootWindowInsets(window.DecorView) : null;
|
|
|
+ var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
|
|
|
|
|
|
if (insets != null)
|
|
|
{
|
|
|
var renderScaling = _topLevel.RenderScaling;
|
|
|
|
|
|
var inset = insets.GetInsets(
|
|
|
- (_displayEdgeToEdge ?
|
|
|
+ _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());
|
|
|
+ WindowInsetsCompat.Type.DisplayCutout() : 0);
|
|
|
|
|
|
return new Thickness(inset.Left / renderScaling,
|
|
|
inset.Top / renderScaling,
|
|
|
inset.Right / renderScaling,
|
|
|
- (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !_displayEdgeToEdge) || !_usesLegacyLayouts) ?
|
|
|
- imeInset.Bottom - (_displayEdgeToEdge ? 0 : navBarInset.Bottom) :
|
|
|
- inset.Bottom) / renderScaling);
|
|
|
+ inset.Bottom / renderScaling);
|
|
|
+ }
|
|
|
+
|
|
|
+ return default;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public Rect OccludedRect
|
|
|
+ {
|
|
|
+ get
|
|
|
+ {
|
|
|
+ var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
|
|
|
+
|
|
|
+ if (insets != null)
|
|
|
+ {
|
|
|
+ var navbarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()).Bottom;
|
|
|
+
|
|
|
+ var height = Math.Max((float)((insets.GetInsets(WindowInsetsCompat.Type.Ime()).Bottom - navbarInset) / _topLevel.RenderScaling), 0);
|
|
|
+
|
|
|
+ return new Rect(0, _topLevel.ClientSize.Height - SafeAreaPadding.Bottom - height, _topLevel.ClientSize.Width, height);
|
|
|
}
|
|
|
|
|
|
return default;
|
|
@@ -113,8 +136,16 @@ namespace Avalonia.Android.Platform
|
|
|
|
|
|
public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets)
|
|
|
{
|
|
|
- NotifySafeAreaChanged(SafeAreaPadding);
|
|
|
insets = ViewCompat.OnApplyWindowInsets(v, insets);
|
|
|
+ NotifySafeAreaChanged(SafeAreaPadding);
|
|
|
+
|
|
|
+ if (_previousRect == default)
|
|
|
+ {
|
|
|
+ _previousRect = OccludedRect;
|
|
|
+ }
|
|
|
+
|
|
|
+ State = insets.IsVisible(WindowInsetsCompat.Type.Ime()) ? InputPaneState.Open : InputPaneState.Closed;
|
|
|
+
|
|
|
return insets;
|
|
|
}
|
|
|
|
|
@@ -126,6 +157,12 @@ namespace Avalonia.Android.Platform
|
|
|
public void OnGlobalLayout()
|
|
|
{
|
|
|
NotifySafeAreaChanged(SafeAreaPadding);
|
|
|
+
|
|
|
+ if (_usesLegacyLayouts)
|
|
|
+ {
|
|
|
+ var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
|
|
|
+ State = insets?.IsVisible(WindowInsetsCompat.Type.Ime()) == true ? InputPaneState.Open : InputPaneState.Closed;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
public SystemBarTheme? SystemBarTheme
|
|
@@ -134,7 +171,7 @@ namespace Avalonia.Android.Platform
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
- var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View);
|
|
|
+ var compat = new WindowInsetsControllerCompat(Window, _topLevel.View);
|
|
|
|
|
|
return compat.AppearanceLightStatusBars ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark;
|
|
|
}
|
|
@@ -152,7 +189,7 @@ namespace Avalonia.Android.Platform
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View);
|
|
|
+ var compat = new WindowInsetsControllerCompat(Window, _topLevel.View);
|
|
|
|
|
|
if (_isDefaultSystemBarLightTheme == null)
|
|
|
{
|
|
@@ -161,7 +198,7 @@ namespace Avalonia.Android.Platform
|
|
|
|
|
|
if (value == null)
|
|
|
{
|
|
|
- value = (bool)_isDefaultSystemBarLightTheme ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark;
|
|
|
+ value = _isDefaultSystemBarLightTheme.Value ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark;
|
|
|
}
|
|
|
|
|
|
compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light;
|
|
@@ -173,7 +210,7 @@ namespace Avalonia.Android.Platform
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
- if(_activity.Window == null)
|
|
|
+ if (_activity.Window == null)
|
|
|
{
|
|
|
return true;
|
|
|
}
|
|
@@ -190,7 +227,7 @@ namespace Avalonia.Android.Platform
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- var compat = WindowCompat.GetInsetsController(_activity.Window, _topLevel.View);
|
|
|
+ var compat = WindowCompat.GetInsetsController(Window, _topLevel.View);
|
|
|
|
|
|
if (value == null || value.Value)
|
|
|
{
|
|
@@ -210,7 +247,7 @@ namespace Avalonia.Android.Platform
|
|
|
|
|
|
public Color? SystemBarColor
|
|
|
{
|
|
|
- get => _systemBarColor;
|
|
|
+ get => _systemBarColor;
|
|
|
set
|
|
|
{
|
|
|
_systemBarColor = value;
|
|
@@ -240,40 +277,48 @@ namespace Avalonia.Android.Platform
|
|
|
SystemBarColor = _systemBarColor;
|
|
|
}
|
|
|
|
|
|
- private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback
|
|
|
+ public override WindowInsetsAnimationCompat.BoundsCompat OnStart(WindowInsetsAnimationCompat animation, WindowInsetsAnimationCompat.BoundsCompat bounds)
|
|
|
{
|
|
|
- public InsetsAnimationCallback(int dispatchMode) : base(dispatchMode)
|
|
|
+ if ((animation.TypeMask & WindowInsetsCompat.Type.Ime()) != 0)
|
|
|
{
|
|
|
- }
|
|
|
-
|
|
|
- public AndroidInsetsManager InsetsManager { get; set; }
|
|
|
+ var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
|
|
|
|
|
|
- public override WindowInsetsCompat OnProgress(WindowInsetsCompat insets, IList<WindowInsetsAnimationCompat> runningAnimations)
|
|
|
- {
|
|
|
- foreach (var anim in runningAnimations)
|
|
|
+ if (insets != null)
|
|
|
{
|
|
|
- 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 navbarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()).Bottom;
|
|
|
+ var height = Math.Max(0, (float)((bounds.LowerBound.Bottom - navbarInset) / _topLevel.RenderScaling));
|
|
|
+ var upperRect = new Rect(0, _topLevel.ClientSize.Height - SafeAreaPadding.Bottom - height, _topLevel.ClientSize.Width, height);
|
|
|
+ height = Math.Max(0, (float)((bounds.UpperBound.Bottom - navbarInset) / _topLevel.RenderScaling));
|
|
|
+ var lowerRect = new Rect(0, _topLevel.ClientSize.Height - SafeAreaPadding.Bottom - height, _topLevel.ClientSize.Width, height);
|
|
|
|
|
|
+ var duration = TimeSpan.FromMilliseconds(animation.DurationMillis);
|
|
|
|
|
|
- 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;
|
|
|
- }
|
|
|
+ bool isOpening = State == InputPaneState.Open;
|
|
|
+ StateChanged?.Invoke(this, new InputPaneStateEventArgs(State, isOpening ? upperRect : lowerRect, isOpening ? lowerRect : upperRect, duration, new AnimationEasing(animation.Interpolator)));
|
|
|
}
|
|
|
- return insets;
|
|
|
}
|
|
|
+
|
|
|
+ return base.OnStart(animation, bounds);
|
|
|
+ }
|
|
|
+
|
|
|
+ public override WindowInsetsCompat OnProgress(WindowInsetsCompat insets, IList<WindowInsetsAnimationCompat> runningAnimations)
|
|
|
+ {
|
|
|
+ return insets;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ internal sealed class AnimationEasing : Easing
|
|
|
+ {
|
|
|
+ private readonly IInterpolator _interpolator;
|
|
|
+
|
|
|
+ public AnimationEasing(IInterpolator interpolator)
|
|
|
+ {
|
|
|
+ _interpolator = interpolator;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override double Ease(double progress)
|
|
|
+ {
|
|
|
+ return _interpolator.GetInterpolation((float)progress);
|
|
|
}
|
|
|
}
|
|
|
}
|