diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
index 20ca0576d4..1a68c4d732 100644
--- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
+++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
@@ -16,7 +16,7 @@
Resources\Resource.Designer.cs
Off
False
- v10.0
+ v11.0
Properties\AndroidManifest.xml
diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
index 02e97f3065..9effda7e79 100644
--- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
+++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/src/Android/Avalonia.Android/ActivityTracker.cs b/src/Android/Avalonia.Android/ActivityTracker.cs
deleted file mode 100644
index 2ad1d9e361..0000000000
--- a/src/Android/Avalonia.Android/ActivityTracker.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using Android.App;
-using Android.OS;
-
-namespace Avalonia.Android
-{
- internal class ActivityTracker : Java.Lang.Object, global::Android.App.Application.IActivityLifecycleCallbacks
- {
- public static Activity Current { get; private set; }
- public void OnActivityCreated(Activity activity, Bundle savedInstanceState)
- {
- Current = activity;
- }
-
- public void OnActivityDestroyed(Activity activity)
- {
- if (Current == activity)
- Current = null;
- }
-
- public void OnActivityPaused(Activity activity)
- {
- if (Current == activity)
- Current = null;
- }
-
- public void OnActivityResumed(Activity activity)
- {
- Current = activity;
- }
-
- public void OnActivitySaveInstanceState(Activity activity, Bundle outState)
- {
- Current = activity;
- }
-
- public void OnActivityStarted(Activity activity)
- {
- Current = activity;
- }
-
- public void OnActivityStopped(Activity activity)
- {
- if (Current == activity)
- Current = null;
- }
- }
-}
\ No newline at end of file
diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs
index e0ceb0c8b7..e0ed9b1fda 100644
--- a/src/Android/Avalonia.Android/AndroidPlatform.cs
+++ b/src/Android/Avalonia.Android/AndroidPlatform.cs
@@ -32,6 +32,7 @@ namespace Avalonia.Android
class AndroidPlatform : IPlatformSettings, IWindowingPlatform
{
public static readonly AndroidPlatform Instance = new AndroidPlatform();
+ public static AndroidPlatformOptions Options { get; private set; }
public Size DoubleClickSize => new Size(4, 4);
public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200);
public double RenderScalingFactor => _scalingFactor;
@@ -46,23 +47,23 @@ namespace Avalonia.Android
public static void Initialize(Type appType, AndroidPlatformOptions options)
{
+ Options = options;
+
AvaloniaLocator.CurrentMutable
.Bind().ToTransient()
- .Bind().ToTransient()
+ .Bind().ToTransient()
.Bind().ToSingleton()
.Bind().ToConstant(Instance)
.Bind().ToConstant(new AndroidThreadingInterface())
.Bind().ToTransient()
.Bind().ToConstant(Instance)
.Bind().ToSingleton()
- .Bind().ToConstant(new DefaultRenderTimer(60))
+ .Bind().ToConstant(new ChoreographerTimer())
.Bind().ToConstant(new RenderLoop())
.Bind().ToSingleton()
.Bind().ToConstant(new AssetLoader(appType.Assembly));
SkiaPlatform.Initialize();
- ((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext)
- .RegisterActivityLifecycleCallbacks(new ActivityTracker());
if (options.UseGpu)
{
@@ -83,6 +84,7 @@ namespace Avalonia.Android
public sealed class AndroidPlatformOptions
{
+ public bool UseDeferredRendering { get; set; } = true;
public bool UseGpu { get; set; } = true;
}
}
diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj
index c170e8449c..8c6775733f 100644
--- a/src/Android/Avalonia.Android/Avalonia.Android.csproj
+++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj
@@ -1,6 +1,6 @@
- monoandroid90
+ monoandroid11.0
true
diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs
index d3696aa41d..52b68f8e2f 100644
--- a/src/Android/Avalonia.Android/AvaloniaActivity.cs
+++ b/src/Android/Avalonia.Android/AvaloniaActivity.cs
@@ -13,9 +13,8 @@ namespace Avalonia.Android
protected override void OnCreate(Bundle savedInstanceState)
{
- RequestWindowFeature(WindowFeatures.NoTitle);
View = new AvaloniaView(this);
- if(_content != null)
+ if (_content != null)
View.Content = _content;
SetContentView(View);
TakeKeyEvents(true);
diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs
index 72732a1f95..8de3657283 100644
--- a/src/Android/Avalonia.Android/AvaloniaView.cs
+++ b/src/Android/Avalonia.Android/AvaloniaView.cs
@@ -1,11 +1,12 @@
using System;
using Android.Content;
+using Android.Runtime;
using Android.Views;
using Android.Widget;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
-using Avalonia.Platform;
+using Avalonia.Rendering;
namespace Avalonia.Android
{
@@ -14,6 +15,8 @@ namespace Avalonia.Android
private readonly EmbeddableControlRoot _root;
private readonly ViewImpl _view;
+ private IDisposable? _timerSubscription;
+
public AvaloniaView(Context context) : base(context)
{
_view = new ViewImpl(context);
@@ -33,6 +36,36 @@ namespace Avalonia.Android
return _view.View.DispatchKeyEvent(e);
}
+ public override void OnVisibilityAggregated(bool isVisible)
+ {
+ base.OnVisibilityAggregated(isVisible);
+ OnVisibilityChanged(isVisible);
+ }
+
+ protected override void OnVisibilityChanged(View changedView, [GeneratedEnum] ViewStates visibility)
+ {
+ base.OnVisibilityChanged(changedView, visibility);
+ OnVisibilityChanged(visibility == ViewStates.Visible);
+ }
+
+ private void OnVisibilityChanged(bool isVisible)
+ {
+ if (isVisible)
+ {
+ if (AvaloniaLocator.Current.GetService() is ChoreographerTimer timer)
+ {
+ _timerSubscription = timer.SubscribeView(this);
+ }
+
+ _root.Renderer.Start();
+ }
+ else
+ {
+ _root.Renderer.Stop();
+ _timerSubscription?.Dispose();
+ }
+ }
+
class ViewImpl : TopLevelImpl
{
public ViewImpl(Context context) : base(context)
diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs
new file mode 100644
index 0000000000..1d898261a3
--- /dev/null
+++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using System.Threading.Tasks;
+
+using Android.OS;
+using Android.Views;
+
+using Avalonia.Rendering;
+
+using Java.Lang;
+
+namespace Avalonia.Android
+{
+ internal sealed class ChoreographerTimer : Java.Lang.Object, IRenderTimer, Choreographer.IFrameCallback
+ {
+ private readonly object _lock = new object();
+
+ private readonly Thread _thread;
+ private readonly TaskCompletionSource _choreographer = new TaskCompletionSource();
+
+ private readonly ISet _views = new HashSet();
+
+ private Action _tick;
+ private int _count;
+
+ public ChoreographerTimer()
+ {
+ _thread = new Thread(Loop);
+ _thread.Start();
+ }
+
+ public event Action Tick
+ {
+ add
+ {
+ lock (_lock)
+ {
+ _tick += value;
+ _count++;
+
+ if (_count == 1)
+ {
+ _choreographer.Task.Result.PostFrameCallback(this);
+ }
+ }
+ }
+ remove
+ {
+ lock (_lock)
+ {
+ _tick -= value;
+ _count--;
+ }
+ }
+ }
+
+ internal IDisposable SubscribeView(AvaloniaView view)
+ {
+ lock (_lock)
+ {
+ _views.Add(view);
+
+ if (_views.Count == 1)
+ {
+ _choreographer.Task.Result.PostFrameCallback(this);
+ }
+ }
+
+ return Disposable.Create(
+ () =>
+ {
+ lock (_lock)
+ {
+ _views.Remove(view);
+ }
+ }
+ );
+ }
+
+ private void Loop()
+ {
+ Looper.Prepare();
+ _choreographer.SetResult(Choreographer.Instance);
+ Looper.Loop();
+ }
+
+ public void DoFrame(long frameTimeNanos)
+ {
+ _tick?.Invoke(TimeSpan.FromTicks(frameTimeNanos / 100));
+
+ lock (_lock)
+ {
+ if (_count > 0 && _views.Count > 0)
+ {
+ Choreographer.Instance.PostFrameCallback(this);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Android/Avalonia.Android/CursorFactory.cs b/src/Android/Avalonia.Android/CursorFactory.cs
index 9eb28c67f9..6293637d4e 100644
--- a/src/Android/Avalonia.Android/CursorFactory.cs
+++ b/src/Android/Avalonia.Android/CursorFactory.cs
@@ -1,12 +1,21 @@
-using System;
using Avalonia.Input;
using Avalonia.Platform;
namespace Avalonia.Android
{
- internal class CursorFactory : IStandardCursorFactory
+ internal class CursorFactory : ICursorFactory
{
- public IPlatformHandle GetCursor(StandardCursorType cursorType)
- => new PlatformHandle(IntPtr.Zero, "ZeroCursor");
+ public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => CursorImpl.ZeroCursor;
+
+ public ICursorImpl GetCursor(StandardCursorType cursorType) => CursorImpl.ZeroCursor;
+
+ private sealed class CursorImpl : ICursorImpl
+ {
+ public static CursorImpl ZeroCursor { get; } = new CursorImpl();
+
+ private CursorImpl() { }
+
+ public void Dispose() { }
+ }
}
}
diff --git a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
index 4f4c03fe77..a9710039f8 100644
--- a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
+++ b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
@@ -1,6 +1,4 @@
-using System.Linq;
-
-using Avalonia.OpenGL.Egl;
+using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces;
namespace Avalonia.Android.OpenGL
@@ -17,7 +15,7 @@ namespace Avalonia.Android.OpenGL
}
public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() =>
- new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle));
+ new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle), _info.Handle);
public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info)
{
diff --git a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
index 75bbd15e3e..f9071d9b27 100644
--- a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
+++ b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
@@ -1,23 +1,30 @@
-using Avalonia.OpenGL.Egl;
+using System;
+
+using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces;
namespace Avalonia.Android.OpenGL
{
- internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase
+ internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase, IGlPlatformSurfaceRenderTargetWithCorruptionInfo
{
private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info;
private readonly EglSurface _surface;
+ private readonly IntPtr _handle;
public GlRenderTarget(
EglPlatformOpenGlInterface egl,
EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info,
- EglSurface surface)
+ EglSurface surface,
+ IntPtr handle)
: base(egl)
{
_info = info;
_surface = surface;
+ _handle = handle;
}
+ public bool IsCorrupted => _handle != _info.Handle;
+
public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info);
}
}
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
index a8c7f7af9b..10bf414f25 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
@@ -98,10 +98,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IEnumerable
@@ -150,4 +150,4 @@
-
\ No newline at end of file
+
diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
index e8e81da9de..57ee503005 100644
--- a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
+++ b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs
index b59fd7abde..aab6a41890 100644
--- a/src/Avalonia.Controls/AutoCompleteBox.cs
+++ b/src/Avalonia.Controls/AutoCompleteBox.cs
@@ -483,7 +483,9 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect(
nameof(SelectedItem),
o => o.SelectedItem,
- (o, v) => o.SelectedItem = v);
+ (o, v) => o.SelectedItem = v,
+ defaultBindingMode: BindingMode.TwoWay,
+ enableDataValidation: true);
///
/// Identifies the
@@ -1333,7 +1335,7 @@ namespace Avalonia.Controls
base.OnApplyTemplate(e);
}
-
+
///
/// Called to update the validation state for properties for which data validation is
/// enabled.
@@ -1342,7 +1344,7 @@ namespace Avalonia.Controls
/// The new binding value for the property.
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value)
{
- if (property == TextProperty)
+ if (property == TextProperty || property == SelectedItemProperty)
{
DataValidationErrors.SetError(this, value.Error);
}
diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
index b6b71644a3..abfbc038eb 100644
--- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
+++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
@@ -91,14 +91,14 @@ namespace Avalonia.Controls
///
public static readonly DirectProperty TextProperty =
AvaloniaProperty.RegisterDirect(nameof(Text), o => o.Text, (o, v) => o.Text = v,
- defaultBindingMode: BindingMode.TwoWay);
+ defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
///
/// Defines the property.
///
public static readonly DirectProperty ValueProperty =
AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value,
- (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay);
+ (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
///
/// Defines the property.
@@ -370,6 +370,20 @@ namespace Avalonia.Controls
}
}
+ ///
+ /// Called to update the validation state for properties for which data validation is
+ /// enabled.
+ ///
+ /// The property.
+ /// The new binding value for the property.
+ protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value)
+ {
+ if (property == TextProperty || property == ValueProperty)
+ {
+ DataValidationErrors.SetError(this, value.Error);
+ }
+ }
+
///
/// Called when the property value changed.
///
diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
index 3e78e951e2..b346fca330 100644
--- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
@@ -14,6 +14,8 @@ using Avalonia.UnitTests;
using Moq;
using Xunit;
using System.Collections.ObjectModel;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
namespace Avalonia.Controls.UnitTests
{
@@ -396,6 +398,36 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(control.Text, control.ItemSelector(input, selectedItem));
});
}
+
+ [Fact]
+ public void Text_Validation()
+ {
+ RunTest((control, textbox) =>
+ {
+ var exception = new InvalidCastException("failed validation");
+ var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError));
+ control.Bind(AutoCompleteBox.TextProperty, textObservable);
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.Equal(DataValidationErrors.GetHasErrors(control), true);
+ Assert.Equal(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }), true);
+ });
+ }
+
+ [Fact]
+ public void SelectedItem_Validation()
+ {
+ RunTest((control, textbox) =>
+ {
+ var exception = new InvalidCastException("failed validation");
+ var itemObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError));
+ control.Bind(AutoCompleteBox.SelectedItemProperty, itemObservable);
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.Equal(DataValidationErrors.GetHasErrors(control), true);
+ Assert.Equal(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }), true);
+ });
+ }
///
/// Retrieves a defined predicate filter through a new AutoCompleteBox
diff --git a/tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs b/tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs
new file mode 100644
index 0000000000..4cef7e4d05
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Linq;
+using System.Reactive.Subjects;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
+using Avalonia.Threading;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+ public class NumericUpDownTests
+ {
+ private static TestServices Services => TestServices.StyledWindow;
+
+ [Fact]
+ public void Text_Validation()
+ {
+ RunTest((control, textbox) =>
+ {
+ var exception = new InvalidCastException("failed validation");
+ var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError));
+ control.Bind(NumericUpDown.TextProperty, textObservable);
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.True(DataValidationErrors.GetHasErrors(control));
+ Assert.True(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }));
+ });
+ }
+
+ [Fact]
+ public void Value_Validation()
+ {
+ RunTest((control, textbox) =>
+ {
+ var exception = new InvalidCastException("failed validation");
+ var valueObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError));
+ control.Bind(NumericUpDown.ValueProperty, valueObservable);
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.True(DataValidationErrors.GetHasErrors(control));
+ Assert.True(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }));
+ });
+ }
+
+ private void RunTest(Action test)
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ var control = CreateControl();
+ TextBox textBox = GetTextBox(control);
+ var window = new Window { Content = control };
+ window.ApplyTemplate();
+ window.Presenter.ApplyTemplate();
+ Dispatcher.UIThread.RunJobs();
+ test.Invoke(control, textBox);
+ }
+ }
+
+ private NumericUpDown CreateControl()
+ {
+ var control = new NumericUpDown
+ {
+ Template = CreateTemplate()
+ };
+
+ control.ApplyTemplate();
+ return control;
+ }
+ private TextBox GetTextBox(NumericUpDown control)
+ {
+ return control.GetTemplateChildren()
+ .OfType()
+ .Select(b => b.Content)
+ .OfType()
+ .First();
+ }
+ private IControlTemplate CreateTemplate()
+ {
+ return new FuncControlTemplate((control, scope) =>
+ {
+ var textBox =
+ new TextBox
+ {
+ Name = "PART_TextBox"
+ }.RegisterInNameScope(scope);
+ return new ButtonSpinner
+ {
+ Name = "PART_Spinner",
+ Content = textBox,
+ }.RegisterInNameScope(scope);
+ });
+ }
+ }
+}