Browse Source

Add android implementation

Max Katz 3 years ago
parent
commit
fbbd93f4cd

+ 27 - 0
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

+ 12 - 0
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<App>
+{
+}

+ 50 - 0
samples/interop/NativeEmbedSample.Android/NativeEmbedSample.Android.csproj

@@ -0,0 +1,50 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>net6.0-android</TargetFramework>
+    <SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
+    <OutputType>Exe</OutputType>
+    <Nullable>enable</Nullable>
+    <ApplicationId>com.Avalonia.NativeEmbedSample</ApplicationId>
+    <ApplicationVersion>1</ApplicationVersion>
+    <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
+    <AndroidPackageFormat>apk</AndroidPackageFormat>
+    <MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
+  </PropertyGroup>
+  <ItemGroup>
+    <None Remove="Assets\AboutAssets.txt" />
+  </ItemGroup>
+  <ItemGroup>
+    <AndroidResource Include="..\..\..\build\Assets\Icon.png">
+      <Link>Resources\drawable\Icon.png</Link>
+    </AndroidResource>
+  </ItemGroup>
+
+  <PropertyGroup Condition="'$(Configuration)'=='Release' and '$(TF_BUILD)' == ''">
+    <DebugSymbols>True</DebugSymbols>
+    <RunAOTCompilation>True</RunAOTCompilation>
+    <EnableLLVM>True</EnableLLVM>
+    <AndroidEnableProfiledAot>True</AndroidEnableProfiledAot>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)'=='Debug'">
+    <EmbedAssembliesIntoApk>False</EmbedAssembliesIntoApk>
+    <RunAOTCompilation>False</RunAOTCompilation>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)'=='Release'">
+    <EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />
+    <PackageReference Include="Xamarin.AndroidX.Lifecycle.ViewModel" Version="2.3.1.3" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" />
+    <ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
+    <ProjectReference Include="..\NativeEmbedSample\NativeEmbedSample.csproj" />
+  </ItemGroup>
+
+  <Import Project="..\..\..\build\BuildTargets.targets" />
+</Project>

+ 4 - 0
samples/interop/NativeEmbedSample.Android/Properties/AndroidManifest.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
+	<application android:label="ControlCatalog.Android" android:icon="@drawable/Icon"></application>
+</manifest>

+ 44 - 0
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.

+ 13 - 0
samples/interop/NativeEmbedSample.Android/Resources/drawable/splash_screen.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+  <item>
+    <color android:color="@color/splash_background"/>
+  </item>
+
+  <item android:drawable="@drawable/icon"
+        android:width="120dp"
+        android:height="120dp"
+        android:gravity="center" />
+
+</layer-list>

+ 4 - 0
samples/interop/NativeEmbedSample.Android/Resources/values/colors.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <color name="splash_background">#FFFFFF</color>
+</resources>

+ 17 - 0
samples/interop/NativeEmbedSample.Android/Resources/values/styles.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<resources>
+
+  <style name="MyTheme">
+  </style>
+
+  <style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
+    <item name="android:windowActionBar">false</item>
+    <item name="android:windowNoTitle">true</item>
+  </style>
+
+  <style name="MyTheme.Splash" parent ="MyTheme.NoActionBar">
+    <item name="android:windowBackground">@drawable/splash_screen</item>
+    <item name="android:windowContentOverlay">@null</item>
+  </style>
+
+</resources>

+ 16 - 0
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)));
+    }
+}

+ 17 - 3
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)

+ 4 - 3
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;

+ 187 - 0
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<IPlatformHandle, IPlatformHandle> 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<View>(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);
+            }
+        }
+    }
+}

+ 7 - 3
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<ViewImpl>(_view);
             _keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
             _touchHelper = new AndroidTouchEventsHelper<TopLevelImpl>(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();