From f33dca36a0e29c6f878215d20f04fb881bac3d39 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 13 Jun 2025 19:14:11 -0700 Subject: [PATCH 1/7] Use NDK instead of C#-JNI for choreographer --- .../Avalonia.Android/ChoreographerTimer.cs | 71 +++++++++++++------ .../SkiaPlatform/AndroidFramebuffer.cs | 18 ++++- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs index 06ec9a9cd6..edc82a6aac 100644 --- a/src/Android/Avalonia.Android/ChoreographerTimer.cs +++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs @@ -1,39 +1,42 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; - -using Android.OS; using Android.Views; - using Avalonia.Reactive; using Avalonia.Rendering; -using ThreadPriority = System.Threading.ThreadPriority; +using static Avalonia.Android.Platform.SkiaPlatform.AndroidFramebuffer; + +using Looper = Android.OS.Looper; namespace Avalonia.Android { - internal sealed class ChoreographerTimer : Java.Lang.Object, IRenderTimer, Choreographer.IFrameCallback + internal sealed class ChoreographerTimer : IRenderTimer { + private static readonly bool s_supports64Callback = OperatingSystem.IsAndroidVersionAtLeast(10); private readonly object _lock = new(); - - private AutoResetEvent _event = new(false); - private long _lastTime; - private readonly TaskCompletionSource _choreographer = new(); - - private readonly ISet _views = new HashSet(); + private readonly TaskCompletionSource _choreographer = new(); + private readonly AutoResetEvent _event = new(false); + private readonly GCHandle _timerHandle; + private readonly HashSet _views = new(); private Action? _tick; + private long _lastTime; private int _count; public ChoreographerTimer() { + _timerHandle = GCHandle.Alloc(this); new Thread(Loop) { - Priority = ThreadPriority.AboveNormal + Priority = ThreadPriority.AboveNormal, + Name = "Choreographer Thread" }.Start(); new Thread(RenderLoop) { - Priority = ThreadPriority.AboveNormal + Priority = ThreadPriority.AboveNormal, + Name = "Render Thread" }.Start(); } @@ -50,7 +53,7 @@ namespace Avalonia.Android if (_count == 1) { - _choreographer.Task.Result.PostFrameCallback(this); + PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle)); } } } @@ -72,7 +75,7 @@ namespace Avalonia.Android if (_views.Count == 1) { - _choreographer.Task.Result.PostFrameCallback(this); + PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle)); } } @@ -90,10 +93,10 @@ namespace Avalonia.Android private void Loop() { Looper.Prepare(); - _choreographer.SetResult(Choreographer.Instance!); + _choreographer.SetResult(AChoreographer_getInstance()); Looper.Loop(); } - + private void RenderLoop() { while (true) @@ -108,18 +111,46 @@ namespace Avalonia.Android } } - - public void DoFrame(long frameTimeNanos) + private void DoFrameCallback(long frameTimeNanos, IntPtr data) { lock (_lock) { if (_count > 0 && _views.Count > 0) { - Choreographer.Instance!.PostFrameCallback(this); + PostFrameCallback(_choreographer.Task.Result, data); } _lastTime = frameTimeNanos; _event.Set(); } } + + private static unsafe void PostFrameCallback(IntPtr choreographer, IntPtr data) + { + // AChoreographer_postFrameCallback is deprecated on 10.0+. + if (s_supports64Callback) + { + AChoreographer_postFrameCallback64(choreographer, &FrameCallback64, data); + } + else + { + AChoreographer_postFrameCallback(choreographer, &FrameCallback, data); + } + + return; + + [UnmanagedCallersOnly] + static void FrameCallback(int frameTimeNanos, IntPtr data) + { + var timer = (ChoreographerTimer)GCHandle.FromIntPtr(data).Target!; + timer.DoFrameCallback(frameTimeNanos, data); + } + + [UnmanagedCallersOnly] + static void FrameCallback64(long frameTimeNanos, IntPtr data) + { + var timer = (ChoreographerTimer)GCHandle.FromIntPtr(data).Target!; + timer.DoFrameCallback(frameTimeNanos, data); + } + } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs index 7db13704ab..ef224d6c37 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using Android.Runtime; using Android.Views; using Avalonia.Platform; @@ -60,6 +61,19 @@ namespace Avalonia.Android.Platform.SkiaPlatform [DllImport("android")] internal static extern void ANativeWindow_unlockAndPost(IntPtr window); + [DllImport("android")] + internal static extern IntPtr AChoreographer_getInstance(); + + [DllImport("android")] + [UnsupportedOSPlatform("android10.0")] + internal static extern void AChoreographer_postFrameCallback( + IntPtr choreographer, delegate* unmanaged callback, IntPtr data); + + [DllImport("android")] + [SupportedOSPlatform("android10.0")] + internal static extern void AChoreographer_postFrameCallback64( + IntPtr choreographer, delegate* unmanaged callback, IntPtr data); + [DllImport("android")] internal static extern int ANativeWindow_lock(IntPtr window, ANativeWindow_Buffer* outBuffer, ARect* inOutDirtyBounds); public enum AndroidPixelFormat @@ -69,6 +83,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform WINDOW_FORMAT_RGB_565 = 4, } + [StructLayout(LayoutKind.Sequential)] internal struct ARect { public int left; @@ -76,7 +91,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform public int right; public int bottom; } - + + [StructLayout(LayoutKind.Sequential)] internal struct ANativeWindow_Buffer { // The number of pixels that are shown horizontally. From 5525e99335559dc853394bfb95f15b95f31465d2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 4 Jun 2025 00:54:50 -0700 Subject: [PATCH 2/7] Mark IAndroidView as obsolete, it was replaced long time ago --- src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs index c72de8e197..056912835a 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs @@ -1,9 +1,11 @@ +using System; using Android.Views; namespace Avalonia.Android.Platform.Specific { public interface IAndroidView { + [Obsolete("Use TopLevel.TryGetPlatformHandle instead, which can be casted to AndroidViewControlHandle.")] View View { get; } } } From b9531fb5b2a7e7b7216120539bcdbad3fd3e864f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 13 Jun 2025 19:18:47 -0700 Subject: [PATCH 3/7] Minor cleanup in TopLevelImpl, remove unused methods --- .../Avalonia.Android/ChoreographerTimer.cs | 1 - .../Platform/SkiaPlatform/TopLevelImpl.cs | 35 ++++--------------- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs index edc82a6aac..ce10f951c3 100644 --- a/src/Android/Avalonia.Android/ChoreographerTimer.cs +++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Android.Views; using Avalonia.Reactive; using Avalonia.Rendering; using static Avalonia.Android.Platform.SkiaPlatform.AndroidFramebuffer; diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 26e1fe9ea0..ffa55f6612 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -32,9 +32,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform { class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfoWithWaitPolicy { - private readonly IGlPlatformSurface _gl; - private readonly IFramebufferPlatformSurface _framebuffer; - private readonly AndroidKeyboardEventsHelper _keyboardHelper; private readonly AndroidMotionEventsHelper _pointerHelper; private readonly AndroidInputMethod _textInputMethod; @@ -59,13 +56,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform _textInputMethod = new AndroidInputMethod(_view); _keyboardHelper = new AndroidKeyboardEventsHelper(this); _pointerHelper = new AndroidMotionEventsHelper(this); - _gl = new EglGlPlatformSurface(this); - _framebuffer = new FramebufferManager(this); _clipboard = new ClipboardImpl(avaloniaView.Context.GetSystemService(Context.ClipboardService).JavaCast()); _screens = new AndroidScreens(avaloniaView.Context); - RenderScaling = _view.Scaling; - if (avaloniaView.Context is Activity mainActivity) { _insetsManager = new AndroidInsetsManager(mainActivity, this); @@ -78,15 +71,16 @@ namespace Avalonia.Android.Platform.SkiaPlatform _systemNavigationManager = new AndroidSystemNavigationManagerImpl(avaloniaView.Context as IActivityNavigationService); - Surfaces = new object[] { _gl, _framebuffer, _view }; + var gl = new EglGlPlatformSurface(this); + var framebuffer = new FramebufferManager(this); + Surfaces = [gl, framebuffer, _view]; Handle = new AndroidViewControlHandle(_view); } public IInputRoot? InputRoot { get; private set; } - public virtual Size ClientSize => _view.Size.ToSize(RenderScaling); - - public Size? FrameSize => null; + public Size ClientSize => _view.Size.ToSize(RenderScaling); + public double RenderScaling => _view.Scaling; public Action? Closed { get; set; } @@ -110,16 +104,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform public Compositor Compositor => AndroidPlatform.Compositor ?? throw new InvalidOperationException("Android backend wasn't initialized. Make sure .UseAndroid() was executed."); - public virtual void Hide() - { - _view.Visibility = ViewStates.Invisible; - } - - public void Invalidate(Rect rect) - { - if (_view.Holder?.Surface?.IsValid == true) _view.Invalidate(); - } - public Point PointToClient(PixelPoint point) { return point.ToPoint(RenderScaling); @@ -140,13 +124,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform InputRoot = inputRoot; } - public virtual void Show() - { - _view.Visibility = ViewStates.Visible; - } - - public double RenderScaling { get; } - void Draw() { Paint?.Invoke(new Rect(new Point(0, 0), ClientSize)); @@ -159,7 +136,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _view = null!; } - protected virtual void OnResized(Size size) + protected void OnResized(Size size) { Resized?.Invoke(size, WindowResizeReason.Unspecified); } From 78b47a61ae5abeedd1bbe72dbe4b43db382e3d4f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 13 Jun 2025 19:46:49 -0700 Subject: [PATCH 4/7] Remove ancient renderer support with Invalidate/Paint calls, drastically simplify InvalidationAwareSurfaceView --- .../InvalidationAwareSurfaceView.cs | 98 ++++++++----------- .../Platform/SkiaPlatform/TopLevelImpl.cs | 39 +++----- 2 files changed, 58 insertions(+), 79 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 91b03a2aa5..fcfa1a6ffc 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -1,30 +1,28 @@ using System; using Android.Content; using Android.Graphics; -using Android.OS; using Android.Runtime; -using Android.Util; using Android.Views; using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Logging; using Avalonia.Platform; namespace Avalonia.Android { internal abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, INativePlatformHandleSurface { - bool _invalidateQueued; - private bool _isDisposed; - private bool _isSurfaceValid; - readonly object _lock = new object(); - private readonly Handler _handler; + private IntPtr _nativeWindowHandle = IntPtr.Zero; + private PixelSize _size = new(1, 1); + private double _scaling = 1; - internal event EventHandler? SurfaceWindowCreated; + public event EventHandler? SurfaceWindowCreated; + public PixelSize Size => _size; + public double Scaling => _scaling; - IntPtr IPlatformHandle.Handle => _isSurfaceValid && Holder?.Surface?.Handle is { } handle ? - AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, handle) : - default; + IntPtr IPlatformHandle.Handle => _nativeWindowHandle; + string IPlatformHandle.HandleDescriptor => "SurfaceView"; - public InvalidationAwareSurfaceView(Context context) : base(context) + protected InvalidationAwareSurfaceView(Context context) : base(context) { if (Holder is null) throw new InvalidOperationException( @@ -32,71 +30,59 @@ namespace Avalonia.Android Holder.AddCallback(this); Holder.SetFormat(global::Android.Graphics.Format.Transparent); - _handler = new Handler(context.MainLooper!); } - public override void Invalidate() + protected override void Dispose(bool disposing) { - lock (_lock) - { - if (_invalidateQueued) - return; - _handler.Post(() => - { - if (_isDisposed || Holder?.Surface?.IsValid != true) - return; - try - { - DoDraw(); - } - catch (Exception e) - { - Log.WriteLine(LogPriority.Error, "Avalonia", e.ToString()); - } - }); - } - } - - internal new void Dispose() - { - _isDisposed = true; + ReleaseNativeWindowHandle(); + base.Dispose(disposing); } - public void SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) + public virtual void SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) { - _isSurfaceValid = true; - Log.Info("AVALONIA", "Surface Changed"); - DoDraw(); + CacheSurfaceProperties(holder); + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "InvalidationAwareSurfaceView Changed"); } public void SurfaceCreated(ISurfaceHolder holder) { - _isSurfaceValid = true; - Log.Info("AVALONIA", "Surface Created"); + CacheSurfaceProperties(holder); + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "InvalidationAwareSurfaceView Created"); SurfaceWindowCreated?.Invoke(this, EventArgs.Empty); - DoDraw(); } public void SurfaceDestroyed(ISurfaceHolder holder) { - _isSurfaceValid = false; - Log.Info("AVALONIA", "Surface Destroyed"); + ReleaseNativeWindowHandle(); + _size = new PixelSize(1, 1); + _scaling = 1; + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "InvalidationAwareSurfaceView Destroyed"); + } + + private void CacheSurfaceProperties(ISurfaceHolder holder) + { + ReleaseNativeWindowHandle(); + var surface = holder?.Surface; + if (surface?.Handle is { } handle) + _nativeWindowHandle = AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, handle); + else + _nativeWindowHandle = IntPtr.Zero; + var frame = holder?.SurfaceFrame; + _size = frame != null ? new PixelSize(frame.Width(), frame.Height()) : new PixelSize(1, 1); + _scaling = Resources?.DisplayMetrics?.Density ?? 1; } - protected void DoDraw() + private void ReleaseNativeWindowHandle() { - lock (_lock) + if (_nativeWindowHandle != IntPtr.Zero) { - _invalidateQueued = false; + AndroidFramebuffer.ANativeWindow_release(_nativeWindowHandle); + _nativeWindowHandle = IntPtr.Zero; } - Draw(); } - protected abstract void Draw(); - public string HandleDescriptor => "SurfaceView"; - - public PixelSize Size => new(Holder?.SurfaceFrame?.Width() ?? 1, Holder?.SurfaceFrame?.Height() ?? 1); - - public double Scaling => Resources?.DisplayMetrics?.Density ?? 1; } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index ffa55f6612..3f8d92b047 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -124,11 +124,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform InputRoot = inputRoot; } - void Draw() - { - Paint?.Invoke(new Rect(new Point(0, 0), ClientSize)); - } - public virtual void Dispose() { _systemNavigationManager.Dispose(); @@ -146,10 +141,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform Resized?.Invoke(size, WindowResizeReason.Layout); } - class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo + sealed class ViewImpl : InvalidationAwareSurfaceView, IInitEditorInfo { private readonly TopLevelImpl _tl; private Size _oldSize; + private double _oldScaling; public ViewImpl(Context context, TopLevelImpl tl, bool placeOnTop) : base(context) { @@ -158,13 +154,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform SetZOrderOnTop(true); } - public TopLevelImpl TopLevelImpl => _tl; - - protected override void Draw() - { - _tl.Draw(); - } - protected override void DispatchDraw(global::Android.Graphics.Canvas canvas) { // Workaround issue #9230 on where screen remains gray after splash screen. @@ -211,17 +200,24 @@ namespace Avalonia.Android.Platform.SkiaPlatform return res ?? baseResult; } - void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) + public override void SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) { - var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling); + base.SurfaceChanged(holder, format, width, height); + + var newSize = Size.ToSize(Scaling); + var newScaling = Scaling; if (newSize != _oldSize) { _oldSize = newSize; _tl.OnResized(newSize); } - - base.SurfaceChanged(holder, format, width, height); + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (newScaling != _oldScaling) + { + _oldScaling = newScaling; + _tl.ScalingChanged?.Invoke(newScaling); + } } public sealed override bool OnCheckIsTextEditor() @@ -236,11 +232,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform _initEditorInfo = init; } - public sealed override IInputConnection OnCreateInputConnection(EditorInfo? outAttrs) + public override IInputConnection OnCreateInputConnection(EditorInfo? outAttrs) { return _initEditorInfo?.Invoke(_tl, outAttrs!)!; } - } public IPopupImpl? CreatePopup() => null; @@ -280,10 +275,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle; bool EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfoWithWaitPolicy.SkipWaits => true; - - public PixelSize Size => _view.Size; - - public double Scaling => RenderScaling; + PixelSize EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Size => _view.Size; + double EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Scaling => _view.Scaling; public void SetTransparencyLevelHint(IReadOnlyList transparencyLevels) { From 8826fc7382431c993ecd4c1bfbdfc4f768959ae6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 13 Jun 2025 19:52:23 -0700 Subject: [PATCH 5/7] Some simple mitigation against of double release --- .../InvalidationAwareSurfaceView.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index fcfa1a6ffc..3bafc4328c 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using Android.Content; using Android.Graphics; using Android.Runtime; @@ -64,12 +65,18 @@ namespace Avalonia.Android private void CacheSurfaceProperties(ISurfaceHolder holder) { - ReleaseNativeWindowHandle(); var surface = holder?.Surface; + var newHandle = IntPtr.Zero; if (surface?.Handle is { } handle) - _nativeWindowHandle = AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, handle); - else - _nativeWindowHandle = IntPtr.Zero; + { + newHandle = AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, handle); + } + + if (Interlocked.Exchange(ref _nativeWindowHandle, newHandle) is var oldHandle + && oldHandle != IntPtr.Zero) + { + AndroidFramebuffer.ANativeWindow_release(oldHandle); + } var frame = holder?.SurfaceFrame; _size = frame != null ? new PixelSize(frame.Width(), frame.Height()) : new PixelSize(1, 1); @@ -78,10 +85,10 @@ namespace Avalonia.Android private void ReleaseNativeWindowHandle() { - if (_nativeWindowHandle != IntPtr.Zero) + if (Interlocked.Exchange(ref _nativeWindowHandle, IntPtr.Zero) is var oldHandle + && oldHandle != IntPtr.Zero) { - AndroidFramebuffer.ANativeWindow_release(_nativeWindowHandle); - _nativeWindowHandle = IntPtr.Zero; + AndroidFramebuffer.ANativeWindow_release(oldHandle); } } } From c449c097ccfe784e894ee150fce7af8b78984b27 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 13 Jun 2025 20:09:59 -0700 Subject: [PATCH 6/7] Implement ISurfaceHolderCallback2 and handle SurfaceRedrawNeeded --- .../Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs | 8 +++++++- .../Platform/SkiaPlatform/TopLevelImpl.cs | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 3bafc4328c..4b716a07e1 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -10,7 +10,7 @@ using Avalonia.Platform; namespace Avalonia.Android { - internal abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, INativePlatformHandleSurface + internal abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback2, INativePlatformHandleSurface { private IntPtr _nativeWindowHandle = IntPtr.Zero; private PixelSize _size = new(1, 1); @@ -63,6 +63,12 @@ namespace Avalonia.Android .Log(this, "InvalidationAwareSurfaceView Destroyed"); } + public virtual void SurfaceRedrawNeeded(ISurfaceHolder holder) + { + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "InvalidationAwareSurfaceView RedrawNeeded"); + } + private void CacheSurfaceProperties(ISurfaceHolder holder) { var surface = holder?.Surface; diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 3f8d92b047..b4ec5ba0bb 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -220,6 +220,14 @@ namespace Avalonia.Android.Platform.SkiaPlatform } } + public override void SurfaceRedrawNeeded(ISurfaceHolder holder) + { + // Compositor Renderer handles Paint event in-sync, which is perfect for sync SurfaceRedrawNeeded + // In the future we might to handle SurfaceRedrawNeededAsync as well. + _tl.Paint?.Invoke(new Rect(new Point(), Size.ToSize(Scaling))); + base.SurfaceRedrawNeeded(holder); + } + public sealed override bool OnCheckIsTextEditor() { return true; From 3a3dc4e8fe43b11e41b954c239a8c285e46854e3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 13 Jun 2025 20:29:04 -0700 Subject: [PATCH 7/7] Handle SurfaceRedrawNeededAsync as well --- .../SkiaPlatform/InvalidationAwareSurfaceView.cs | 7 +++++++ .../Platform/SkiaPlatform/TopLevelImpl.cs | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 4b716a07e1..f15322a6a7 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -7,6 +7,7 @@ using Android.Views; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Logging; using Avalonia.Platform; +using Java.Lang; namespace Avalonia.Android { @@ -69,6 +70,12 @@ namespace Avalonia.Android .Log(this, "InvalidationAwareSurfaceView RedrawNeeded"); } + public virtual void SurfaceRedrawNeededAsync(ISurfaceHolder holder, IRunnable drawingFinished) + { + Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)? + .Log(this, "InvalidationAwareSurfaceView RedrawNeededAsync"); + } + private void CacheSurfaceProperties(ISurfaceHolder holder) { var surface = holder?.Surface; diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index b4ec5ba0bb..00ae95abaf 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -223,12 +223,17 @@ namespace Avalonia.Android.Platform.SkiaPlatform public override void SurfaceRedrawNeeded(ISurfaceHolder holder) { // Compositor Renderer handles Paint event in-sync, which is perfect for sync SurfaceRedrawNeeded - // In the future we might to handle SurfaceRedrawNeededAsync as well. _tl.Paint?.Invoke(new Rect(new Point(), Size.ToSize(Scaling))); base.SurfaceRedrawNeeded(holder); } - public sealed override bool OnCheckIsTextEditor() + public override void SurfaceRedrawNeededAsync(ISurfaceHolder holder, IRunnable drawingFinished) + { + _tl.Compositor.RequestCompositionUpdate(drawingFinished.Run); + base.SurfaceRedrawNeededAsync(holder, drawingFinished); + } + + public override bool OnCheckIsTextEditor() { return true; }