diff --git a/README.md b/README.md index fa0ee1d..f27d66b 100644 --- a/README.md +++ b/README.md @@ -94,21 +94,14 @@ ___Please star ⭐ if you like it, helps very much!___ ## What's New -### Nuget 1.2.9.7 +### Nuget 1.2.9.8 for SkiaSharp 2.88.9-preview.2.2 -* [FluentExtensions](https://github.com/taublast/DrawnUi.Maui/blob/main/src/Engine/Internals/Extensions/FluentExtensions.cs) for code-behind -* Added gestures delegate `OnGestures` to SkiaLayout. -* HotFix for SkiaCarousel always setting index at 0 upon initialization. -* HotFix for random crash accessing disposed LoadedImageSource. -* [HotFix](https://github.com/taublast/DrawnUi.Maui/issues/136) for loading images from StreamImageSource -* SkiaShape new Types: Polygon and Line. New property for their Points: Smooth (0-1) to smooth angles. -* Shapes demo page inside SandBox project. -* VisualElement Shadow property now supported everywhere as an optional addition to existing shadows. -* Removed SkiaImage clipping to better support shadows. -* SkiaLabel new property AutoFont: Find and set system font where the first glyph in text is present. Useful for some quick unicode rendering like emoji etc. -* Updated Getsures nuget for correct lock inside MAUI native ScrollView, use Getures="Lock" for Canvas. -* Fixed controls sometimes not invalidated when canvas suface size changes -* Other fixes. + +* DisposeObject optimized for background. +* Templated layout measurement optimized. +* HotFix for gestures crash on iOS when targeting ios lower than 18. +* Nuget targets ios 17. + ## About diff --git a/dev/github_uploadnugets.bat b/dev/github_uploadnugets.bat index 2252670..0558fdb 100644 --- a/dev/github_uploadnugets.bat +++ b/dev/github_uploadnugets.bat @@ -13,8 +13,8 @@ REM Define the source directory for the packages set "source_dir=E:\Nugets" REM Define the list of file masks for the packages -set "mask[1]=DrawnUi.Maui*.1.2.9.7*.nupkg" -set "mask[2]=AppoMobi.Maui.DrawnUi.1.2.9.7*.*nupkg" +set "mask[1]=DrawnUi.Maui*.1.2.9.8*.nupkg" +set "mask[2]=AppoMobi.Maui.DrawnUi.1.2.9.8*.*nupkg" set "mask_count=2" REM Loop through each file mask diff --git a/dev/nuget_uploadnugets.bat b/dev/nuget_uploadnugets.bat index d1d3722..6b5d8d9 100644 --- a/dev/nuget_uploadnugets.bat +++ b/dev/nuget_uploadnugets.bat @@ -13,8 +13,8 @@ REM Define the source directory for the packages set "source_dir=E:\Nugets" REM Define the list of file masks for the packages -set "mask[1]=DrawnUi.Maui*.1.2.9.7*.nupkg" -set "mask[2]=AppoMobi.Maui.DrawnUi.1.2.9.7*.*nupkg" +set "mask[1]=DrawnUi.Maui*.1.2.9.8*.nupkg" +set "mask[2]=AppoMobi.Maui.DrawnUi.1.2.9.8*.*nupkg" set "mask_count=2" REM Loop through each file mask diff --git a/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj b/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj index 6dbbf4a..566b3e6 100644 --- a/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj +++ b/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj @@ -1,6 +1,6 @@  - net8.0;net8.0-android;net8.0-ios;net8.0-maccatalyst + net8.0;net8.0-android;net8.0-ios17.0;net8.0-maccatalyst17.0 $(TargetFrameworks);net8.0-windows10.0.19041.0 true true @@ -44,7 +44,7 @@ - + \ No newline at end of file diff --git a/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj b/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj index 8232e47..1e2b06a 100644 --- a/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj +++ b/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj @@ -1,6 +1,6 @@  - net8.0;net8.0-android;net8.0-ios;net8.0-maccatalyst + net8.0;net8.0-android;net8.0-ios17.0;net8.0-maccatalyst17.0 $(TargetFrameworks);net8.0-windows10.0.19041.0 true true @@ -42,7 +42,7 @@ - + diff --git a/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj b/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj index d06c087..afb2499 100644 --- a/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj +++ b/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj @@ -1,6 +1,6 @@  - net8.0;net8.0-android;net8.0-ios;net8.0-maccatalyst + net8.0;net8.0-android;net8.0-ios17.0;net8.0-maccatalyst17.0 $(TargetFrameworks);net8.0-windows10.0.19041.0 true true @@ -54,7 +54,7 @@ - + diff --git a/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj b/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj index 694847f..0ff5d8e 100644 --- a/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj +++ b/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj @@ -1,6 +1,6 @@  - net8.0;net8.0-android;net8.0-ios;net8.0-maccatalyst + net8.0;net8.0-android;net8.0-ios17.0;net8.0-maccatalyst17.0 $(TargetFrameworks);net8.0-windows10.0.19041.0 true true @@ -47,7 +47,7 @@ - + diff --git a/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj b/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj index 9d1c496..bac1512 100644 --- a/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj +++ b/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj @@ -1,6 +1,6 @@  - net8.0;net8.0-android;net8.0-ios;net8.0-maccatalyst + net8.0;net8.0-android;net8.0-ios17.0;net8.0-maccatalyst17.0 $(TargetFrameworks);net8.0-windows10.0.19041.0 true true @@ -41,7 +41,7 @@ - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ba4d863..fef7825 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,13 +5,13 @@ Using SkiaSharp 2.xx. Checkout the DrawnUi Sandbox project for usage example. - 1.2.9.7 + 1.2.9.8 $(DefineConstants);SKIA3 Using SkiaSharp 3-preview. New handlers, SKSL, WinUI hardware acceleration etc.. - 1.3.56.1-pre + 1.3.56.2-pre \ No newline at end of file diff --git a/src/Engine/Controls/Navigation/SkiaShell.NavigationLayer.cs b/src/Engine/Controls/Navigation/SkiaShell.NavigationLayer.cs index a36dcba..215c9ba 100644 --- a/src/Engine/Controls/Navigation/SkiaShell.NavigationLayer.cs +++ b/src/Engine/Controls/Navigation/SkiaShell.NavigationLayer.cs @@ -74,12 +74,9 @@ public async Task Close(T control, bool animated) await _shell.UnfreezeRootLayout(control, animated); } - control.SetParent(null); + control?.DisposeObject(); + control?.SetParent(null); - Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => - { - control?.Dispose(); - }); } catch (Exception e) { @@ -122,4 +119,4 @@ public async Task CloseAll() } -} \ No newline at end of file +} diff --git a/src/Engine/Controls/Navigation/SkiaShell.cs b/src/Engine/Controls/Navigation/SkiaShell.cs index eb03508..466338c 100644 --- a/src/Engine/Controls/Navigation/SkiaShell.cs +++ b/src/Engine/Controls/Navigation/SkiaShell.cs @@ -1,7 +1,7 @@ -using AppoMobi.Maui.Navigation; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Numerics; using System.Runtime.CompilerServices; +using AppoMobi.Maui.Navigation; namespace DrawnUi.Maui.Controls { @@ -1020,10 +1020,7 @@ async Task RemoveModal(SkiaControl control, bool animated) OnLayersChanged(control); control.SetParent(null); //unregister gestures etc - Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => - { - control.Dispose(); - }); + ShellLayout?.DisposeObject(control); } } @@ -1193,10 +1190,7 @@ public virtual async Task UnfreezeRootLayout(SkiaControl control, bool animated) RootLayout.RemoveSubView(screenshot); - Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => - { - screenshot?.Dispose(); - }); + RootLayout.DisposeObject(screenshot); } } @@ -1788,10 +1782,7 @@ void Act() }); } if (kill != null) - Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => - { - kill?.Dispose(); - }); + ShellLayout?.DisposeObject(kill); } protected virtual void ReplaceRootLayout(ISkiaControl newLayout) @@ -1813,10 +1804,7 @@ protected virtual void ReplaceRootLayout(ISkiaControl newLayout) ImportRootLayout(); if (kill != null) - Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => - { - kill?.Dispose(); - }); + ShellLayout?.DisposeObject(kill); } protected virtual void ImportShellLayout() diff --git a/src/Engine/Controls/PlayFrames/SkiaGif.cs b/src/Engine/Controls/PlayFrames/SkiaGif.cs index 5ce90b4..d7594d5 100644 --- a/src/Engine/Controls/PlayFrames/SkiaGif.cs +++ b/src/Engine/Controls/PlayFrames/SkiaGif.cs @@ -1,6 +1,4 @@ using DrawnUi.Maui.Features.Images; -using System.Collections.Concurrent; -using System.Text; namespace DrawnUi.Maui.Controls; @@ -163,7 +161,7 @@ public void SetAnimation(GifAnimation animation, bool disposePrevious) Start(); if (kill != null && disposePrevious) - Tasks.StartDelayed(TimeSpan.FromSeconds(2), () => { kill.Dispose(); }); + DisposeObject(kill); Invalidate(); @@ -262,4 +260,4 @@ public string Source set => SetValue(SourceProperty, value); } -} \ No newline at end of file +} diff --git a/src/Engine/Controls/ViewSwitcher/SkiaViewSwitcher.cs b/src/Engine/Controls/ViewSwitcher/SkiaViewSwitcher.cs index 028d2df..b16144c 100644 --- a/src/Engine/Controls/ViewSwitcher/SkiaViewSwitcher.cs +++ b/src/Engine/Controls/ViewSwitcher/SkiaViewSwitcher.cs @@ -824,42 +824,42 @@ protected virtual async Task SetupTransitionAnimation( switch (doubleViewTransitionType) { case DoubleViewTransitionType.Pop: - //from left to right - break; + //from left to right + break; case DoubleViewTransitionType.Push: - previousVisibleView.ZIndex = -1; - newVisibleView.ZIndex = 0; + previousVisibleView.ZIndex = -1; + newVisibleView.ZIndex = 0; - //from right to left - translateTo = this.Width; - newVisibleView.Opacity = 0.001; - newVisibleView.TranslationX = (float)translateTo; - break; + //from right to left + translateTo = this.Width; + newVisibleView.Opacity = 0.001; + newVisibleView.TranslationX = (float)translateTo; + break; case DoubleViewTransitionType.SelectRightTab: - translateTo = this.Width; + translateTo = this.Width; - newVisibleView.ZIndex = 1; - previousVisibleView.ZIndex = 0; - newVisibleView.Opacity = 0.001; - newVisibleView.TranslationX = (float)translateTo * 0.75; - break; + newVisibleView.ZIndex = 1; + previousVisibleView.ZIndex = 0; + newVisibleView.Opacity = 0.001; + newVisibleView.TranslationX = (float)translateTo * 0.75; + break; case DoubleViewTransitionType.SelectLeftTab: - translateTo = -this.Width; + translateTo = -this.Width; - newVisibleView.ZIndex = 1; - previousVisibleView.ZIndex = 0; - newVisibleView.Opacity = 0.001; - newVisibleView.TranslationX = (float)translateTo * 0.75; - break; + newVisibleView.ZIndex = 1; + previousVisibleView.ZIndex = 0; + newVisibleView.Opacity = 0.001; + newVisibleView.TranslationX = (float)translateTo * 0.75; + break; default: - newVisibleView.TranslationX = 0; - newVisibleView.TranslationY = 0; - newVisibleView.Opacity = 1; - break; + newVisibleView.TranslationX = 0; + newVisibleView.TranslationY = 0; + newVisibleView.Opacity = 1; + break; } } @@ -875,79 +875,79 @@ protected virtual async Task ExecuteTransitionAnimation( switch (doubleViewTransitionType) { case DoubleViewTransitionType.Pop: - //from left to right - easing = PagesAnimationEasing; - speed = PagesAnimationSpeed; - Task animateOld1 = previousVisibleView.TranslateToAsync(translateTo, 0, speed, easing); - Task animateOld2 = previousVisibleView.FadeToAsync(0.9, speed, easing); + //from left to right + easing = PagesAnimationEasing; + speed = PagesAnimationSpeed; + Task animateOld1 = previousVisibleView.TranslateToAsync(translateTo, 0, speed, easing); + Task animateOld2 = previousVisibleView.FadeToAsync(0.9, speed, easing); - try - { - var cancelAnimation = new CancellationTokenSource(TimeSpan.FromSeconds(2)); - await Task.WhenAll(animateOld1, animateOld2).WithCancellation(cancelAnimation.Token); - } - catch (Exception e) - { - Debug.WriteLine(e); - } - break; + try + { + var cancelAnimation = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await Task.WhenAll(animateOld1, animateOld2).WithCancellation(cancelAnimation.Token); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + break; case DoubleViewTransitionType.Push: - easing = PagesAnimationEasing; - speed = PagesAnimationSpeed; - //from right to left - Task in1 = newVisibleView.TranslateToAsync(0, 0, speed, easing); - Task in2 = newVisibleView.FadeToAsync(1.0, speed, easing); - try - { - var cancelAnimation = new CancellationTokenSource(TimeSpan.FromSeconds(2)); - await Task.WhenAll(in1, in2).WithCancellation(cancelAnimation.Token).WithCancellation(cancelAnimation.Token); - } - catch (Exception e) - { - Debug.WriteLine(e); - } - break; + easing = PagesAnimationEasing; + speed = PagesAnimationSpeed; + //from right to left + Task in1 = newVisibleView.TranslateToAsync(0, 0, speed, easing); + Task in2 = newVisibleView.FadeToAsync(1.0, speed, easing); + try + { + var cancelAnimation = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await Task.WhenAll(in1, in2).WithCancellation(cancelAnimation.Token).WithCancellation(cancelAnimation.Token); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + break; case DoubleViewTransitionType.SelectLeftTab: - translateTo = -this.Width; + translateTo = -this.Width; - Task animateOldM = previousVisibleView.TranslateToAsync(-translateTo, 0, (uint)speed, easing); - Task animateNewM = newVisibleView.TranslateToAsync(0, 0, (uint)speed, easing); - Task animateNewM1 = newVisibleView.FadeToAsync(1.0, (uint)speed, Easing.Linear); + Task animateOldM = previousVisibleView.TranslateToAsync(-translateTo, 0, (uint)speed, easing); + Task animateNewM = newVisibleView.TranslateToAsync(0, 0, (uint)speed, easing); + Task animateNewM1 = newVisibleView.FadeToAsync(1.0, (uint)speed, Easing.Linear); - try - { - var cancelAnimation = - new CancellationTokenSource(TimeSpan.FromSeconds(2)); + try + { + var cancelAnimation = + new CancellationTokenSource(TimeSpan.FromSeconds(2)); - await Task.WhenAll(animateOldM, animateNewM, animateNewM1).WithCancellation(cancelAnimation.Token); - } - catch (Exception e) - { - Debug.WriteLine(e); - } - break; + await Task.WhenAll(animateOldM, animateNewM, animateNewM1).WithCancellation(cancelAnimation.Token); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + break; case DoubleViewTransitionType.SelectRightTab: - translateTo = this.Width; + translateTo = this.Width; - animateOldM = previousVisibleView.TranslateToAsync(-translateTo, 0, (uint)speed, easing); - animateNewM = newVisibleView.TranslateToAsync(0, 0, (uint)speed, easing); - animateNewM1 = newVisibleView.FadeToAsync(1.0, (uint)speed, Easing.Linear); + animateOldM = previousVisibleView.TranslateToAsync(-translateTo, 0, (uint)speed, easing); + animateNewM = newVisibleView.TranslateToAsync(0, 0, (uint)speed, easing); + animateNewM1 = newVisibleView.FadeToAsync(1.0, (uint)speed, Easing.Linear); - try - { - var cancelAnimation = - new CancellationTokenSource(TimeSpan.FromSeconds(2)); + try + { + var cancelAnimation = + new CancellationTokenSource(TimeSpan.FromSeconds(2)); - await Task.WhenAll(animateOldM, animateNewM, animateNewM1).WithCancellation(cancelAnimation.Token); - } - catch (Exception e) - { - Debug.WriteLine(e); - } - break; + await Task.WhenAll(animateOldM, animateNewM, animateNewM1).WithCancellation(cancelAnimation.Token); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + break; } } @@ -1198,7 +1198,7 @@ protected void UnloadView(NavigationStackEntry view) SendOnDisappearing(view.View); if (!view.Preserve && view.View is IDisposable disposable) { - disposable.Dispose(); + DisposeObject(disposable); } } else diff --git a/src/Engine/Draw/Base/SkiaControl.Cache.cs b/src/Engine/Draw/Base/SkiaControl.Cache.cs index 6fb5dc8..e70c87f 100644 --- a/src/Engine/Draw/Base/SkiaControl.Cache.cs +++ b/src/Engine/Draw/Base/SkiaControl.Cache.cs @@ -397,12 +397,10 @@ protected virtual bool UseRenderingObject(SkiaDrawingContext context, SKRect dra var kill = RenderObjectPrevious; RenderObjectPrevious = null; RenderObjectPreviousNeedsUpdate = false; + if (kill != null) { - Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => - { - kill.Dispose(); - }); + DisposeObject(kill); } } @@ -725,10 +723,7 @@ protected void CreateRenderingObjectAndPaint( } if (!UsesCacheDoubleBuffering && usingCacheType != SkiaCacheType.ImageComposite) { - Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => - { - oldObject.Dispose(); - }); + DisposeObject(oldObject); } } diff --git a/src/Engine/Draw/Base/SkiaControl.Shared.cs b/src/Engine/Draw/Base/SkiaControl.Shared.cs index 66a268c..41e82cb 100644 --- a/src/Engine/Draw/Base/SkiaControl.Shared.cs +++ b/src/Engine/Draw/Base/SkiaControl.Shared.cs @@ -2537,6 +2537,14 @@ public static (double X1, double Y1, double X2, double Y2) LinearGradientAngleTo return (startPoint.x, startPoint.y, endPoint.x, endPoint.y); } + public static TimeSpan DisposalDelay = TimeSpan.FromSeconds(3.5); + + public ObjectAliveType IsAlive { get; set; } + + public void DisposeObject() + { + DisposeObject(this); + } /// /// Dispose with needed delay. @@ -2550,7 +2558,7 @@ public virtual void DisposeObject(IDisposable disposable) { try { - view.ToBeDisposed.Enqueue(disposable); + view.DisposeObject(disposable); } catch (Exception e) { @@ -2559,11 +2567,20 @@ public virtual void DisposeObject(IDisposable disposable) } else { - Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => + if (disposable is SkiaControl skia) + { + skia.IsAlive = ObjectAliveType.BeingDisposed; + } + + Tasks.StartDelayed(DisposalDelay, () => { try { disposable?.Dispose(); + if (disposable is SkiaControl skia) + { + skia.IsAlive = ObjectAliveType.Disposed; + } } catch (Exception e) { @@ -4002,7 +4019,7 @@ public void Dispose() SizeChanged -= ViewSizeChanged; //for the double buffering case it's safer to delay - Tasks.StartDelayed(TimeSpan.FromSeconds(1), () => + Tasks.StartDelayed(DisposalDelay, () => { RenderObject = null; diff --git a/src/Engine/Draw/Layout/SkiaLayout.ColumnRow.cs b/src/Engine/Draw/Layout/SkiaLayout.ColumnRow.cs index eb26b94..631947a 100644 --- a/src/Engine/Draw/Layout/SkiaLayout.ColumnRow.cs +++ b/src/Engine/Draw/Layout/SkiaLayout.ColumnRow.cs @@ -305,13 +305,13 @@ public virtual ScaledSize MeasureStack(SKRect rectForChildrenPixels, float scale var layoutStructure = BuildStackStructure(scale); - bool useOneTemplate = - IsTemplated && - //ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem && - RecyclingTemplate == RecyclingTemplate.Enabled; + bool standalone = false; + bool useOneTemplate = IsTemplated && ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem && + RecyclingTemplate != RecyclingTemplate.Disabled; if (useOneTemplate) { + standalone = true; template = ChildrenFactory.GetTemplateInstance(); } @@ -328,6 +328,17 @@ public virtual ScaledSize MeasureStack(SKRect rectForChildrenPixels, float scale var columnsCount = layoutStructure.GetColumnCountForRow(row); + var needMeasureAll = true; + if (useOneTemplate) + { + needMeasureAll = RecyclingTemplate == RecyclingTemplate.Disabled || + ItemSizingStrategy == ItemSizingStrategy.MeasureAllItems || + (ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem + && columnsCount != Split) + || !(ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem + && firstCell != null); + } + if (!DynamicColumns && columnsCount < Split) { columnsCount = Split; @@ -518,7 +529,19 @@ public virtual ScaledSize MeasureStack(SKRect rectForChildrenPixels, float scale if (useOneTemplate) { - ChildrenFactory.ReleaseView(template); + if (standalone) + ChildrenFactory.ReleaseTemplateInstance(template); + else + ChildrenFactory.ReleaseView(template); + } + + if (HorizontalOptions.Alignment == LayoutAlignment.Fill && WidthRequest < 0) + { + stackWidth = rectForChildrenPixels.Width; + } + if (VerticalOptions.Alignment == LayoutAlignment.Fill && HeightRequest < 0) + { + stackHeight = rectForChildrenPixels.Height; } return ScaledSize.FromPixels(stackWidth, stackHeight, scale); diff --git a/src/Engine/Draw/Layout/SkiaLayout.ViewsAdapter.cs b/src/Engine/Draw/Layout/SkiaLayout.ViewsAdapter.cs index e4181b3..fb205ee 100644 --- a/src/Engine/Draw/Layout/SkiaLayout.ViewsAdapter.cs +++ b/src/Engine/Draw/Layout/SkiaLayout.ViewsAdapter.cs @@ -1,14 +1,13 @@ using System.Collections; namespace DrawnUi.Maui.Draw; - /// /// Top level class for working with ItemTemplates. Holds visible views. /// public class ViewsAdapter : IDisposable { public static bool LogEnabled = false; - + public bool IsDisposed; private readonly SkiaLayout _parent; public ViewsAdapter(SkiaLayout parent) @@ -19,6 +18,8 @@ public ViewsAdapter(SkiaLayout parent) public void Dispose() { DisposeViews(); + + IsDisposed = true; } object lockVisible = new(); @@ -27,6 +28,9 @@ protected void UpdateVisibleViews() { lock (lockVisible) { + if (IsDisposed) + return; + foreach (var view in _dicoCellsInUse.Values.ToList()) { view.InvalidateInternal(); @@ -39,6 +43,9 @@ protected void DisposeVisibleViews() { lock (lockVisible) { + if (IsDisposed) + return; + foreach (var view in _dicoCellsInUse.Values) { view.Dispose(); @@ -49,6 +56,9 @@ protected void DisposeVisibleViews() protected void DisposeViews() { + if (IsDisposed) + return; + _templatedViewsPool?.Dispose(); DisposeVisibleViews(); } @@ -76,6 +86,9 @@ public void MarkViewAsHidden(int index) { lock (lockVisible) { + if (IsDisposed) + return; + if (_parent.IsTemplated && _parent.RecyclingTemplate == RecyclingTemplate.Enabled) { if (_dicoCellsInUse.ContainsKey(index)) @@ -104,6 +117,9 @@ protected virtual void AttachView(SkiaControl view, int index) //todo check how it behaves when sources changes //lock (_lockTemplates) { + if (IsDisposed) + return; + view.Parent = _parent; if (index == 0 || view.ContextIndex != index) //if (view.BindingContext == null || _parent.RecyclingTemplate == RecyclingTemplate.Enabled) @@ -138,6 +154,9 @@ protected virtual void AttachView(SkiaControl view, int index) /// public void AddMoreToPool(int oversize) { + if (IsDisposed) + return; + if (_templatedViewsPool != null && AddedMore < oversize) { var add = oversize - AddedMore; @@ -160,6 +179,10 @@ public void AddMoreToPool(int oversize) /// public void FillPool(int size, IList context) { + if (IsDisposed) + return; + + FillPool(size); if (context == null) @@ -177,6 +200,9 @@ public void FillPool(int size, IList context) /// public void FillPool(int size) { + if (IsDisposed) + return; + if (size > 0) { while (_templatedViewsPool.Size < size && _templatedViewsPool.Size < _templatedViewsPool.MaxSize) @@ -193,6 +219,9 @@ public string GetDebugInfo() public SkiaControl GetChildAt(int index, SkiaControl template = null) { + if (IsDisposed) + return null; + if (index >= 0) { //lock (lockVisible) @@ -200,26 +229,26 @@ public SkiaControl GetChildAt(int index, SkiaControl template = null) if (_parent.IsTemplated) { - if (_dicoCellsInUse.TryGetValue(index, out SkiaControl ready)) + if (template == null && _dicoCellsInUse.TryGetValue(index, out SkiaControl ready)) { if (LogEnabled) { Trace.WriteLine($"[ViewsAdapter] {_parent.Tag} returned a ready view {ready.Uid}"); } - //#if DEBUG - // try - // { - // if (!object.Equals(_dataContexts[index], ready.BindingContext)) - // { - // Trace.WriteLine($"[ViewsAdapter] {_parent.Tag} ready view has different context!"); - // } - // } - // catch (Exception e) - // { - // Trace.WriteLine(e); - // } - //#endif +#if DEBUG + //try + //{ + // if (!object.Equals(_dataContexts[index], ready.BindingContext)) + // { + // Trace.WriteLine($"[ViewsAdapter] {_parent.Tag} ready view has different context!"); + // } + //} + //catch (Exception e) + //{ + // Trace.WriteLine(e); + //} +#endif AttachView(ready, index); return ready; @@ -232,6 +261,20 @@ public SkiaControl GetChildAt(int index, SkiaControl template = null) return null; } +#if DEBUG + //try + //{ + // if (!object.Equals(_dataContexts[index], view.BindingContext)) + // { + // Trace.WriteLine($"[ViewsAdapter] {_parent.Tag} ready view has different context!"); + // } + //} + //catch (Exception e) + //{ + // Trace.WriteLine(e); + //} +#endif + AttachView(view, index); //save visible view for future use only if template is not provided @@ -251,7 +294,6 @@ public SkiaControl GetChildAt(int index, SkiaControl template = null) } return view; - } else { @@ -269,6 +311,10 @@ public SkiaControl GetChildAt(int index, SkiaControl template = null) public int GetChildrenCount() { + if (IsDisposed) + return 0; + + if (!_parent.IsTemplated) { var children = _parent.GetUnorderedSubviews(); @@ -335,6 +381,7 @@ public int PoolSize private float _forScale; private int _forSplit; + /// /// Main method to initialize templates, can use InitializeTemplatesInBackground as an option. /// @@ -344,6 +391,10 @@ public int PoolSize /// Pre-create number of views to avoid lag spikes later, useful to do in backgound. public void InitializeTemplates(Func template, IList dataContexts, int poolSize, int reserve = 0) { + if (IsDisposed || _parent != null && _parent.IsDisposing) + return; + + { //Debug.WriteLine("[CELLS] InitializeTemplates"); if (template == null) @@ -357,42 +408,42 @@ async Task InitializeFull(bool measure) { lock (_lockTemplates) { - //lock (_parent.LockMeasure) was needed testing two-threaded rendering - { - TemplesInvalidating = false; - - var kill = _templatedViewsPool; + TemplesInvalidating = false; - _dicoCellsInUse.Clear(); - _templatedViewsPool = new TemplatedViewsPool(template, poolSize); + var kill = _templatedViewsPool; - FillPool(reserve, dataContexts); + _dicoCellsInUse.Clear(); + _templatedViewsPool = new TemplatedViewsPool(template, poolSize, (k) => + { + _parent?.DisposeObject(k); + }); - if (kill != null) - { - //we need a delay here for several threads access, if previous cells are still being drawn. not elegant but.. remains in global todo to find a better way. - Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => - { - kill?.Dispose(); - }); - } + FillPool(reserve, dataContexts); - if (measure) - { - //Debug.WriteLine($"Measuring upon InitializeFull for {_parent.Tag} for H: {_parent._lastMeasuredForHeight}"); - //_parent.MeasureLayout(new(_parent._lastMeasuredForWidth, _parent._lastMeasuredForHeight, _parent.RenderingScale), true); - //_parent.Update(); - _parent.Invalidate(); - } + if (kill != null) + { + kill.IsDisposing = true; - TemplatesAvailable(); + _parent?.DisposeObject(kill); } Monitor.PulseAll(_lockTemplates); - } + if (measure) + { + TemplatesBusy = true; + + while (_parent.IsMeasuring) + { + await Task.Delay(10); + } + _dataContexts = dataContexts; + TemplatesInvalidated = false; //enable TemplatesAvailable otherwise beackground measure will fail + _parent.MeasureLayout(new(_parent._lastMeasuredForWidth, _parent._lastMeasuredForHeight, _parent.RenderingScale), true); + } + TemplatesAvailable(); } void InitializeSoft(bool layoutChanged) @@ -449,12 +500,6 @@ bool CheckTemplateChanged() AddedMore = 0; } - if (dataContexts.Count == 0) - { - TemplatesAvailable(); - return; - } - if (_parent.InitializeTemplatesInBackgroundDelay > 0) { //postpone initialization to be executed in background @@ -467,7 +512,7 @@ bool CheckTemplateChanged() try { - await InitializeFull(_parent.RecyclingTemplate == RecyclingTemplate.Disabled || _parent.NeedAutoSize); + await InitializeFull(_parent.RecyclingTemplate == RecyclingTemplate.Disabled); } catch (Exception e) { @@ -479,7 +524,7 @@ bool CheckTemplateChanged() } else { - InitializeFull(false).ConfigureAwait(false); + InitializeFull(false);//.ConfigureAwait(false); } } else @@ -492,7 +537,6 @@ bool CheckTemplateChanged() public void ContextCollectionChanged(Func template, IList dataContexts, int poolSize, int reserve = 0) { - //todo change this!!! //if (_templatedViewsPool == null) { InitializeTemplates(template, dataContexts, poolSize, reserve); @@ -503,7 +547,7 @@ public void ContextCollectionChanged(Func template, IList dataContexts, lock (lockVisible) { - foreach (var view in _dicoCellsInUse.Values) + foreach (var view in _dicoCellsInUse.Values.ToList()) { view.InvalidateChildrenTree(); } @@ -591,6 +635,7 @@ public void DisposeWrapper() public SkiaControl GetViewAtIndex(int index, SkiaControl template = null) { + //lock (_lockTemplates) //to avoid getting same view for different indexes { @@ -638,6 +683,9 @@ public void PrintDebugVisible() public void ReleaseView(SkiaControl viewModel, bool reset = false) { + if (viewModel == null) + return; + //lock (_lockTemplates) { if (reset) @@ -658,46 +706,66 @@ public SkiaControl GetTemplateInstance() throw new InvalidOperationException("Templates have not been initialized."); } - SkiaControl viewModel = _templatedViewsPool.Get(); - + SkiaControl viewModel = _templatedViewsPool.GetStandalone();// .Get(); return viewModel; } } - - - /// - /// Used by ViewsProvider - /// - public class TemplatedViewsPool : IDisposable + public void ReleaseTemplateInstance(SkiaControl viewModel, bool reset = false) { - private readonly Stack _pool; - public Func CreateTemplate { get; protected set; } - public int MaxSize { get; set; } - private readonly object _syncLock = new object(); - private bool _disposed = false; + if (viewModel == null) + return; - public void Dispose() + //lock (_lockTemplates) { - Dispose(true); - //GC.SuppressFinalize(this); + if (reset) + { + viewModel.SetParent(null); + //viewModel.BindingContext = null; + } + _templatedViewsPool.ReturnStandalone(viewModel); } + } + + +} + +/// +/// Used by ViewsProvider +/// +public class TemplatedViewsPool : IDisposable +{ + private readonly Stack _pool; + public Func CreateTemplate { get; protected set; } + public int MaxSize { get; set; } + private readonly object _syncLock = new object(); + public bool _disposed = false; + + public bool IsDisposing; + + public void Dispose() + { + IsDisposing = true; + + Dispose(true); + //GC.SuppressFinalize(this); + } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + lock (_syncLock) { + IsDisposing = true; + if (!_disposed) { if (disposing) { - foreach (var viewModel in _pool) + foreach (var control in _pool) { - if (viewModel != null) + if (control != null) { - viewModel.ContextIndex = -1; - viewModel.BindingContext = null; - if (viewModel is IDisposable disposableViewModel) - { - disposableViewModel.Dispose(); - } + control.ContextIndex = -1; + control.Dispose(); } } } @@ -705,268 +773,329 @@ protected virtual void Dispose(bool disposing) _disposed = true; } } + } + + public TemplatedViewsPool(Func initialViewModel, int maxSize, Action dispose) + { + CreateTemplate = initialViewModel; + MaxSize = maxSize; + _pool = new(); + _dispose = dispose; + } + + public int Size + { + get + { + lock (_syncLock) + { + return _pool.Count; + } + } + } + + /// + /// unsafe + /// + /// + SkiaControl CreateFromTemplate() + { + if (IsDisposing) + return null; - public TemplatedViewsPool(Func initialViewModel, int maxSize) + var create = CreateTemplate(); + + if (ViewsAdapter.LogEnabled) + Trace.WriteLine($"[ViewsAdapter] created new view !"); + + if (create is SkiaControl element) { - CreateTemplate = initialViewModel; - MaxSize = maxSize; - _pool = new(); + return element; } + return (SkiaControl)create; + } - public int Size + /// + /// Just create template and save for the future + /// + public void Reserve() + { + //lock (_syncLock) { - get + if (IsDisposing) + return; + + + if (_pool.Count < MaxSize) { - //lock (_syncLock) + try + { + _pool.Push(CreateFromTemplate()); + } + catch (Exception e) { - return _pool.Count; + Trace.WriteLine(e); + throw; } } } - /// - /// unsafe - /// - /// - SkiaControl CreateFromTemplate() + } + + public SkiaControl GetStandalone() + { + lock (_syncLock) { - var create = CreateTemplate(); + if (IsDisposing) + return null; - if (LogEnabled) - Trace.WriteLine($"[ViewsAdapter] created new view !"); - if (create is SkiaControl element) + if (_standalone != null && _standalone.IsDisposing) + { + _standalone = null; + } + + var ret = _standalone; + if (ret == null) { - return element; + ret = CreateFromTemplate(); } - return (SkiaControl)create; + return ret; } + } - /// - /// Just create template and save for the future - /// - public void Reserve() + public void ReturnStandalone(SkiaControl ret) + { + lock (_syncLock) { - //lock (_syncLock) + if (IsDisposing) + return; + + if (_standalone != null) { - if (_pool.Count < MaxSize) - { - try - { - _pool.Push(CreateFromTemplate()); - } - catch (Exception e) - { - Trace.WriteLine(e); - throw; - } - } - } + var kill = _standalone; + _standalone = null; + _dispose?.Invoke(kill); + } + if (_standalone == null) + { + _standalone = ret; + } } + } + + private SkiaControl _standalone; + private readonly Action _dispose; - public SkiaControl Get() + public SkiaControl Get() + { + //lock (_syncLock) { - //lock (_syncLock) - { - SkiaControl viewModel = null; + if (IsDisposing) + return null; - try - { - if (_pool.Count > 0) - viewModel = _pool.Pop(); - if (viewModel != null) - { - return viewModel; - } - if (_pool.Count < MaxSize) - { - return CreateFromTemplate(); - } + SkiaControl viewModel = null; - // Wait and try again if the pool is full + try + { + if (_pool.Count > 0) viewModel = _pool.Pop(); - while (viewModel != null) - { - System.Threading.Thread.Sleep(10); //todo add cancellation - viewModel = _pool.Pop(); - } - } - catch (Exception e) + if (viewModel != null) { - Super.Log(e); - throw e; + return viewModel; } + if (_pool.Count < MaxSize) + { + return CreateFromTemplate(); + } - - return viewModel; + // Wait and try again if the pool is full + viewModel = _pool.Pop(); + while (viewModel != null) + { + System.Threading.Thread.Sleep(10); //todo add cancellation + viewModel = _pool.Pop(); + } + } + catch (Exception e) + { + Super.Log(e); + throw e; } + + return viewModel; } - public void Return(SkiaControl viewModel) - { - //lock (_syncLock) - { - _pool.Push(viewModel); - } - } } - /// - /// To iterate over virtual views - /// - public class ViewsIterator : IEnumerable, IDisposable + public void Return(SkiaControl viewModel) { - private TemplatedViewsPool _templatedViewsPool; - private IList _dataContexts; - private IEnumerable _views; - private DataContextIterator _iterator; - - public bool IsTemplated { get; } + //lock (_syncLock) + { + if (IsDisposing) + return; - public TemplatedViewsPool TemplatedViewsPool => _templatedViewsPool; - public IList DataContexts => _dataContexts; - public IEnumerable Views => _views; - public ViewsIterator(TemplatedViewsPool templatedViewsPool, IList dataContexts) - { - _templatedViewsPool = templatedViewsPool; - _dataContexts = dataContexts; - IsTemplated = true; + _pool.Push(viewModel); } + } +} - public void SetViews(IEnumerable views) - { - _views = views; - } - public ViewsIterator(IEnumerable views) - { - _views = views; - IsTemplated = false; - } +/// +/// To iterate over virtual views +/// +public class ViewsIterator : IEnumerable, IDisposable +{ + private TemplatedViewsPool _templatedViewsPool; + private IList _dataContexts; + private IEnumerable _views; + private DataContextIterator _iterator; - public IEnumerator GetEnumerator() - { - _iterator = new DataContextIterator(this); - return _iterator; - } + public bool IsTemplated { get; } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + public TemplatedViewsPool TemplatedViewsPool => _templatedViewsPool; + public IList DataContexts => _dataContexts; + public IEnumerable Views => _views; - public void Dispose() - { - if (_iterator != null) - { - _iterator.Dispose(); - _iterator = null; - } - } + public ViewsIterator(TemplatedViewsPool templatedViewsPool, IList dataContexts) + { + _templatedViewsPool = templatedViewsPool; + _dataContexts = dataContexts; + IsTemplated = true; } + public void SetViews(IEnumerable views) + { + _views = views; + } - public class DataContextIterator : IEnumerator + public ViewsIterator(IEnumerable views) { - private readonly ViewsIterator _viewsProvider; - private int _currentIndex; - private SkiaControl _currentViewModel; - private IEnumerator _viewEnumerator; + _views = views; + IsTemplated = false; + } - //added this to use more that 1 view at a time - private readonly Queue _viewsInUse; + public IEnumerator GetEnumerator() + { + _iterator = new DataContextIterator(this); + return _iterator; + } - public DataContextIterator(ViewsIterator viewsProvider) - { - _viewsProvider = viewsProvider; - _currentIndex = -1; - _viewsInUse = new Queue(); + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } - if (!_viewsProvider.IsTemplated) - { - _viewEnumerator = _viewsProvider.Views.GetEnumerator(); - } + public void Dispose() + { + if (_iterator != null) + { + _iterator.Dispose(); + _iterator = null; } + } +} - public SkiaControl Current => _currentViewModel; - object IEnumerator.Current => Current; +public class DataContextIterator : IEnumerator +{ + private readonly ViewsIterator _viewsProvider; + private int _currentIndex; + private SkiaControl _currentViewModel; + private IEnumerator _viewEnumerator; - public bool MoveNext() - { - if (_viewsProvider.IsTemplated) - { + //added this to use more that 1 view at a time + private readonly Queue _viewsInUse; - // Dequeue and return the oldest view if we're at capacity. - if (_viewsInUse.Count >= _viewsProvider.TemplatedViewsPool.MaxSize) - { - var oldestView = _viewsInUse.Dequeue(); + public DataContextIterator(ViewsIterator viewsProvider) + { + _viewsProvider = viewsProvider; + _currentIndex = -1; + _viewsInUse = new Queue(); - _viewsProvider.TemplatedViewsPool.Return(oldestView); - } + if (!_viewsProvider.IsTemplated) + { + _viewEnumerator = _viewsProvider.Views.GetEnumerator(); + } + } - _currentIndex++; + public SkiaControl Current => _currentViewModel; - if (_currentIndex < _viewsProvider.DataContexts.Count) - { - _currentViewModel = _viewsProvider.TemplatedViewsPool.Get(); + object IEnumerator.Current => Current; + + public bool MoveNext() + { + if (_viewsProvider.IsTemplated) + { - _viewsInUse.Enqueue(_currentViewModel); // Keep track of the views in use. + // Dequeue and return the oldest view if we're at capacity. + if (_viewsInUse.Count >= _viewsProvider.TemplatedViewsPool.MaxSize) + { + var oldestView = _viewsInUse.Dequeue(); - _currentViewModel.ContextIndex = _currentIndex; - _currentViewModel.BindingContext = _viewsProvider.DataContexts[_currentIndex]; - return true; - } + _viewsProvider.TemplatedViewsPool.Return(oldestView); + } + _currentIndex++; + if (_currentIndex < _viewsProvider.DataContexts.Count) + { + _currentViewModel = _viewsProvider.TemplatedViewsPool.Get(); + _viewsInUse.Enqueue(_currentViewModel); // Keep track of the views in use. - return false; + _currentViewModel.ContextIndex = _currentIndex; + _currentViewModel.BindingContext = _viewsProvider.DataContexts[_currentIndex]; + return true; } - bool hasMore = _viewEnumerator.MoveNext(); - if (hasMore) - { - _currentViewModel = _viewEnumerator.Current; - } - return hasMore; + + + return false; } - public void Reset() + bool hasMore = _viewEnumerator.MoveNext(); + if (hasMore) { - if (_viewsProvider.IsTemplated) - { - if (_currentIndex >= 0 && _currentIndex < _viewsProvider.DataContexts.Count) - { - _viewsProvider.TemplatedViewsPool.Return(_currentViewModel); - } - - _currentIndex = -1; - } - else - { - _viewEnumerator.Reset(); - } + _currentViewModel = _viewEnumerator.Current; } + return hasMore; + + } - public void Dispose() + public void Reset() + { + if (_viewsProvider.IsTemplated) { - if (_viewsProvider.IsTemplated && _currentIndex >= 0 && _currentIndex < _viewsProvider.DataContexts.Count) + if (_currentIndex >= 0 && _currentIndex < _viewsProvider.DataContexts.Count) { _viewsProvider.TemplatedViewsPool.Return(_currentViewModel); } + + _currentIndex = -1; + } + else + { + _viewEnumerator.Reset(); } } - + public void Dispose() + { + if (_viewsProvider.IsTemplated && _currentIndex >= 0 && _currentIndex < _viewsProvider.DataContexts.Count) + { + _viewsProvider.TemplatedViewsPool.Return(_currentViewModel); + } + } } + diff --git a/src/Engine/Draw/Layout/SkiaLayout.cs b/src/Engine/Draw/Layout/SkiaLayout.cs index 6e78b40..d7b8753 100644 --- a/src/Engine/Draw/Layout/SkiaLayout.cs +++ b/src/Engine/Draw/Layout/SkiaLayout.cs @@ -686,6 +686,7 @@ public override ScaledSize MeasureAbsolute(SKRect rectForChildrenPixels, float s var maxHeight = 0.0f; var maxWidth = 0.0f; + bool standalone = false; if (!ChildrenFactory.TemplatesAvailable) { return ScaledSize.CreateEmpty(scale); @@ -694,6 +695,7 @@ public override ScaledSize MeasureAbsolute(SKRect rectForChildrenPixels, float s { if (this.ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem) { + standalone = true; var template = ChildrenFactory.GetTemplateInstance(); var child = ChildrenFactory.GetChildAt(0, template); @@ -716,7 +718,10 @@ public override ScaledSize MeasureAbsolute(SKRect rectForChildrenPixels, float s maxHeight = measured.Pixels.Height; } - ChildrenFactory.ReleaseView(template); + if (standalone) + ChildrenFactory.ReleaseTemplateInstance(template); + else + ChildrenFactory.ReleaseView(template); } else if (this.ItemSizingStrategy == ItemSizingStrategy.MeasureAllItems diff --git a/src/Engine/Draw/Layout/StackLayoutStructure.cs b/src/Engine/Draw/Layout/StackLayoutStructure.cs index b0d8ade..111d217 100644 --- a/src/Engine/Draw/Layout/StackLayoutStructure.cs +++ b/src/Engine/Draw/Layout/StackLayoutStructure.cs @@ -15,12 +15,13 @@ public StackLayoutStructure(SkiaLayout layout) public virtual IEnumerable EnumerateViewsForMeasurement() { + bool standalone = false; SkiaControl template = null; IReadOnlyList views = null; - bool useOneTemplate = _layout.IsTemplated && - //ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem && - _layout.RecyclingTemplate == RecyclingTemplate.Enabled; + bool useOneTemplate = _layout.IsTemplated; + //ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem && + //&& _layout.RecyclingTemplate == RecyclingTemplate.Enabled; if (_layout.IsTemplated) @@ -28,6 +29,7 @@ public virtual IEnumerable EnumerateViewsForMeasurement() //in the other case template will be null and views adapter will get us a fresh template if (useOneTemplate) { + standalone = true; template = _layout.ChildrenFactory.GetTemplateInstance(); } ChildrenCount = _layout.ChildrenFactory.GetChildrenCount(); @@ -58,7 +60,10 @@ public virtual IEnumerable EnumerateViewsForMeasurement() if (useOneTemplate) { - _layout.ChildrenFactory.ReleaseView(template); + if (standalone) + _layout.ChildrenFactory.ReleaseTemplateInstance(template); + else + _layout.ChildrenFactory.ReleaseView(template); } } @@ -156,4 +161,4 @@ public virtual ControlInStack CreateWrapper(int i, SkiaControl control) return add; } -} \ No newline at end of file +} diff --git a/src/Engine/Draw/Text/SkiaLabel.cs b/src/Engine/Draw/Text/SkiaLabel.cs index 1664225..f217916 100644 --- a/src/Engine/Draw/Text/SkiaLabel.cs +++ b/src/Engine/Draw/Text/SkiaLabel.cs @@ -84,7 +84,7 @@ void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) bo.PropertyChanged -= OnItemPropertyChanged; if (newItems == null || newItems != null && !newItems.Contains(bo)) { - bo.Dispose(); + DisposeObject(bo); } } } @@ -621,7 +621,7 @@ public override void OnDisposing() _spans.CollectionChanged -= OnCollectionChanged; foreach (var span in _spans) { - span.Dispose(); + DisposeObject(span); } _spans.Clear(); } diff --git a/src/Engine/DrawnUi.Maui.csproj b/src/Engine/DrawnUi.Maui.csproj index c503e4b..0b51f62 100644 --- a/src/Engine/DrawnUi.Maui.csproj +++ b/src/Engine/DrawnUi.Maui.csproj @@ -1,6 +1,6 @@  - net8.0;net8.0-android;net8.0-ios;net8.0-maccatalyst + net8.0;net8.0-android;net8.0-ios17.0;net8.0-maccatalyst17.0 $(TargetFrameworks);net8.0-windows10.0.19041.0 true true @@ -97,7 +97,7 @@ - + diff --git a/src/Engine/Internals/Enums/GradientType.cs b/src/Engine/Internals/Enums/GradientType.cs index 9c3adc9..bb9d4af 100644 --- a/src/Engine/Internals/Enums/GradientType.cs +++ b/src/Engine/Internals/Enums/GradientType.cs @@ -8,4 +8,4 @@ public enum GradientType Oval, Sweep, Conical -} \ No newline at end of file +} diff --git a/src/Engine/Internals/Enums/ObjectAliveType.cs b/src/Engine/Internals/Enums/ObjectAliveType.cs new file mode 100644 index 0000000..0e96d08 --- /dev/null +++ b/src/Engine/Internals/Enums/ObjectAliveType.cs @@ -0,0 +1,10 @@ +namespace DrawnUi.Maui.Draw +{ + public enum ObjectAliveType + { + Alive = 0, + ToBeKilled = 1, + BeingDisposed = 2, + Disposed = 3 + } +} diff --git a/src/Engine/Views/Canvas.cs b/src/Engine/Views/Canvas.cs index 24fd1da..48ee69b 100644 --- a/src/Engine/Views/Canvas.cs +++ b/src/Engine/Views/Canvas.cs @@ -349,10 +349,23 @@ protected ScaledSize MeasureChild(SkiaControl child, double availableWidth, doub #endregion + protected override void OnHandlerChanged() + { + base.OnHandlerChanged(); + + if (Handler!=null) + { + OnGesturesAttachChanged(); + } + } + #region GESTURES protected virtual void OnGesturesAttachChanged() { + if (Handler==null) + return; + if (this.Gestures == GesturesMode.Disabled) { TouchEffect.SetForceAttach(this, false); diff --git a/src/Engine/Views/DrawnView.cs b/src/Engine/Views/DrawnView.cs index 310843a..10ee253 100644 --- a/src/Engine/Views/DrawnView.cs +++ b/src/Engine/Views/DrawnView.cs @@ -1,4 +1,5 @@ -using System.Collections.ObjectModel; +using System.Collections.Concurrent; +using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Runtime.CompilerServices; @@ -72,8 +73,6 @@ public bool IsDirty bool _isDirty; - public Queue ToBeDisposed { get; } = new(); - public virtual bool IsVisibleInViewTree() { return IsVisible; //this is used by animators, do not make this heavier! @@ -804,14 +803,14 @@ public void Dispose() IsDisposed = true; + DisposeManager.Dispose(); + Parent = null; PaintSystem?.Dispose(); DestroySkiaView(); - DisposeDisposables(); - GestureListeners.Clear(); ClearChildren(); @@ -1514,21 +1513,200 @@ public double CanvasFps long renderedFrames; - public virtual void DisposeDisposables() + #region DISPOSE STUFF + + public void DisposeObject(IDisposable resource) { - try + if (this.IsDisposed) + return; + + DisposeManager.EnqueueDisposable(resource); + } + + protected DisposableManager DisposeManager { get; } = new(3.5); + + public readonly struct TimedDisposable : IDisposable + { + public IDisposable Disposable { get; } + public DateTime EnqueuedTime { get; } + + public TimedDisposable(IDisposable disposable) + { + Disposable = disposable ?? throw new ArgumentNullException(nameof(disposable)); + EnqueuedTime = DateTime.UtcNow; + } + + public void Dispose() + { + Disposable.Dispose(); + } + } + + public class DisposableManager : IDisposable + { + private readonly ConcurrentQueue _toBeDisposed = new ConcurrentQueue(); + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private readonly Task _disposalTask; + private readonly double _disposeDelaySeconds; + private bool _disposed = false; + + /// + /// Initializes a new instance of the DisposableManager class. + /// + /// The delay in seconds before an IDisposable is disposed after being enqueued. Default is 3.5 seconds. + public DisposableManager(double disposeDelaySeconds = 3.5) + { + if (disposeDelaySeconds <= 0) + throw new ArgumentOutOfRangeException(nameof(disposeDelaySeconds), "Dispose delay must be positive."); + + _disposeDelaySeconds = disposeDelaySeconds; + _disposalTask = Task.Run(() => PeriodicDisposeAsync(_cancellationTokenSource.Token)); + } + + /// + /// Enqueues an IDisposable object with the current timestamp. + /// + /// The IDisposable object to enqueue. + public void EnqueueDisposable(IDisposable disposable) + { + if (disposable == null) + return; + + var timedDisposable = new TimedDisposable(disposable); + _toBeDisposed.Enqueue(timedDisposable); + } + + /// + /// Periodically disposes of eligible disposables. + /// + /// Cancellation token to stop the disposal loop. + private async Task PeriodicDisposeAsync(CancellationToken cancellationToken) { - while (ToBeDisposed.TryDequeue(out var disposable)) + try + { + while (!cancellationToken.IsCancellationRequested) + { + DisposeDisposables(); + await Task.Delay(500, cancellationToken).ConfigureAwait(false); + } + } + catch (TaskCanceledException) { - disposable?.Dispose(); + // Expected when cancellationToken is canceled + } + catch (Exception ex) + { + // Log unexpected exceptions + LogError(ex); } } - catch (Exception e) + + /// + /// Disposes of all IDisposable objects that have been in the queue for more than the specified delay. + /// + private void DisposeDisposables() { - Super.Log(e); + DateTime now = DateTime.UtcNow; + + while (_toBeDisposed.TryPeek(out var timedDisposable)) + { + var elapsed = now - timedDisposable.EnqueuedTime; + if (elapsed.TotalSeconds >= _disposeDelaySeconds) + { + if (_toBeDisposed.TryDequeue(out var disposableToDispose)) + { + try + { + disposableToDispose.Dispose(); + } + catch (Exception ex) + { + // Log the exception and continue disposing other items + LogError(ex); + } + } + } + else + { + // Since the queue is FIFO, no need to check further + break; + } + } + } + + + private void LogError(Exception ex) + { + Super.Log(ex); + } + + /// + /// Disposes all remaining disposables immediately and stops the periodic disposal task. + /// + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + + // Cancel the disposal loop + _cancellationTokenSource.Cancel(); + + try + { + // Wait for the disposal task to complete + _disposalTask.Wait(); + } + catch (AggregateException ae) + { + ae.Handle(ex => + { + LogError(ex); + return true; + }); + } + + // Dispose remaining disposables + DisposeAllRemaining(); + + // Dispose the cancellation token source + _cancellationTokenSource.Dispose(); + } + + /// + /// Disposes all remaining IDisposable objects in the queue. + /// + private void DisposeAllRemaining() + { + List remainingDisposables = new List(); + + while (_toBeDisposed.TryDequeue(out var timedDisposable)) + { + remainingDisposables.Add(timedDisposable); + } + + foreach (var disposable in remainingDisposables) + { + try + { + disposable.Dispose(); + } + catch (Exception ex) + { + // Log the exception and continue disposing other items + LogError(ex); + } + } } } + + + #endregion + + + public void PostponeInvalidation(SkiaControl key, Action action) { lock (_lockInvalidations) @@ -1698,8 +1876,7 @@ protected virtual void Draw(SkiaDrawingContext context, SKRect destination, floa ++renderedFrames; //Debug.WriteLine($"[DRAW] {Tag}"); - - DisposeDisposables(); + if (IsDisposed || UpdateLocked) { diff --git a/src/samples/Sandbox/Game/SpaceShooter.BulletSprite.cs b/src/samples/Sandbox/Game/SpaceShooter.BulletSprite.cs index d0bd822..bb4a7b4 100644 --- a/src/samples/Sandbox/Game/SpaceShooter.BulletSprite.cs +++ b/src/samples/Sandbox/Game/SpaceShooter.BulletSprite.cs @@ -1,6 +1,8 @@ // NOTE: Parts of the code below are based on // https://www.mooict.com/wpf-c-tutorial-create-a-space-battle-shooter-game-in-visual-studio/7/ + + namespace SpaceShooter.Game; public partial class SpaceShooter @@ -62,4 +64,4 @@ public void UpdatePosition(float deltaTime) TranslationY -= SpeedRatio * Speed * deltaTime; } } -} \ No newline at end of file +} diff --git a/src/samples/Sandbox/MainPageCodeForVideo.cs b/src/samples/Sandbox/MainPageCodeForVideo.cs index affa8bf..98a63ce 100644 --- a/src/samples/Sandbox/MainPageCodeForVideo.cs +++ b/src/samples/Sandbox/MainPageCodeForVideo.cs @@ -92,7 +92,7 @@ void Build() var shadow = c.Shadows[0]; shadow.X = 0; - shadow.X = 0; + shadow.Y = 0; c.BackgroundColor = Colors.Blue; @@ -109,7 +109,7 @@ void Build() var shadow = c.Shadows[0]; shadow.X = 2; - shadow.X = 2; + shadow.Y = 2; } if (args.Type == TouchActionResult.Tapped) diff --git a/src/samples/Sandbox/MainPagePdfFixes.xaml b/src/samples/Sandbox/MainPagePdfFixes.xaml new file mode 100644 index 0000000..8bb99da --- /dev/null +++ b/src/samples/Sandbox/MainPagePdfFixes.xaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/samples/Sandbox/MainPagePdfFixes.xaml.cs b/src/samples/Sandbox/MainPagePdfFixes.xaml.cs new file mode 100644 index 0000000..046c2fd --- /dev/null +++ b/src/samples/Sandbox/MainPagePdfFixes.xaml.cs @@ -0,0 +1,20 @@ +namespace Sandbox; + +public partial class MainPagePdfFixes +{ + public MainPagePdfFixes() + { + try + { + InitializeComponent(); + } + catch (Exception e) + { + Super.DisplayException(this, e); + } + + } + + + +} diff --git a/src/samples/Sandbox/MauiProgram.cs b/src/samples/Sandbox/MauiProgram.cs index 7e66947..3f31bd4 100644 --- a/src/samples/Sandbox/MauiProgram.cs +++ b/src/samples/Sandbox/MauiProgram.cs @@ -1,5 +1,6 @@ global using DrawnUi.Maui.Draw; global using SkiaSharp; +using AppoMobi.Maui.Gestures; using CommunityToolkit.Maui; using Microsoft.Extensions.Logging; @@ -7,7 +8,6 @@ namespace Sandbox { public static class MauiProgram { - //public static string RTL = "لمصممة"; public static string RTL = "مرحبًا بكم في عصر العناصر البصرية المصممة\r\nحيث الدقة تلتقي بالإبداع في كل تفصيل\r\nنقدم لكم تجربة مستخدم فريدة ومتطورة\r\nاستمتع بالسلاسة والأداء العالي في التصميمات\r\nنحن نبتكر لنجعل تجربتكم أكثر تميزًا وسهولة"; public static string Multiline = "This is a single label with a multile text. The label that follows this one will have Spans defined.\r\nAnd a new line comes in. We can adjust space between paragraphs and characters. This text is aligned to Fill Words.\r\nLorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries."; @@ -48,6 +48,7 @@ public static MauiApp CreateMauiApp() }); + builder.UseDrawnUi(new() { UseDesktopKeyboard = true, //will not work with maui shell on apple!! diff --git a/src/samples/Sandbox/Sandbox.csproj b/src/samples/Sandbox/Sandbox.csproj index 93fc025..2ed170b 100644 --- a/src/samples/Sandbox/Sandbox.csproj +++ b/src/samples/Sandbox/Sandbox.csproj @@ -38,7 +38,7 @@ false - Automatic + iPhone Developer