Browse Source
* Add and implement ISingleTopLevelApplicationLifetime (internal API) * Fix Android initialization order, so we can have AfterSetup with usable TopLevel in the callback * Fix android and futher simplify its initialization * Return removed public API, make it all work together, introduce AvaloniaActivity * Adjust some comments and minor bug fixes * Add CreateAppBuilder to iOS and Tizen as well for consistency * Add AfterApplicationSetup private api, so our backends have a safe place to setup avalonia views. * Keep number of breaking changes minimalpull/15074/head
committed by
GitHub
18 changed files with 395 additions and 256 deletions
@ -0,0 +1,170 @@ |
|||
#nullable enable |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Runtime.Versioning; |
|||
using Android.App; |
|||
using Android.Content; |
|||
using Android.Content.PM; |
|||
using Android.OS; |
|||
using Android.Runtime; |
|||
using Android.Views; |
|||
using AndroidX.AppCompat.App; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
|
|||
namespace Avalonia.Android; |
|||
|
|||
/// <summary>
|
|||
/// Common implementation of android activity that is integrated with Avalonia views.
|
|||
/// If you need a base class for main activity of Avalonia app, see <see cref="AvaloniaMainActivity"/> or <see cref="AvaloniaMainActivity{TApp}"/>.
|
|||
/// </summary>
|
|||
public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity |
|||
{ |
|||
private EventHandler<ActivatedEventArgs>? _onActivated, _onDeactivated; |
|||
private GlobalLayoutListener? _listener; |
|||
private object? _content; |
|||
internal AvaloniaView? _view; |
|||
|
|||
public Action<int, Result, Intent?>? ActivityResult { get; set; } |
|||
public Action<int, string[], Permission[]>? RequestPermissionsResult { get; set; } |
|||
|
|||
public event EventHandler<AndroidBackRequestedEventArgs>? BackRequested; |
|||
|
|||
public object? Content |
|||
{ |
|||
get => _content; |
|||
set |
|||
{ |
|||
if (_content != value) |
|||
{ |
|||
_content = value; |
|||
if (_view is not null) |
|||
{ |
|||
_view.Content = _content; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
event EventHandler<ActivatedEventArgs>? IAvaloniaActivity.Activated |
|||
{ |
|||
add { _onActivated += value; } |
|||
remove { _onActivated -= value; } |
|||
} |
|||
|
|||
event EventHandler<ActivatedEventArgs>? IAvaloniaActivity.Deactivated |
|||
{ |
|||
add { _onDeactivated += value; } |
|||
remove { _onDeactivated -= value; } |
|||
} |
|||
|
|||
[ObsoletedOSPlatform("android33.0")] |
|||
public override void OnBackPressed() |
|||
{ |
|||
var eventArgs = new AndroidBackRequestedEventArgs(); |
|||
|
|||
BackRequested?.Invoke(this, eventArgs); |
|||
|
|||
if (!eventArgs.Handled) |
|||
{ |
|||
base.OnBackPressed(); |
|||
} |
|||
} |
|||
|
|||
protected override void OnCreate(Bundle? savedInstanceState) |
|||
{ |
|||
InitializeAvaloniaView(_content); |
|||
|
|||
base.OnCreate(savedInstanceState); |
|||
|
|||
SetContentView(_view); |
|||
|
|||
_listener = new GlobalLayoutListener(_view); |
|||
|
|||
_view.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener); |
|||
|
|||
if (Intent?.Data is {} androidUri |
|||
&& androidUri.IsAbsolute |
|||
&& Uri.TryCreate(androidUri.ToString(), UriKind.Absolute, out var protocolUri)) |
|||
{ |
|||
_onActivated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, protocolUri)); |
|||
} |
|||
} |
|||
|
|||
protected override void OnStop() |
|||
{ |
|||
_onDeactivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); |
|||
base.OnStop(); |
|||
} |
|||
|
|||
protected override void OnStart() |
|||
{ |
|||
_onActivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); |
|||
base.OnStart(); |
|||
} |
|||
|
|||
protected override void OnResume() |
|||
{ |
|||
base.OnResume(); |
|||
|
|||
// Android only respects LayoutInDisplayCutoutMode value if it has been set once before window becomes visible.
|
|||
if (OperatingSystem.IsAndroidVersionAtLeast(28) && Window is { Attributes: { } attributes }) |
|||
{ |
|||
attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges; |
|||
} |
|||
} |
|||
|
|||
protected override void OnDestroy() |
|||
{ |
|||
if (_view is not null) |
|||
{ |
|||
_view.Content = null; |
|||
_view.ViewTreeObserver?.RemoveOnGlobalLayoutListener(_listener); |
|||
_view.Dispose(); |
|||
_view = null; |
|||
} |
|||
|
|||
base.OnDestroy(); |
|||
} |
|||
|
|||
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent? data) |
|||
{ |
|||
base.OnActivityResult(requestCode, resultCode, data); |
|||
|
|||
ActivityResult?.Invoke(requestCode, resultCode, data); |
|||
} |
|||
|
|||
[SupportedOSPlatform("android23.0")] |
|||
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) |
|||
{ |
|||
base.OnRequestPermissionsResult(requestCode, permissions, grantResults); |
|||
|
|||
RequestPermissionsResult?.Invoke(requestCode, permissions, grantResults); |
|||
} |
|||
|
|||
[MemberNotNull(nameof(_view))] |
|||
private protected virtual void InitializeAvaloniaView(object? initialContent) |
|||
{ |
|||
if (Avalonia.Application.Current is null) |
|||
{ |
|||
throw new InvalidOperationException( |
|||
"Avalonia Application was not initialized. Make sure you have created AvaloniaMainActivity."); |
|||
} |
|||
|
|||
_view = new AvaloniaView(this) { Content = initialContent }; |
|||
} |
|||
|
|||
private class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener |
|||
{ |
|||
private readonly AvaloniaView _view; |
|||
|
|||
public GlobalLayoutListener(AvaloniaView view) |
|||
{ |
|||
_view = view; |
|||
} |
|||
|
|||
public void OnGlobalLayout() |
|||
{ |
|||
_view.TopLevelImpl?.Resize(_view.TopLevelImpl.ClientSize); |
|||
} |
|||
} |
|||
} |
|||
@ -1,66 +1,15 @@ |
|||
#nullable enable |
|||
|
|||
using Android.OS; |
|||
using Android.Views; |
|||
using Avalonia.Android.Platform; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Android |
|||
{ |
|||
partial class AvaloniaMainActivity<TApp> where TApp : Application, new() |
|||
{ |
|||
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseAndroid(); |
|||
|
|||
private static AppBuilder? s_appBuilder; |
|||
internal static object? ViewContent; |
|||
|
|||
public object? Content |
|||
{ |
|||
get |
|||
{ |
|||
return ViewContent; |
|||
} |
|||
set |
|||
{ |
|||
ViewContent = value; |
|||
if (View != null) |
|||
View.Content = value; |
|||
} |
|||
} |
|||
|
|||
protected AppBuilder CreateAppBuilder() |
|||
{ |
|||
var builder = AppBuilder.Configure<TApp>(); |
|||
|
|||
return CustomizeAppBuilder(builder); |
|||
} |
|||
|
|||
private void InitializeApp() |
|||
{ |
|||
if (s_appBuilder == null) |
|||
{ |
|||
var builder = CreateAppBuilder(); |
|||
namespace Avalonia.Android; |
|||
|
|||
builder.SetupWithLifetime(new SingleViewLifetime()); |
|||
|
|||
s_appBuilder = builder; |
|||
} |
|||
|
|||
if (Avalonia.Application.Current?.TryGetFeature<IActivatableLifetime>() |
|||
is AndroidActivatableLifetime activatableLifetime) |
|||
{ |
|||
activatableLifetime.Activity = this; |
|||
} |
|||
|
|||
View = new AvaloniaView(this); |
|||
if (ViewContent != null) |
|||
{ |
|||
View.Content = ViewContent; |
|||
} |
|||
|
|||
if (Avalonia.Application.Current?.ApplicationLifetime is SingleViewLifetime lifetime) |
|||
{ |
|||
lifetime.View = View; |
|||
} |
|||
} |
|||
} |
|||
public class AvaloniaMainActivity<TApp> : AvaloniaMainActivity |
|||
where TApp : Application, new() |
|||
{ |
|||
protected override AppBuilder CreateAppBuilder() => AppBuilder.Configure<TApp>(); |
|||
} |
|||
|
|||
@ -1,141 +1,63 @@ |
|||
#nullable enable |
|||
|
|||
using System; |
|||
using System.Runtime.Versioning; |
|||
using Android.App; |
|||
using Android.Content; |
|||
using Android.Content.PM; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Android.OS; |
|||
using Android.Runtime; |
|||
using Android.Views; |
|||
using AndroidX.AppCompat.App; |
|||
using Avalonia.Android.Platform; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Android |
|||
{ |
|||
public class AvaloniaMainActivity : AppCompatActivity, IAvaloniaActivity |
|||
{ |
|||
private EventHandler<ActivatedEventArgs> _onActivated, _onDeactivated; |
|||
|
|||
public Action<int, Result, Intent> ActivityResult { get; set; } |
|||
public Action<int, string[], Permission[]> RequestPermissionsResult { get; set; } |
|||
namespace Avalonia.Android; |
|||
|
|||
public event EventHandler<AndroidBackRequestedEventArgs> BackRequested; |
|||
event EventHandler<ActivatedEventArgs> IAvaloniaActivity.Activated |
|||
{ |
|||
add { _onActivated += value; } |
|||
remove { _onActivated -= value; } |
|||
} |
|||
|
|||
event EventHandler<ActivatedEventArgs> IAvaloniaActivity.Deactivated |
|||
{ |
|||
add { _onDeactivated += value; } |
|||
remove { _onDeactivated -= value; } |
|||
} |
|||
|
|||
[ObsoletedOSPlatform("android33.0")] |
|||
public override void OnBackPressed() |
|||
{ |
|||
var eventArgs = new AndroidBackRequestedEventArgs(); |
|||
|
|||
BackRequested?.Invoke(this, eventArgs); |
|||
|
|||
if (!eventArgs.Handled) |
|||
{ |
|||
base.OnBackPressed(); |
|||
} |
|||
} |
|||
|
|||
protected override void OnCreate(Bundle savedInstanceState) |
|||
{ |
|||
base.OnCreate(savedInstanceState); |
|||
|
|||
if (Intent?.Data is {} androidUri |
|||
&& androidUri.IsAbsolute |
|||
&& Uri.TryCreate(androidUri.ToString(), UriKind.Absolute, out var protocolUri)) |
|||
{ |
|||
_onActivated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, protocolUri)); |
|||
} |
|||
} |
|||
|
|||
protected override void OnStop() |
|||
{ |
|||
_onDeactivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); |
|||
base.OnStop(); |
|||
} |
|||
|
|||
protected override void OnStart() |
|||
{ |
|||
_onActivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); |
|||
base.OnStart(); |
|||
} |
|||
public class AvaloniaMainActivity : AvaloniaActivity |
|||
{ |
|||
private protected static SingleViewLifetime? Lifetime; |
|||
|
|||
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data) |
|||
public override void OnCreate(Bundle? savedInstanceState, PersistableBundle? persistentState) |
|||
{ |
|||
// Global IActivatableLifetime expects a main activity, so we need to replace it on each OnCreate.
|
|||
if (Avalonia.Application.Current?.TryGetFeature<IActivatableLifetime>() |
|||
is AndroidActivatableLifetime activatableLifetime) |
|||
{ |
|||
base.OnActivityResult(requestCode, resultCode, data); |
|||
|
|||
ActivityResult?.Invoke(requestCode, resultCode, data); |
|||
activatableLifetime.Activity = this; |
|||
} |
|||
|
|||
[SupportedOSPlatform("android23.0")] |
|||
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) |
|||
{ |
|||
base.OnRequestPermissionsResult(requestCode, permissions, grantResults); |
|||
|
|||
RequestPermissionsResult?.Invoke(requestCode, permissions, grantResults); |
|||
} |
|||
base.OnCreate(savedInstanceState, persistentState); |
|||
} |
|||
|
|||
public abstract partial class AvaloniaMainActivity<TApp> : AvaloniaMainActivity where TApp : Application, new() |
|||
private protected override void InitializeAvaloniaView(object? initialContent) |
|||
{ |
|||
internal AvaloniaView View { get; set; } |
|||
|
|||
private GlobalLayoutListener _listener; |
|||
|
|||
protected override void OnCreate(Bundle savedInstanceState) |
|||
{ |
|||
InitializeApp(); |
|||
|
|||
base.OnCreate(savedInstanceState); |
|||
|
|||
SetContentView(View); |
|||
|
|||
_listener = new GlobalLayoutListener(View); |
|||
|
|||
View.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener); |
|||
} |
|||
|
|||
protected override void OnResume() |
|||
{ |
|||
base.OnResume(); |
|||
|
|||
// Android only respects LayoutInDisplayCutoutMode value if it has been set once before window becomes visible.
|
|||
if (OperatingSystem.IsAndroidVersionAtLeast(28) && Window is { Attributes: { } attributes }) |
|||
{ |
|||
attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges; |
|||
} |
|||
} |
|||
|
|||
protected override void OnDestroy() |
|||
{ |
|||
View.Content = null; |
|||
|
|||
View.ViewTreeObserver?.RemoveOnGlobalLayoutListener(_listener); |
|||
|
|||
base.OnDestroy(); |
|||
} |
|||
|
|||
private class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener |
|||
{ |
|||
private readonly AvaloniaView _view; |
|||
|
|||
public GlobalLayoutListener(AvaloniaView view) |
|||
{ |
|||
_view = view; |
|||
} |
|||
|
|||
public void OnGlobalLayout() |
|||
{ |
|||
_view.TopLevelImpl?.Resize(_view.TopLevelImpl.ClientSize); |
|||
} |
|||
// Android can run OnCreate + InitializeAvaloniaView multiple times per process lifetime.
|
|||
// On each call we need to create new AvaloniaView, but we can't recreate Avalonia nor Avalonia controls.
|
|||
// So, if lifetime was already created previously - recreate AvaloniaView.
|
|||
// If not, initialize Avalonia, and create AvaloniaView inside of AfterSetup callback.
|
|||
// We need this AfterSetup callback to match iOS/Browser behavior and ensure that view/toplevel is available in custom AfterSetup calls.
|
|||
if (Lifetime is not null) |
|||
{ |
|||
Lifetime.Activity = this; |
|||
_view = new AvaloniaView(this) { Content = initialContent }; |
|||
} |
|||
else |
|||
{ |
|||
var builder = CreateAppBuilder(); |
|||
builder = CustomizeAppBuilder(builder); |
|||
|
|||
Lifetime = new SingleViewLifetime(); |
|||
Lifetime.Activity = this; |
|||
|
|||
builder |
|||
.AfterApplicationSetup(_ => |
|||
{ |
|||
_view = new AvaloniaView(this) { Content = initialContent }; |
|||
}) |
|||
.SetupWithLifetime(Lifetime); |
|||
|
|||
// AfterPlatformServicesSetup should always be called. If it wasn't, we have an unusual problem.
|
|||
if (_view is null) |
|||
throw new InvalidOperationException("Unknown error: AvaloniaView initialization has failed."); |
|||
} |
|||
} |
|||
|
|||
protected virtual AppBuilder CreateAppBuilder() => AppBuilder.Configure<Application>().UseAndroid(); |
|||
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder; |
|||
} |
|||
|
|||
@ -1,10 +1,12 @@ |
|||
using System; |
|||
#nullable enable |
|||
using System; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
|
|||
namespace Avalonia.Android; |
|||
|
|||
public interface IAvaloniaActivity : IActivityResultHandler, IActivityNavigationService |
|||
{ |
|||
event EventHandler<ActivatedEventArgs> Activated; |
|||
event EventHandler<ActivatedEventArgs> Deactivated; |
|||
object? Content { get; set; } |
|||
event EventHandler<ActivatedEventArgs>? Activated; |
|||
event EventHandler<ActivatedEventArgs>? Deactivated; |
|||
} |
|||
|
|||
@ -1,26 +1,47 @@ |
|||
using Avalonia.Controls; |
|||
#nullable enable |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
|
|||
namespace Avalonia.Android |
|||
namespace Avalonia.Android; |
|||
|
|||
internal class SingleViewLifetime : ISingleViewApplicationLifetime, ISingleTopLevelApplicationLifetime |
|||
{ |
|||
internal class SingleViewLifetime : ISingleViewApplicationLifetime |
|||
{ |
|||
private AvaloniaView _view; |
|||
private Control? _mainView; |
|||
private AvaloniaMainActivity? _activity; |
|||
|
|||
public AvaloniaView View |
|||
/// <summary>
|
|||
/// Since Main Activity can be swapped, we should adjust litetime as well.
|
|||
/// </summary>
|
|||
public AvaloniaMainActivity Activity |
|||
{ |
|||
[return: MaybeNull] get => _activity!; |
|||
internal set |
|||
{ |
|||
if (_activity != null) |
|||
{ |
|||
_activity.Content = null; |
|||
} |
|||
_activity = value; |
|||
_activity.Content = _mainView; |
|||
} |
|||
} |
|||
|
|||
public Control? MainView |
|||
{ |
|||
get => _mainView; |
|||
set |
|||
{ |
|||
get => _view; internal set |
|||
if (_mainView != value) |
|||
{ |
|||
if (_view != null) |
|||
_mainView = value; |
|||
if (_activity != null) |
|||
{ |
|||
_view.Content = null; |
|||
_view.Dispose(); |
|||
_activity.Content = _mainView; |
|||
} |
|||
_view = value; |
|||
_view.Content = MainView; |
|||
} |
|||
} |
|||
|
|||
public Control MainView { get; set; } |
|||
} |
|||
|
|||
public TopLevel? TopLevel => _activity?._view?.TopLevel; |
|||
} |
|||
|
|||
@ -0,0 +1,15 @@ |
|||
using System.ComponentModel; |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Controls.ApplicationLifetimes; |
|||
|
|||
/// <summary>
|
|||
/// Used in our internal projects. Until we figure out way to add this information to the public API.
|
|||
/// </summary>
|
|||
[NotClientImplementable] |
|||
[PrivateApi] |
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public interface ISingleTopLevelApplicationLifetime : IApplicationLifetime |
|||
{ |
|||
TopLevel? TopLevel { get; } |
|||
} |
|||
@ -1,16 +1,45 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
|
|||
namespace Avalonia.iOS; |
|||
|
|||
internal class SingleViewLifetime : ISingleViewApplicationLifetime |
|||
{ |
|||
public AvaloniaView? View; |
|||
internal class SingleViewLifetime : ISingleViewApplicationLifetime, ISingleTopLevelApplicationLifetime |
|||
{ |
|||
private Control? _mainView; |
|||
private AvaloniaView? _view; |
|||
|
|||
public AvaloniaView View |
|||
{ |
|||
[return: MaybeNull] get => _view!; |
|||
internal set |
|||
{ |
|||
if (_view != null) |
|||
{ |
|||
_view.Content = null; |
|||
_view.Dispose(); |
|||
} |
|||
_view = value; |
|||
_view.Content = _mainView; |
|||
} |
|||
} |
|||
|
|||
public Control? MainView |
|||
{ |
|||
get => View!.Content; |
|||
set => View!.Content = value; |
|||
get => _mainView; |
|||
set |
|||
{ |
|||
if (_mainView != value) |
|||
{ |
|||
_mainView = value; |
|||
if (_view != null) |
|||
{ |
|||
_view.Content = _mainView; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public TopLevel? TopLevel => View?.TopLevel; |
|||
} |
|||
|
|||
Loading…
Reference in new issue