A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

332 lines
12 KiB

using System;
using System.Collections.Generic;
using Android.App;
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 Avalonia.Threading;
using AndroidWindow = Android.Views.Window;
namespace Avalonia.Android.Platform
{
internal sealed class AndroidInsetsManager : WindowInsetsAnimationCompat.Callback, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener, IInputPane
{
private readonly Activity _activity;
private readonly TopLevelImpl _topLevel;
private bool _displayEdgeToEdge;
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;
NotifyStateChanged(value, _previousRect, currentRect, TimeSpan.Zero, null);
_previousRect = currentRect;
}
}
}
public bool DisplayEdgeToEdge
{
get => _displayEdgeToEdge;
set
{
_displayEdgeToEdge = value;
if (OperatingSystem.IsAndroidVersionAtLeast(28) && Window.Attributes is { } attributes)
{
attributes.LayoutInDisplayCutoutMode = value ? LayoutInDisplayCutoutMode.ShortEdges : LayoutInDisplayCutoutMode.Default;
}
WindowCompat.SetDecorFitsSystemWindows(Window, !value);
if (value)
{
Window.AddFlags(WindowManagerFlags.TranslucentStatus);
Window.AddFlags(WindowManagerFlags.TranslucentNavigation);
}
else
{
SystemBarColor = _systemBarColor;
}
}
}
internal AndroidInsetsManager(Activity activity, TopLevelImpl topLevel) : base(DispatchModeStop)
{
_activity = activity;
_topLevel = topLevel;
ViewCompat.SetOnApplyWindowInsetsListener(Window.DecorView, this);
if (Build.VERSION.SdkInt < BuildVersionCodes.R)
{
_usesLegacyLayouts = true;
_activity.Window?.DecorView.ViewTreeObserver?.AddOnGlobalLayoutListener(this);
}
DisplayEdgeToEdge = false;
ViewCompat.SetWindowInsetsAnimationCallback(Window.DecorView, this);
}
public Thickness SafeAreaPadding
{
get
{
var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
if (insets != null)
{
var renderScaling = _topLevel.RenderScaling;
var inset = insets.GetInsets(
_displayEdgeToEdge ?
WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() |
WindowInsetsCompat.Type.DisplayCutout() : 0);
return new Thickness(inset.Left / renderScaling,
inset.Top / renderScaling,
inset.Right / 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;
}
}
public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets)
{
insets = ViewCompat.OnApplyWindowInsets(v, insets);
NotifySafeAreaChanged(SafeAreaPadding);
if (_previousRect == default)
{
_previousRect = OccludedRect;
}
State = insets.IsVisible(WindowInsetsCompat.Type.Ime()) ? InputPaneState.Open : InputPaneState.Closed;
return insets;
}
private void NotifySafeAreaChanged(Thickness safeAreaPadding)
{
Dispatcher.UIThread.Send(_ => SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(safeAreaPadding)));
}
private void NotifyStateChanged(InputPaneState newState, Rect? startRect, Rect endRect, TimeSpan animationDuration, IEasing? easing)
{
Dispatcher.UIThread.Send(_ => StateChanged?.Invoke(this, new InputPaneStateEventArgs(newState, startRect, endRect, animationDuration, easing)));
}
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
{
get
{
try
{
var compat = new WindowInsetsControllerCompat(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(Window, _topLevel.View);
if (_isDefaultSystemBarLightTheme == null)
{
_isDefaultSystemBarLightTheme = compat.AppearanceLightStatusBars;
}
if (value == null)
{
value = _isDefaultSystemBarLightTheme.Value ? 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
{
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(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;
}
}
}
}
public Color? SystemBarColor
{
get => _systemBarColor;
set
{
_systemBarColor = value;
if (_systemBarColor is { } color && !_displayEdgeToEdge && _activity.Window != null)
{
_activity.Window.ClearFlags(WindowManagerFlags.TranslucentStatus);
_activity.Window.ClearFlags(WindowManagerFlags.TranslucentNavigation);
_activity.Window.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
var androidColor = global::Android.Graphics.Color.Argb(color.A, color.R, color.G, color.B);
_activity.Window.SetStatusBarColor(androidColor);
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
// As we can only change the navigation bar's foreground api 26 and newer, we only change the background color if running on those versions
_activity.Window.SetNavigationBarColor(androidColor);
}
}
}
}
internal void ApplyStatusBarState()
{
IsSystemBarVisible = _systemUiVisibility;
SystemBarTheme = _statusBarTheme;
SystemBarColor = _systemBarColor;
}
public override WindowInsetsAnimationCompat.BoundsCompat OnStart(WindowInsetsAnimationCompat animation, WindowInsetsAnimationCompat.BoundsCompat bounds)
{
if ((animation.TypeMask & WindowInsetsCompat.Type.Ime()) != 0)
{
var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
if (insets != null)
{
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);
bool isOpening = State == InputPaneState.Open;
NotifyStateChanged(State, isOpening ? upperRect : lowerRect, isOpening ? lowerRect : upperRect, duration,
animation.Interpolator is { } interpolator ? new AnimationEasing(interpolator) : null);
}
}
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);
}
}
}