diff --git a/Avalonia.sln b/Avalonia.sln index 0a33bc4150..4728d25bfa 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -219,6 +219,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeEmbedSample.Android", "samples\interop\NativeEmbedSample.Android\NativeEmbedSample.Android.csproj", "{7D287579-7DB4-4415-A52A-46A5CD6FE30F}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeEmbedSample.Desktop", "samples\interop\NativeEmbedSample.Desktop\NativeEmbedSample.Desktop.csproj", "{F2389463-DDB4-4317-B894-D4DF9FF6B763}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeEmbedSample.iOS", "samples\interop\NativeEmbedSample.iOS\NativeEmbedSample.iOS.csproj", "{28DB5AD1-656D-4619-BE0B-5B475E138DF8}" @@ -1993,6 +1995,30 @@ Global {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhone.Build.0 = Release|Any CPU {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.AppStore|iPhone.Build.0 = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Debug|iPhone.Build.0 = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Release|Any CPU.Build.0 = Release|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Release|iPhone.ActiveCfg = Release|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Release|iPhone.Build.0 = Release|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {7D287579-7DB4-4415-A52A-46A5CD6FE30F}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {F2389463-DDB4-4317-B894-D4DF9FF6B763}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {F2389463-DDB4-4317-B894-D4DF9FF6B763}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {F2389463-DDB4-4317-B894-D4DF9FF6B763}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU @@ -2100,6 +2126,7 @@ Global {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {CE910927-CE5A-456F-BC92-E4C757354A5C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} + {7D287579-7DB4-4415-A52A-46A5CD6FE30F} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {F2389463-DDB4-4317-B894-D4DF9FF6B763} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {28DB5AD1-656D-4619-BE0B-5B475E138DF8} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} EndGlobalSection diff --git a/samples/interop/NativeEmbedSample.Android/MainActivity.cs b/samples/interop/NativeEmbedSample.Android/MainActivity.cs new file mode 100644 index 0000000000..5d2e06a330 --- /dev/null +++ b/samples/interop/NativeEmbedSample.Android/MainActivity.cs @@ -0,0 +1,12 @@ +using Android.App; +using Android.Content.PM; + +using Avalonia; +using Avalonia.Android; + +namespace NativeEmbedSample.Android; + +[Activity(Label = "NativeEmbedSample", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] +public class MainActivity : AvaloniaActivity +{ +} diff --git a/samples/interop/NativeEmbedSample.Android/NativeEmbedSample.Android.csproj b/samples/interop/NativeEmbedSample.Android/NativeEmbedSample.Android.csproj new file mode 100644 index 0000000000..adfd74b9d5 --- /dev/null +++ b/samples/interop/NativeEmbedSample.Android/NativeEmbedSample.Android.csproj @@ -0,0 +1,50 @@ + + + net6.0-android + 21 + Exe + enable + com.Avalonia.NativeEmbedSample + 1 + 1.0 + apk + true + + + + + + + Resources\drawable\Icon.png + + + + + True + True + True + True + + + + False + False + + + + True + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/interop/NativeEmbedSample.Android/Properties/AndroidManifest.xml b/samples/interop/NativeEmbedSample.Android/Properties/AndroidManifest.xml new file mode 100644 index 0000000000..aa570ec504 --- /dev/null +++ b/samples/interop/NativeEmbedSample.Android/Properties/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/samples/interop/NativeEmbedSample.Android/Resources/AboutResources.txt b/samples/interop/NativeEmbedSample.Android/Resources/AboutResources.txt new file mode 100644 index 0000000000..c2bca974c4 --- /dev/null +++ b/samples/interop/NativeEmbedSample.Android/Resources/AboutResources.txt @@ -0,0 +1,44 @@ +Images, layout descriptions, binary blobs and string dictionaries can be included +in your application as resource files. Various Android APIs are designed to +operate on the resource IDs instead of dealing with images, strings or binary blobs +directly. + +For example, a sample Android app that contains a user interface layout (main.axml), +an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) +would keep its resources in the "Resources" directory of the application: + +Resources/ + drawable/ + icon.png + + layout/ + main.axml + + values/ + strings.xml + +In order to get the build system to recognize Android resources, set the build action to +"AndroidResource". The native Android APIs do not operate directly with filenames, but +instead operate on resource IDs. When you compile an Android application that uses resources, +the build system will package the resources for distribution and generate a class called "R" +(this is an Android convention) that contains the tokens for each one of the resources +included. For example, for the above Resources layout, this is what the R class would expose: + +public class R { + public class drawable { + public const int icon = 0x123; + } + + public class layout { + public const int main = 0x456; + } + + public class strings { + public const int first_string = 0xabc; + public const int second_string = 0xbcd; + } +} + +You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main +to reference the layout/main.axml file, or R.strings.first_string to reference the first +string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/samples/interop/NativeEmbedSample.Android/Resources/drawable/splash_screen.xml b/samples/interop/NativeEmbedSample.Android/Resources/drawable/splash_screen.xml new file mode 100644 index 0000000000..2e920b4b3b --- /dev/null +++ b/samples/interop/NativeEmbedSample.Android/Resources/drawable/splash_screen.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/samples/interop/NativeEmbedSample.Android/Resources/values/colors.xml b/samples/interop/NativeEmbedSample.Android/Resources/values/colors.xml new file mode 100644 index 0000000000..59279d5d32 --- /dev/null +++ b/samples/interop/NativeEmbedSample.Android/Resources/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/samples/interop/NativeEmbedSample.Android/Resources/values/styles.xml b/samples/interop/NativeEmbedSample.Android/Resources/values/styles.xml new file mode 100644 index 0000000000..2759d2904a --- /dev/null +++ b/samples/interop/NativeEmbedSample.Android/Resources/values/styles.xml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/samples/interop/NativeEmbedSample.Android/SplashActivity.cs b/samples/interop/NativeEmbedSample.Android/SplashActivity.cs new file mode 100644 index 0000000000..78c555f5c5 --- /dev/null +++ b/samples/interop/NativeEmbedSample.Android/SplashActivity.cs @@ -0,0 +1,16 @@ +using Android.App; +using Android.Content; +using Android.OS; + +namespace NativeEmbedSample.Android; + +[Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)] +public class SplashActivity : Activity +{ + protected override void OnResume() + { + base.OnResume(); + + StartActivity(new Intent(Application.Context, typeof(MainActivity))); + } +} diff --git a/samples/interop/NativeEmbedSample/Android/EmbedSample.Android.cs b/samples/interop/NativeEmbedSample/Android/EmbedSample.Android.cs index ed3b9aeeb0..4569992f56 100644 --- a/samples/interop/NativeEmbedSample/Android/EmbedSample.Android.cs +++ b/samples/interop/NativeEmbedSample/Android/EmbedSample.Android.cs @@ -3,7 +3,6 @@ using System; using System.IO; using System.Diagnostics; using Android.Views; -using Android.Webkit; using Avalonia.Controls.Platform; using Avalonia.Platform; @@ -13,9 +12,24 @@ public partial class EmbedSample { private IPlatformHandle CreateAndroid(IPlatformHandle parent) { - var button = new Android.Widget.Button(Android.App.Application.Context) { Text = "Android button" }; + if (IsSecond) + { + var webView = new Android.Webkit.WebView(Android.App.Application.Context); + webView.LoadUrl("https://www.android.com/"); - return new AndroidViewHandle(button); + return new AndroidViewHandle(webView); + } + else + { + var button = new Android.Widget.Button(Android.App.Application.Context) { Text = "Hello world" }; + var clickCount = 0; + button.Click += (sender, args) => + { + clickCount++; + button.Text = $"Click count {clickCount}"; + }; + return new AndroidViewHandle(button); + } } private void DestroyAndroid(IPlatformHandle control) diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 8177cf1f69..be0aa27393 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -19,9 +19,8 @@ namespace Avalonia.Android public AvaloniaView(Context context) : base(context) { - _view = new ViewImpl(context); + _view = new ViewImpl(this); AddView(_view.View); - } internal void Prepare () @@ -30,6 +29,8 @@ namespace Avalonia.Android _root.Prepare(); } + internal TopLevelImpl TopLevelImpl => _view; + public object Content { get { return _root.Content; } @@ -73,7 +74,7 @@ namespace Avalonia.Android class ViewImpl : TopLevelImpl { - public ViewImpl(Context context) : base(context) + public ViewImpl(AvaloniaView avaloniaView) : base(avaloniaView) { View.Focusable = true; View.FocusChange += ViewImpl_FocusChange; diff --git a/src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs b/src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs new file mode 100644 index 0000000000..57d897f6f5 --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/AndroidNativeControlHostImpl.cs @@ -0,0 +1,187 @@ +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; +using Android.Views; +using Android.Widget; + +using Avalonia.Controls.Platform; +using Avalonia.Platform; + +namespace Avalonia.Android.Platform +{ + internal class AndroidNativeControlHostImpl : INativeControlHostImpl + { + private readonly AvaloniaView _avaloniaView; + + public AndroidNativeControlHostImpl(AvaloniaView avaloniaView) + { + _avaloniaView = avaloniaView; + } + + public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) + { + return new AndroidViewControlHandle(new FrameLayout(_avaloniaView.Context!), false); + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create) + { + var holder = new AndroidViewControlHandle(_avaloniaView, false); + AndroidNativeControlAttachment? attachment = null; + try + { + var child = create(holder); + // It has to be assigned to the variable before property setter is called so we dispose it on exception +#pragma warning disable IDE0017 // Simplify object initialization + attachment = new AndroidNativeControlAttachment(child); +#pragma warning restore IDE0017 // Simplify object initialization + attachment.AttachedTo = this; + return attachment; + } + catch + { + attachment?.Dispose(); + holder?.Destroy(); + throw; + } + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) + { + return new AndroidNativeControlAttachment(handle) + { + AttachedTo = this + }; + } + + public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == AndroidViewControlHandle.AndroidDescriptor; + + class AndroidNativeControlAttachment : INativeControlHostControlTopLevelAttachment + { + // ReSharper disable once NotAccessedField.Local (keep GC reference) + private IPlatformHandle? _child; + private View? _view; + private AndroidNativeControlHostImpl? _attachedTo; + + public AndroidNativeControlAttachment(IPlatformHandle child) + { + _child = child; + + _view = (child as AndroidViewControlHandle)?.View + ?? Java.Lang.Object.GetObject(child.Handle, global::Android.Runtime.JniHandleOwnership.DoNotTransfer); + } + + [MemberNotNull(nameof(_view))] + private void CheckDisposed() + { + if (_view == null) + throw new ObjectDisposedException(nameof(AndroidNativeControlAttachment)); + } + + public void Dispose() + { + if (_view != null && _attachedTo?._avaloniaView is ViewGroup parent) + { + parent.RemoveView(_view); + } + _child = null; + _attachedTo = null; + _view?.Dispose(); + _view = null; + } + + public INativeControlHostImpl? AttachedTo + { + get => _attachedTo; + set + { + CheckDisposed(); + + var oldAttachedTo = _attachedTo; + _attachedTo = (AndroidNativeControlHostImpl?)value; + if (_attachedTo == null) + { + oldAttachedTo?._avaloniaView.RemoveView(_view); + } + else + { + _attachedTo._avaloniaView.AddView(_view); + } + } + } + + public bool IsCompatibleWith(INativeControlHostImpl host) => host is AndroidNativeControlHostImpl; + + public void HideWithSize(Size size) + { + CheckDisposed(); + _view.Visibility = ViewStates.Gone; + } + + public void ShowInBounds(Rect bounds) + { + CheckDisposed(); + if (_attachedTo == null) + throw new InvalidOperationException("The control isn't currently attached to a toplevel"); + + bounds *= _attachedTo._avaloniaView.TopLevelImpl.RenderScaling; + _view.Visibility = ViewStates.Visible; + _view.LayoutParameters = new FrameLayout.LayoutParams((int)bounds.Width, (int)bounds.Height) + { + LeftMargin = (int)bounds.X, + TopMargin = (int)bounds.Y + }; + _view.RequestLayout(); + } + } + } + + public class AndroidViewControlHandle : INativeControlHostDestroyableControlHandle, IDisposable + { + internal const string AndroidDescriptor = "JavaHandle"; + + private View? _view; + private bool _disposeView; + + public AndroidViewControlHandle(View view, bool disposeView) + { + _view = view; + _disposeView = disposeView; + } + + public View View => _view ?? throw new ObjectDisposedException(nameof(AndroidViewControlHandle)); + + public string HandleDescriptor => AndroidDescriptor; + + IntPtr IPlatformHandle.Handle => _view?.Handle ?? default; + + public void Destroy() + { + Dispose(true); + } + + void IDisposable.Dispose() + { + Dispose(true); + } + + ~AndroidViewControlHandle() + { + Dispose(false); + } + + private void Dispose(bool disposing) + { + if (_disposeView) + { + _view?.Dispose(); + } + + _view = null; + if (disposing) + { + GC.SuppressFinalize(this); + } + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 8a475676a5..dbcc9bccdd 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -20,7 +20,7 @@ using Avalonia.Rendering; namespace Avalonia.Android.Platform.SkiaPlatform { - class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod + class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost { private readonly IGlPlatformSurface _gl; private readonly IFramebufferPlatformSurface _framebuffer; @@ -30,9 +30,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform private readonly ITextInputMethodImpl _textInputMethod; private ViewImpl _view; - public TopLevelImpl(Context context, bool placeOnTop = false) + public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false) { - _view = new ViewImpl(context, this, placeOnTop); + _view = new ViewImpl(avaloniaView.Context, this, placeOnTop); _textInputMethod = new AndroidInputMethod(_view); _keyboardHelper = new AndroidKeyboardEventsHelper(this); _touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot, @@ -44,6 +44,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); + + NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView); } public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) => @@ -222,6 +224,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform public ITextInputMethodImpl TextInputMethod => _textInputMethod; + public INativeControlHostImpl NativeControlHost { get; } + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { throw new NotImplementedException();