Browse Source

Cleanup InvalidationAwareSurfaceView related code (#19045)

* Use NDK instead of C#-JNI for choreographer

* Mark IAndroidView as obsolete, it was replaced long time ago

* Minor cleanup in TopLevelImpl, remove unused methods

* Remove ancient renderer support with Invalidate/Paint calls, drastically simplify InvalidationAwareSurfaceView

* Some simple mitigation against of double release

* Implement ISurfaceHolderCallback2 and handle SurfaceRedrawNeeded

* Handle SurfaceRedrawNeededAsync as well
pull/19100/head
Max Katz 1 year ago
committed by GitHub
parent
commit
1a9463effd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 72
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  2. 18
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  3. 118
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  4. 87
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  5. 2
      src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs

72
src/Android/Avalonia.Android/ChoreographerTimer.cs

@ -1,39 +1,41 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Android.OS;
using Android.Views;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Rendering; using Avalonia.Rendering;
using ThreadPriority = System.Threading.ThreadPriority; using static Avalonia.Android.Platform.SkiaPlatform.AndroidFramebuffer;
using Looper = Android.OS.Looper;
namespace Avalonia.Android 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 readonly object _lock = new();
private readonly TaskCompletionSource<IntPtr> _choreographer = new();
private AutoResetEvent _event = new(false); private readonly AutoResetEvent _event = new(false);
private long _lastTime; private readonly GCHandle _timerHandle;
private readonly TaskCompletionSource<Choreographer> _choreographer = new(); private readonly HashSet<AvaloniaView> _views = new();
private readonly ISet<AvaloniaView> _views = new HashSet<AvaloniaView>();
private Action<TimeSpan>? _tick; private Action<TimeSpan>? _tick;
private long _lastTime;
private int _count; private int _count;
public ChoreographerTimer() public ChoreographerTimer()
{ {
_timerHandle = GCHandle.Alloc(this);
new Thread(Loop) new Thread(Loop)
{ {
Priority = ThreadPriority.AboveNormal Priority = ThreadPriority.AboveNormal,
Name = "Choreographer Thread"
}.Start(); }.Start();
new Thread(RenderLoop) new Thread(RenderLoop)
{ {
Priority = ThreadPriority.AboveNormal Priority = ThreadPriority.AboveNormal,
Name = "Render Thread"
}.Start(); }.Start();
} }
@ -50,7 +52,7 @@ namespace Avalonia.Android
if (_count == 1) if (_count == 1)
{ {
_choreographer.Task.Result.PostFrameCallback(this); PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle));
} }
} }
} }
@ -72,7 +74,7 @@ namespace Avalonia.Android
if (_views.Count == 1) if (_views.Count == 1)
{ {
_choreographer.Task.Result.PostFrameCallback(this); PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle));
} }
} }
@ -90,10 +92,10 @@ namespace Avalonia.Android
private void Loop() private void Loop()
{ {
Looper.Prepare(); Looper.Prepare();
_choreographer.SetResult(Choreographer.Instance!); _choreographer.SetResult(AChoreographer_getInstance());
Looper.Loop(); Looper.Loop();
} }
private void RenderLoop() private void RenderLoop()
{ {
while (true) while (true)
@ -108,18 +110,46 @@ namespace Avalonia.Android
} }
} }
private void DoFrameCallback(long frameTimeNanos, IntPtr data)
public void DoFrame(long frameTimeNanos)
{ {
lock (_lock) lock (_lock)
{ {
if (_count > 0 && _views.Count > 0) if (_count > 0 && _views.Count > 0)
{ {
Choreographer.Instance!.PostFrameCallback(this); PostFrameCallback(_choreographer.Task.Result, data);
} }
_lastTime = frameTimeNanos; _lastTime = frameTimeNanos;
_event.Set(); _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);
}
}
} }
} }

18
src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Android.Runtime; using Android.Runtime;
using Android.Views; using Android.Views;
using Avalonia.Platform; using Avalonia.Platform;
@ -60,6 +61,19 @@ namespace Avalonia.Android.Platform.SkiaPlatform
[DllImport("android")] [DllImport("android")]
internal static extern void ANativeWindow_unlockAndPost(IntPtr window); 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<int, IntPtr, void> callback, IntPtr data);
[DllImport("android")]
[SupportedOSPlatform("android10.0")]
internal static extern void AChoreographer_postFrameCallback64(
IntPtr choreographer, delegate* unmanaged<long, IntPtr, void> callback, IntPtr data);
[DllImport("android")] [DllImport("android")]
internal static extern int ANativeWindow_lock(IntPtr window, ANativeWindow_Buffer* outBuffer, ARect* inOutDirtyBounds); internal static extern int ANativeWindow_lock(IntPtr window, ANativeWindow_Buffer* outBuffer, ARect* inOutDirtyBounds);
public enum AndroidPixelFormat public enum AndroidPixelFormat
@ -69,6 +83,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
WINDOW_FORMAT_RGB_565 = 4, WINDOW_FORMAT_RGB_565 = 4,
} }
[StructLayout(LayoutKind.Sequential)]
internal struct ARect internal struct ARect
{ {
public int left; public int left;
@ -76,7 +91,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public int right; public int right;
public int bottom; public int bottom;
} }
[StructLayout(LayoutKind.Sequential)]
internal struct ANativeWindow_Buffer internal struct ANativeWindow_Buffer
{ {
// The number of pixels that are shown horizontally. // The number of pixels that are shown horizontally.

118
src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs

@ -1,30 +1,30 @@
using System; using System;
using System.Threading;
using Android.Content; using Android.Content;
using Android.Graphics; using Android.Graphics;
using Android.OS;
using Android.Runtime; using Android.Runtime;
using Android.Util;
using Android.Views; using Android.Views;
using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Logging;
using Avalonia.Platform; using Avalonia.Platform;
using Java.Lang;
namespace Avalonia.Android namespace Avalonia.Android
{ {
internal abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, INativePlatformHandleSurface internal abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback2, INativePlatformHandleSurface
{ {
bool _invalidateQueued; private IntPtr _nativeWindowHandle = IntPtr.Zero;
private bool _isDisposed; private PixelSize _size = new(1, 1);
private bool _isSurfaceValid; private double _scaling = 1;
readonly object _lock = new object();
private readonly Handler _handler;
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 ? IntPtr IPlatformHandle.Handle => _nativeWindowHandle;
AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, handle) : string IPlatformHandle.HandleDescriptor => "SurfaceView";
default;
public InvalidationAwareSurfaceView(Context context) : base(context) protected InvalidationAwareSurfaceView(Context context) : base(context)
{ {
if (Holder is null) if (Holder is null)
throw new InvalidOperationException( throw new InvalidOperationException(
@ -32,71 +32,77 @@ namespace Avalonia.Android
Holder.AddCallback(this); Holder.AddCallback(this);
Holder.SetFormat(global::Android.Graphics.Format.Transparent); Holder.SetFormat(global::Android.Graphics.Format.Transparent);
_handler = new Handler(context.MainLooper!);
} }
public override void Invalidate() protected override void Dispose(bool disposing)
{ {
lock (_lock) ReleaseNativeWindowHandle();
{ base.Dispose(disposing);
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;
} }
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; CacheSurfaceProperties(holder);
Log.Info("AVALONIA", "Surface Changed"); Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
DoDraw(); .Log(this, "InvalidationAwareSurfaceView Changed");
} }
public void SurfaceCreated(ISurfaceHolder holder) public void SurfaceCreated(ISurfaceHolder holder)
{ {
_isSurfaceValid = true; CacheSurfaceProperties(holder);
Log.Info("AVALONIA", "Surface Created"); Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "InvalidationAwareSurfaceView Created");
SurfaceWindowCreated?.Invoke(this, EventArgs.Empty); SurfaceWindowCreated?.Invoke(this, EventArgs.Empty);
DoDraw();
} }
public void SurfaceDestroyed(ISurfaceHolder holder) public void SurfaceDestroyed(ISurfaceHolder holder)
{ {
_isSurfaceValid = false; ReleaseNativeWindowHandle();
Log.Info("AVALONIA", "Surface Destroyed"); _size = new PixelSize(1, 1);
_scaling = 1;
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "InvalidationAwareSurfaceView Destroyed");
}
public virtual void SurfaceRedrawNeeded(ISurfaceHolder holder)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "InvalidationAwareSurfaceView RedrawNeeded");
}
public virtual void SurfaceRedrawNeededAsync(ISurfaceHolder holder, IRunnable drawingFinished)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "InvalidationAwareSurfaceView RedrawNeededAsync");
} }
protected void DoDraw() private void CacheSurfaceProperties(ISurfaceHolder holder)
{ {
lock (_lock) var surface = holder?.Surface;
var newHandle = IntPtr.Zero;
if (surface?.Handle is { } handle)
{ {
_invalidateQueued = false; newHandle = AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, handle);
} }
Draw();
}
protected abstract void Draw();
public string HandleDescriptor => "SurfaceView";
public PixelSize Size => new(Holder?.SurfaceFrame?.Width() ?? 1, Holder?.SurfaceFrame?.Height() ?? 1); 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);
_scaling = Resources?.DisplayMetrics?.Density ?? 1;
}
public double Scaling => Resources?.DisplayMetrics?.Density ?? 1; private void ReleaseNativeWindowHandle()
{
if (Interlocked.Exchange(ref _nativeWindowHandle, IntPtr.Zero) is var oldHandle
&& oldHandle != IntPtr.Zero)
{
AndroidFramebuffer.ANativeWindow_release(oldHandle);
}
}
} }
} }

87
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -32,9 +32,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{ {
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfoWithWaitPolicy class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfoWithWaitPolicy
{ {
private readonly IGlPlatformSurface _gl;
private readonly IFramebufferPlatformSurface _framebuffer;
private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper; private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
private readonly AndroidMotionEventsHelper _pointerHelper; private readonly AndroidMotionEventsHelper _pointerHelper;
private readonly AndroidInputMethod<ViewImpl> _textInputMethod; private readonly AndroidInputMethod<ViewImpl> _textInputMethod;
@ -59,13 +56,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_textInputMethod = new AndroidInputMethod<ViewImpl>(_view); _textInputMethod = new AndroidInputMethod<ViewImpl>(_view);
_keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this); _keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
_pointerHelper = new AndroidMotionEventsHelper(this); _pointerHelper = new AndroidMotionEventsHelper(this);
_gl = new EglGlPlatformSurface(this);
_framebuffer = new FramebufferManager(this);
_clipboard = new ClipboardImpl(avaloniaView.Context.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>()); _clipboard = new ClipboardImpl(avaloniaView.Context.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>());
_screens = new AndroidScreens(avaloniaView.Context); _screens = new AndroidScreens(avaloniaView.Context);
RenderScaling = _view.Scaling;
if (avaloniaView.Context is Activity mainActivity) if (avaloniaView.Context is Activity mainActivity)
{ {
_insetsManager = new AndroidInsetsManager(mainActivity, this); _insetsManager = new AndroidInsetsManager(mainActivity, this);
@ -78,15 +71,16 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_systemNavigationManager = new AndroidSystemNavigationManagerImpl(avaloniaView.Context as IActivityNavigationService); _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); Handle = new AndroidViewControlHandle(_view);
} }
public IInputRoot? InputRoot { get; private set; } public IInputRoot? InputRoot { get; private set; }
public virtual Size ClientSize => _view.Size.ToSize(RenderScaling); public Size ClientSize => _view.Size.ToSize(RenderScaling);
public double RenderScaling => _view.Scaling;
public Size? FrameSize => null;
public Action? Closed { get; set; } public Action? Closed { get; set; }
@ -110,16 +104,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public Compositor Compositor => AndroidPlatform.Compositor ?? public Compositor Compositor => AndroidPlatform.Compositor ??
throw new InvalidOperationException("Android backend wasn't initialized. Make sure .UseAndroid() was executed."); 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) public Point PointToClient(PixelPoint point)
{ {
return point.ToPoint(RenderScaling); return point.ToPoint(RenderScaling);
@ -140,18 +124,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
InputRoot = inputRoot; 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));
}
public virtual void Dispose() public virtual void Dispose()
{ {
_systemNavigationManager.Dispose(); _systemNavigationManager.Dispose();
@ -159,7 +131,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view = null!; _view = null!;
} }
protected virtual void OnResized(Size size) protected void OnResized(Size size)
{ {
Resized?.Invoke(size, WindowResizeReason.Unspecified); Resized?.Invoke(size, WindowResizeReason.Unspecified);
} }
@ -169,10 +141,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
Resized?.Invoke(size, WindowResizeReason.Layout); Resized?.Invoke(size, WindowResizeReason.Layout);
} }
class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo sealed class ViewImpl : InvalidationAwareSurfaceView, IInitEditorInfo
{ {
private readonly TopLevelImpl _tl; private readonly TopLevelImpl _tl;
private Size _oldSize; private Size _oldSize;
private double _oldScaling;
public ViewImpl(Context context, TopLevelImpl tl, bool placeOnTop) : base(context) public ViewImpl(Context context, TopLevelImpl tl, bool placeOnTop) : base(context)
{ {
@ -181,13 +154,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
SetZOrderOnTop(true); SetZOrderOnTop(true);
} }
public TopLevelImpl TopLevelImpl => _tl;
protected override void Draw()
{
_tl.Draw();
}
protected override void DispatchDraw(global::Android.Graphics.Canvas canvas) protected override void DispatchDraw(global::Android.Graphics.Canvas canvas)
{ {
// Workaround issue #9230 on where screen remains gray after splash screen. // Workaround issue #9230 on where screen remains gray after splash screen.
@ -234,20 +200,40 @@ namespace Avalonia.Android.Platform.SkiaPlatform
return res ?? baseResult; 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) if (newSize != _oldSize)
{ {
_oldSize = newSize; _oldSize = newSize;
_tl.OnResized(newSize); _tl.OnResized(newSize);
} }
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (newScaling != _oldScaling)
{
_oldScaling = newScaling;
_tl.ScalingChanged?.Invoke(newScaling);
}
}
base.SurfaceChanged(holder, format, width, height); public override void SurfaceRedrawNeeded(ISurfaceHolder holder)
{
// Compositor Renderer handles Paint event in-sync, which is perfect for sync SurfaceRedrawNeeded
_tl.Paint?.Invoke(new Rect(new Point(), Size.ToSize(Scaling)));
base.SurfaceRedrawNeeded(holder);
}
public override void SurfaceRedrawNeededAsync(ISurfaceHolder holder, IRunnable drawingFinished)
{
_tl.Compositor.RequestCompositionUpdate(drawingFinished.Run);
base.SurfaceRedrawNeededAsync(holder, drawingFinished);
} }
public sealed override bool OnCheckIsTextEditor() public override bool OnCheckIsTextEditor()
{ {
return true; return true;
} }
@ -259,11 +245,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_initEditorInfo = init; _initEditorInfo = init;
} }
public sealed override IInputConnection OnCreateInputConnection(EditorInfo? outAttrs) public override IInputConnection OnCreateInputConnection(EditorInfo? outAttrs)
{ {
return _initEditorInfo?.Invoke(_tl, outAttrs!)!; return _initEditorInfo?.Invoke(_tl, outAttrs!)!;
} }
} }
public IPopupImpl? CreatePopup() => null; public IPopupImpl? CreatePopup() => null;
@ -303,10 +288,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle; IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle;
bool EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfoWithWaitPolicy.SkipWaits => true; bool EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfoWithWaitPolicy.SkipWaits => true;
PixelSize EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Size => _view.Size;
public PixelSize Size => _view.Size; double EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Scaling => _view.Scaling;
public double Scaling => RenderScaling;
public void SetTransparencyLevelHint(IReadOnlyList<WindowTransparencyLevel> transparencyLevels) public void SetTransparencyLevelHint(IReadOnlyList<WindowTransparencyLevel> transparencyLevels)
{ {

2
src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs

@ -1,9 +1,11 @@
using System;
using Android.Views; using Android.Views;
namespace Avalonia.Android.Platform.Specific namespace Avalonia.Android.Platform.Specific
{ {
public interface IAndroidView public interface IAndroidView
{ {
[Obsolete("Use TopLevel.TryGetPlatformHandle instead, which can be casted to AndroidViewControlHandle.")]
View View { get; } View View { get; }
} }
} }

Loading…
Cancel
Save