diff --git a/README.md b/README.md index 82b0c868..bb23f4d3 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,11 @@ https://github.com/taublast/DrawnUi.Maui/assets/25801194/3b360229-ce3b-4d33-a85b ## What's new -* `SkiaScroll` fixed scrolling vertically + horizontally at the same time (`Orientation="Both"`). -* `SkiaShell` Navigated and Navigating events now report popups too. -* `SkiaMediaImage` a subclassed `SkiaImage` for displaying any kind of images (image/animated gif/more..) -* `SkiaGif` a dedicated lightweight GIF-player with playback control properties. See Sandbox project. -* Fixed gestures inside `ImageCacheComposite` cache. -* Fixed bug `SkiaShell` navigation gets locked when spamming popups. -* Layout optimizations. -* Nuget 1.2.3.4 +* Breaking ``ISkiaCell`` changed, check out demo app FestCellWithBanner new usage. +* Android loop changed along with its OpenGL renderer. +* Gestures fix: will not trigger Tapped after raised Longpressing. +* Other fixes. +* Nuget 1.2.3.6 ## Demo Apps @@ -595,6 +592,15 @@ It will render a mask over its children when hovered, think of it as an inverted ## Previously +* `SkiaScroll` fixed scrolling vertically + horizontally at the same time (`Orientation="Both"`). +* `SkiaShell` Navigated and Navigating events now report popups too. +* `SkiaMediaImage` a subclassed `SkiaImage` for displaying any kind of images (image/animated gif/more..) +* `SkiaGif` a dedicated lightweight GIF-player with playback control properties. See Sandbox project. +* Fixed gestures inside `ImageCacheComposite` cache. +* Fixed bug `SkiaShell` navigation gets locked when spamming popups. +* Layout optimizations. +* Nuget 1.2.3.4 +* * `SkiaLayout` Column/Row uses 2 layout passes when needed, can now use full alignment options inside. * Critical fixes for Release builds. diff --git a/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj b/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj index 0638ac6c..a8af1595 100644 --- a/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj +++ b/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj @@ -43,7 +43,7 @@ - + \ No newline at end of file diff --git a/src/Addons/DrawnUi.Maui.Camera/README.md b/src/Addons/DrawnUi.Maui.Camera/README.md new file mode 100644 index 00000000..4fa92bad --- /dev/null +++ b/src/Addons/DrawnUi.Maui.Camera/README.md @@ -0,0 +1,4 @@ + +Only Android is actually implemented. + +When i have spare time, hopefully soon.. \ 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 47bf2d87..00fbff4e 100644 --- a/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj +++ b/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj @@ -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 20305346..7596db34 100644 --- a/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj +++ b/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj @@ -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 2a4ec53d..f10b34eb 100644 --- a/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj +++ b/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj @@ -47,7 +47,7 @@ - + diff --git a/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj b/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj index 56f6f9c3..2626989b 100644 --- a/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj +++ b/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj @@ -41,7 +41,7 @@ - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 1eab784b..502bc9c5 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ Using SkiaSharp 2.xx. Checkout the DrawnUi Sandbox project for usage example. - 1.2.3.4 + 1.2.3.6 diff --git a/src/Engine/Controls/Button/SkiaButton.cs b/src/Engine/Controls/Button/SkiaButton.cs index 2ebf9a1e..97af8a62 100644 --- a/src/Engine/Controls/Button/SkiaButton.cs +++ b/src/Engine/Controls/Button/SkiaButton.cs @@ -78,7 +78,7 @@ protected override void CreateDefaultContent() /// Clip effects with rounded rect of the frame inside /// /// - public override SKPath CreateClip(object arguments, bool usePosition) + public override SKPath CreateClip(object arguments, bool usePosition, SKPath path = null) { if (MainFrame != null) { diff --git a/src/Engine/Controls/Layouts/SkiaDecoratedGrid.cs b/src/Engine/Controls/Layouts/SkiaDecoratedGrid.cs index 33df3d21..b396d36b 100644 --- a/src/Engine/Controls/Layouts/SkiaDecoratedGrid.cs +++ b/src/Engine/Controls/Layouts/SkiaDecoratedGrid.cs @@ -181,20 +181,28 @@ protected override void OnLayoutChanged() base.OnLayoutChanged(); UpdateLines(); + } + + protected override void PostArrange(SKRect destination, float widthRequest, float heightRequest, float scale) + { + base.PostArrange(destination, widthRequest, heightRequest, scale); + if (ContainerLines == null) + { + CreateLines(); + } } protected override void Draw(SkiaDrawingContext context, SKRect destination, float scale) { base.Draw(context, destination, scale); - //if (ContainerLines == null) - //{ - // CreateLines(); - //} + if (ContainerLines != null) + { ContainerLines.Render(context, GetDrawingRectForChildren(Destination, scale), scale); + } FinalizeDrawingWithRenderObject(context, scale); } diff --git a/src/Engine/Controls/Pickers/ScrollPickerWheel.cs b/src/Engine/Controls/Pickers/ScrollPickerWheel.cs index e5d3d2c9..f32342d9 100644 --- a/src/Engine/Controls/Pickers/ScrollPickerWheel.cs +++ b/src/Engine/Controls/Pickers/ScrollPickerWheel.cs @@ -176,7 +176,7 @@ protected override bool DrawChild(SkiaDrawingContext context, SKRect dest, ISkia var ret = base.DrawChild(context, dest, child, scale); - context.Canvas.Restore(); + context.Canvas.RestoreToCount(saved); return true; } diff --git a/src/Engine/Controls/PlayFrames/AnimatedFramesRenderer.cs b/src/Engine/Controls/PlayFrames/AnimatedFramesRenderer.cs index 725323a2..32128a97 100644 --- a/src/Engine/Controls/PlayFrames/AnimatedFramesRenderer.cs +++ b/src/Engine/Controls/PlayFrames/AnimatedFramesRenderer.cs @@ -127,10 +127,15 @@ public void InitializeAnimator() OnAnimatorInitializing(); - if (AutoPlay && CheckCanStartAnimator()) + if (_delayedPlay || AutoPlay && CheckCanStartAnimator()) + { + _delayedPlay = false; Start(); + } } + bool _delayedPlay; + protected virtual void OnAnimatorInitializing() { } @@ -161,6 +166,13 @@ public void Seek(double frame) public virtual void Start(int delayMs = 0) { + + if (Animator == null) + { + _delayedPlay = true; + return; + } + if (Animator.IsRunning) { Animator.Stop(); diff --git a/src/Engine/Controls/Shapes/SkiaHoverMask.cs b/src/Engine/Controls/Shapes/SkiaHoverMask.cs index 726381ac..f9720c66 100644 --- a/src/Engine/Controls/Shapes/SkiaHoverMask.cs +++ b/src/Engine/Controls/Shapes/SkiaHoverMask.cs @@ -26,7 +26,7 @@ protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float using var clipContent = CreateClip(arguments, true); clipInsideParent.AddPath(clipContent); - ctx.Canvas.ClipPath(clipInsideParent, SKClipOperation.Difference, true); + ClipSmart(ctx.Canvas, clipInsideParent, SKClipOperation.Difference); //paint this taking viewport dimensions ctx.Canvas.DrawRect(Parent.DrawingRect, paint); @@ -35,7 +35,7 @@ protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float //todo add stroke property? - ctx.Canvas.Restore(); + ctx.Canvas.RestoreToCount(saved); } } diff --git a/src/Engine/Draw/Layout/SkiaLayout.ViewsAdapter.cs b/src/Engine/Draw/Layout/SkiaLayout.ViewsAdapter.cs index 7118b522..4cc40c78 100644 --- a/src/Engine/Draw/Layout/SkiaLayout.ViewsAdapter.cs +++ b/src/Engine/Draw/Layout/SkiaLayout.ViewsAdapter.cs @@ -86,10 +86,10 @@ public void MarkViewAsHidden(int index) { if (_dicoCellsInUse.TryGetValue(index, out SkiaControl hiddenView)) { - if (hiddenView is ISkiaCell notify) - { - notify.OnDisappeared(); - } + //if (hiddenView is ISkiaCell notify) + //{ + // notify.OnDisappeared(); + //} _dicoCellsInUse.Remove(index); ReleaseView(hiddenView); @@ -248,10 +248,10 @@ public SkiaControl GetChildAt(int index, SkiaControl template = null) //Debug.WriteLine($"[InUse] {_dicoCellsInUse.Keys.Select(k => k.ToString()).Aggregate((current, next) => $"{current},{next}")}"); - if (view is ISkiaCell notify) - { - notify.OnAppearing(); - } + //if (view is ISkiaCell notify) + //{ + // notify.OnAppearing(); + //} } return view; diff --git a/src/Engine/Draw/Layout/SkiaLayout.Wrap.cs b/src/Engine/Draw/Layout/SkiaLayout.Wrap.cs index 54badbf9..aa21ffd3 100644 --- a/src/Engine/Draw/Layout/SkiaLayout.Wrap.cs +++ b/src/Engine/Draw/Layout/SkiaLayout.Wrap.cs @@ -46,12 +46,12 @@ protected virtual int DrawStack( //PASS 1 - VISIBILITY //we need this pass before drawing to recycle views that became hidden - var viewsTotal = 0; + var currentIndex = -1; foreach (var cell in structure.GetChildrenAsSpans()) { - viewsTotal++; - viewsTotal++; - if (cell.Destination == SKRect.Empty) + currentIndex++; + + if (cell.Destination == SKRect.Empty || cell.Measured.Pixels.IsEmpty) { cell.IsVisible = false; } diff --git a/src/Engine/Draw/Layout/SkiaLayout.cs b/src/Engine/Draw/Layout/SkiaLayout.cs index a15767e7..ef6a5e09 100644 --- a/src/Engine/Draw/Layout/SkiaLayout.cs +++ b/src/Engine/Draw/Layout/SkiaLayout.cs @@ -980,6 +980,10 @@ protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float { SetupRenderingWithComposition(ctx, destination); } + else + { + DirtyChildren.Clear(); + } base.Paint(ctx, destination, scale, arguments); @@ -993,16 +997,10 @@ protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float drawnChildrenCount = DrawViews(ctx, rectForChildren, scale); } else - //grid if (Type == LayoutType.Grid) //todo add optimization for OptimizeRenderingViewport { drawnChildrenCount = DrawChildrenGrid(ctx, rectForChildren, scale); } - //else - //if (Type == LayoutType.Row || Type == LayoutType.Column) - //{ - // drawnChildrenCount = DrawChildrenStack(ctx, rectForChildren, scale); - //} else //stacklayout if (IsStack) @@ -1288,13 +1286,6 @@ private static void ItemsSourcePropertyChanged(BindableObject bindable, object o } - //if (newvalue is IList newList) - //{ - // foreach (var context in newList) - // { - // //todo - // } - //} if (newvalue is INotifyCollectionChanged newCollection) { @@ -1302,9 +1293,7 @@ private static void ItemsSourcePropertyChanged(BindableObject bindable, object o newCollection.CollectionChanged += skiaControl.ItemsSourceCollectionChanged; } - skiaControl.PostponeInvalidation(nameof(OnItemSourceChanged), skiaControl.OnItemSourceChanged); - - //skiaControl.OnItemSourceChanged(); + skiaControl.OnItemSourceChanged(); } private static void NeedUpdateItemsSource(BindableObject bindable, object oldvalue, object newvalue) @@ -1313,8 +1302,8 @@ private static void NeedUpdateItemsSource(BindableObject bindable, object oldval skiaControl.PostponeInvalidation(nameof(UpdateItemsSource), skiaControl.UpdateItemsSource); - //skiaControl.OnItemSourceChanged(); - //skiaControl.Invalidate(); + skiaControl.Update(); + } void UpdateItemsSource() @@ -1326,8 +1315,8 @@ void UpdateItemsSource() public override void OnItemTemplateChanged() { - PostponeInvalidation(nameof(OnItemSourceChanged), OnItemSourceChanged); - //OnItemSourceChanged(); + //PostponeInvalidation(nameof(OnItemSourceChanged), OnItemSourceChanged); + OnItemSourceChanged(); } public bool ApplyNewItemsSource { get; set; } @@ -1516,9 +1505,8 @@ protected virtual void ItemsSourceCollectionChanged(object sender, NotifyCollect GetTemplatesPoolLimit(), GetTemplatesPoolPrefill()); - // Invalidate(); - - // return; + Invalidate(); + return; } break; diff --git a/src/Engine/Draw/Scroll/SkiaScroll.Scrolling.cs b/src/Engine/Draw/Scroll/SkiaScroll.Scrolling.cs index f75f4f37..be81b330 100644 --- a/src/Engine/Draw/Scroll/SkiaScroll.Scrolling.cs +++ b/src/Engine/Draw/Scroll/SkiaScroll.Scrolling.cs @@ -486,6 +486,7 @@ void BounceX(float offsetFrom, float offsetTo, float velocity) if (displacement != 0) { var spring = new Spring((float)(1 * (1 + RubberDamping)), 200, (float)(0.5f * (1 + RubberDamping))); + _animatorFlingX.Stop(); _vectorAnimatorBounceX.Initialize(offsetTo, displacement, velocity, spring); _vectorAnimatorBounceX.Start(); } @@ -505,6 +506,7 @@ void BounceY(float offsetFrom, float offsetTo, float velocity) if (displacement != 0) { + _animatorFlingY.Stop(); var spring = new Spring((float)(1 * (1 + RubberDamping)), 200, (float)(0.5f * (1 + RubberDamping))); _vectorAnimatorBounceY.Initialize(offsetTo, displacement, velocity, spring); _vectorAnimatorBounceY.Start(); diff --git a/src/Engine/Draw/Scroll/SkiaScroll.cs b/src/Engine/Draw/Scroll/SkiaScroll.cs index c1b8e50f..2001e2a2 100644 --- a/src/Engine/Draw/Scroll/SkiaScroll.cs +++ b/src/Engine/Draw/Scroll/SkiaScroll.cs @@ -914,12 +914,14 @@ ISkiaGestureListener PassToChildren() if (Math.Abs(velocityX) > _minVelocity && !bounceX) { IsUserFocused = false; + _vectorAnimatorBounceX.Stop(); fling = StartToFlingFrom(_animatorFlingX, ViewportOffsetX, velocityX); } if (Math.Abs(velocityY) > _minVelocity && !bounceY) { IsUserFocused = false; + _vectorAnimatorBounceY.Stop(); fling = StartToFlingFrom(_animatorFlingY, ViewportOffsetY, velocityY); } } @@ -2615,17 +2617,20 @@ protected override void Draw(SkiaDrawingContext context, SKRect destination, var posX = (float)Math.Round(ViewportOffsetX * _zoomedScale); var posY = (float)Math.Round(ViewportOffsetY * _zoomedScale); + //var posX = (float)(ViewportOffsetX * _zoomedScale); + //var posY = (float)(ViewportOffsetY * _zoomedScale); + var needReposition = _updatedViewportForPixY != posY || _updatedViewportForPixX != posX - || _destination != Destination; + || _destination != destination; //reposition viewport (scroll) if (needReposition) { _updatedViewportForPixX = posX; _updatedViewportForPixY = posY; - _destination = Destination; + _destination = destination; PositionViewport(DrawingRect, new(posX, posY), _zoomedScale, (float)scale); diff --git a/src/Engine/Draw/Scroll/SkiaScrollLooped.cs b/src/Engine/Draw/Scroll/SkiaScrollLooped.cs index df3fe0d9..d137b916 100644 --- a/src/Engine/Draw/Scroll/SkiaScrollLooped.cs +++ b/src/Engine/Draw/Scroll/SkiaScrollLooped.cs @@ -368,7 +368,7 @@ protected override void OnDrawn(SkiaDrawingContext context, DrawViews(context, childRect, zoomedScale, debug); - context.Canvas.Restore(); + context.Canvas.RestoreToCount(count); } else { diff --git a/src/Engine/Draw/SkiaControl.cs b/src/Engine/Draw/SkiaControl.cs index 453cd52b..0a1ba078 100644 --- a/src/Engine/Draw/SkiaControl.cs +++ b/src/Engine/Draw/SkiaControl.cs @@ -32,8 +32,11 @@ namespace DrawnUi.Maui.Draw [DebuggerDisplay("{DebugString}")] [ContentProperty("Children")] public partial class SkiaControl : VisualElement, - IHasAfterEffects, ISkiaControl, - IVisualTreeElement, IReloadHandler, IHotReloadableView + IHasAfterEffects, + ISkiaControl, + IVisualTreeElement, + IReloadHandler, + IHotReloadableView { public SkiaControl() { @@ -1479,6 +1482,21 @@ private void Init() CalculateSizeRequest(); } + public static readonly BindableProperty ClipFromProperty = BindableProperty.Create( + nameof(ClipFrom), + typeof(SkiaControl), + typeof(SkiaControl), + default(SkiaControl), + propertyChanged: OnControlClipFromChanged); + + /// + /// Use clipping area from another control + /// + public new SkiaControl ClipFrom + { + get { return (SkiaControl)GetValue(ClipFromProperty); } + set { SetValue(ClipFromProperty, value); } + } public static readonly BindableProperty ParentProperty = BindableProperty.Create( nameof(Parent), @@ -1538,6 +1556,14 @@ protected virtual void OnParentVisibilityChanged(bool newvalue) Superview?.SetViewTreeVisibilityByParent(this, newvalue); + if (!newvalue) + { + if (this.UsingCacheType == SkiaCacheType.GPU) + { + RenderObject = null; + } + } + if (!IsVisible) { //though shell not pass @@ -1565,6 +1591,10 @@ public virtual void OnVisibilityChanged(bool newvalue) { if (!newvalue) { + if (this.UsingCacheType == SkiaCacheType.GPU) + { + RenderObject = null; + } //DestroyRenderingObject(); } // need to this to: @@ -1591,6 +1621,7 @@ public virtual void OnVisibilityChanged(bool newvalue) /// public virtual void OnDisposing() { + ClippedBy = null; Disposing?.Invoke(this, null); Superview?.UnregisterGestureListener(this as ISkiaGestureListener); Superview?.UnregisterAllAnimatorsByParent(this); @@ -2256,6 +2287,13 @@ public double TransformPivotPointY } + private static void OnControlClipFromChanged(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaControl control) + { + control.ClippedBy = newvalue as SkiaControl; + } + } private static void OnControlParentChanged(BindableObject bindable, object oldvalue, object newvalue) { @@ -2801,6 +2839,7 @@ protected virtual void OnSizeChanged() public Action Clipping { get; set; } + public SkiaControl ClippedBy { get; set; } /// @@ -4332,9 +4371,17 @@ public virtual void OnSuperviewShouldRenderChanged(bool state) { Update(); } - foreach (var view in Views) //will crash? why adapter nor used?? + + try { - view.OnSuperviewShouldRenderChanged(state); + foreach (var view in Views.ToList()) + { + view.OnSuperviewShouldRenderChanged(state); + } + } + catch (Exception e) + { + Super.Log(e); } } @@ -4787,7 +4834,7 @@ public CachedObject RenderObject if (_renderObject != null) //if we already have something in actual cache then { if (UsingCacheType == SkiaCacheType.ImageDoubleBuffered - || UsingCacheType == SkiaCacheType.Image //to just reuse same surface + //|| UsingCacheType == SkiaCacheType.Image //to just reuse same surface || UsingCacheType == SkiaCacheType.ImageComposite) { RenderObjectPrevious = _renderObject; //send it to back for special cases @@ -4860,14 +4907,21 @@ public void DrawWithClipAndTransforms( bool useClipping, Action draw) { - bool isClipping = (WillClipBounds || Clipping != null) && useClipping; + bool isClipping = (WillClipBounds || Clipping != null || ClippedBy != null) && useClipping; if (isClipping) { _preparedClipBounds ??= new SKPath(); _preparedClipBounds.Reset(); - _preparedClipBounds.AddRect(destination); - Clipping?.Invoke(_preparedClipBounds, destination); + if (ClippedBy != null) + { + ClippedBy.CreateClip(null, true, _preparedClipBounds); + } + else + { + _preparedClipBounds.AddRect(destination); + Clipping?.Invoke(_preparedClipBounds, destination); + } } bool applyOpacity = useOpacity && Opacity < 1; @@ -4883,14 +4937,16 @@ public void DrawWithClipAndTransforms( _paintWithOpacity.Color = SKColors.White.WithAlpha((byte)(0xFF * Opacity)); + var restore = 0; + if (applyOpacity || CustomizeLayerPaint != null) { CustomizeLayerPaint?.Invoke(_paintWithOpacity, destination); - ctx.Canvas.SaveLayer(_paintWithOpacity); + restore = ctx.Canvas.SaveLayer(_paintWithOpacity); } else { - ctx.Canvas.Save(); + restore = ctx.Canvas.Save(); } if (needTransform) @@ -4900,11 +4956,12 @@ public void DrawWithClipAndTransforms( if (isClipping) { - ctx.Canvas.ClipPath(_preparedClipBounds, SKClipOperation.Intersect, true); + ClipSmart(ctx.Canvas, _preparedClipBounds); } draw(ctx); - ctx.Canvas.Restore(); + + ctx.Canvas.RestoreToCount(restore); } else { @@ -4975,7 +5032,63 @@ protected virtual void ApplyTransforms(SkiaDrawingContext ctx, SKRect destinatio ctx.Canvas.SetMatrix(drawingMatrix); } + public static bool IsSimpleRectangle(SKPath path) + { + if (path == null) + return false; + + if (path.VerbCount != 5) + return false; + + var iterator = path.CreateRawIterator(); + var points = new SKPoint[4]; + int lineToCount = 0; + bool moveToFound = false; + + SKPathVerb verb; + while ((verb = iterator.Next(points)) != SKPathVerb.Done) + { + switch (verb) + { + case SKPathVerb.Move: + if (moveToFound) + return false; // Multiple MoveTo commands + moveToFound = true; + break; + case SKPathVerb.Line: + if (lineToCount < 4) + { + lineToCount++; + } + else + { + return false; // More than 4 LineTo commands + } + break; + + case SKPathVerb.Close: + return lineToCount == 4; // Ensure we have exactly 4 LineTo commands before Close + + default: + return false; // Any other command invalidates the rectangle check + } + } + + return false; + } + + /// + /// Use antialiasing if path is not rectangle + /// + /// + /// + /// + public static void ClipSmart(SKCanvas canvas, SKPath path, SKClipOperation operation = SKClipOperation.Intersect) + { + bool isRectangle = IsSimpleRectangle(path); + canvas.ClipPath(path, operation, !isRectangle); // Disable anti-aliasing if it's a rectangle + } public virtual bool NeedMeasure @@ -5259,7 +5372,7 @@ public async Task ProcessOffscreenCacheRenderingAsync() } } - if (NeedUpdate) //someone changed us while rendering inner content + if (NeedUpdate || RenderObjectNeedsUpdate) //someone changed us while rendering inner content { Update(); //kick } @@ -5399,7 +5512,7 @@ protected virtual CachedObject CreateRenderingObject( grContext = accelerated.GRContext; //hardware accelerated surface = SKSurface.Create(accelerated.GRContext, - true, + false, cacheSurfaceInfo); } } @@ -5571,7 +5684,8 @@ protected void CreateRenderingObjectAndPaint( oldObject = RenderObject; } else - if (usingCacheType == SkiaCacheType.Image || usingCacheType == SkiaCacheType.ImageComposite) + if (usingCacheType == SkiaCacheType.Image + || usingCacheType == SkiaCacheType.ImageComposite) { oldObject = RenderObjectPrevious; } @@ -5638,6 +5752,9 @@ protected virtual void Paint(SkiaDrawingContext ctx, SKRect destination, float s } private bool _wasDrawn; + /// + /// Signals if this control was drawn on canvas one time at least, it will be set by Paint method. + /// [EditorBrowsable(EditorBrowsableState.Never)] public bool WasDrawn { @@ -5665,9 +5782,10 @@ public bool WasDrawn /// /// /// - public virtual SKPath CreateClip(object arguments, bool usePosition) + public virtual SKPath CreateClip(object arguments, bool usePosition, SKPath path = null) { - var path = new SKPath(); + path ??= new SKPath(); + if (usePosition) { path.AddRect(DrawingRect); @@ -5680,6 +5798,7 @@ public virtual SKPath CreateClip(object arguments, bool usePosition) } private bool _RenderObjectPreviousNeedsUpdate; + [EditorBrowsable(EditorBrowsableState.Never)] public bool RenderObjectPreviousNeedsUpdate { get @@ -5699,6 +5818,7 @@ public bool RenderObjectPreviousNeedsUpdate /// /// Should delete RenderObject when starting new frame rendering /// + [EditorBrowsable(EditorBrowsableState.Never)] public bool RenderObjectNeedsUpdate { get @@ -5734,6 +5854,7 @@ public double UseTranslationX } } + [EditorBrowsable(EditorBrowsableState.Never)] public bool HasTransform { get @@ -5748,6 +5869,7 @@ public bool HasTransform } } + [EditorBrowsable(EditorBrowsableState.Never)] public bool IsDistorted { get @@ -5844,6 +5966,7 @@ protected virtual int RenderViewsList(IEnumerable skiaControls, return count; } + [EditorBrowsable(EditorBrowsableState.Never)] public virtual bool UsesRenderingTree { get @@ -5877,9 +6000,9 @@ public record SkiaControlWithRect(SkiaControl Control, public bool Invalidated { get; set; } = true; /// - /// For internal use + /// For internal use, set by Update method /// - + [EditorBrowsable(EditorBrowsableState.Never)] public bool NeedUpdate { get @@ -5901,10 +6024,11 @@ public bool NeedUpdate - - DrawnView _superview; - + /// + /// Our canvas + /// + [EditorBrowsable(EditorBrowsableState.Never)] public DrawnView Superview { get @@ -5932,6 +6056,10 @@ public DrawnView Superview } } + /// + /// For virtualization + /// + /// public virtual ScaledRect GetOnScreenVisibleArea() { if (this.UsingCacheType != SkiaCacheType.None) @@ -5979,6 +6107,7 @@ public virtual void InvalidateViewport() /// //protected SkiaControl DirtyChild { get; set; } + protected readonly ControlsTracker DirtyChildren = new(); //protected readonly ConcurrentBag DirtyChildren = new(); @@ -5995,6 +6124,7 @@ public virtual void UpdateByChild(SkiaControl control) /// /// Used to check whether to apply IsClippedToBounds property /// + [EditorBrowsable(EditorBrowsableState.Never)] public virtual bool WillClipBounds { get @@ -6004,6 +6134,7 @@ public virtual bool WillClipBounds } } + [EditorBrowsable(EditorBrowsableState.Never)] public virtual bool WillClipEffects { get @@ -6030,6 +6161,9 @@ protected virtual void UpdateInternal() Parent?.UpdateByChild(this); } + /// + /// Main method to invalidate cache and invoke rendering + /// public virtual void Update() { InvalidateCache(); @@ -6039,6 +6173,9 @@ public virtual void Update() Updated?.Invoke(this, null); } + /// + /// Triggered by Update method + /// public event EventHandler Updated; public static MemoryStream StreamFromString(string value) @@ -6056,7 +6193,10 @@ public static double PixelsToDeviceUnits(double units) return units / GetDensity(); } - + /// + /// For internal use + /// + [EditorBrowsable(EditorBrowsableState.Never)] public SKRect Destination { get; protected set; } protected SKPaint PaintSystem { get; set; } @@ -6153,11 +6293,11 @@ protected void ActionWithClipping(SKRect viewport, SKCanvas canvas, Action draw) var saved = canvas.Save(); - canvas.ClipPath(clip, SKClipOperation.Intersect, true); + ClipSmart(canvas, clip); draw(); - canvas.Restore(); + canvas.RestoreToCount(saved); } @@ -6184,7 +6324,9 @@ public virtual void Invalidate() InvalidateParent(); } - + /// + /// Summing up Margins and AddMargin.. properties + /// public virtual void CalculateMargins() { //use Margin property as starting point @@ -6356,6 +6498,10 @@ protected static void NeedInvalidateViewport(BindableObject bindable, object old } } + [EditorBrowsable(EditorBrowsableState.Never)] + /// + /// Is using ItemTemplate or no + /// public virtual bool IsTemplated { get diff --git a/src/Engine/Draw/SkiaShape.cs b/src/Engine/Draw/SkiaShape.cs index 15771852..8693977c 100644 --- a/src/Engine/Draw/SkiaShape.cs +++ b/src/Engine/Draw/SkiaShape.cs @@ -360,6 +360,8 @@ protected void CalculateSizeForStroke(SKRect destination, float scale) protected SKRoundRect DrawRoundedRect { get; set; } + SKPath ClipContentPath { get; set; } = new(); + public override void OnDisposing() { @@ -371,6 +373,7 @@ public override void OnDisposing() DrawPathAligned?.Dispose(); DrawRoundedRect?.Dispose(); DrawPathShape?.Dispose(); + ClipContentPath?.Dispose(); base.OnDisposing(); } @@ -384,8 +387,10 @@ public override object CreatePaintArguments() }; } - public override SKPath CreateClip(object arguments, bool usePosition) + public override SKPath CreateClip(object arguments, bool usePosition, SKPath path = null) { + path ??= new SKPath(); + var strokeAwareSize = MeasuredStrokeAwareSize; var strokeAwareChildrenSize = MeasuredStrokeAwareChildrenSize; @@ -403,8 +408,6 @@ public override SKPath CreateClip(object arguments, bool usePosition) strokeAwareChildrenSize.Width + offsetToZero.X, strokeAwareChildrenSize.Height + offsetToZero.Y); } - var path = new SKPath(); - switch (Type) { case ShapeType.Path: @@ -685,15 +688,16 @@ void PaintWithShadows(Action render) if (ClipBackgroundColor) { - using var clip = new SKPath(); - using var clipContent = CreateClip(arguments, true); - clip.AddPath(clipContent); + ClipContentPath ??= new(); + ClipContentPath.Reset(); + CreateClip(arguments, true, ClipContentPath); + var saved = ctx.Canvas.Save(); - ctx.Canvas.ClipPath(clip, SKClipOperation.Difference, true); + ClipSmart(ctx.Canvas, ClipContentPath, SKClipOperation.Difference); render(); - ctx.Canvas.Restore(); + ctx.Canvas.RestoreToCount(saved); } else { @@ -728,17 +732,20 @@ void PaintWithShadows(Action render) }); //draw children views clipped with shape - using var clip = new SKPath(); - using var clipContent = CreateClip(arguments, true); - clip.AddPath(clipContent); + ClipContentPath ??= new(); + ClipContentPath.Reset(); + + CreateClip(arguments, true, ClipContentPath); + var saved = ctx.Canvas.Save(); - ctx.Canvas.ClipPath(clip, SKClipOperation.Intersect, true); + + ClipSmart(ctx.Canvas, ClipContentPath); var rectForChildren = ContractPixelsRect(strokeAwareChildrenSize, scale, Padding); DrawViews(ctx, rectForChildren, scale); - ctx.Canvas.Restore(); + ctx.Canvas.RestoreToCount(saved); //last pass for stroke over background or children if (willStroke) diff --git a/src/Engine/Draw/Svg/SkiaSvg.cs b/src/Engine/Draw/Svg/SkiaSvg.cs index fa613241..5c925c76 100644 --- a/src/Engine/Draw/Svg/SkiaSvg.cs +++ b/src/Engine/Draw/Svg/SkiaSvg.cs @@ -914,11 +914,11 @@ protected void DrawPicture(SKCanvas canvas, SKPicture picture, SKRect dest, var saved = canvas.Save(); - canvas.ClipPath(path, SKClipOperation.Intersect, true); + ClipSmart(canvas, path); canvas.DrawPicture(picture, display.Left, display.Top, paint); - canvas.Restore(); + canvas.RestoreToCount(saved); } } else @@ -1147,11 +1147,11 @@ protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float } var saved = ctx.Canvas.Save(); - ctx.Canvas.ClipPath(clipPath, SKClipOperation.Intersect, true); + ClipSmart(ctx.Canvas, clipPath); ctx.Canvas.DrawPicture(Svg.Picture, ref matrix, paint); - ctx.Canvas.Restore(); + ctx.Canvas.RestoreToCount(saved); clipPath.Dispose(); } diff --git a/src/Engine/Draw/Text/SkiaLabel.cs b/src/Engine/Draw/Text/SkiaLabel.cs index 7cf3af42..96799008 100644 --- a/src/Engine/Draw/Text/SkiaLabel.cs +++ b/src/Engine/Draw/Text/SkiaLabel.cs @@ -481,6 +481,7 @@ public override ScaledSize Measure(float widthConstraint, float heightConstraint if (PaintDefault.Typeface == null) { + PaintDefault.Typeface = SKTypeface.Default; UpdateFont(); return MeasuredSize; } @@ -498,6 +499,10 @@ public override ScaledSize Measure(float widthConstraint, float heightConstraint UpdateFontMetrics(PaintDefault); var usePaint = PaintDefault; + if (PaintDefault.Typeface == null) + { + PaintDefault.Typeface = SKTypeface.Default; + } if (Spans.Count == 0) { @@ -769,11 +774,18 @@ public override void OnDisposing() private void DisposePaint(ref SKPaint paint) { - if (paint != null) + try + { + if (paint != null) + { + paint.Typeface = SKTypeface.Default; // Preserve cached font from disposing + paint.Dispose(); + paint = null; + } + } + catch (Exception e) { - paint.Typeface = SKTypeface.Default; // Preserve cached font from disposing - paint.Dispose(); - paint = null; + Console.WriteLine(e); } } @@ -1506,6 +1518,10 @@ protected void ReplaceFont() _replaceFont = null; OnFontUpdated(); } + if (TypeFace == null) + { + TypeFace = SKTypeface.Default; + } } protected float _fontUnderline; diff --git a/src/Engine/Draw/Text/TextSpan.cs b/src/Engine/Draw/Text/TextSpan.cs index 788e64d5..7327fe9c 100644 --- a/src/Engine/Draw/Text/TextSpan.cs +++ b/src/Engine/Draw/Text/TextSpan.cs @@ -182,9 +182,11 @@ public SKTypeface TypeFace get => _typeFace; set { - if (Equals(value, _typeFace)) return; - _typeFace = value; - OnPropertyChanged(); + if (_typeFace != value) + { + _typeFace = value; + OnPropertyChanged(); + } } } @@ -334,13 +336,21 @@ public virtual void Dispose() { Parent = null; - if (Paint != null) + try { - Paint.Typeface = SKTypeface.Default; //do not dipose typeface that could be cached and reused - Paint.Dispose(); - Paint = null; + if (Paint != null) + { + Paint.Typeface = SKTypeface.Default; //do not dipose typeface that could be cached and reused + Paint.Dispose(); + Paint = null; + } + } + catch (Exception e) + { + Console.WriteLine(e); } + CommandTapped = null; Tapped = null; } @@ -436,7 +446,7 @@ public TextSpan() Paint = new() { IsAntialias = true, - Typeface = TypeFace + Typeface = _typeFace }; } diff --git a/src/Engine/DrawnUi.Maui.csproj b/src/Engine/DrawnUi.Maui.csproj index eaf410da..7ed105dd 100644 --- a/src/Engine/DrawnUi.Maui.csproj +++ b/src/Engine/DrawnUi.Maui.csproj @@ -27,6 +27,7 @@ maui drawnui skia skiasharp draw ui Nick Kovalsky aka AppoMobi (c) AppoMobi, 2023 - present day + MIT icon128.png https://github.com/taublast/DrawnUi.Maui @@ -87,7 +88,7 @@ - + @@ -120,6 +121,7 @@ + diff --git a/src/Engine/DrawnUi.Maui.sln b/src/Engine/DrawnUi.Maui.sln deleted file mode 100644 index fb7b6541..00000000 --- a/src/Engine/DrawnUi.Maui.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.9.34728.123 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DrawnUi.Maui", "DrawnUi.Maui.csproj", "{21A0DB3A-226E-48CA-ACE1-D82EC08D6C1A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {21A0DB3A-226E-48CA-ACE1-D82EC08D6C1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {21A0DB3A-226E-48CA-ACE1-D82EC08D6C1A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {21A0DB3A-226E-48CA-ACE1-D82EC08D6C1A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {21A0DB3A-226E-48CA-ACE1-D82EC08D6C1A}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {DD752EBC-E624-4C69-8009-CF480E4CACD8} - EndGlobalSection -EndGlobal diff --git a/src/Engine/Features/Animations/Animators/EdgeGlowAnimator.cs b/src/Engine/Features/Animations/Animators/EdgeGlowAnimator.cs index f3802025..75c6693e 100644 --- a/src/Engine/Features/Animations/Animators/EdgeGlowAnimator.cs +++ b/src/Engine/Features/Animations/Animators/EdgeGlowAnimator.cs @@ -56,12 +56,13 @@ void Draw() clipContent.Offset((float)(control.TranslationX * scale), (float)(control.TranslationY * scale)); clipInsideParent.AddPath(clipContent); - context.Canvas.Save(); - context.Canvas.ClipPath(clipInsideParent, SKClipOperation.Intersect, true); + var count = context.Canvas.Save(); + + SkiaControl.ClipSmart(context.Canvas, clipInsideParent); Draw(); - context.Canvas.Restore(); + context.Canvas.RestoreToCount(count); } } else diff --git a/src/Engine/Features/Animations/Animators/RenderingAnimator.cs b/src/Engine/Features/Animations/Animators/RenderingAnimator.cs index 038dbaaf..dd1cdbb8 100644 --- a/src/Engine/Features/Animations/Animators/RenderingAnimator.cs +++ b/src/Engine/Features/Animations/Animators/RenderingAnimator.cs @@ -58,10 +58,12 @@ void Render() { ApplyControlClipping(control, clipInsideParent, selfDrawingLocation); - context.Canvas.Save(); - context.Canvas.ClipPath(clipInsideParent, SKClipOperation.Intersect, true); + var count = context.Canvas.Save(); + + SkiaControl.ClipSmart(context.Canvas, clipInsideParent); draw(); - context.Canvas.Restore(); + + context.Canvas.RestoreToCount(count); } } else @@ -101,5 +103,6 @@ protected static void ApplyControlClipping(IDrawnBase control, SKPath clipInside clipContent.Offset((float)(control.TranslationX * control.RenderingScale), (float)(control.TranslationY * control.RenderingScale)); clipInsideParent.AddPath(clipContent); } + clipContent.Dispose(); } } \ No newline at end of file diff --git a/src/Engine/Features/Animations/Animators/ShimmerAnimator.cs b/src/Engine/Features/Animations/Animators/ShimmerAnimator.cs index 59026cbe..2a1149b6 100644 --- a/src/Engine/Features/Animations/Animators/ShimmerAnimator.cs +++ b/src/Engine/Features/Animations/Animators/ShimmerAnimator.cs @@ -92,7 +92,7 @@ protected override bool OnRendering(IDrawnBase control, SkiaDrawingContext conte canvas.DrawRect(rect, Paint); - canvas.Restore(); + canvas.RestoreToCount(saved); } }); diff --git a/src/Engine/Features/Animations/Animators/SkiaValueAnimator.cs b/src/Engine/Features/Animations/Animators/SkiaValueAnimator.cs index 9b419006..eb894605 100644 --- a/src/Engine/Features/Animations/Animators/SkiaValueAnimator.cs +++ b/src/Engine/Features/Animations/Animators/SkiaValueAnimator.cs @@ -23,7 +23,6 @@ public override void Stop() this.tcs?.TrySetCanceled(); } cancellationTokenRegistration.Dispose(); - } public virtual async Task RunAsync(Action initialize, CancellationToken cancellationToken = default) @@ -34,16 +33,16 @@ public virtual async Task RunAsync(Action initialize, CancellationToken cancella } this.tcs = new TaskCompletionSource(); - this.cancellationTokenRegistration = cancellationToken.Register(() => { - if (this.IsRunning) - { - this.Stop(); - } + //if (this.IsRunning) + //{ + // this.Stop(); + //} }); initialize?.Invoke(); + Start(); await this.tcs.Task; diff --git a/src/Engine/Features/Effects/ChainDropShadowsEffect.cs b/src/Engine/Features/Effects/ChainDropShadowsEffect.cs index 72151676..8a707835 100644 --- a/src/Engine/Features/Effects/ChainDropShadowsEffect.cs +++ b/src/Engine/Features/Effects/ChainDropShadowsEffect.cs @@ -134,11 +134,11 @@ public override ChainEffectResult Draw(SKRect destination, SkiaDrawingContext ct (float)shadow.Blur, (float)shadow.Blur, shadow.Color.ToSKColor()); - ctx.Canvas.SaveLayer(Paint); + var saved = ctx.Canvas.SaveLayer(Paint); drawControl(ctx); - ctx.Canvas.Restore(); + ctx.Canvas.RestoreToCount(saved); } return ChainEffectResult.Create(false); diff --git a/src/Engine/Features/Effects/IStateEffect.cs b/src/Engine/Features/Effects/IStateEffect.cs index 3770e487..1d4ce0e4 100644 --- a/src/Engine/Features/Effects/IStateEffect.cs +++ b/src/Engine/Features/Effects/IStateEffect.cs @@ -3,7 +3,7 @@ public interface IStateEffect : ISkiaEffect { /// - /// Will be invoked before actually painting but after gestures processing and other internal calculations + /// Will be invoked before actually painting but after gestures processing and other internal calculations. By SkiaControl.OnBeforeDrawing method. /// void UpdateState(); } \ No newline at end of file diff --git a/src/Engine/Features/Effects/SkiaShaderEffect.cs b/src/Engine/Features/Effects/SkiaShaderEffect.cs index 070ae5d9..11e675f6 100644 --- a/src/Engine/Features/Effects/SkiaShaderEffect.cs +++ b/src/Engine/Features/Effects/SkiaShaderEffect.cs @@ -1,10 +1,6 @@ -namespace DrawnUi.Maui.Draw; - -public class ShaderAnimatedEffect : SkiaShaderEffect -{ - -} +using System.ComponentModel; +namespace DrawnUi.Maui.Draw; public class StateEffect : SkiaEffect, IStateEffect @@ -80,7 +76,12 @@ public virtual void Render(SkiaDrawingContext ctx, SKRect destination) { if (_paintWithShader == null) { - _paintWithShader = new SKPaint(); + _paintWithShader = new SKPaint() + { + //todo check how if this affect anything after upcoming skiasharp3 fix + //FilterQuality = SKFilterQuality.High, + //IsDither = true + }; } SKImage source = Parent.RenderObject.Image; @@ -90,6 +91,23 @@ public virtual void Render(SkiaDrawingContext ctx, SKRect destination) ctx.Canvas.DrawRect(destination, _paintWithShader); } + protected SKShader PrimaryTexture; + private SKImage _lastSource; + + protected virtual SKShader CompilePrimaryTexture(SKImage source) + { + //if (_lastSource == null && source != null) //snapshot changed + if (source != _lastSource && source != null) //snapshot changed + { + _lastSource = source; + var dispose = PrimaryTexture; + PrimaryTexture = source.ToShader(SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); + if (dispose != PrimaryTexture) + dispose?.Dispose(); + } + return PrimaryTexture; + } + public virtual SKShader CreateShader(SkiaDrawingContext ctx, SKRect destination, SKImage source) { //we need to @@ -98,9 +116,10 @@ public virtual SKShader CreateShader(SkiaDrawingContext ctx, SKRect destination, //step 3: prepare uniforms to pass, including those textures //step 4: with all above create an SKShader to use in SKPaint - if (CompiledShader == null) + if (CompiledShader == null || _hasNewShader) { CompileShader(); + _hasNewShader = false; } if (NeedApply) @@ -110,9 +129,8 @@ public virtual SKShader CreateShader(SkiaDrawingContext ctx, SKRect destination, source = CreateSnapshot(ctx, destination); } - //if textures didn't change.. use previous? var killTextures = TexturesUniforms; - TexturesUniforms = CreateTexturesUniforms(ctx, destination, source); + TexturesUniforms = CreateTexturesUniforms(ctx, destination, CompilePrimaryTexture(source)); #if SKIA3 if (Parent != null && killTextures != null) @@ -160,12 +178,10 @@ protected virtual SKRuntimeEffectUniforms CreateUniforms(SKRect destination) return uniforms; } - protected virtual SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingContext ctx, SKRect destination, SKImage snapshot) + protected virtual SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingContext ctx, SKRect destination, SKShader texture1) { - if (snapshot != null) + if (texture1 != null) { - var texture1 = snapshot - .ToShader(); //.ToShader(SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); return new SKRuntimeEffectChildren(CompiledShader) { @@ -187,16 +203,32 @@ protected virtual void CompileShader() CompiledShader = SkSl.Compile(shaderCode); } - public static readonly BindableProperty ShaderFilenameProperty = BindableProperty.Create(nameof(ShaderSource), + protected virtual void ApplyShaderSource() + { + _hasNewShader = true; + Update(); + } + + private bool _hasNewShader; + + public static readonly BindableProperty ShaderSourceProperty = BindableProperty.Create(nameof(ShaderSource), typeof(string), typeof(SkiaShaderEffect), - string.Empty, propertyChanged: NeedUpdate); - public string ShaderSource + string.Empty, propertyChanged: NeedChangeSource); + + private static void NeedChangeSource(BindableObject bindable, object oldvalue, object newvalue) { - get { return (string)GetValue(ShaderFilenameProperty); } - set { SetValue(ShaderFilenameProperty, value); } + if (bindable is SkiaShaderEffect control) + { + control.ApplyShaderSource(); + } } + public string ShaderSource + { + get { return (string)GetValue(ShaderSourceProperty); } + set { SetValue(ShaderSourceProperty, value); } + } public override void Update() @@ -210,6 +242,8 @@ public override void Update() else kill?.Dispose(); + //Parent?.Repaint(); + base.Update(); } @@ -217,6 +251,7 @@ protected override void OnDisposing() { Shader?.Dispose(); Shader = null; + _lastSource = null; CompiledShader?.Dispose(); #if SKIA3 @@ -224,6 +259,8 @@ protected override void OnDisposing() #endif _paintWithShader?.Dispose(); + PrimaryTexture?.Dispose(); + base.OnDisposing(); } diff --git a/src/Engine/Features/Fonts/SkiaFontManager.cs b/src/Engine/Features/Fonts/SkiaFontManager.cs index a6e0c443..37534b19 100644 --- a/src/Engine/Features/Fonts/SkiaFontManager.cs +++ b/src/Engine/Features/Fonts/SkiaFontManager.cs @@ -90,7 +90,14 @@ public async Task GetFont(string fontFamily, int fontWeight) return SKTypeface.Default; } var alias = GetRegisteredAlias(fontFamily, fontWeight); - return await GetFont(alias); + var font = await GetFont(alias); + + //safety check to avoid any chance of crash split_config.arm64_v8a.apk!libSkiaSharp.so (sk_font_set_typeface+60) + if (font == null) + { + return SKTypeface.Default; + } + return font; } /// diff --git a/src/Engine/Features/Gestures/AddGestures.cs b/src/Engine/Features/Gestures/AddGestures.cs index dacb67a0..4c64a485 100644 --- a/src/Engine/Features/Gestures/AddGestures.cs +++ b/src/Engine/Features/Gestures/AddGestures.cs @@ -68,6 +68,20 @@ public override ISkiaGestureListener ProcessGestures(SkiaGesturesParameters args if (_parent == null || !_parent.CanDraw) return null; + + if (args.Type == TouchActionResult.LongPressing) + { + var command = GetCommandLongPressing(_parent); + if (command != null) + { + var parameter = GetCommandLongPressingParameter(_parent); + if (parameter == null) + parameter = _parent.BindingContext; + command?.Execute(parameter); + return this; + } + } + else if (args.Type == TouchActionResult.Tapped) { var anim = GetAnimationTapped(_parent); @@ -111,19 +125,6 @@ public override ISkiaGestureListener ProcessGestures(SkiaGesturesParameters args } } else - if (args.Type == TouchActionResult.LongPressing) - { - var command = GetCommandLongPressing(_parent); - if (command != null) - { - var parameter = GetCommandLongPressingParameter(_parent); - if (parameter == null) - parameter = _parent.BindingContext; - command?.Execute(parameter); - return this; - } - } - else if (args.Type == TouchActionResult.Panning) { if (GetLockPanning(_parent)) diff --git a/src/Engine/Internals/Extensions/DependencyExtensions.cs b/src/Engine/Internals/Extensions/DependencyExtensions.cs index 5aa13580..98c733d3 100644 --- a/src/Engine/Internals/Extensions/DependencyExtensions.cs +++ b/src/Engine/Internals/Extensions/DependencyExtensions.cs @@ -9,6 +9,7 @@ using Microsoft.Maui.Platform; using Polly; using Polly.Timeout; +using SkiaSharp.Views.Maui.Controls.Compatibility; using SkiaSharp.Views.Maui.Controls.Hosting; using System.Net; using System.Net.Http.Headers; @@ -57,6 +58,11 @@ public static MauiAppBuilder UseDrawnUi(this MauiAppBuilder builder, UiSettings builder.ConfigureMauiHandlers(handlers => { #if ANDROID + +#if !SKIA3 + handlers.AddHandler(typeof(SkiaViewAccelerated), typeof(FixedSKGLViewRenderer)); +#endif + handlers.AddHandler(typeof(DrawnUiBasePage), typeof(DrawnUiBasePageHandler)); handlers.AddHandler(typeof(MauiEntry), typeof(MauiEntryHandler)); handlers.AddHandler(typeof(MauiEditor), typeof(MauiEditorHandler)); diff --git a/src/Engine/Internals/Interfaces/IDrawnBase.cs b/src/Engine/Internals/Interfaces/IDrawnBase.cs index b759ef31..63d3e235 100644 --- a/src/Engine/Internals/Interfaces/IDrawnBase.cs +++ b/src/Engine/Internals/Interfaces/IDrawnBase.cs @@ -40,7 +40,7 @@ public interface IDrawnBase : IDisposable, ICanBeUpdatedWithContext /// If applyPosition is false will create clip without using drawing posiition, like if was drawing at 0,0. /// /// - SKPath CreateClip(object arguments, bool usePosition); + SKPath CreateClip(object arguments, bool usePosition, SKPath path = null); bool RegisterAnimator(ISkiaAnimator animator); diff --git a/src/Engine/Internals/Interfaces/ISelectableOption.cs b/src/Engine/Internals/Interfaces/ISelectableOption.cs new file mode 100644 index 00000000..163c8411 --- /dev/null +++ b/src/Engine/Internals/Interfaces/ISelectableOption.cs @@ -0,0 +1,6 @@ +namespace DrawnUi.Maui.Draw; + +public interface ISelectableOption : IHasTitleWithId, ICanBeSelected +{ + public bool IsReadOnly { get; } +} \ No newline at end of file diff --git a/src/Engine/Internals/Interfaces/ISkiaCell.cs b/src/Engine/Internals/Interfaces/ISkiaCell.cs index a2d653b5..4633bc83 100644 --- a/src/Engine/Internals/Interfaces/ISkiaCell.cs +++ b/src/Engine/Internals/Interfaces/ISkiaCell.cs @@ -1,29 +1,6 @@ namespace DrawnUi.Maui.Draw; -public interface ISkiaCell : IVisibilityAware +public interface ISkiaCell { - // Cell went on screen inside parent viewport. This will be called after binding context was set - //OnAppearing - - // Cell went offscreen from parent viewport - // OnDisappeared - public void OnScrolled(); } - -public interface IVisibilityAware -{ - /// - /// This can sometimes be omitted, - /// - void OnAppearing(); - - /// - /// This event can sometimes be called without prior OnAppearing - /// - void OnAppeared(); - - void OnDisappeared(); - - void OnDisappearing(); -} \ No newline at end of file diff --git a/src/Engine/Internals/Interfaces/IVisibilityAware.cs b/src/Engine/Internals/Interfaces/IVisibilityAware.cs new file mode 100644 index 00000000..fa533710 --- /dev/null +++ b/src/Engine/Internals/Interfaces/IVisibilityAware.cs @@ -0,0 +1,18 @@ +namespace DrawnUi.Maui.Draw; + +public interface IVisibilityAware +{ + /// + /// This can sometimes be omitted, + /// + void OnAppearing(); + + /// + /// This event can sometimes be called without prior OnAppearing + /// + void OnAppeared(); + + void OnDisappeared(); + + void OnDisappearing(); +} \ No newline at end of file diff --git a/src/Engine/Internals/Models/SelectableAction.cs b/src/Engine/Internals/Models/SelectableAction.cs new file mode 100644 index 00000000..15dfb0cc --- /dev/null +++ b/src/Engine/Internals/Models/SelectableAction.cs @@ -0,0 +1,28 @@ +namespace DrawnUi.Maui.Internals; + +public class SelectableAction : TitleWithStringId, ISelectableOption +{ + public SelectableAction() + { + Id = Guid.NewGuid().ToString(); + } + public SelectableAction(string title, Action action) + { + Id = Guid.NewGuid().ToString(); + Title = title; + Action = action; + } + + public SelectableAction(string id, string title, Action action) + { + Id = id; + Title = title; + Action = action; + } + + + public Action Action { get; set; } + public bool Selected { get; set; } + + public bool IsReadOnly { get; } = false; +} \ No newline at end of file diff --git a/src/Engine/Internals/Models/TitleWithStringId.cs b/src/Engine/Internals/Models/TitleWithStringId.cs new file mode 100644 index 00000000..2c81b3c1 --- /dev/null +++ b/src/Engine/Internals/Models/TitleWithStringId.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DrawnUi.Maui.Internals +{ + + + public class TitleWithStringId + { + public string Id { get; set; } + public string Title { get; set; } + } +} diff --git a/src/Engine/Platforms/Android/DrawnView.Android.cs b/src/Engine/Platforms/Android/DrawnView.Android.cs index 860a3079..2e445ac7 100644 --- a/src/Engine/Platforms/Android/DrawnView.Android.cs +++ b/src/Engine/Platforms/Android/DrawnView.Android.cs @@ -82,7 +82,7 @@ public void CheckElementVisibility(VisualElement element) protected virtual void DisposePlatform() { - Super.ChoreographerCallback -= OnChoreographer; + Super.OnFrame -= OnChoreographer; } object lockFrame = new(); @@ -113,8 +113,8 @@ private void OnChoreographer(object sender, EventArgs e) public virtual void SetupRenderingLoop() { - Super.ChoreographerCallback -= OnChoreographer; - Super.ChoreographerCallback += OnChoreographer; + Super.OnFrame -= OnChoreographer; + Super.OnFrame += OnChoreographer; } protected virtual void PlatformHardwareAccelerationChanged() diff --git a/src/Engine/Platforms/Android/Files.Android.cs b/src/Engine/Platforms/Android/Files.Android.cs index fb5436b4..33036691 100644 --- a/src/Engine/Platforms/Android/Files.Android.cs +++ b/src/Engine/Platforms/Android/Files.Android.cs @@ -1,4 +1,5 @@ -using Android.Media; +using Android.Content.Res; +using Android.Media; namespace DrawnUi.Maui.Infrastructure { @@ -21,6 +22,16 @@ public static void RefreshSystem(FileDescriptor file) // .AbsolutePath; //} + /// + /// tries to get all resources from assets folder Resources/Raw/{subfolder} + /// + /// + public static List ListAssets(string subfolder) + { + AssetManager assets = Platform.AppContext.Assets; + return assets.List(subfolder).ToList(); + } + public static void Share(string message, IEnumerable fullFilenames) { MainThread.BeginInvokeOnMainThread(async () => diff --git a/src/Engine/Platforms/Android/Legacy/Class1.cs b/src/Engine/Platforms/Android/Legacy/Class1.cs new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/src/Engine/Platforms/Android/Legacy/Class1.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Engine/Platforms/Android/Legacy/FixedSKGLViewRenderer.cs b/src/Engine/Platforms/Android/Legacy/FixedSKGLViewRenderer.cs new file mode 100644 index 00000000..a0b24099 --- /dev/null +++ b/src/Engine/Platforms/Android/Legacy/FixedSKGLViewRenderer.cs @@ -0,0 +1,197 @@ +using Android.Content; +using Android.Opengl; +using Microsoft.Maui.Controls.Compatibility.Platform.Android; +using Microsoft.Maui.Controls.Platform; +using SkiaSharp; +using SkiaSharp.Views.Maui.Controls.Compatibility; +using SkiaSharp.Views.Maui.Platform; +using System; +using System; +using System.ComponentModel; +using SKFormsView = DrawnUi.Maui.Views.SkiaViewAccelerated; +using SKNativePaintGLSurfaceEventArgs = SkiaSharp.Views.Android.SKPaintGLSurfaceEventArgs; +using SKNativeView = DrawnUi.Maui.SkiaGLTexture; + +//[assembly: ExportRenderer(typeof(SKFormsView), typeof(FixedSKGLViewRenderer))] +// => +// handlers.AddHandler(typeof(SkiaViewAccelerated), typeof(FixedSKGLViewRenderer)); +namespace DrawnUi.Maui; + +public class FixedSKGLViewRenderer : FixedSKGLViewRendererBase +{ + public FixedSKGLViewRenderer(Context context) + : base(context) + { + } + + protected override void SetupRenderLoop(bool oneShot) + { + if (oneShot) + { + Control.RequestRender(); + } + + Control.RenderMode = Element.HasRenderLoop + ? Rendermode.Continuously + : Rendermode.WhenDirty; + } + + protected override SKNativeView CreateNativeControl() + { + var view = GetType() == typeof(SKGLViewRenderer) + ? new SKNativeView(Context) + : base.CreateNativeControl(); + + // Force the opacity to false for consistency with the other platforms + view.SetOpaque(false); + + return view; + } +} + +public abstract class FixedSKGLViewRendererBase : Microsoft.Maui.Controls.Handlers.Compatibility.ViewRenderer + where TFormsView : SKFormsView + where TNativeView : SKNativeView +{ + + + protected FixedSKGLViewRendererBase(Context context) + : base(context) + { + Initialize(); + } + + private void Initialize() + { + + } + + private bool measured; + + public GRContext GRContext => Control.GRContext; + + public static object lockUpdate = new(); + + protected override void OnElementChanged(ElementChangedEventArgs e) + { + if (e.OldElement != null) + { + var oldController = (ISKGLViewController)e.OldElement; + + // unsubscribe from events + oldController.SurfaceInvalidated -= OnSurfaceInvalidated; + oldController.GetCanvasSize -= OnGetCanvasSize; + oldController.GetGRContext -= OnGetGRContext; + } + + if (e.NewElement != null) + { + var newController = (ISKGLViewController)e.NewElement; + + // create the native view + if (Control == null) + { + var view = CreateNativeControl(); + view.PaintSurface += OnPaintSurface; + SetNativeControl(view); + } + + // subscribe to events from the user + newController.SurfaceInvalidated += OnSurfaceInvalidated; + newController.GetCanvasSize += OnGetCanvasSize; + newController.GetGRContext += OnGetGRContext; + + // start the rendering + SetupRenderLoop(false); + } + + base.OnElementChanged(e); + } + + + protected override TNativeView CreateNativeControl() + { + return (TNativeView)Activator.CreateInstance(typeof(TNativeView), new[] { Context }); + } + + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + // refresh the render loop + if (e.PropertyName == SKFormsView.HasRenderLoopProperty.PropertyName) + { + SetupRenderLoop(false); + } + else if (e.PropertyName == SKFormsView.EnableTouchEventsProperty.PropertyName) + { + + } + + } + + protected override void Dispose(bool disposing) + { + // detach all events before disposing + var controller = (ISKGLViewController)Element; + if (controller != null) + { + controller.SurfaceInvalidated -= OnSurfaceInvalidated; + controller.GetCanvasSize -= OnGetCanvasSize; + controller.GetGRContext -= OnGetGRContext; + } + + var control = Control; + if (control != null) + { + control.PaintSurface -= OnPaintSurface; + } + + + + base.Dispose(disposing); + } + + protected abstract void SetupRenderLoop(bool oneShot); + + private SKPoint GetScaledCoord(double x, double y) + { + return new SKPoint((float)x, (float)y); + } + + + // the user asked to repaint + private void OnSurfaceInvalidated(object sender, EventArgs eventArgs) + { + // if we aren't in a loop, then refresh once + if (!Element.HasRenderLoop) + { + SetupRenderLoop(true); + } + } + + // the user asked for the size + private void OnGetCanvasSize(object sender, GetPropertyValueEventArgs e) + { + e.Value = Control?.CanvasSize ?? SKSize.Empty; + } + + // the user asked for the current GRContext + private void OnGetGRContext(object sender, GetPropertyValueEventArgs e) + { + e.Value = Control?.GRContext; + } + + private void OnPaintSurface(object sender, SKNativePaintGLSurfaceEventArgs e) + { + lock (lockUpdate) + { + var controller = Element as ISKGLViewController; + + // the control is being repainted, let the user know + controller?.OnPaintSurface(new SKPaintGLSurfaceEventArgs(e.Surface, e.BackendRenderTarget)); + } + } +} + diff --git a/src/Engine/Platforms/Android/Legacy/SkiaGLTexture.cs b/src/Engine/Platforms/Android/Legacy/SkiaGLTexture.cs new file mode 100644 index 00000000..fc2e385e --- /dev/null +++ b/src/Engine/Platforms/Android/Legacy/SkiaGLTexture.cs @@ -0,0 +1,61 @@ +using Android.Content; +using Android.Util; +using System.ComponentModel; +using SKPaintGLSurfaceEventArgs = SkiaSharp.Views.Android.SKPaintGLSurfaceEventArgs; + +namespace DrawnUi.Maui +{ + public partial class SkiaGLTexture : SkiaGLTextureView + { + private SkiaGLTextureRenderer renderer; + + public SkiaGLTexture(Context context) + : base(context) + { + Initialize(); + } + + public SkiaGLTexture(Context context, IAttributeSet attrs) + : base(context, attrs) + { + Initialize(); + } + + private void Initialize() + { + SetEGLContextClientVersion(2); + SetEGLConfigChooser(8, 8, 8, 8, 0, 8); + + renderer = new InternalRenderer(this); + SetRenderer(renderer); + } + + public SKSize CanvasSize => renderer.CanvasSize; + + public GRContext GRContext => renderer.GRContext; + + public event EventHandler PaintSurface; + + protected virtual void OnPaintSurface(SKPaintGLSurfaceEventArgs e) + { + PaintSurface?.Invoke(this, e); + } + + + private class InternalRenderer : SkiaGLTextureRenderer + { + private readonly SkiaGLTexture textureView; + + public InternalRenderer(SkiaGLTexture textureView) + { + this.textureView = textureView; + } + + protected override void OnPaintSurface(SKPaintGLSurfaceEventArgs e) + { + textureView.OnPaintSurface(e); + } + + } + } +} \ No newline at end of file diff --git a/src/Engine/Platforms/Android/Legacy/SkiaGLTextureRenderer.cs b/src/Engine/Platforms/Android/Legacy/SkiaGLTextureRenderer.cs new file mode 100644 index 00000000..c0ea5452 --- /dev/null +++ b/src/Engine/Platforms/Android/Legacy/SkiaGLTextureRenderer.cs @@ -0,0 +1,141 @@ +using Android.Opengl; +using System.ComponentModel; +using SKPaintGLSurfaceEventArgs = SkiaSharp.Views.Android.SKPaintGLSurfaceEventArgs; + +namespace DrawnUi.Maui; + +public abstract partial class SkiaGLTextureRenderer : Java.Lang.Object, SkiaGLTextureView.IRenderer +{ + private const SKColorType colorType = SKColorType.Rgba8888; + private const GRSurfaceOrigin surfaceOrigin = GRSurfaceOrigin.BottomLeft; + + private GRContext context; + private GRGlFramebufferInfo glInfo; + private GRBackendRenderTarget renderTarget; + private SKSurface surface; + private SKCanvas canvas; + + private SKSizeI lastSize; + private SKSizeI newSize; + + public SKSize CanvasSize => lastSize; + + public GRContext GRContext => context; + + + protected virtual void OnPaintSurface(SKPaintGLSurfaceEventArgs e) + { + } + + + public void OnDrawFrame() + { + + GLES10.GlClear(GLES10.GlColorBufferBit | GLES10.GlDepthBufferBit | GLES10.GlStencilBufferBit); + + // create the contexts if not done already + if (context == null) + { + var glInterface = GRGlInterface.Create(); + context = GRContext.CreateGl(glInterface); + } + + // manage the drawing surface + if (renderTarget == null || lastSize != newSize || !renderTarget.IsValid) + { + // create or update the dimensions + lastSize = newSize; + + // read the info from the buffer + var buffer = new int[3]; + GLES20.GlGetIntegerv(GLES20.GlFramebufferBinding, buffer, 0); + GLES20.GlGetIntegerv(GLES20.GlStencilBits, buffer, 1); + GLES20.GlGetIntegerv(GLES20.GlSamples, buffer, 2); + var samples = buffer[2]; + var maxSamples = context.GetMaxSurfaceSampleCount(colorType); + if (samples > maxSamples) + samples = maxSamples; + glInfo = new GRGlFramebufferInfo((uint)buffer[0], colorType.ToGlSizedFormat()); + + // destroy the old surface + surface?.Dispose(); + surface = null; + canvas = null; + + // re-create the render target + renderTarget?.Dispose(); + renderTarget = new GRBackendRenderTarget(newSize.Width, newSize.Height, samples, buffer[1], glInfo); + } + + // create the surface + if (surface == null) + { + surface = SKSurface.Create(context, renderTarget, surfaceOrigin, colorType); + } + + if (surface != null) + { + canvas = surface.Canvas; + } + + if (canvas != null) + { + var restore = canvas.Save(); + + var e = new SKPaintGLSurfaceEventArgs(surface, renderTarget, surfaceOrigin, colorType); + OnPaintSurface(e); + + canvas.RestoreToCount(restore); + + canvas.Flush(); + context.Flush(); + } + + } + + + + public void OnSurfaceChanged(int width, int height) + { + GLES20.GlViewport(0, 0, width, height); + + // get the new surface size + newSize = new SKSizeI(width, height); + } + + + + public void OnSurfaceCreated(EGLConfig config) + { + // Create the context and resources + if (context != null) + { + FreeContext(); + } + + } + + public void OnSurfaceDestroyed() + { + FreeContext(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + FreeContext(); + } + base.Dispose(disposing); + } + + private void FreeContext() + { + surface?.Dispose(); + surface = null; + renderTarget?.Dispose(); + renderTarget = null; + context?.Dispose(); + context = null; + } +} \ No newline at end of file diff --git a/src/Engine/Platforms/Android/Legacy/SkiaGLTextureView.cs b/src/Engine/Platforms/Android/Legacy/SkiaGLTextureView.cs new file mode 100644 index 00000000..e156180f --- /dev/null +++ b/src/Engine/Platforms/Android/Legacy/SkiaGLTextureView.cs @@ -0,0 +1,1401 @@ +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.Graphics; +using Android.Opengl; +using Android.Opengl; +using Android.Runtime; +using Android.Util; +using Android.Views; +using Javax.Microedition.Khronos.Egl; +using Application = Android.App.Application; +using EGLConfig = Android.Opengl.EGLConfig; +using EGLContext = Android.Opengl.EGLContext; +using EGLDisplay = Android.Opengl.EGLDisplay; +using EGLSurface = Android.Opengl.EGLSurface; +using View = Android.Views.View; + +namespace DrawnUi.Maui; + +public class SkiaGLTextureView : TextureView, TextureView.ISurfaceTextureListener, View.IOnLayoutChangeListener +{ + public static bool EnableLogging = false; + + private WeakReference thisWeakRef; + private GLThread glThread; + private IRenderer renderer; + private bool detachedFromWindow; + private IEGLConfigChooser eglConfigChooser; + private IEGLContextFactory eglContextFactory; + private IEGLWindowSurfaceFactory eglWindowSurfaceFactory; + private int eglContextClientVersion; + + public SkiaGLTextureView(Context context) + : base(context) + { + Initialize(); + } + + public SkiaGLTextureView(Context context, IAttributeSet attrs) + : base(context, attrs) + { + Initialize(); + } + + private void Initialize() + { + thisWeakRef = new WeakReference(this); + + SurfaceTextureListener = this; + AddOnLayoutChangeListener(this); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (glThread != null) + { + // GLThread may still be running if this view was never attached to a window. + glThread.RequestExitAndWait(); + } + } + + base.Dispose(disposing); + } + + public bool PreserveEGLContextOnPause { get; set; } + + + public void SetRenderer(IRenderer renderer) + { + CheckRenderThreadState(); + if (eglConfigChooser == null) + { + eglConfigChooser = new SimpleEGLConfigChooser(this, true); + } + if (eglContextFactory == null) + { + eglContextFactory = new DefaultContextFactory(this); + } + if (eglWindowSurfaceFactory == null) + { + eglWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); + } + this.renderer = renderer; + glThread = new GLThread(thisWeakRef); + glThread.Start(); + } + + public void SetEGLContextFactory(IEGLContextFactory factory) + { + CheckRenderThreadState(); + eglContextFactory = factory; + } + + public void SetEGLWindowSurfaceFactory(IEGLWindowSurfaceFactory factory) + { + CheckRenderThreadState(); + eglWindowSurfaceFactory = factory; + } + + public void SetEGLConfigChooser(IEGLConfigChooser configChooser) + { + CheckRenderThreadState(); + eglConfigChooser = configChooser; + } + + public void SetEGLConfigChooser(bool needDepth) + { + SetEGLConfigChooser(new SimpleEGLConfigChooser(this, needDepth)); + } + + public void SetEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) + { + SetEGLConfigChooser(new ComponentSizeChooser(this, redSize, greenSize, blueSize, alphaSize, depthSize, stencilSize)); + } + + public void SetEGLContextClientVersion(int version) + { + CheckRenderThreadState(); + eglContextClientVersion = version; + } + + public Rendermode RenderMode + { + get { return glThread.GetRenderMode(); } + set { glThread.SetRenderMode(value); } + } + + public void RequestRender() + { + glThread.RequestRender(); + } + + public void OnSurfaceTextureUpdated(SurfaceTexture surface) + { + //glThread.RequestRender(); + } + + public void OnSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) + { + glThread.OnSurfaceCreated(); + glThread.RequestRender(); + } + + public bool OnSurfaceTextureDestroyed(SurfaceTexture surface) + { + // Surface will be destroyed when we return + glThread.OnSurfaceDestroyed(); + return true; + } + + public void OnSurfaceTextureSizeChanged(SurfaceTexture surface, int w, int h) + { + glThread.OnWindowResize(w, h); + } + + public void OnPause() + { + glThread.OnPause(); + } + + public void OnResume() + { + glThread.OnResume(); + } + + public void QueueEvent(Action r) + { + QueueEvent(new Java.Lang.Runnable(r)); + } + + public void QueueEvent(Java.Lang.IRunnable r) + { + glThread.QueueEvent(r); + } + + protected override void OnAttachedToWindow() + { + base.OnAttachedToWindow(); + + LogDebug($" OnAttachedToWindow"); + + if (detachedFromWindow && (renderer != null)) + { + var renderMode = Rendermode.Continuously; + if (glThread != null) + { + renderMode = glThread.GetRenderMode(); + glThread.RequestExitAndWait(); + } + glThread = new GLThread(thisWeakRef); + if (renderMode != Rendermode.Continuously) + { + glThread.SetRenderMode(renderMode); + } + glThread.Start(); + } + detachedFromWindow = false; + } + + protected override void OnDetachedFromWindow() + { + LogDebug($" OnDetachedFromWindow reattach={detachedFromWindow}"); + + if (glThread != null) + { + glThread.RequestExitAndWait(); + } + detachedFromWindow = true; + base.OnDetachedFromWindow(); + } + + private void CheckRenderThreadState() + { + if (glThread != null) + { + throw new Exception("setRenderer has already been called for this instance."); + } + } + + public void OnLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) + { + OnSurfaceTextureSizeChanged(SurfaceTexture, right - left, bottom - top); + } + + [Conditional("DEBUG")] + private static void LogDebug(string message) + { + if (EnableLogging) + { + Log.Debug("SkiaGLTextureView", message); + } + } + + [Conditional("DEBUG")] + private static void LogError(string message) + { + Log.Error("SkiaGLTextureView", message); + } + + + + public interface IEGLConfigChooser + { + EGLConfig ChooseConfig(EGLDisplay display); + } + + public interface IEGLContextFactory + { + EGLContext CreateContext(EGLDisplay display, EGLConfig eglConfig); + + void DestroyContext(EGLDisplay display, EGLContext context); + } + + public interface IEGLWindowSurfaceFactory + { + EGLSurface CreateWindowSurface(EGLDisplay display, EGLConfig config, Java.Lang.Object nativeWindow); + + void DestroySurface(EGLDisplay display, EGLSurface surface); + } + + public interface IRenderer + { + void OnDrawFrame(); + + void OnSurfaceChanged(int width, int height); + + void OnSurfaceCreated(EGLConfig config); + + void OnSurfaceDestroyed(); + } + + private class DefaultContextFactory : IEGLContextFactory + { + private SkiaGLTextureView textureView; + + public DefaultContextFactory(SkiaGLTextureView textureView) + { + this.textureView = textureView; + } + + public EGLContext CreateContext(EGLDisplay display, EGLConfig config) + { + var attribList = new int[] { + EglHelper.EGL_CONTEXT_CLIENT_VERSION, textureView.eglContextClientVersion, + EGL14.EglNone + }; + + return EGL14.EglCreateContext( + display, config, + EGL14.EglNoContext, + textureView.eglContextClientVersion != 0 ? attribList : null + , 0); + } + + public void DestroyContext(EGLDisplay display, EGLContext context) + { + LogDebug($"[DefaultContextFactory] DestroyContext tid={Thread.CurrentThread.ManagedThreadId} display={display} context={context}"); + + if (!EGL14.EglDestroyContext(display, context)) + { + var error = EGL14.EglGetError(); + LogError($"[DefaultContextFactory] eglDestroyContext failed: {error}"); + throw new Exception($"eglDestroyContext failed: {error}"); + } + } + } + + private class DefaultWindowSurfaceFactory : IEGLWindowSurfaceFactory + { + public EGLSurface CreateWindowSurface(EGLDisplay display, EGLConfig config, Java.Lang.Object nativeWindow) + { + EGLSurface result = null; + try + { + int[] attribList = { + EGL14.EglNone + }; + result = EGL14.EglCreateWindowSurface(display, config, nativeWindow, attribList, 0); + } + catch (Exception ex) + { + // This exception indicates that the surface flinger surface + // is not valid. This can happen if the surface flinger surface has + // been torn down, but the application has not yet been + // notified via SurfaceHolder.Callback.surfaceDestroyed. + // In theory the application should be notified first, + // but in practice sometimes it is not. See b/4588890 + LogError($"[DefaultWindowSurfaceFactory] eglCreateWindowSurface failed: {ex}"); + } + return result; + } + + public void DestroySurface(EGLDisplay display, EGLSurface surface) + { + EGL14.EglDestroySurface(display, surface); + } + } + + private abstract class BaseConfigChooser : IEGLConfigChooser + { + private SkiaGLTextureView textureView; + private int[] configSpec; + + public BaseConfigChooser(SkiaGLTextureView textureView, int[] configSpec) + { + this.textureView = textureView; + this.configSpec = FilterConfigSpec(configSpec); + } + + public EGLConfig ChooseConfig(EGLDisplay display) + { + int[] configAttribs = { + EGL14.EglRedSize, 8, + EGL14.EglGreenSize, 8, + EGL14.EglBlueSize, 8, + EGL14.EglAlphaSize, 8, + EGL14.EglDepthSize, 16, + EGL14.EglSurfaceType, EGL14.EglWindowBit, + EGL14.EglRenderableType, EGL14.EglOpenglEs2Bit, + EGL14.EglNone + }; + + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + if (!EGL14.EglChooseConfig(display, configAttribs, 0, configs, 0, configs.Length, numConfigs, 0)) + { + throw new Exception("eglChooseConfig failed"); + } + + if (numConfigs[0] <= 0) + { + throw new Exception("No configs match configSpec"); + } + + return configs[0]; + } + + public abstract EGLConfig ChooseConfig(EGLDisplay display, EGLConfig[] configs); + + private int[] FilterConfigSpec(int[] spec) + { + if (textureView.eglContextClientVersion != 2) + { + return spec; + } + + // We know none of the subclasses define EGL_RENDERABLE_TYPE. + // And we know the configSpec is well formed. + var len = spec.Length; + var newConfigSpec = new int[len + 2]; + Array.Copy(spec, 0, newConfigSpec, 0, len - 1); + newConfigSpec[len - 1] = EGL14.EglRenderableType; + newConfigSpec[len] = EglHelper.EGL_OPENGL_ES2_BIT; + newConfigSpec[len + 1] = EGL14.EglNone; + return newConfigSpec; + } + } + + private class ComponentSizeChooser : BaseConfigChooser + { + private int[] value; + private int redSize; + private int greenSize; + private int blueSize; + private int alphaSize; + private int depthSize; + private int stencilSize; + + public ComponentSizeChooser(SkiaGLTextureView textureView, int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) + : base(textureView, new int[] { + EGL14.EglRedSize, redSize, + EGL14.EglGreenSize, greenSize, + EGL14.EglBlueSize, blueSize, + EGL14.EglAlphaSize, alphaSize, + EGL14.EglDepthSize, depthSize, + EGL14.EglStencilSize, stencilSize, + EGL14.EglNone + }) + { + value = new int[1]; + this.redSize = redSize; + this.greenSize = greenSize; + this.blueSize = blueSize; + this.alphaSize = alphaSize; + this.depthSize = depthSize; + this.stencilSize = stencilSize; + } + + public override EGLConfig ChooseConfig(EGLDisplay display, EGLConfig[] configs) + { + foreach (var config in configs) + { + var d = FindConfigAttrib(display, config, EGL14.EglDepthSize, 0); + var s = FindConfigAttrib(display, config, EGL14.EglStencilSize, 0); + if ((d >= depthSize) && (s >= stencilSize)) + { + var r = FindConfigAttrib(display, config, EGL14.EglRedSize, 0); + var g = FindConfigAttrib(display, config, EGL14.EglGreenSize, 0); + var b = FindConfigAttrib(display, config, EGL14.EglBlueSize, 0); + var a = FindConfigAttrib(display, config, EGL14.EglAlphaSize, 0); + if ((r == redSize) && (g == greenSize) && (b == blueSize) && (a == alphaSize)) + { + return config; + } + } + } + return null; + } + + private int FindConfigAttrib(EGLDisplay display, EGLConfig config, int attribute, int defaultValue) + { + int[] value = new int[1]; + if (EGL14.EglGetConfigAttrib(display, config, attribute, value, 0)) + { + return value[0]; + } + return defaultValue; + } + } + + private class SimpleEGLConfigChooser : ComponentSizeChooser + { + public SimpleEGLConfigChooser(SkiaGLTextureView textureView, bool withDepthBuffer) + : base(textureView, 8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0) + { + } + } + + + private class GLThread + { + private Thread thread; + private volatile GLThreadManager threadManager; + private EglHelper eglHelper; + private WeakReference textureViewWeakRef; + + // Once the thread is started, all accesses to the following member + // variables are protected by the sGLThreadManager monitor + private volatile bool shouldExit; + public volatile bool exited; + private volatile bool requestPaused; + private volatile bool paused; + private volatile bool hasSurface; + private volatile bool surfaceIsBad; + private volatile bool waitingForSurface; + private volatile bool haveEglContext; + private volatile bool haveEglSurface; + private volatile bool finishedCreatingEglSurface; + private volatile bool shouldReleaseEglContext; + private volatile int width; + private volatile int height; + private Rendermode renderMode; + private Queue eventQueue = new Queue(); + private volatile bool surfaceSizeChanged = true; + private volatile bool requestRender; + private volatile bool renderComplete; + // End of member variables protected by the sGLThreadManager monitor. + + public GLThread(WeakReference glTextureViewWeakRef) + { + threadManager = new GLThreadManager(); + + width = 0; + height = 0; + requestRender = true; + renderMode = Rendermode.Continuously; + textureViewWeakRef = glTextureViewWeakRef; + thread = new Thread(new ThreadStart(Run)); + } + + public int Id => thread.ManagedThreadId; + + public void Start() + { + thread.Start(); + } + + public void Run() + { + thread.Name = "GLThread " + thread.ManagedThreadId; + + LogDebug($"[GLThread {Id}] Starting '{thread.Name}'"); + + try + { + GuardedRun(); + } + finally + { + threadManager.ThreadExiting(this); + } + } + + private void StopEglSurfaceLocked() + { + if (haveEglSurface) + { + haveEglSurface = false; + eglHelper.DestroySurface(); + } + } + + private void StopEglContextLocked() + { + if (haveEglContext) + { + eglHelper.Finish(); + haveEglContext = false; + threadManager.ReleaseEglContextLocked(this); + } + } + + private void GuardedRun() + { + eglHelper = new EglHelper(textureViewWeakRef); + haveEglContext = false; + haveEglSurface = false; + try + { + var createEglContext = false; + var createEglSurface = false; + var createGlInterface = false; + var lostEglContext = false; + var sizeChanged = false; + var wantRenderNotification = false; + var doRenderNotification = false; + var askedToReleaseEglContext = false; + var w = 0; + var h = 0; + Java.Lang.IRunnable ev = null; + + while (true) + { + lock (threadManager) + { + while (true) + { + if (shouldExit) + { + return; + } + + if (eventQueue.Count > 0) + { + ev = eventQueue.Dequeue(); + break; + } + + // Update the pause state. + var pausing = false; + if (paused != requestPaused) + { + pausing = requestPaused; + paused = requestPaused; + Monitor.PulseAll(threadManager); + + LogDebug($"[GLThread {Id}] paused is now {paused}"); + } + + // Do we need to give up the EGL context? + if (shouldReleaseEglContext) + { + LogDebug($"[GLThread {Id}] Releasing EGL context because asked to"); + + StopEglSurfaceLocked(); + StopEglContextLocked(); + shouldReleaseEglContext = false; + askedToReleaseEglContext = true; + } + + // Have we lost the EGL context? + if (lostEglContext) + { + StopEglSurfaceLocked(); + StopEglContextLocked(); + lostEglContext = false; + } + + // When pausing, release the EGL surface: + if (pausing && haveEglSurface) + { + LogDebug($"[GLThread {Id}] Releasing EGL surface because paused"); + + StopEglSurfaceLocked(); + } + + // When pausing, optionally release the EGL Context: + if (pausing && haveEglContext) + { + textureViewWeakRef.TryGetTarget(out SkiaGLTextureView view); + var preserveEglContextOnPause = view == null ? false : view.PreserveEGLContextOnPause; + if (!preserveEglContextOnPause || threadManager.ShouldReleaseEGLContextWhenPausing()) + { + StopEglContextLocked(); + + LogDebug($"[GLThread {Id}] Releasing EGL context because paused"); + } + } + + // When pausing, optionally terminate EGL: + if (pausing) + { + if (threadManager.ShouldTerminateEGLWhenPausing()) + { + eglHelper.Finish(); + + LogDebug($"[GLThread {Id}] Terminating EGL because paused"); + } + } + + // Have we lost the TextureView surface? + if ((!hasSurface) && (!waitingForSurface)) + { + LogDebug($"[GLThread {Id}] Noticed TextureView surface lost"); + + if (haveEglSurface) + { + StopEglSurfaceLocked(); + } + waitingForSurface = true; + surfaceIsBad = false; + Monitor.PulseAll(threadManager); + } + + // Have we acquired the surface view surface? + if (hasSurface && waitingForSurface) + { + LogDebug($"[GLThread {Id}] Noticed TextureView surface acquired"); + + waitingForSurface = false; + Monitor.PulseAll(threadManager); + } + + if (doRenderNotification) + { + LogDebug($"[GLThread {Id}] Sending render notification"); + + wantRenderNotification = false; + doRenderNotification = false; + renderComplete = true; + Monitor.PulseAll(threadManager); + } + + // Ready to draw? + if (IsReadyToDraw()) + // https://stackoverflow.com/questions/67513816/xamarin-android-jni-error-accessed-deleted-global-0x000000 + // crashing somewhere down here + { + // If we don't have an EGL context, try to acquire one. + if (!haveEglContext) + { + if (askedToReleaseEglContext) + { + askedToReleaseEglContext = false; + } + else + if (threadManager.TryAcquireEglContextLocked(this)) + { + try + { + eglHelper.Start(); + } + catch (Exception) + { + threadManager.ReleaseEglContextLocked(this); + throw; + } + haveEglContext = true; + createEglContext = true; + + Monitor.PulseAll(threadManager); + } + } + + if (haveEglContext && !haveEglSurface) + { + haveEglSurface = true; + createEglSurface = true; + createGlInterface = true; + sizeChanged = true; + } + + if (haveEglSurface) + { + if (surfaceSizeChanged) + { + sizeChanged = true; + w = width; + h = height; + wantRenderNotification = true; + + LogDebug($"[GLThread {Id}] Noticing that we want render notification"); + + // Destroy and recreate the EGL surface. + createEglSurface = true; + surfaceSizeChanged = false; + } + requestRender = false; + Monitor.PulseAll(threadManager); + break; + } + } + + LogDebug($"[GLThread {Id}] Waiting mHaveEglContext={haveEglContext} mHaveEglSurface={haveEglSurface} mFinishedCreatingEglSurface={finishedCreatingEglSurface} paused={paused} hasSurface={hasSurface} surfaceIsBad={surfaceIsBad} mWaitingForSurface={waitingForSurface} mWidth={width} mHeight={height} mRequestRender={requestRender} mRenderMode={renderMode}"); + + // By design, this is the only place in a GLThread thread where we Wait(). + Monitor.Wait(threadManager); + } + } // end of lock(sGLThreadManager) + + if (ev != null) + { + ev.Run(); + ev = null; + continue; + } + + if (createEglSurface) + { + LogDebug($"[GLThread {Id}] EGL create surface"); + + if (eglHelper.CreateSurface()) + { + lock (threadManager) + { + finishedCreatingEglSurface = true; + Monitor.PulseAll(threadManager); + } + } + else + { + lock (threadManager) + { + finishedCreatingEglSurface = true; + surfaceIsBad = true; + Monitor.PulseAll(threadManager); + } + continue; + } + createEglSurface = false; + } + + if (createGlInterface) + { + threadManager.CheckGLDriver(); + createGlInterface = false; + } + + if (createEglContext) + { + LogDebug($"[GLThread {Id}] OnSurfaceCreated"); + + if (textureViewWeakRef.TryGetTarget(out SkiaGLTextureView view)) + { + view.renderer.OnSurfaceCreated(eglHelper.EglConfig); + } + createEglContext = false; + } + + if (sizeChanged) + { + LogDebug($"[GLThread {Id}] OnSurfaceChanged({w}, {h})"); + + if (textureViewWeakRef.TryGetTarget(out SkiaGLTextureView view)) + { + view.renderer.OnSurfaceChanged(w, h); + } + sizeChanged = false; + } + + { + LogDebug($"[GLThread {Id}] OnDrawFrame"); + + if (textureViewWeakRef.TryGetTarget(out SkiaGLTextureView view)) + { + view.renderer.OnDrawFrame(); + } + } + + var swapError = eglHelper.Swap(); + switch (swapError) + { + case EGL14.EglSuccess: + break; + case EGL11.EglContextLost: + LogDebug($"[GLThread {Id}] EGL context lost"); + lostEglContext = true; + break; + default: + // Other errors typically mean that the current surface is bad, + // probably because the TextureView surface has been destroyed, + // but we haven't been notified yet. + LogError($"[GLThread {Id}] eglSwapBuffers failed: {swapError}"); + + lock (threadManager) + { + surfaceIsBad = true; + Monitor.PulseAll(threadManager); + } + break; + } + + if (wantRenderNotification) + { + doRenderNotification = true; + } + } + } + finally + { + lock (threadManager) + { + StopEglSurfaceLocked(); + StopEglContextLocked(); + } + } + } + + public bool IsAbleToDraw() + { + return haveEglContext && haveEglSurface && IsReadyToDraw(); + } + + private bool IsReadyToDraw() + { + return (!paused) && hasSurface && (!surfaceIsBad) && (width > 0) && (height > 0) && (requestRender || (renderMode == Rendermode.Continuously)); + } + + public void SetRenderMode(Rendermode mode) + { + lock (threadManager) + { + renderMode = mode; + Monitor.PulseAll(threadManager); + } + } + + public Rendermode GetRenderMode() + { + lock (threadManager) + { + return renderMode; + } + } + + public void RequestRender() + { + lock (threadManager) + { + requestRender = true; + Monitor.PulseAll(threadManager); + } + } + + public void OnSurfaceCreated() + { + lock (threadManager) + { + LogDebug($"[GLThread {Id}] OnSurfaceCreated"); + + hasSurface = true; + finishedCreatingEglSurface = false; + Monitor.PulseAll(threadManager); + while (waitingForSurface && !finishedCreatingEglSurface && !exited) + { + try + { + Monitor.Wait(threadManager); + } + catch (Exception) + { + Thread.CurrentThread.Interrupt(); + } + } + } + } + + public void OnSurfaceDestroyed() + { + lock (threadManager) + { + LogDebug($"[GLThread {Id}] OnSurfaceDestroyed"); + + hasSurface = false; + Monitor.PulseAll(threadManager); + while ((!waitingForSurface) && (!exited)) + { + try + { + Monitor.Wait(threadManager); + } + catch (Exception) + { + Thread.CurrentThread.Interrupt(); + } + } + } + } + + public void OnPause() + { + lock (threadManager) + { + LogDebug($"[GLThread {Id}] OnPause"); + + requestPaused = true; + Monitor.PulseAll(threadManager); + + while ((!exited) && (!paused)) + { + LogDebug($"[GLThread {Id}] OnPause: Waiting for paused==True"); + + try + { + Monitor.Wait(threadManager); + } + catch (Exception) + { + Thread.CurrentThread.Interrupt(); + } + } + } + } + + public void OnResume() + { + lock (threadManager) + { + LogDebug($"[GLThread {Id}] OnResume"); + + requestPaused = false; + requestRender = true; + renderComplete = false; + Monitor.PulseAll(threadManager); + while ((!exited) && paused && (!renderComplete)) + { + LogDebug($"[GLThread {Id}] OnResume: Waiting for paused==False"); + + try + { + Monitor.Wait(threadManager); + } + catch (Exception) + { + Thread.CurrentThread.Interrupt(); + } + } + } + } + + public void OnWindowResize(int w, int h) + { + lock (threadManager) + { + width = w; + height = h; + surfaceSizeChanged = true; + requestRender = true; + renderComplete = false; + Monitor.PulseAll(threadManager); + + // Wait for thread to react to resize and render a frame + while (!exited && !paused && !renderComplete && IsAbleToDraw()) + { + LogDebug($"[GLThread {Id}] OnWindowResize: Waiting for render complete"); + + try + { + Monitor.Wait(threadManager); + } + catch (Exception) + { + Thread.CurrentThread.Interrupt(); + } + } + } + } + + public void RequestExitAndWait() + { + // don't call this from GLThread thread or it is a guaranteed deadlock! + lock (threadManager) + { + shouldExit = true; + Monitor.PulseAll(threadManager); + while (!exited) + { + try + { + Monitor.Wait(threadManager); + } + catch (Exception) + { + Thread.CurrentThread.Interrupt(); + } + } + } + } + + public void RequestReleaseEglContextLocked() + { + shouldReleaseEglContext = true; + Monitor.PulseAll(threadManager); + } + + public void QueueEvent(Java.Lang.IRunnable r) + { + if (r == null) + { + throw new ArgumentNullException(nameof(r)); + } + + lock (threadManager) + { + eventQueue.Enqueue(r); + Monitor.PulseAll(threadManager); + } + } + } + + private class LogWriter : Java.IO.Writer + { + private Java.Lang.StringBuilder builder = new Java.Lang.StringBuilder(); + + public override void Close() + { + FlushBuilder(); + } + + public override void Flush() + { + FlushBuilder(); + } + + public override void Write(char[] buf, int offset, int count) + { + for (var i = 0; i < count; i++) + { + var c = buf[offset + i]; + if (c == '\n') + { + FlushBuilder(); + } + else + { + builder.Append(c); + } + } + } + + private void FlushBuilder() + { + if (builder.Length() > 0) + { + LogDebug($"[LogWriter] {builder.ToString()}"); + builder.Delete(0, builder.Length()); + } + } + } + + private class GLThreadManager + { + private bool glesVersionCheckComplete; + private int glesVersion; + private bool glesDriverCheckComplete; + private bool multipleGLESContextsAllowed; + private bool limitedGLESContexts; + private GLThread eglOwner; + + public void ThreadExiting(GLThread thread) + { + lock (this) + { + LogDebug($"[GLThreadManager] ThreadExiting: tid = '{eglOwner?.Id}'"); + + thread.exited = true; + if (eglOwner == thread) + { + eglOwner = null; + } + Monitor.PulseAll(this); + } + } + + public bool TryAcquireEglContextLocked(GLThread thread) + { + if (eglOwner == thread || eglOwner == null) + { + eglOwner = thread; + Monitor.PulseAll(this); + return true; + } + CheckGLESVersion(); + if (multipleGLESContextsAllowed) + { + return true; + } + // Notify the owning thread that it should release the context. + if (eglOwner != null) + { + eglOwner.RequestReleaseEglContextLocked(); + } + return false; + } + + public void ReleaseEglContextLocked(GLThread thread) + { + if (eglOwner == thread) + { + eglOwner = null; + } + Monitor.PulseAll(this); + } + + public bool ShouldReleaseEGLContextWhenPausing() + { + lock (this) + { + // Release the EGL context when pausing even if + // the hardware supports multiple EGL contexts. + // Otherwise the device could run out of EGL contexts. + return limitedGLESContexts; + } + } + + public bool ShouldTerminateEGLWhenPausing() + { + lock (this) + { + CheckGLESVersion(); + return !multipleGLESContextsAllowed; + } + } + + public void CheckGLDriver() + { + lock (this) + { + if (!glesDriverCheckComplete) + { + CheckGLESVersion(); + var renderer = GLES10.GlGetString(GLES10.GlRenderer); + if (glesVersion < EglHelper.GLES_20) + { + multipleGLESContextsAllowed = !renderer.StartsWith(EglHelper.MSM7K_RENDERER_PREFIX); + Monitor.PulseAll(this); + } + limitedGLESContexts = !multipleGLESContextsAllowed; + + LogDebug($"[GLThreadManager] CheckGLDriver: renderer = '{renderer}' multipleContextsAllowed = '{multipleGLESContextsAllowed}' mLimitedGLESContexts = '{limitedGLESContexts}'"); + + glesDriverCheckComplete = true; + } + } + } + + private void CheckGLESVersion() + { + // This check was required for some pre-Android-3.0 hardware. Android 3.0 provides + // support for hardware-accelerated views, therefore multiple EGL contexts are + // supported on all Android 3.0+ EGL drivers. + if (!glesVersionCheckComplete) + { + // SystemProperties.getInt("ro.opengles.version", ConfigurationInfo.GL_ES_VERSION_UNDEFINED) + var activityManager = ActivityManager.FromContext(Application.Context); + var configInfo = activityManager.DeviceConfigurationInfo; + if (configInfo.ReqGlEsVersion != ConfigurationInfo.GlEsVersionUndefined) + { + glesVersion = configInfo.ReqGlEsVersion; + } + else + { + glesVersion = 1 << 16; // Lack of property means OpenGL ES version 1 + } + if (glesVersion >= EglHelper.GLES_20) + { + multipleGLESContextsAllowed = true; + } + glesVersionCheckComplete = true; + } + } + } + + + private class EglHelper + { + public const int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + public const int EGL_OPENGL_ES2_BIT = 4; + public const string MSM7K_RENDERER_PREFIX = "Q3Dimension MSM7500 "; + public const int GLES_20 = 0x20000; + + private WeakReference textureViewWeakRef; + private EGLDisplay eglDisplay; + private EGLSurface eglSurface; + private EGLContext eglContext; + private EGLConfig eglConfig; + + public EglHelper(WeakReference glTextureViewWeakRef) + { + textureViewWeakRef = glTextureViewWeakRef; + } + + public EGLConfig EglConfig => eglConfig; + + private int CurrentThreadId => Thread.CurrentThread.ManagedThreadId; + + public void Start() + { + LogDebug($"[GLThread {CurrentThreadId}][EglHelper] Start"); + + + // Get to the default display. + eglDisplay = EGL14.EglGetDisplay(EGL14.EglDefaultDisplay); + + if (eglDisplay == EGL14.EglNoDisplay) + { + throw new Exception("eglGetDisplay failed"); + } + + // We can now initialize EGL for that display + var versionMaj = new int[2]; + var versionMin = new int[2]; + if (!EGL14.EglInitialize(eglDisplay, versionMaj, 0, versionMin, 0)) + { + throw new Exception("eglInitialize failed"); + } + if (!textureViewWeakRef.TryGetTarget(out SkiaGLTextureView view)) + { + eglConfig = null; + eglContext = null; + } + else + { + eglConfig = view.eglConfigChooser.ChooseConfig(eglDisplay); + // Create an EGL context. We want to do this as rarely as we can, because an + // EGL context is a somewhat heavy object. + eglContext = view.eglContextFactory.CreateContext(eglDisplay, eglConfig); + } + if (eglContext == null || eglContext == EGL14.EglNoContext) + { + eglContext = null; + + var error = EGL14.EglGetError(); + LogError($"[GLThread {CurrentThreadId}][EglHelper] createContext failed: {error}"); + throw new Exception($"createContext failed: {error}"); + } + + LogDebug($"[GLThread {CurrentThreadId}][EglHelper] createContext {eglContext}"); + + eglSurface = null; + } + + public bool CreateSurface() + { + LogDebug($"[GLThread {CurrentThreadId}][EglHelper] CreateSurface"); + + if (eglDisplay == null) + { + throw new Exception("eglDisplay not initialized"); + } + if (eglConfig == null) + { + throw new Exception("mEglConfig not initialized"); + } + // The window size has changed, so we need to create a new surface. + DestroySurfaceImpl(); + + // Create an EGL surface we can render into. + if (textureViewWeakRef.TryGetTarget(out SkiaGLTextureView view)) + { + eglSurface = view.eglWindowSurfaceFactory.CreateWindowSurface(eglDisplay, eglConfig, view.SurfaceTexture); + } + else + { + eglSurface = null; + } + if (eglSurface == null || eglSurface == EGL14.EglNoSurface) + { + var error = EGL14.EglGetError(); + if (error == EGL14.EglBadNativeWindow) + { + LogError($"[GLThread {CurrentThreadId}][EglHelper] createWindowSurface returned EGL_BAD_NATIVE_WINDOW"); + } + return false; + } + // Before we can issue IGL commands, we need to make sure the context is + // current and bound to a surface. + if (!EGL14.EglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) + { + // Could not make the context current, probably because the underlying + // TextureView surface has been destroyed. + LogError($"[GLThread {CurrentThreadId}][EglHelper] eglMakeCurrent failed: {EGL14.EglGetError()}"); + return false; + } + + return true; + } + + + public int Swap() + { + //GLES20.GlFlush(); + + //VSync ON + EGL14.EglSwapInterval(eglDisplay, 1); + + if (!EGL14.EglSwapBuffers(eglDisplay, eglSurface)) + { + return EGL14.EglGetError(); + } + return EGL14.EglSuccess; + } + + public void DestroySurface() + { + LogDebug($"[GLThread {CurrentThreadId}][EglHelper] DestroySurface"); + + if (textureViewWeakRef.TryGetTarget(out SkiaGLTextureView view)) + { + view.renderer.OnSurfaceDestroyed(); + } + DestroySurfaceImpl(); + } + + private void DestroySurfaceImpl() + { + if (eglSurface != null && eglSurface != EGL14.EglNoSurface) + { + EGL14.EglMakeCurrent(eglDisplay, EGL14.EglNoSurface, EGL14.EglNoSurface, EGL14.EglNoContext); + if (textureViewWeakRef.TryGetTarget(out SkiaGLTextureView view)) + { + view.eglWindowSurfaceFactory.DestroySurface(eglDisplay, eglSurface); + } + eglSurface = null; + } + } + + public void Finish() + { + LogDebug($"[GLThread {CurrentThreadId}][EglHelper] Finish"); + + if (eglContext != null) + { + if (textureViewWeakRef.TryGetTarget(out SkiaGLTextureView view)) + { + view.eglContextFactory.DestroyContext(eglDisplay, eglContext); + } + eglContext = null; + } + if (eglDisplay != null) + { + EGL14.EglTerminate(eglDisplay); + eglDisplay = null; + } + } + } + + +} \ No newline at end of file diff --git a/src/Engine/Platforms/Android/Super.Android.cs b/src/Engine/Platforms/Android/Super.Android.cs index 894b3980..78db147f 100644 --- a/src/Engine/Platforms/Android/Super.Android.cs +++ b/src/Engine/Platforms/Android/Super.Android.cs @@ -14,9 +14,12 @@ public partial class Super public static Android.App.Activity MainActivity { get; set; } private static FrameCallback _frameCallback; + static bool _loopStarting = false; static bool _loopStarted = false; - public static event EventHandler ChoreographerCallback; + + public static event EventHandler OnFrame; + static Looper Looper { get; set; } public static void Init(Android.App.Activity activity) { @@ -48,36 +51,61 @@ public static void Init(Android.App.Activity activity) VisualDiagnostics.VisualTreeChanged += OnVisualTreeChanged; + bool isRendering = false; + object lockFrane = new(); + + /* Tasks.StartDelayed(TimeSpan.FromMilliseconds(250), async () => { _frameCallback = new FrameCallback((nanos) => { - ChoreographerCallback?.Invoke(null, null); + if (isRendering) + return; + isRendering = true; + OnFrame?.Invoke(null, null); Choreographer.Instance.PostFrameCallback(_frameCallback); + isRendering = false; }); while (!_loopStarted) { MainThread.BeginInvokeOnMainThread(async () => { - if (_loopStarting) - return; - _loopStarting = true; - - if (MainThread.IsMainThread) //Choreographer is available + lock (lockFrane) { - if (!_loopStarted) + if (_loopStarting) + return; + + _loopStarting = true; + + if (MainThread.IsMainThread) // Choreographer is available { - _loopStarted = true; - Choreographer.Instance.PostFrameCallback(_frameCallback); + if (!_loopStarted) + { + _loopStarted = true; + Choreographer.Instance.PostFrameCallback(_frameCallback); + } } + + _loopStarting = false; } - _loopStarting = false; }); + + if (_loopStarted) + break; + await Task.Delay(100); } + + }); + */ + + Looper = new(() => + { + OnFrame?.Invoke(null, null); }); + Looper.StartOnMainThread(120); } diff --git a/src/Engine/Platforms/MacCatalyst/Files.Mac.cs b/src/Engine/Platforms/MacCatalyst/Files.Mac.cs index d0c6bc10..fa342a74 100644 --- a/src/Engine/Platforms/MacCatalyst/Files.Mac.cs +++ b/src/Engine/Platforms/MacCatalyst/Files.Mac.cs @@ -1,4 +1,6 @@ -namespace DrawnUi.Maui.Infrastructure +using Foundation; + +namespace DrawnUi.Maui.Infrastructure { public partial class Files { @@ -7,6 +9,23 @@ public static void RefreshSystem(FileDescriptor file) } + public static List ListAssets(string subfolder) + { + NSBundle mainBundle = NSBundle.MainBundle; + string resourcesPath = mainBundle.ResourcePath; + string subfolderPath = Path.Combine(resourcesPath, subfolder); + + if (Directory.Exists(subfolderPath)) + { + string[] files = Directory.GetFiles(subfolderPath); + return files.Select(Path.GetFileName).ToList(); + } + else + { + return new List(); + } + } + public static string GetPublicDirectory() { return FileSystem.Current.AppDataDirectory; diff --git a/src/Engine/Platforms/Windows/DrawnView.Windows.cs b/src/Engine/Platforms/Windows/DrawnView.Windows.cs index 70e5fe7e..d86dd64f 100644 --- a/src/Engine/Platforms/Windows/DrawnView.Windows.cs +++ b/src/Engine/Platforms/Windows/DrawnView.Windows.cs @@ -1,5 +1,6 @@ -using Microsoft.Maui.Platform; -using Microsoft.UI.Xaml; +using Microsoft.Maui.Controls.PlatformConfiguration; +using Microsoft.Maui.Platform; +using Microsoft.Maui.Platform; using System.Runtime.CompilerServices; using Visibility = Microsoft.UI.Xaml.Visibility; @@ -38,8 +39,10 @@ protected virtual void OnSizeChanged() public virtual void SetupRenderingLoop() { +#if !LEGACY Super.OnFrame -= OnSuperFrame; Super.OnFrame += OnSuperFrame; +#endif } protected virtual void PlatformHardwareAccelerationChanged() @@ -47,6 +50,37 @@ protected virtual void PlatformHardwareAccelerationChanged() } + + + +#if LEGACY + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool CheckCanDraw() + { + if (UpdateLocked && StopDrawingWhenUpdateIsLocked) + return false; + + return CanvasView != null + && !IsRendering + && IsDirty + && IsVisible; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void UpdatePlatform() + { + IsDirty = true; + if (!OrderedDraw && CheckCanDraw()) + { + OrderedDraw = true; + InvalidateCanvas(); + } + } + + + +#else [MethodImpl(MethodImplOptions.AggressiveInlining)] protected void UpdatePlatform() { @@ -65,6 +99,8 @@ public bool CheckCanDraw() && IsVisible && Super.EnableRendering; } +#endif + private long test; private void OnSuperFrame(object sender, EventArgs e) { diff --git a/src/Engine/Platforms/Windows/Files.Windows.cs b/src/Engine/Platforms/Windows/Files.Windows.cs index 8c5ccbd0..317e454c 100644 --- a/src/Engine/Platforms/Windows/Files.Windows.cs +++ b/src/Engine/Platforms/Windows/Files.Windows.cs @@ -1,4 +1,6 @@ -namespace DrawnUi.Maui.Infrastructure +using Windows.Storage; + +namespace DrawnUi.Maui.Infrastructure { public partial class Files { @@ -12,6 +14,14 @@ public static string GetPublicDirectory() return Windows.Storage.KnownFolders.DocumentsLibrary.Path; } + public static List ListAssets(string sub) + { + StorageFolder installFolder = Windows.ApplicationModel.Package.Current.InstalledLocation; + StorageFolder subfolder = installFolder.GetFolderAsync(sub).GetAwaiter().GetResult(); + IReadOnlyList files = subfolder.GetFilesAsync().GetAwaiter().GetResult(); + return files.Select(f => f.Name).ToList(); + } + public static void Share(string message, IEnumerable fullFilenames) { MainThread.BeginInvokeOnMainThread(async () => diff --git a/src/Engine/Platforms/Windows/Super.Windows.cs b/src/Engine/Platforms/Windows/Super.Windows.cs index d990d603..3594018e 100644 --- a/src/Engine/Platforms/Windows/Super.Windows.cs +++ b/src/Engine/Platforms/Windows/Super.Windows.cs @@ -33,7 +33,7 @@ public static void Init() static Looper Looper { get; set; } - public static EventHandler OnFrame; + public static event EventHandler OnFrame; /// /// Opens web link in native browser @@ -56,13 +56,13 @@ public static void OpenLink(string link) /// /// /// - public static IEnumerable ListAssets(string subfolder) + public static List ListAssets(string subfolder) { StorageFolder installFolder = Windows.ApplicationModel.Package.Current.InstalledLocation; StorageFolder sub = installFolder.GetFolderAsync(subfolder).GetAwaiter().GetResult(); IReadOnlyList files = sub.GetFilesAsync().GetAwaiter().GetResult(); - return files.Select(f => f.Name); + return files.Select(f => f.Name).ToList(); } } diff --git a/src/Engine/Platforms/iOS/Files.iOS.cs b/src/Engine/Platforms/iOS/Files.iOS.cs index d0c6bc10..fa342a74 100644 --- a/src/Engine/Platforms/iOS/Files.iOS.cs +++ b/src/Engine/Platforms/iOS/Files.iOS.cs @@ -1,4 +1,6 @@ -namespace DrawnUi.Maui.Infrastructure +using Foundation; + +namespace DrawnUi.Maui.Infrastructure { public partial class Files { @@ -7,6 +9,23 @@ public static void RefreshSystem(FileDescriptor file) } + public static List ListAssets(string subfolder) + { + NSBundle mainBundle = NSBundle.MainBundle; + string resourcesPath = mainBundle.ResourcePath; + string subfolderPath = Path.Combine(resourcesPath, subfolder); + + if (Directory.Exists(subfolderPath)) + { + string[] files = Directory.GetFiles(subfolderPath); + return files.Select(Path.GetFileName).ToList(); + } + else + { + return new List(); + } + } + public static string GetPublicDirectory() { return FileSystem.Current.AppDataDirectory; diff --git a/src/Engine/Views/DrawnView.cs b/src/Engine/Views/DrawnView.cs index 12ce5154..80b9f429 100644 --- a/src/Engine/Views/DrawnView.cs +++ b/src/Engine/Views/DrawnView.cs @@ -11,6 +11,14 @@ namespace DrawnUi.Maui.Views [ContentProperty("Children")] public partial class DrawnView : ContentView, IDrawnBase, IAnimatorsManager, IVisualTreeElement { + + public class DiagnosticData + { + public int LayersSaved { get; set; } + } + + public DiagnosticData Diagnostics = new(); + public virtual void Update() { if (!Super.EnableRendering) @@ -1049,9 +1057,10 @@ public DrawnView() public Action Clipping { get; set; } - public virtual SKPath CreateClip(object arguments, bool usePosition) + public virtual SKPath CreateClip(object arguments, bool usePosition, SKPath path = null) { - var path = new SKPath(); + path ??= new SKPath(); + if (usePosition) { path.AddRect(DrawingRect); @@ -1589,20 +1598,11 @@ protected virtual void Draw(SkiaDrawingContext context, SKRect destination, floa { ++renderedFrames; - - //if (CanvasView is SkiaViewAccelerated accelerated) - //{ - // var c = accelerated.GRContext; - // Console.WriteLine($"[FRAME] {++renderedFrames} {c} {destination.Width}x{destination.Height} at {scale}"); - //} + //Debug.WriteLine($"[DRAW] {Tag}"); DisposeDisposables(); - //Trace.WriteLine($"[1] {destination.Width}x{destination.Height} at {scale}"); - - if (IsDisposed || UpdateLocked - //|| Super.StopRenderingInBackground - ) + if (IsDisposed || UpdateLocked) { return; } @@ -1625,7 +1625,6 @@ protected virtual void Draw(SkiaDrawingContext context, SKRect destination, floa DrawingThreads++; FrameTime = CanvasView.FrameTime; - //context.FrameTimeNanos = FrameTime; FPS = CanvasFps; @@ -2416,6 +2415,7 @@ public ISkiaGestureListener FocusedChild } ISkiaGestureListener _focusedChild; private ISkiaDrawable _canvasView; + private bool _wasBusy; /// /// @@ -2464,14 +2464,16 @@ void Continue() { if (CanvasView != null) { - if (!CanvasView.IsDrawing && CanDraw && !_isWaiting) //passed checks + if (CanDraw && !CanvasView.IsDrawing && !_isWaiting) //passed checks // { + _wasBusy = false; _isWaiting = true; InvalidatedCanvas++; MainThread.BeginInvokeOnMainThread(async () => { try { +#if !WINDOWS //cap fps around 120fps var nowNanos = Super.GetCurrentTimeNanos(); var elapsedMicros = (nowNanos - _lastUpdateTimeNanos) / 1_000.0; @@ -2479,7 +2481,7 @@ void Continue() var needWait = Super.CapMicroSecs -#if IOS || MACCATALYST +#if IOS || MACCATALYST * 2 // apple is double buffered #endif - elapsedMicros; @@ -2490,6 +2492,10 @@ void Continue() if (ms < 1) ms = 1; await Task.Delay(ms); +#else + await Task.Delay(1); +#endif + CanvasView?.Update(); //very rarely could throw on windows here if maui destroys view when navigating, so we secured with try-catch } catch (Exception e) @@ -2499,11 +2505,19 @@ void Continue() finally { _isWaiting = false; + if (_wasBusy) + { + Update(); + } } }); return; } + else + { + _wasBusy = true; + } } OrderedDraw = false; } diff --git a/src/Engine/Views/SkiaView.cs b/src/Engine/Views/SkiaView.cs index 70da151c..5e0ee4eb 100644 --- a/src/Engine/Views/SkiaView.cs +++ b/src/Engine/Views/SkiaView.cs @@ -131,12 +131,12 @@ private void OnPaintingSurface(object sender, SKPaintSurfaceEventArgs paintArgs) bool isDirty = OnDraw.Invoke(paintArgs.Surface, new SKRect(0, 0, paintArgs.Info.Width, paintArgs.Info.Height)); #if ANDROID - if (maybeLowEnd && FPS > 160) + if (maybeLowEnd && FPS > 60) { maybeLowEnd = false; } - if (maybeLowEnd && isDirty && _fps < 55) //kick refresh for low-end devices + if (maybeLowEnd && isDirty && _fps < 30) //kick refresh for low-end devices { InvalidateSurface(); return; diff --git a/src/Engine/Views/SkiaViewAccelerated.cs b/src/Engine/Views/SkiaViewAccelerated.cs index 91677e68..92e74a16 100644 --- a/src/Engine/Views/SkiaViewAccelerated.cs +++ b/src/Engine/Views/SkiaViewAccelerated.cs @@ -172,6 +172,7 @@ public void Update(long nanos) { _nanos = nanos; IsDrawing = true; + InvalidateSurface(); } } @@ -232,12 +233,12 @@ private void OnPaintingSurface(object sender, SKPaintGLSurfaceEventArgs paintArg var isDirty = OnDraw.Invoke(paintArgs.Surface, rect); #if ANDROID - if (maybeLowEnd && FPS > 160) + if (maybeLowEnd && FPS > 60) { maybeLowEnd = false; } - if (maybeLowEnd && isDirty && _fps < 55) //kick refresh for low-end devices + if (maybeLowEnd && isDirty && _fps < 30) //kick refresh for low-end devices { InvalidateSurface(); return; diff --git a/src/Engine/skia2.props b/src/Engine/skia2.props index a8e4088c..4edee202 100644 --- a/src/Engine/skia2.props +++ b/src/Engine/skia2.props @@ -1,8 +1,8 @@ - - + + \ No newline at end of file diff --git a/src/samples/Sandbox/MainPageDev.xaml b/src/samples/Sandbox/MainPageDev.xaml index 3d0ee4bb..01732e61 100644 --- a/src/samples/Sandbox/MainPageDev.xaml +++ b/src/samples/Sandbox/MainPageDev.xaml @@ -5,14 +5,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:controls="clr-namespace:Sandbox.Views.Controls" xmlns:draw="http://schemas.appomobi.com/drawnUi/2023/draw" - xmlns:mauiNet8="using:MauiNet8" xmlns:views="clr-namespace:Sandbox.Views" - xmlns:xaml2Pdf="clr-namespace:Sandbox.Views.Xaml2Pdf" + x:Name="ThisPage" BackgroundColor="Black"> - - - - - - - - + + + + Tag="Content" + Type="Column"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - + _shaders; + public MainPageDev() { try @@ -39,6 +43,8 @@ public MainPageDev() InitializeComponent(); Test(); + + _shaders = Files.ListAssets(path); } catch (Exception e) { @@ -48,7 +54,84 @@ public MainPageDev() void Test() { - // string shaderCode = SkSl.LoadFromResources($"{MauiProgram.ShadersFolder}/apple.sksl"); + // string shaderCode = SkSl.LoadFromResources($"{MauiProgram.ShadersFolder}/apple.sksl"); //var effect = SkSl.Compile(shaderCode); } + + async void SelectFIle() + { + if (_shaders.Count > 1) + { + var options = _shaders.Select(name => new SelectableAction + { + Action = async () => + { + ShaderFile = name; + }, + Title = name + }).ToList(); + var selected = await PresentSelection(options, "Select Shader") as SelectableAction; + selected?.Action(); + } + } + + public async Task PresentSelection(IEnumerable options, + string title = null, string cancel = null) + { + if (string.IsNullOrEmpty(title)) + title = "Select"; + + if (string.IsNullOrEmpty(cancel)) + cancel = "Cancel"; + + var result = await App.Current.MainPage.DisplayActionSheet(title, cancel, + null, options.Select(x => x.Title).ToArray() + ); + + if (string.IsNullOrEmpty(result)) + { + return null; //cancel + } + + var selected = options.FirstOrDefault(x => x.Title == result); + return selected; + } + + private void SkiaButton_OnTapped(object sender, SkiaGesturesParameters e) + { + MainThread.BeginInvokeOnMainThread(SelectFIle); + } + + private string path = @"Shaders\transitions"; + + public string FullShaderPath + { + get + { + return $"{path}\\{ShaderFile}"; + } + set + { + + } + } + + private string _ShaderFile = "dreamy.sksl"; + public string ShaderFile + { + get + { + return _ShaderFile; + } + set + { + if (_ShaderFile != value) + { + _ShaderFile = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(FullShaderPath)); + } + } + } + } \ No newline at end of file diff --git a/src/samples/Sandbox/Sandbox.csproj b/src/samples/Sandbox/Sandbox.csproj index a9edc551..76e99793 100644 --- a/src/samples/Sandbox/Sandbox.csproj +++ b/src/samples/Sandbox/Sandbox.csproj @@ -8,7 +8,7 @@ true true enable - + true false com.companyname.sandbox2 @@ -182,9 +182,9 @@ - - - + + + @@ -284,15 +284,6 @@ - - - MSBuild:Compile - - - MSBuild:Compile - - - diff --git a/src/samples/Sandbox/Views/Controls/ContentFolder.cs b/src/samples/Sandbox/Views/Controls/ContentFolder.cs index b485bf05..656c8e9d 100644 --- a/src/samples/Sandbox/Views/Controls/ContentFolder.cs +++ b/src/samples/Sandbox/Views/Controls/ContentFolder.cs @@ -27,43 +27,43 @@ public double VerticalMargin set { SetValue(VerticalMarginProperty, value); } } - public static readonly BindableProperty BacksideSourceProperty = BindableProperty.Create( - nameof(BacksideSource), + public static readonly BindableProperty SecondarySourceProperty = BindableProperty.Create( + nameof(SecondarySource), typeof(string), typeof(ContentFolder), defaultValue: null, propertyChanged: ApplySourceProperty); - public string BacksideSource + public string SecondarySource { - get { return (string)GetValue(BacksideSourceProperty); } - set { SetValue(BacksideSourceProperty, value); } + get { return (string)GetValue(SecondarySourceProperty); } + set { SetValue(SecondarySourceProperty, value); } } private static void ApplySourceProperty(BindableObject bindable, object oldvalue, object newvalue) { if (oldvalue != newvalue && bindable is ContentFolder control) { - control.ApplyBacksideSource((string)newvalue); + control.ApplySecondarySource((string)newvalue); } } - public void SetBackside(SKImage image) + public void SetSecondary(SKImage image) { - var dispose = _imageBackside; - _imageBackside = image; - if (dispose != _imageBackside) + var dispose = _imageSecondary; + _imageSecondary = image; + if (dispose != _imageSecondary) dispose?.Dispose(); UpdateTextures(); } - void ApplyBacksideSource(string source) + void ApplySecondarySource(string source) { Task.Run(async () => { var background = await LoadSource(source); - SetBackside(background); + SetSecondary(background); }); } @@ -73,7 +73,7 @@ protected override void OnLayoutChanged() { base.OnLayoutChanged(); - ApplyBacksideSource(this.BacksideSource); + ApplySecondarySource(this.SecondarySource); } /// @@ -158,7 +158,7 @@ public override void OnDisposing() _textureFront?.Dispose(); _textureBack?.Dispose(); - _imageBackside?.Dispose(); + _imageSecondary?.Dispose(); base.OnDisposing(); } @@ -195,7 +195,7 @@ void BuildTextures(SKImage front, SKImage back) _textureFront = front.ToShader(); if (back != null) { - _textureBack = _imageBackside.ToShader(SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); + _textureBack = _imageSecondary.ToShader(SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); } } @@ -240,7 +240,7 @@ void UpdateTextures() var cache = content.RenderObject; if (_compiledShader != null && cache is { Image: not null }) { - BuildTextures(cache.Image, _imageBackside); + BuildTextures(cache.Image, _imageSecondary); } } } @@ -296,7 +296,7 @@ private void DrawContentImage(CachedObject cache, SkiaDrawingContext ctx, SKRect /// private SKRuntimeEffectChildren _passTextures; - private SKImage _imageBackside; + private SKImage _imageSecondary; protected Vector2 _offset; protected Vector2 _origin; diff --git a/src/samples/Sandbox/Views/Controls/DrawnSlider.xaml b/src/samples/Sandbox/Views/Controls/DrawnSlider.xaml index d7b13f04..6e4e6dfd 100644 --- a/src/samples/Sandbox/Views/Controls/DrawnSlider.xaml +++ b/src/samples/Sandbox/Views/Controls/DrawnSlider.xaml @@ -11,7 +11,7 @@ SliderHeight="35" Tag="SliderFun" Type="Column" - UseCache="Operations"> + UseCache="ImageDoubleBuffered"> diff --git a/src/samples/Sandbox/Views/Controls/MultiRippleWithTouchEffect.cs b/src/samples/Sandbox/Views/Controls/MultiRippleWithTouchEffect.cs index 1964fa3d..980344b9 100644 --- a/src/samples/Sandbox/Views/Controls/MultiRippleWithTouchEffect.cs +++ b/src/samples/Sandbox/Views/Controls/MultiRippleWithTouchEffect.cs @@ -1,10 +1,9 @@ using AppoMobi.Maui.Gestures; -using AppoMobi.Specials; using System.Collections.Concurrent; namespace Sandbox.Views.Controls; -public class MultiRippleWithTouchEffect : SkiaShaderEffect, IStateEffect, ISkiaGestureProcessor +public class MultiRippleWithTouchEffect : ShaderDoubleTexturesEffect, IStateEffect, ISkiaGestureProcessor { public MultiRippleWithTouchEffect() { @@ -13,9 +12,10 @@ public MultiRippleWithTouchEffect() bool _initialized; private PointF _mouse; - private SkiaControl _controlSource; - public void UpdateState() + #region IStateEffect + + void IStateEffect.UpdateState() { if (Parent != null && !_initialized && Parent.IsLayoutReady) { @@ -29,9 +29,12 @@ public override void Attach(SkiaControl parent) { base.Attach(parent); - UpdateState(); + (this as IStateEffect).UpdateState(); } + #endregion + + protected override SKRuntimeEffectUniforms CreateUniforms(SKRect destination) { var uniforms = base.CreateUniforms(destination); @@ -68,252 +71,6 @@ protected override SKRuntimeEffectUniforms CreateUniforms(SKRect destination) return uniforms; } - #region REFLECTION - - private SKShader _textureBackground; - - protected override SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingContext ctx, SKRect destination, SKImage snapshot) - { - var texture2 = GetReflectionTexture(); - - if (snapshot != null && texture2 != null) - { - var texture1 = snapshot.ToShader(); - - return new SKRuntimeEffectChildren(CompiledShader) - { - { "iImage1", texture1 }, //background - { "iImage2", texture2 } //reflection - }; - } - else - { - return new SKRuntimeEffectChildren(CompiledShader) - { - }; - } - } - - private void OnCacheCreatedTo(object sender, CachedObject e) - { - Update(); - } - - #region ReflectionFromFile - - public static readonly BindableProperty BacksideSourceProperty = BindableProperty.Create( - nameof(BacksideSource), - typeof(string), - typeof(MultiRippleWithTouchEffect), - defaultValue: null, - propertyChanged: ApplyBacksideSourceProperty); - - public string BacksideSource - { - get { return (string)GetValue(BacksideSourceProperty); } - set { SetValue(BacksideSourceProperty, value); } - } - - private static void ApplyBacksideSourceProperty(BindableObject bindable, object oldvalue, object newvalue) - { - if (oldvalue != newvalue && bindable is MultiRippleWithTouchEffect control) - { - control.ApplyBacksideSource((string)newvalue); - } - } - - void ApplyBacksideSource(string source) - { - Task.Run(async () => - { - await LoadSource(source); - if (ParentReady() && _loadedReflectionBitmap != null) - { - SetBackside(); - } - - }); - } - - private bool _reflectionSet; - - public void SetBackside() - { - SKImage image = null; - - if (_loadedReflectionBitmap != null) - { - var outRect = Parent.DrawingRect; - var info = new SKImageInfo((int)outRect.Width, (int)outRect.Height); - var resizedBitmap = new SKBitmap(info); - using (var canvas = new SKCanvas(resizedBitmap)) - { - // This will stretch the original image to fill the new size - var rect = new SKRect(0, 0, (int)outRect.Width, (int)outRect.Height); - canvas.DrawBitmap(_loadedReflectionBitmap, rect); - canvas.Flush(); - } - - image = SKImage.FromBitmap(resizedBitmap); - } - - var dispose = _textureReflection; - - if (image != null) - { - _textureReflection = image.ToShader(SKShaderTileMode.Mirror, SKShaderTileMode.Mirror); ; - } - - if (dispose != _textureReflection) - dispose?.Dispose(); - - _reflectionSet = true; - - Update(); - } - - private SKShader _textureReflection; - - SKShader GetReflectionTexture() - { - if (!_reflectionSet && ParentReady()) - { - SetBackside(); - } - return _textureReflection; - } - - //todo move this to helper whatever to share code - - protected bool ParentReady() - { - return !(Parent == null || Parent.DrawingRect.Width <= 0 || Parent.DrawingRect.Height <= 0); - } - - private SemaphoreSlim _semaphoreLoadFile = new(1, 1); - - /// - /// Loading from local files only - /// - /// - /// - public async Task LoadSource(string fileName) - { - if (string.IsNullOrEmpty(fileName)) - return; - - await _semaphoreLoadFile.WaitAsync(); - - try - { - if (fileName.SafeContainsInLower("file://")) - { - var fullFilename = fileName.Replace("file://", "", StringComparison.InvariantCultureIgnoreCase); - using var stream = new FileStream(fullFilename, System.IO.FileMode.Open); - _loadedReflectionBitmap = SKBitmap.Decode(stream); - } - else - { - using var stream = await FileSystem.OpenAppPackageFileAsync(fileName); - _loadedReflectionBitmap = SKBitmap.Decode(stream); - } - - _reflectionSet = false; - return; - } - catch (Exception e) - { - Console.WriteLine($"LoadSource failed to load animation {fileName}"); - Console.WriteLine(e); - return; - } - finally - { - _semaphoreLoadFile.Release(); - } - } - - protected override void OnDisposing() - { - base.OnDisposing(); - - _textureReflection?.Dispose(); - _loadedReflectionBitmap?.Dispose(); - } - - SKBitmap _loadedReflectionBitmap; - - #endregion - - - #region ReflectionFromControl - - - - /* - - SKShader GetReflectionTexture() - { - if (ReflectionSourceControl == null || ReflectionSourceControl.RenderObject == null) - { - return null; - } - return ReflectionSourceControl.RenderObject.Image.ToShader(SKShaderTileMode.Mirror, SKShaderTileMode.Mirror);;; - } - - void DetachTo() - { - if (_controlSource != null) - { - _controlSource.CreatedCache -= OnCacheCreatedTo; - _controlSource = null; - } - } - - private static void ApplyReflectionSourceControlProperty(BindableObject bindable, object oldvalue, object newvalue) - { - if (oldvalue != newvalue && bindable is MultiRippleWithTouchEffect control) - { - control.ApplyReflectionSourceControl(newvalue as SkiaControl); - } - } - - void ApplyReflectionSourceControl(SkiaControl control) - { - if (_controlSource == control) - return; - - DetachTo(); - _controlSource = control; - if (_controlSource != null) - { - _controlSource.CreatedCache += OnCacheCreatedTo; - } - } - - public static readonly BindableProperty ReflectionSourceControlProperty = BindableProperty.Create( - nameof(ReflectionSourceControl), - typeof(SkiaControl), typeof(TestLoopEffect), - null, - propertyChanged: ApplyReflectionSourceControlProperty); - - public SkiaControl ReflectionSourceControl - { - get { return (SkiaControl)GetValue(ReflectionSourceControlProperty); } - set { SetValue(ReflectionSourceControlProperty, value); } - } - - protected override void OnDisposing() - { - base.OnDisposing(); - DetachTo(); - } - */ - - #endregion - - #endregion - #region RIPPLES public class Ripple diff --git a/src/samples/Sandbox/Views/Controls/ShaderDoubleTexturesEffect.cs b/src/samples/Sandbox/Views/Controls/ShaderDoubleTexturesEffect.cs new file mode 100644 index 00000000..9913cd66 --- /dev/null +++ b/src/samples/Sandbox/Views/Controls/ShaderDoubleTexturesEffect.cs @@ -0,0 +1,183 @@ +using AppoMobi.Specials; + +namespace Sandbox.Views.Controls; + +/// +/// Base shader effect class that has 2 input textures +/// +public class ShaderDoubleTexturesEffect : SkiaShaderEffect +{ + protected bool ParentReady() + { + return !(Parent == null || Parent.DrawingRect.Width <= 0 || Parent.DrawingRect.Height <= 0); + } + + #region SecondaryTexture + + #region FromFile + + protected SKShaderTileMode TilingSecondaryTexture = SKShaderTileMode.Mirror; + + protected override SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingContext ctx, SKRect destination, SKShader texture1) + { + var texture2 = GetSecondaryTexture(); + + if (texture1 != null && texture2 != null) + { + //var texture1 = snapshot.ToShader(); + + return new SKRuntimeEffectChildren(CompiledShader) + { + { "iImage1", texture1 }, //main + { "iImage2", texture2 } //secondary + }; + } + else + { + return new SKRuntimeEffectChildren(CompiledShader) + { + }; + } + } + + public static readonly BindableProperty SecondarySourceProperty = BindableProperty.Create( + nameof(SecondarySource), + typeof(string), + typeof(ShaderDoubleTexturesEffect), + defaultValue: null, + propertyChanged: ApplySecondarySourceProperty); + + public string SecondarySource + { + get { return (string)GetValue(SecondarySourceProperty); } + set { SetValue(SecondarySourceProperty, value); } + } + + private static void ApplySecondarySourceProperty(BindableObject bindable, object oldvalue, object newvalue) + { + if (oldvalue != newvalue && bindable is ShaderDoubleTexturesEffect control) + { + control.ApplySecondarySource((string)newvalue); + } + } + + void ApplySecondarySource(string source) + { + Task.Run(async () => + { + await LoadSource(source); + if (ParentReady() && _loadedReflectionBitmap != null) + { + CompileSecondaryTexture(); + } + + }); + } + + private bool _secondarySourceSet; + + public void CompileSecondaryTexture() + { + SKImage image = null; + + if (_loadedReflectionBitmap != null) + { + var outRect = Parent.DrawingRect; + var info = new SKImageInfo((int)outRect.Width, (int)outRect.Height); + var resizedBitmap = new SKBitmap(info); + using (var canvas = new SKCanvas(resizedBitmap)) + { + // This will stretch the original image to fill the new size + var rect = new SKRect(0, 0, (int)outRect.Width, (int)outRect.Height); + canvas.DrawBitmap(_loadedReflectionBitmap, rect); + canvas.Flush(); + } + + image = SKImage.FromBitmap(resizedBitmap); + } + + var dispose = SecondaryTexture; + + if (image != null) + { + SecondaryTexture = image.ToShader(TilingSecondaryTexture, TilingSecondaryTexture); + } + + if (dispose != SecondaryTexture) + dispose?.Dispose(); + + _secondarySourceSet = true; + + Update(); + } + + protected SKShader SecondaryTexture; + + protected virtual SKShader GetSecondaryTexture() + { + if (!_secondarySourceSet && ParentReady()) + { + CompileSecondaryTexture(); + } + return SecondaryTexture; + } + + + private SemaphoreSlim _semaphoreLoadFile = new(1, 1); + + /// + /// Loading from local files only + /// + /// + /// + public async Task LoadSource(string fileName) + { + if (string.IsNullOrEmpty(fileName)) + return; + + await _semaphoreLoadFile.WaitAsync(); + + try + { + if (fileName.SafeContainsInLower("file://")) + { + var fullFilename = fileName.Replace("file://", "", StringComparison.InvariantCultureIgnoreCase); + using var stream = new FileStream(fullFilename, System.IO.FileMode.Open); + _loadedReflectionBitmap = SKBitmap.Decode(stream); + } + else + { + using var stream = await FileSystem.OpenAppPackageFileAsync(fileName); + _loadedReflectionBitmap = SKBitmap.Decode(stream); + } + + _secondarySourceSet = false; + return; + } + catch (Exception e) + { + Console.WriteLine($"LoadSource failed to load animation {fileName}"); + Console.WriteLine(e); + return; + } + finally + { + _semaphoreLoadFile.Release(); + } + } + + protected override void OnDisposing() + { + base.OnDisposing(); + + SecondaryTexture?.Dispose(); + _loadedReflectionBitmap?.Dispose(); + } + + SKBitmap _loadedReflectionBitmap; + + #endregion + + + #endregion +} \ No newline at end of file diff --git a/src/samples/Sandbox/Views/Controls/ShaderTransition.cs b/src/samples/Sandbox/Views/Controls/ShaderTransition.cs index ee7533f8..664ea1dd 100644 --- a/src/samples/Sandbox/Views/Controls/ShaderTransition.cs +++ b/src/samples/Sandbox/Views/Controls/ShaderTransition.cs @@ -3,11 +3,6 @@ namespace Sandbox.Views.Controls; -public class ShaderTransitionEffect : ShaderAnimatedEffect -{ - -} - public class TestLoopEffect : SkiaShaderEffect, IStateEffect, ISkiaGestureProcessor { @@ -53,7 +48,7 @@ protected override SKRuntimeEffectUniforms CreateUniforms(SKRect destination) return uniforms; } - protected override SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingContext ctx, SKRect destination, SKImage snapshot) + protected override SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingContext ctx, SKRect destination, SKShader texture1) { if (ControlTo == null || ControlTo.RenderObject == null) { @@ -64,9 +59,9 @@ protected override SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingCon var snapshot2 = ControlTo.RenderObject.Image; - if (snapshot != null && snapshot2 != null) + if (texture1 != null && snapshot2 != null) { - var texture1 = snapshot.ToShader(SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); + //var texture1 = snapshot.ToShader(SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); var texture2 = snapshot2.ToShader(SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); return new SKRuntimeEffectChildren(CompiledShader) diff --git a/src/samples/Sandbox/Views/Controls/ShaderTransitionEffect.cs b/src/samples/Sandbox/Views/Controls/ShaderTransitionEffect.cs new file mode 100644 index 00000000..17499239 --- /dev/null +++ b/src/samples/Sandbox/Views/Controls/ShaderTransitionEffect.cs @@ -0,0 +1,203 @@ +using AppoMobi.Maui.Gestures; + +namespace Sandbox.Views.Controls; + +/// +/// Will animate from Parent control to Secondary then call TransitionEnded event that could make Parent invisible, dispose, whatever. +/// +public class ShaderTransitionEffect : ShaderDoubleTexturesEffect, IStateEffect, ISkiaGestureProcessor +{ + + protected virtual void OnTransitionEnded() + { + TransitionEnded?.Invoke(this, EventArgs.Empty); + } + + public event EventHandler TransitionEnded; + + bool _initialized; + private PointF _mouse; + + #region IStateEffect + + public void UpdateState() + { + if (Parent != null && !_initialized && Parent.IsLayoutReady) + { + _initialized = true; + if (_animator == null) + { + _animator = new(Parent); + + _animator.Start((v) => + { + this.Progress = v; + Update(); + }, 0, 1, 3500); + } + } + + base.Update(); + } + + public override void Attach(SkiaControl parent) + { + base.Attach(parent); + + UpdateState(); + } + + #endregion + + #region GESTURES + + public virtual ISkiaGestureListener ProcessGestures( + SkiaGesturesParameters args, + GestureEventProcessingInfo apply) + { + _mouse = args.Event.Location; + + if (args.Type == TouchActionResult.Down && _initialized) + { + + //var ripple = CreateRipple(_mouse); + + ////run new animator for every Down + ////we use this helper task so that every new rangeanimator is disposed properly at the end + //Task.Run(async () => + //{ + // await Parent.AnimateRangeAsync((v) => + // { + // ripple.Progress = v; + // Update(); + // }, 0, 1, 4500); + + // RemoveRipple(ripple.Uid); + + //}).ConfigureAwait(false); + + } + + return null; + } + + #endregion + + void ApplyReflectionSourceControl(SkiaControl control) + { + if (_controlSource == control) + return; + + DetachTo(); + _controlSource = control; + if (_controlSource != null) + { + _controlSource.CreatedCache += OnCacheCreatedTo; + } + } + + public static readonly BindableProperty TargetProperty = BindableProperty.Create( + nameof(Target), + typeof(SkiaControl), + typeof(ShaderTransitionEffect), + defaultValue: null, + propertyChanged: ApplyTargetProperty); + + private SkiaControl _controlSource; + + public SkiaControl Target + { + get { return (SkiaControl)GetValue(TargetProperty); } + set { SetValue(TargetProperty, value); } + } + + private static void ApplyTargetProperty(BindableObject bindable, object oldvalue, object newvalue) + { + if (oldvalue != newvalue && bindable is ShaderTransitionEffect control) + { + control.ApplyReflectionSourceControl((SkiaControl)newvalue); + } + } + + protected override SKShader GetSecondaryTexture() + { + if (Target == null) + { + //from file + return base.GetSecondaryTexture(); + } + + if (!_secondarySourceSet && ParentReady()) + { + SetSecondaryFromTarget(); + } + return SecondaryTexture; + } + + private bool _secondarySourceSet; + + + + #region TargetCache + + void SetSecondaryFromTarget() + { + if (Target != null && Target.RenderObject != null && Target.RenderObject.Image != null) + { + var dispose = SecondaryTexture; + + SecondaryTexture = Target.RenderObject.Image.ToShader(TilingSecondaryTexture, TilingSecondaryTexture); + + if (dispose != SecondaryTexture) + dispose?.Dispose(); + + _secondarySourceSet = true; + + Update(); + } + } + + protected void DetachTo() + { + if (Target != null) + { + Target.CreatedCache -= OnCacheCreatedTo; + Target = null; + } + } + + void OnCacheCreatedTo(object sender, CachedObject e) + { + Update(); + } + + + protected override void OnDisposing() + { + base.OnDisposing(); + DetachTo(); + } + + #endregion + + #region PROGRESS ANIMATOR + + private PingPongAnimator _animator; + + public double Progress { get; set; } + + protected override SKRuntimeEffectUniforms CreateUniforms(SKRect destination) + { + var uniforms = base.CreateUniforms(destination); + + uniforms["progress"] = (float)Progress; + uniforms["ratio"] = (float)(destination.Width / destination.Height); + + //uniforms["iMouse"] = new[] { _mouse.X, _mouse.Y, 0f, 0f }; + + return uniforms; + } + + #endregion + +} \ No newline at end of file diff --git a/src/samples/Sandbox/Views/Controls/SmallButton.xaml.cs b/src/samples/Sandbox/Views/Controls/SmallButton.xaml.cs index b2210afb..f69dffd9 100644 --- a/src/samples/Sandbox/Views/Controls/SmallButton.xaml.cs +++ b/src/samples/Sandbox/Views/Controls/SmallButton.xaml.cs @@ -13,14 +13,14 @@ public SmallButton() /// Clip effects with rounded rect of the frame inside /// /// - public override SKPath CreateClip(object arguments, bool usePosition) + public override SKPath CreateClip(object arguments, bool usePosition, SKPath path = null) { if (MainFrame != null) { - return MainFrame.CreateClip(arguments, usePosition); + return MainFrame.CreateClip(arguments, usePosition, path); } - return base.CreateClip(arguments, usePosition); + return base.CreateClip(arguments, usePosition, path); } async void AnimatePress(SkiaControl icon) diff --git a/src/samples/Sandbox/Views/MainPageIOS17_Tabs.xaml b/src/samples/Sandbox/Views/MainPageIOS17_Tabs.xaml index b4903b22..bbd09593 100644 --- a/src/samples/Sandbox/Views/MainPageIOS17_Tabs.xaml +++ b/src/samples/Sandbox/Views/MainPageIOS17_Tabs.xaml @@ -31,7 +31,7 @@ -