diff --git a/dev/github_uploadnugets.bat b/dev/github_uploadnugets.bat index 120ccf16..c51855f1 100644 --- a/dev/github_uploadnugets.bat +++ b/dev/github_uploadnugets.bat @@ -13,8 +13,8 @@ REM Define the source directory for the packages set "source_dir=E:\Nugets" REM Define the file mask for the packages -set "file_mask=DrawnUi.Maui*.1.2.3.6*.nupkg" -REM set "file_mask=AppoMobi.Maui.DrawnUi.1.2.3.6*.*nupkg" +REM set "file_mask=DrawnUi.Maui*.1.2.3.8*.nupkg" +set "file_mask=AppoMobi.Maui.DrawnUi.1.2.3.8*.*nupkg" REM Loop through each package file in the source directory for %%f in ("%source_dir%\%file_mask%") do ( diff --git a/dev/nuget_uploadnugets.bat b/dev/nuget_uploadnugets.bat index c10f6e62..b6c5141d 100644 --- a/dev/nuget_uploadnugets.bat +++ b/dev/nuget_uploadnugets.bat @@ -13,8 +13,8 @@ REM Define the source directory for the packages set "source_dir=E:\Nugets" REM Define the file mask for the packages -set "file_mask=DrawnUi.Maui*.1.2.3.6*.nupkg" -REM set "file_mask=AppoMobi.Maui.DrawnUi.1.2.3.6*.*nupkg" +REM set "file_mask=DrawnUi.Maui*.1.2.3.8*.nupkg" +set "file_mask=AppoMobi.Maui.DrawnUi.1.2.3.8*.*nupkg" REM Loop through each package file in the source directory for %%f in ("%source_dir%\%file_mask%") do ( diff --git a/src/Addons/DrawnUi.Maui.Camera/Platforms/Android/NativeCamera.cs b/src/Addons/DrawnUi.Maui.Camera/Platforms/Android/NativeCamera.cs index 4e729fc0..ecf7ede3 100644 --- a/src/Addons/DrawnUi.Maui.Camera/Platforms/Android/NativeCamera.cs +++ b/src/Addons/DrawnUi.Maui.Camera/Platforms/Android/NativeCamera.cs @@ -31,7 +31,6 @@ namespace DrawnUi.Maui.Camera; - public partial class NativeCamera : Java.Lang.Object, ImageReader.IOnImageAvailableListener, INativeCamera { public void SetZoom(float zoom) @@ -904,22 +903,22 @@ bool SetupCamera(CameraUnit cameraUnit) switch (FormsControl.CapturePhotoQuality) { - case CaptureQuality.Max: - selectedSize = validSizes.First(); - break; + case CaptureQuality.Max: + selectedSize = validSizes.First(); + break; - case CaptureQuality.Medium: - selectedSize = validSizes[validSizes.Count / 3]; - break; + case CaptureQuality.Medium: + selectedSize = validSizes[validSizes.Count / 3]; + break; - case CaptureQuality.Low: - selectedSize = validSizes.Last(); - break; + case CaptureQuality.Low: + selectedSize = validSizes.Last(); + break; - default: - //todo: handle case of Preview - selectedSize = new(1, 1); - break; + default: + //todo: handle case of Preview + selectedSize = new(1, 1); + break; } CaptureWidth = selectedSize.Width; @@ -1082,15 +1081,15 @@ public CameraProcessorState State { switch (value) { - case CameraProcessorState.Enabled: - FormsControl.State = CameraState.On; - break; - case CameraProcessorState.Error: - FormsControl.State = CameraState.Error; - break; - default: - FormsControl.State = CameraState.Off; - break; + case CameraProcessorState.Enabled: + FormsControl.State = CameraState.On; + break; + case CameraProcessorState.Error: + FormsControl.State = CameraState.Error; + break; + default: + FormsControl.State = CameraState.Off; + break; } } } @@ -1756,22 +1755,22 @@ public void OnCapturedImage(Image image) switch (FormsControl.DeviceRotation) { - case 90: - FormsControl.CameraDevice.Meta.Orientation = 8; - //newexif.SetAttribute(ExifInterface.TagOrientation, "6"); - break; - case 270: - FormsControl.CameraDevice.Meta.Orientation = 6; - //newexif.SetAttribute(ExifInterface.TagOrientation, "8"); - break; - case 180: - FormsControl.CameraDevice.Meta.Orientation = 3; - //newexif.SetAttribute(ExifInterface.TagOrientation, "3"); - break; - default: - FormsControl.CameraDevice.Meta.Orientation = 1; - //newexif.SetAttribute(ExifInterface.TagOrientation, "1"); - break; + case 90: + FormsControl.CameraDevice.Meta.Orientation = 8; + //newexif.SetAttribute(ExifInterface.TagOrientation, "6"); + break; + case 270: + FormsControl.CameraDevice.Meta.Orientation = 6; + //newexif.SetAttribute(ExifInterface.TagOrientation, "8"); + break; + case 180: + FormsControl.CameraDevice.Meta.Orientation = 3; + //newexif.SetAttribute(ExifInterface.TagOrientation, "3"); + break; + default: + FormsControl.CameraDevice.Meta.Orientation = 1; + //newexif.SetAttribute(ExifInterface.TagOrientation, "1"); + break; } var outImage = new CapturedImage() diff --git a/src/Addons/DrawnUi.Maui.Camera/SkiaCamera.cs b/src/Addons/DrawnUi.Maui.Camera/SkiaCamera.cs index 61aa2085..8187bf15 100644 --- a/src/Addons/DrawnUi.Maui.Camera/SkiaCamera.cs +++ b/src/Addons/DrawnUi.Maui.Camera/SkiaCamera.cs @@ -12,6 +12,8 @@ namespace DrawnUi.Maui.Camera; public partial class SkiaCamera : SkiaControl { + public override bool CanUseCacheDoubleBuffering => false; + #if (!ANDROID && !IOS && !MACCATALYST && !WINDOWS && !TIZEN) public virtual void SetZoom(double value) @@ -276,33 +278,33 @@ SKBitmap Reorient() switch (captured.Orientation) { - case 180: - using (var surface = new SKCanvas(bitmap)) - { - surface.RotateDegrees(180, bitmap.Width / 2.0f, bitmap.Height / 2.0f); - surface.DrawBitmap(bitmap.Copy(), 0, 0); - } - return bitmap; - case 270: - rotated = new SKBitmap(bitmap.Height, bitmap.Width); - using (var surface = new SKCanvas(rotated)) - { - surface.Translate(rotated.Width, 0); - surface.RotateDegrees(90); - surface.DrawBitmap(bitmap, 0, 0); - } - return rotated; - case 90: - rotated = new SKBitmap(bitmap.Height, bitmap.Width); - using (var surface = new SKCanvas(rotated)) - { - surface.Translate(0, rotated.Height); - surface.RotateDegrees(270); - surface.DrawBitmap(bitmap, 0, 0); - } - return rotated; - default: - return bitmap; + case 180: + using (var surface = new SKCanvas(bitmap)) + { + surface.RotateDegrees(180, bitmap.Width / 2.0f, bitmap.Height / 2.0f); + surface.DrawBitmap(bitmap.Copy(), 0, 0); + } + return bitmap; + case 270: + rotated = new SKBitmap(bitmap.Height, bitmap.Width); + using (var surface = new SKCanvas(rotated)) + { + surface.Translate(rotated.Width, 0); + surface.RotateDegrees(90); + surface.DrawBitmap(bitmap, 0, 0); + } + return rotated; + case 90: + rotated = new SKBitmap(bitmap.Height, bitmap.Width); + using (var surface = new SKCanvas(rotated)) + { + surface.Translate(0, rotated.Height); + surface.RotateDegrees(270); + surface.DrawBitmap(bitmap, 0, 0); + } + return rotated; + default: + return bitmap; } } } diff --git a/src/DrawnUi.Maui-dev.sln b/src/DrawnUi.Maui-dev.slnXXX similarity index 100% rename from src/DrawnUi.Maui-dev.sln rename to src/DrawnUi.Maui-dev.slnXXX diff --git a/src/DrawnUi.Maui.sln b/src/DrawnUi.Maui.sln index cedc65fa..e62788c2 100644 --- a/src/DrawnUi.Maui.sln +++ b/src/DrawnUi.Maui.sln @@ -37,6 +37,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DrawnUi.Maui.Camera", "Addo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SomeMauiApp", "..\..\AppoMobi.Maui.DrawnUi.Demo\src\Usual\SomeMauiApp.csproj", "{F71645D2-F7AF-4C1C-9509-D01692622110}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DrawFrameInMaui", "..\..\DrawFrameInMaui\DrawFrameInMaui.csproj", "{09FF4C40-E913-4148-B965-B5475DFD3B53}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppoMobi.Maui.DrawnUi.Demo", "..\..\AppoMobi.Maui.DrawnUi.Demo\src\AllDrawn\AppoMobi.Maui.DrawnUi.Demo.csproj", "{9DCB6C22-2F92-4D59-B23E-0AD6EC233D1F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -87,6 +91,18 @@ Global {F71645D2-F7AF-4C1C-9509-D01692622110}.Release|Any CPU.ActiveCfg = Release|Any CPU {F71645D2-F7AF-4C1C-9509-D01692622110}.Release|Any CPU.Build.0 = Release|Any CPU {F71645D2-F7AF-4C1C-9509-D01692622110}.Release|Any CPU.Deploy.0 = Release|Any CPU + {09FF4C40-E913-4148-B965-B5475DFD3B53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09FF4C40-E913-4148-B965-B5475DFD3B53}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09FF4C40-E913-4148-B965-B5475DFD3B53}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {09FF4C40-E913-4148-B965-B5475DFD3B53}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09FF4C40-E913-4148-B965-B5475DFD3B53}.Release|Any CPU.Build.0 = Release|Any CPU + {09FF4C40-E913-4148-B965-B5475DFD3B53}.Release|Any CPU.Deploy.0 = Release|Any CPU + {9DCB6C22-2F92-4D59-B23E-0AD6EC233D1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9DCB6C22-2F92-4D59-B23E-0AD6EC233D1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9DCB6C22-2F92-4D59-B23E-0AD6EC233D1F}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {9DCB6C22-2F92-4D59-B23E-0AD6EC233D1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9DCB6C22-2F92-4D59-B23E-0AD6EC233D1F}.Release|Any CPU.Build.0 = Release|Any CPU + {9DCB6C22-2F92-4D59-B23E-0AD6EC233D1F}.Release|Any CPU.Deploy.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -102,6 +118,8 @@ Global {1F524822-400E-4801-AE05-4B3A14B603AD} = {8B60E2B6-4347-450E-9999-319D8C7697D3} {D4FC0BDB-3FE8-49C9-8B76-53C83128B899} = {8B60E2B6-4347-450E-9999-319D8C7697D3} {F71645D2-F7AF-4C1C-9509-D01692622110} = {0DDE4AC1-3D8B-42C8-88F2-6F45B89F72F2} + {09FF4C40-E913-4148-B965-B5475DFD3B53} = {0DDE4AC1-3D8B-42C8-88F2-6F45B89F72F2} + {9DCB6C22-2F92-4D59-B23E-0AD6EC233D1F} = {0DDE4AC1-3D8B-42C8-88F2-6F45B89F72F2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {08E673B7-06BF-468C-8D6C-2D1AA8E4A8CA} diff --git a/src/Engine/Controls/Button/SkiaButton.cs b/src/Engine/Controls/Button/SkiaButton.cs index 963c7f38..2c1d010b 100644 --- a/src/Engine/Controls/Button/SkiaButton.cs +++ b/src/Engine/Controls/Button/SkiaButton.cs @@ -10,555 +10,555 @@ namespace DrawnUi.Maui.Draw; /// public partial class SkiaButton : SkiaLayout, ISkiaGestureListener { - public SkiaButton() - { - } - - public override ScaledSize Measure(float widthConstraint, float heightConstraint, float scale) - { - if (IsDisposed || IsDisposing) - return ScaledSize.Default; - - var measured = base.Measure(widthConstraint, heightConstraint, scale); - var test = this.WidthRequest; - return measured; - } - - #region DEFAULT CONTENT - - protected override void CreateDefaultContent() - { - if (!DefaultChildrenCreated && this.Views.Count == 0) - { - if (CreateChildren == null) - { - DefaultChildrenCreated = true; - - if (this.WidthRequest < 0 && HorizontalOptions.Alignment != LayoutAlignment.Fill) - this.WidthRequest = 100; - - if (this.HeightRequest < 0 && VerticalOptions.Alignment != LayoutAlignment.Fill) - this.HeightRequest = 40; - - var shape = new SkiaShape - { - Tag = "BtnShape", - BackgroundColor = Super.ColorAccent, - CornerRadius = 8, - HorizontalOptions = LayoutOptions.Fill, - IsClippedToBounds = true, - VerticalOptions = LayoutOptions.Fill, - }; - shape.SetBinding(SkiaShape.CornerRadiusProperty, new Binding(nameof(CornerRadius), source: this)); - this.AddSubView(shape); - - this.AddSubView(new SkiaLabel() - { - UseCache = SkiaCacheType.Operations, - Tag = "BtnText", - Text = "Test", - TextColor = BlackColor, - HorizontalOptions = LayoutOptions.Center, - VerticalOptions = LayoutOptions.Center, - }); - - ApplyProperties(); - } - - } - else - { - ApplyProperties(); - } - } - - #endregion - - /// - /// Clip effects with rounded rect of the frame inside - /// - /// - public override SKPath CreateClip(object arguments, bool usePosition, SKPath path = null) - { - if (MainFrame != null) - { - return MainFrame.CreateClip(arguments, false); - //var offsetFrame = new SKPoint(MainFrame.DrawingRect.Left - DrawingRect.Left, MainFrame.DrawingRect.Top - DrawingRect.Top); - //var clip = MainFrame.CreateClip(arguments, usePosition); ; - //clip.Offset(offsetFrame); - //return clip; - } - - return base.CreateClip(arguments, usePosition); - } - - protected SkiaLabel MainLabel; - - protected SkiaShape MainFrame; - - public virtual void FindViews() - { - if (MainLabel == null) - { - MainLabel = FindView("BtnText"); - } - if (MainFrame == null) - { - MainFrame = FindView("BtnShape"); - } - } - - public virtual void ApplyProperties() - { - FindViews(); - - if (MainLabel != null) - { - MainLabel.Text = this.Text; - MainLabel.TextColor = this.TextColor; - MainLabel.StrokeColor = TextStrokeColor; - MainLabel.FontFamily = this.FontFamily; - MainLabel.FontSize = this.FontSize; - } - - if (MainFrame != null) - { - MainFrame.BackgroundColor = this.TintColor; - MainFrame.CornerRadius = this.CornerRadius; - } - } - - public virtual bool OnDown(SkiaGesturesParameters args, GestureEventProcessingInfo apply) - { - - //todo check we are inside mainframe OR inside the rect accounting for margins - - if (this.ApplyEffect != SkiaTouchAnimation.None) - { - var control = this as SkiaControl; - if (this.TransformView is SkiaControl other) - { - control = other; - } - - if (ApplyEffect == SkiaTouchAnimation.Ripple) - { - var ptsInsideControl = GetOffsetInsideControlInPoints(args.Event.Location, apply.childOffset); - control.PlayRippleAnimation(TouchEffectColor, ptsInsideControl.X, ptsInsideControl.Y); - } - else - if (ApplyEffect == SkiaTouchAnimation.Shimmer) - { - var color = ShimmerEffectColor; - control.PlayShimmerAnimation(color, ShimmerEffectWidth, ShimmerEffectAngle, ShimmerEffectSpeed); - } - } - - return true; - } - - public virtual void OnUp() - { - - } - - - public virtual bool OnTapped(SkiaGesturesParameters args, SKPoint childOffset) - { - var ret = false; - - if (!IsDisabled) - { - if (Tapped != null) - { - ret = true; - Tapped?.Invoke(this, args); - } - if (CommandTapped != null) - { - ret = true; - Tasks.StartDelayedAsync(TimeSpan.FromMilliseconds(DelayCallbackMs), async () => - { - await Task.Run(() => { CommandTapped?.Execute(CommandTappedParameter); }).ConfigureAwait(false); - }); - } - - } - - return ret; - } - - bool hadDown; - - public static float PanThreshold = 5; - - public override ISkiaGestureListener ProcessGestures(SkiaGesturesParameters args, GestureEventProcessingInfo apply) - { - - //Debug.WriteLine($"SkiaButton {Text}. {args.Type} {args.Event.Distance.Delta}"); - - var point = TranslateInputOffsetToPixels(args.Event.Location, apply.childOffset); - - var ret = false; - - void SetUp() - { - IsPressed = false; - //MainThread.BeginInvokeOnMainThread(() => - //{ - // IsPressed = false; - //}); - hadDown = false; //todo track multifingers - Up?.Invoke(this, args); - OnUp(); - } - - if (args.Type == TouchActionResult.Down) - { - IsPressed = true; - //MainThread.BeginInvokeOnMainThread(() => - //{ - // IsPressed = true; - //}); - _lastDownPts = point; - hadDown = true; - TotalDown++; - Down?.Invoke(this, args); - return OnDown(args, apply) ? this : null; - } - - if (args.Type == TouchActionResult.Panning) - { - if (LockPanning) - { - return this; //no panning for you my friend - } - - var current = point; - var panthreshold = PanThreshold * RenderingScale; - - if (Math.Abs(current.X - _lastDownPts.X) > panthreshold - || Math.Abs(current.Y - _lastDownPts.Y) > panthreshold) - { - if (hadDown) - SetUp(); - hadDown = false; - - return null; - } - } - else - if (args.Type == TouchActionResult.Up) - { - //todo track multifingers? - SetUp(); - //hadDown = false; - //Up?.Invoke(this, args); - //OnUp(); - } - else - if (args.Type == TouchActionResult.Tapped) - { - TotalTapped++; - return OnTapped(args, apply.childOffset) ? this : null; - } - - return hadDown ? this : null; - } - - /// - /// You might want to pause to show effect before executing command. Default is 0. - /// - public static int DelayCallbackMs = 0; - - public event EventHandler Up; - - public event EventHandler Down; - - public event EventHandler Tapped; - - - private long _TotalTapped; - public long TotalTapped - { - get - { - return _TotalTapped; - } - set - { - if (_TotalTapped != value) - { - _TotalTapped = value; - OnPropertyChanged(); - } - } - } - - private long _TotalDown; - public long TotalDown - { - get - { - return _TotalDown; - } - set - { - if (_TotalDown != value) - { - _TotalDown = value; - OnPropertyChanged(); - } - } - } - - - - - #region PROPERTIES - - private static void OnLookChanged(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is SkiaButton control) - { - control.ApplyProperties(); - } - } - - public static readonly BindableProperty LockPanningProperty = BindableProperty.Create(nameof(LockPanning), - typeof(bool), - typeof(SkiaButton), - false); - public bool LockPanning - { - get { return (bool)GetValue(LockPanningProperty); } - set { SetValue(LockPanningProperty, value); } - } - - public static readonly BindableProperty FontSizeProperty = BindableProperty.Create( - nameof(FontSize), - typeof(double), - typeof(SkiaButton), - 12.0, - propertyChanged: OnLookChanged); - - public double FontSize - { - get { return (double)GetValue(FontSizeProperty); } - set { SetValue(FontSizeProperty, value); } - } - - public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create( - nameof(FontFamily), - typeof(string), - typeof(SkiaButton), - defaultValue: string.Empty, - propertyChanged: OnLookChanged); - - public string FontFamily - { - get { return (string)GetValue(FontFamilyProperty); } - set { SetValue(FontFamilyProperty, value); } - } - - public static readonly BindableProperty IsDisabledProperty = BindableProperty.Create( - nameof(IsDisabled), - typeof(bool), - typeof(SkiaButton), - false, propertyChanged: OnLookChanged); - - public bool IsDisabled - { - get { return (bool)GetValue(IsDisabledProperty); } - set { SetValue(IsDisabledProperty, value); } - } - - public static readonly BindableProperty IsPressedProperty = BindableProperty.Create( - nameof(IsPressed), - typeof(bool), - typeof(SkiaButton), - false, - BindingMode.OneWayToSource); - - public bool IsPressed - { - get { return (bool)GetValue(IsPressedProperty); } - set { SetValue(IsPressedProperty, value); } - } - - - public static readonly BindableProperty TextProperty = BindableProperty.Create( - nameof(Text), - typeof(string), - typeof(SkiaButton), - string.Empty, propertyChanged: OnLookChanged); - - /// - /// Bind to your own content! - /// - public string Text - { - get { return (string)GetValue(TextProperty); } - set { SetValue(TextProperty, value); } - } - - - public static readonly BindableProperty ShimmerEffectColorProperty = BindableProperty.Create(nameof(ShimmerEffectColor), - typeof(Color), - typeof(SkiaButton), - WhiteColor.WithAlpha(0.33f)); - public Color ShimmerEffectColor - { - get { return (Color)GetValue(ShimmerEffectColorProperty); } - set { SetValue(ShimmerEffectColorProperty, value); } - } - - public static readonly BindableProperty ShimmerEffectAngleProperty = BindableProperty.Create(nameof(ShimmerEffectAngle), - typeof(float), - typeof(SkiaButton), - 33.0f); - public float ShimmerEffectAngle - { - get { return (float)GetValue(ShimmerEffectAngleProperty); } - set { SetValue(ShimmerEffectAngleProperty, value); } - } - - public static readonly BindableProperty ShimmerEffectWidthProperty = BindableProperty.Create(nameof(ShimmerEffectWidth), - typeof(float), - typeof(SkiaButton), - 150.0f); - public float ShimmerEffectWidth - { - get { return (float)GetValue(ShimmerEffectWidthProperty); } - set { SetValue(ShimmerEffectWidthProperty, value); } - } - - public static readonly BindableProperty ShimmerEffectSpeedProperty = BindableProperty.Create(nameof(ShimmerEffectSpeed), - typeof(int), - typeof(SkiaButton), - 500); - public int ShimmerEffectSpeed - { - get { return (int)GetValue(ShimmerEffectSpeedProperty); } - set { SetValue(ShimmerEffectSpeedProperty, value); } - } - - - public static readonly BindableProperty TouchEffectColorProperty = BindableProperty.Create(nameof(TouchEffectColor), typeof(Color), - typeof(SkiaButton), - WhiteColor); - public Color TouchEffectColor - { - get { return (Color)GetValue(TouchEffectColorProperty); } - set { SetValue(TouchEffectColorProperty, value); } - } - - public static readonly BindableProperty ApplyEffectProperty = BindableProperty.Create(nameof(ApplyEffect), - typeof(SkiaTouchAnimation), - typeof(SkiaButton), SkiaTouchAnimation.Ripple); - public SkiaTouchAnimation ApplyEffect - { - get { return (SkiaTouchAnimation)GetValue(ApplyEffectProperty); } - set { SetValue(ApplyEffectProperty, value); } - } - - public static readonly BindableProperty TransformViewProperty = BindableProperty.Create(nameof(TransformView), typeof(object), - typeof(SkiaButton), null); - public object TransformView - { - get { return (object)GetValue(TransformViewProperty); } - set { SetValue(TransformViewProperty, value); } - } - - public static readonly BindableProperty CommandTappedProperty = BindableProperty.Create(nameof(CommandTapped), typeof(ICommand), - typeof(SkiaButton), - null); - public ICommand CommandTapped - { - get { return (ICommand)GetValue(CommandTappedProperty); } - set { SetValue(CommandTappedProperty, value); } - } - - public static readonly BindableProperty CommandTappedParameterProperty = BindableProperty.Create(nameof(CommandTappedParameter), typeof(object), - typeof(SkiaButton), - null); - public object CommandTappedParameter - { - get { return GetValue(CommandTappedParameterProperty); } - set { SetValue(CommandTappedParameterProperty, value); } - } - - public static readonly BindableProperty CommandLongPressingProperty = BindableProperty.Create(nameof(CommandLongPressing), typeof(ICommand), - typeof(SkiaButton), - null); - public ICommand CommandLongPressing - { - get { return (ICommand)GetValue(CommandLongPressingProperty); } - set { SetValue(CommandLongPressingProperty, value); } - } - - public static readonly BindableProperty CommandLongPressingParameterProperty = BindableProperty.Create(nameof(CommandLongPressingParameter), typeof(object), - typeof(SkiaButton), - null); - public object CommandLongPressingParameter - { - get { return GetValue(CommandLongPressingParameterProperty); } - set { SetValue(CommandLongPressingParameterProperty, value); } - } - - - - public static readonly BindableProperty TintColorProperty = BindableProperty.Create( - nameof(TintColor), - typeof(Color), - typeof(SkiaButton), - RedColor, - propertyChanged: NeedApplyProperties); - - - protected SKPoint _lastDownPts; - - public Color TintColor - { - get { return (Color)GetValue(TintColorProperty); } - set { SetValue(TintColorProperty, value); } - } - - public static readonly BindableProperty TextColorProperty = BindableProperty.Create( - nameof(TextColor), - typeof(Color), - typeof(SkiaButton), - WhiteColor, - propertyChanged: NeedApplyProperties); - - public Color TextColor - { - get { return (Color)GetValue(TextColorProperty); } - set { SetValue(TextColorProperty, value); } - } - - public static readonly BindableProperty TextStrokeColorProperty = BindableProperty.Create( - nameof(TextStrokeColor), - typeof(Color), - typeof(SkiaButton), - TransparentColor, - propertyChanged: NeedApplyProperties); - - public Color TextStrokeColor - { - get { return (Color)GetValue(TextStrokeColorProperty); } - set { SetValue(TextStrokeColorProperty, value); } - } - - private static void NeedApplyProperties(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is SkiaButton control) - { - control.ApplyProperties(); - } - } - - #endregion + public SkiaButton() + { + } + + public override ScaledSize Measure(float widthConstraint, float heightConstraint, float scale) + { + if (IsDisposed || IsDisposing) + return ScaledSize.Default; + + var measured = base.Measure(widthConstraint, heightConstraint, scale); + var test = this.WidthRequest; + return measured; + } + + #region DEFAULT CONTENT + + protected override void CreateDefaultContent() + { + if (!DefaultChildrenCreated && this.Views.Count == 0) + { + if (CreateChildren == null) + { + DefaultChildrenCreated = true; + + if (this.WidthRequest < 0 && HorizontalOptions.Alignment != LayoutAlignment.Fill) + this.WidthRequest = 100; + + if (this.HeightRequest < 0 && VerticalOptions.Alignment != LayoutAlignment.Fill) + this.HeightRequest = 40; + + var shape = new SkiaShape + { + Tag = "BtnShape", + BackgroundColor = Super.ColorAccent, + CornerRadius = 8, + HorizontalOptions = LayoutOptions.Fill, + IsClippedToBounds = true, + VerticalOptions = LayoutOptions.Fill, + }; + shape.SetBinding(SkiaShape.CornerRadiusProperty, new Binding(nameof(CornerRadius), source: this)); + this.AddSubView(shape); + + this.AddSubView(new SkiaLabel() + { + UseCache = SkiaCacheType.Operations, + Tag = "BtnText", + Text = "Test", + TextColor = BlackColor, + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + }); + + ApplyProperties(); + } + + } + else + { + ApplyProperties(); + } + } + + #endregion + + /// + /// Clip effects with rounded rect of the frame inside + /// + /// + public override SKPath CreateClip(object arguments, bool usePosition, SKPath path = null) + { + if (MainFrame != null) + { + return MainFrame.CreateClip(arguments, false); + //var offsetFrame = new SKPoint(MainFrame.DrawingRect.Left - DrawingRect.Left, MainFrame.DrawingRect.Top - DrawingRect.Top); + //var clip = MainFrame.CreateClip(arguments, usePosition); ; + //clip.Offset(offsetFrame); + //return clip; + } + + return base.CreateClip(arguments, usePosition); + } + + protected SkiaLabel MainLabel; + + protected SkiaShape MainFrame; + + public virtual void FindViews() + { + if (MainLabel == null) + { + MainLabel = FindView("BtnText"); + } + if (MainFrame == null) + { + MainFrame = FindView("BtnShape"); + } + } + + public virtual void ApplyProperties() + { + FindViews(); + + if (MainLabel != null) + { + MainLabel.Text = this.Text; + MainLabel.TextColor = this.TextColor; + MainLabel.StrokeColor = TextStrokeColor; + MainLabel.FontFamily = this.FontFamily; + MainLabel.FontSize = this.FontSize; + } + + if (MainFrame != null) + { + MainFrame.BackgroundColor = this.TintColor; + MainFrame.CornerRadius = this.CornerRadius; + } + } + + public virtual bool OnDown(SkiaGesturesParameters args, GestureEventProcessingInfo apply) + { + + //todo check we are inside mainframe OR inside the rect accounting for margins + + if (this.ApplyEffect != SkiaTouchAnimation.None) + { + var control = this as SkiaControl; + if (this.TransformView is SkiaControl other) + { + control = other; + } + + if (ApplyEffect == SkiaTouchAnimation.Ripple) + { + var ptsInsideControl = GetOffsetInsideControlInPoints(args.Event.Location, apply.childOffset); + control.PlayRippleAnimation(TouchEffectColor, ptsInsideControl.X, ptsInsideControl.Y); + } + else + if (ApplyEffect == SkiaTouchAnimation.Shimmer) + { + var color = ShimmerEffectColor; + control.PlayShimmerAnimation(color, ShimmerEffectWidth, ShimmerEffectAngle, ShimmerEffectSpeed); + } + } + + return true; + } + + public virtual void OnUp() + { + + } + + + public virtual bool OnTapped(SkiaGesturesParameters args, SKPoint childOffset) + { + var ret = false; + + if (!IsDisabled) + { + if (Tapped != null) + { + ret = true; + Tapped?.Invoke(this, args); + } + if (CommandTapped != null) + { + ret = true; + Tasks.StartDelayedAsync(TimeSpan.FromMilliseconds(DelayCallbackMs), async () => + { + await Task.Run(() => { CommandTapped?.Execute(CommandTappedParameter); }).ConfigureAwait(false); + }); + } + + } + + return ret; + } + + bool hadDown; + + public static float PanThreshold = 5; + + public override ISkiaGestureListener ProcessGestures(SkiaGesturesParameters args, GestureEventProcessingInfo apply) + { + + //Debug.WriteLine($"SkiaButton {Text}. {args.Type} {args.Event.Distance.Delta}"); + + var point = TranslateInputOffsetToPixels(args.Event.Location, apply.childOffset); + + var ret = false; + + void SetUp() + { + IsPressed = false; + //MainThread.BeginInvokeOnMainThread(() => + //{ + // IsPressed = false; + //}); + hadDown = false; //todo track multifingers + Up?.Invoke(this, args); + OnUp(); + } + + if (args.Type == TouchActionResult.Down) + { + IsPressed = true; + //MainThread.BeginInvokeOnMainThread(() => + //{ + // IsPressed = true; + //}); + _lastDownPts = point; + hadDown = true; + TotalDown++; + Down?.Invoke(this, args); + return OnDown(args, apply) ? this : null; + } + + if (args.Type == TouchActionResult.Panning) + { + if (LockPanning) + { + return this; //no panning for you my friend + } + + var current = point; + var panthreshold = PanThreshold * RenderingScale; + + if (Math.Abs(current.X - _lastDownPts.X) > panthreshold + || Math.Abs(current.Y - _lastDownPts.Y) > panthreshold) + { + if (hadDown) + SetUp(); + hadDown = false; + + return null; + } + } + else + if (args.Type == TouchActionResult.Up) + { + //todo track multifingers? + SetUp(); + //hadDown = false; + //Up?.Invoke(this, args); + //OnUp(); + } + else + if (args.Type == TouchActionResult.Tapped) + { + TotalTapped++; + return OnTapped(args, apply.childOffset) ? this : null; + } + + return hadDown ? this : null; + } + + /// + /// You might want to pause to show effect before executing command. Default is 0. + /// + public static int DelayCallbackMs = 0; + + public event EventHandler Up; + + public event EventHandler Down; + + public event EventHandler Tapped; + + + private long _TotalTapped; + public long TotalTapped + { + get + { + return _TotalTapped; + } + set + { + if (_TotalTapped != value) + { + _TotalTapped = value; + OnPropertyChanged(); + } + } + } + + private long _TotalDown; + public long TotalDown + { + get + { + return _TotalDown; + } + set + { + if (_TotalDown != value) + { + _TotalDown = value; + OnPropertyChanged(); + } + } + } + + + + + #region PROPERTIES + + private static void OnLookChanged(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaButton control) + { + control.ApplyProperties(); + } + } + + public static readonly BindableProperty LockPanningProperty = BindableProperty.Create(nameof(LockPanning), + typeof(bool), + typeof(SkiaButton), + false); + public bool LockPanning + { + get { return (bool)GetValue(LockPanningProperty); } + set { SetValue(LockPanningProperty, value); } + } + + public static readonly BindableProperty FontSizeProperty = BindableProperty.Create( + nameof(FontSize), + typeof(double), + typeof(SkiaButton), + 12.0, + propertyChanged: OnLookChanged); + + public double FontSize + { + get { return (double)GetValue(FontSizeProperty); } + set { SetValue(FontSizeProperty, value); } + } + + public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create( + nameof(FontFamily), + typeof(string), + typeof(SkiaButton), + defaultValue: string.Empty, + propertyChanged: OnLookChanged); + + public string FontFamily + { + get { return (string)GetValue(FontFamilyProperty); } + set { SetValue(FontFamilyProperty, value); } + } + + public static readonly BindableProperty IsDisabledProperty = BindableProperty.Create( + nameof(IsDisabled), + typeof(bool), + typeof(SkiaButton), + false, propertyChanged: OnLookChanged); + + public bool IsDisabled + { + get { return (bool)GetValue(IsDisabledProperty); } + set { SetValue(IsDisabledProperty, value); } + } + + public static readonly BindableProperty IsPressedProperty = BindableProperty.Create( + nameof(IsPressed), + typeof(bool), + typeof(SkiaButton), + false, + BindingMode.OneWayToSource); + + public bool IsPressed + { + get { return (bool)GetValue(IsPressedProperty); } + set { SetValue(IsPressedProperty, value); } + } + + + public static readonly BindableProperty TextProperty = BindableProperty.Create( + nameof(Text), + typeof(string), + typeof(SkiaButton), + string.Empty, propertyChanged: OnLookChanged); + + /// + /// Bind to your own content! + /// + public string Text + { + get { return (string)GetValue(TextProperty); } + set { SetValue(TextProperty, value); } + } + + + public static readonly BindableProperty ShimmerEffectColorProperty = BindableProperty.Create(nameof(ShimmerEffectColor), + typeof(Color), + typeof(SkiaButton), + WhiteColor.WithAlpha(0.33f)); + public Color ShimmerEffectColor + { + get { return (Color)GetValue(ShimmerEffectColorProperty); } + set { SetValue(ShimmerEffectColorProperty, value); } + } + + public static readonly BindableProperty ShimmerEffectAngleProperty = BindableProperty.Create(nameof(ShimmerEffectAngle), + typeof(float), + typeof(SkiaButton), + 33.0f); + public float ShimmerEffectAngle + { + get { return (float)GetValue(ShimmerEffectAngleProperty); } + set { SetValue(ShimmerEffectAngleProperty, value); } + } + + public static readonly BindableProperty ShimmerEffectWidthProperty = BindableProperty.Create(nameof(ShimmerEffectWidth), + typeof(float), + typeof(SkiaButton), + 150.0f); + public float ShimmerEffectWidth + { + get { return (float)GetValue(ShimmerEffectWidthProperty); } + set { SetValue(ShimmerEffectWidthProperty, value); } + } + + public static readonly BindableProperty ShimmerEffectSpeedProperty = BindableProperty.Create(nameof(ShimmerEffectSpeed), + typeof(int), + typeof(SkiaButton), + 500); + public int ShimmerEffectSpeed + { + get { return (int)GetValue(ShimmerEffectSpeedProperty); } + set { SetValue(ShimmerEffectSpeedProperty, value); } + } + + + public static readonly BindableProperty TouchEffectColorProperty = BindableProperty.Create(nameof(TouchEffectColor), typeof(Color), + typeof(SkiaButton), + WhiteColor); + public Color TouchEffectColor + { + get { return (Color)GetValue(TouchEffectColorProperty); } + set { SetValue(TouchEffectColorProperty, value); } + } + + public static readonly BindableProperty ApplyEffectProperty = BindableProperty.Create(nameof(ApplyEffect), + typeof(SkiaTouchAnimation), + typeof(SkiaButton), SkiaTouchAnimation.Ripple); + public SkiaTouchAnimation ApplyEffect + { + get { return (SkiaTouchAnimation)GetValue(ApplyEffectProperty); } + set { SetValue(ApplyEffectProperty, value); } + } + + public static readonly BindableProperty TransformViewProperty = BindableProperty.Create(nameof(TransformView), typeof(object), + typeof(SkiaButton), null); + public object TransformView + { + get { return (object)GetValue(TransformViewProperty); } + set { SetValue(TransformViewProperty, value); } + } + + public static readonly BindableProperty CommandTappedProperty = BindableProperty.Create(nameof(CommandTapped), typeof(ICommand), + typeof(SkiaButton), + null); + public ICommand CommandTapped + { + get { return (ICommand)GetValue(CommandTappedProperty); } + set { SetValue(CommandTappedProperty, value); } + } + + public static readonly BindableProperty CommandTappedParameterProperty = BindableProperty.Create(nameof(CommandTappedParameter), typeof(object), + typeof(SkiaButton), + null); + public object CommandTappedParameter + { + get { return GetValue(CommandTappedParameterProperty); } + set { SetValue(CommandTappedParameterProperty, value); } + } + + public static readonly BindableProperty CommandLongPressingProperty = BindableProperty.Create(nameof(CommandLongPressing), typeof(ICommand), + typeof(SkiaButton), + null); + public ICommand CommandLongPressing + { + get { return (ICommand)GetValue(CommandLongPressingProperty); } + set { SetValue(CommandLongPressingProperty, value); } + } + + public static readonly BindableProperty CommandLongPressingParameterProperty = BindableProperty.Create(nameof(CommandLongPressingParameter), typeof(object), + typeof(SkiaButton), + null); + public object CommandLongPressingParameter + { + get { return GetValue(CommandLongPressingParameterProperty); } + set { SetValue(CommandLongPressingParameterProperty, value); } + } + + + + public static readonly BindableProperty TintColorProperty = BindableProperty.Create( + nameof(TintColor), + typeof(Color), + typeof(SkiaButton), + RedColor, + propertyChanged: NeedApplyProperties); + + + protected SKPoint _lastDownPts; + + public Color TintColor + { + get { return (Color)GetValue(TintColorProperty); } + set { SetValue(TintColorProperty, value); } + } + + public static readonly BindableProperty TextColorProperty = BindableProperty.Create( + nameof(TextColor), + typeof(Color), + typeof(SkiaButton), + WhiteColor, + propertyChanged: NeedApplyProperties); + + public Color TextColor + { + get { return (Color)GetValue(TextColorProperty); } + set { SetValue(TextColorProperty, value); } + } + + public static readonly BindableProperty TextStrokeColorProperty = BindableProperty.Create( + nameof(TextStrokeColor), + typeof(Color), + typeof(SkiaButton), + TransparentColor, + propertyChanged: NeedApplyProperties); + + public Color TextStrokeColor + { + get { return (Color)GetValue(TextStrokeColorProperty); } + set { SetValue(TextStrokeColorProperty, value); } + } + + private static void NeedApplyProperties(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaButton control) + { + control.ApplyProperties(); + } + } + + #endregion } \ No newline at end of file diff --git a/src/Engine/Controls/Carousel/SkiaCarousel.cs b/src/Engine/Controls/Carousel/SkiaCarousel.cs index bfe20316..a84b2bdf 100644 --- a/src/Engine/Controls/Carousel/SkiaCarousel.cs +++ b/src/Engine/Controls/Carousel/SkiaCarousel.cs @@ -22,6 +22,26 @@ public SkiaCarousel() public override bool WillClipBounds => true; + protected virtual void RenderVisibleChild( + SkiaControl view, Vector2 position, + SkiaDrawingContext context, SKRect destination, float scale) + { + view.OptionalOnBeforeDrawing(); //draw even hidden neighboors to be able to preload stuff + if (view.CanDraw) + { + view.LockUpdate(true); + AnimateVisibleChild(view, position); + view.LockUpdate(false); + view.Render(context, destination, scale); + } + } + + protected virtual void AnimateVisibleChild( + SkiaControl view, Vector2 position) + { + view.TranslationX = position.X; + view.TranslationY = position.Y; + } public override void ScrollToNearestAnchor(Vector2 location, Vector2 velocity) { @@ -87,6 +107,26 @@ void InitializeItemsVisibility(int count, bool force) #region METHODS + + /// + /// Will translate child and raise appearing/disappearing events + /// + /// + public override void ApplyPosition(Vector2 currentPosition) + { + CurrentPosition = currentPosition; + + //Debug.WriteLine($"CurrentPosition {currentPosition}"); + + MainThread.BeginInvokeOnMainThread(() => + { + InTransition = !CheckTransitionEnded(); + }); + + Update(); + } + + public virtual void ApplyIndex(bool instant = false) { if (SelectedIndex >= 0 && SelectedIndex < SnapPoints.Count) @@ -147,6 +187,11 @@ protected virtual bool ChildrenReady } } + protected virtual void OnScrollProgressChanged() + { + + } + protected override int RenderViewsList(IEnumerable skiaControls, SkiaDrawingContext context, SKRect destination, float scale, bool debug = false) { @@ -167,8 +212,16 @@ protected override int RenderViewsList(IEnumerable skiaControls, Sk progress = CurrentPosition.X; } - this.ScrollProgress = progress / progressMax; - //Trace.WriteLine($"[C] Progress {ScrollProgress:0.0}"); + LastScrollProgress = ScrollProgress; + ScrollProgress = progress / progressMax; + + if (ScrollProgress != LastScrollProgress) + { + TransitionDirection = ScrollProgress > LastScrollProgress ? + LinearDirectionType.Forward + : LinearDirectionType.Backward; + OnScrollProgressChanged(); + } var childrenCount = ChildrenFactory.GetChildrenCount(); @@ -179,7 +232,7 @@ protected override int RenderViewsList(IEnumerable skiaControls, Sk for (int index = 0; index < childrenCount; index++) { bool wasUsed = false; - var position = CalculateChildPosition(CurrentPosition, index); + var position = CalculateChildPosition(CurrentPosition, index, childrenCount); if (position.OnScreen || position.NextToScreen) { var cell = new ControlInStack() @@ -220,16 +273,7 @@ protected override int RenderViewsList(IEnumerable skiaControls, Sk if (cell.IsVisible || this.PreloadNeighboors) { - view.OptionalOnBeforeDrawing(); //draw even hidden neighboors to be able to preload stuff - if (view.CanDraw) - { - view.LockUpdate(true); - view.TranslationX = cell.Offset.X; - view.TranslationY = cell.Offset.Y; - view.LockUpdate(false); - view.Render(context, destination, scale); - } - + RenderVisibleChild(view, cell.Offset, context, destination, scale); } if (cell.IsVisible) //but handle gestures only for visible views @@ -261,7 +305,7 @@ protected override int RenderViewsList(IEnumerable skiaControls, Sk private double _ScrollAmount; /// - /// Scroll amount from 0 to 1 of the current (SelectedIndex) slide. Another similar but different property would be ScrollProgress. + /// Scroll amount from 0 to 1 of the current (SelectedIndex) slide. Another similar but different property would be ScrollProgress. This is not linear as SelectedIndex changes earlier than 0 or 1 are attained. /// public double ScrollAmount { @@ -275,6 +319,7 @@ public double ScrollAmount { _ScrollAmount = value; OnPropertyChanged(); + //Debug.WriteLine($"ScrollAmount {value:0.000}"); } } } @@ -297,10 +342,40 @@ public double ScrollProgress { _ScrollProgress = value; OnPropertyChanged(); + OnPropertyChanged(nameof(TransitionProgress)); + //Debug.WriteLine($"ScrollAmount {value / 3.0:0.000}"); } } } + protected double LastScrollProgress { get; set; } + + public double TransitionProgress + { + get + { + if (MaxIndex < 1) + return 0.0; + + int numberOfTransitions = MaxIndex; + double scaledProgress = ScrollProgress * numberOfTransitions; + double value = scaledProgress - Math.Floor(scaledProgress); + + //if (value == 0 && TransitionDirection == LinearDirectionType.Forward) value = 1.0; + + //if (TransitionDirection == LinearDirectionType.Backward) + // value = 1.0 - value; + + //Debug.WriteLine($"TransitionAmount {value:0.00} scroll {ScrollProgress:0.00}"); + + return value; + + + } + } + + + protected void AdaptTemplate(SkiaControl skiaControl) { var margin = SidesOffset; @@ -390,7 +465,7 @@ public override void OnItemSourceChanged() #region ENGINE - protected (Vector2 Offset, bool OnScreen, bool NextToScreen) CalculateChildPosition(Vector2 currentPosition, int index) + protected virtual (Vector2 Offset, bool OnScreen, bool NextToScreen) CalculateChildPosition(Vector2 currentPosition, int index, int childrenCount) { var childPos = SnapPoints[index]; float newX = 0; @@ -403,27 +478,14 @@ public override void OnItemSourceChanged() if (IsVertical) { newY = (currentPosition.Y + Math.Abs(childPos.Y)); - - if (newY + SidesOffset * 2 > Height - 1 || newY + (Height - SidesOffset * 2) + SidesOffset * 2 < 1) - { - isVisible = false; - if (Math.Abs(newY) - Width - SidesOffset - Spacing <= nextToScreenOffset) - { - nextToScreen = true; - } - } + isVisible = newY + SidesOffset * 2 <= Height && newY + (Height - SidesOffset * 2) + SidesOffset * 2 >= 0; + nextToScreen = Math.Abs(newY) - Width - SidesOffset - Spacing <= nextToScreenOffset; } else { newX = (currentPosition.X + Math.Abs(childPos.X)); - if (newX + SidesOffset * 1 > Width - 1 || newX + (Width - SidesOffset * 1) + SidesOffset * 1 < 1) - { - isVisible = false; - if (Math.Abs(newX) - Width - SidesOffset - Spacing <= nextToScreenOffset) - { - nextToScreen = true; - } - } + isVisible = newX + SidesOffset * 2 <= Width && newX + (Width - SidesOffset * 2) + SidesOffset * 2 >= 0; + nextToScreen = Math.Abs(newX) - Width - SidesOffset - Spacing <= nextToScreenOffset; } return (new Vector2(newX, newY), isVisible, nextToScreen); @@ -431,25 +493,6 @@ public override void OnItemSourceChanged() - - /// - /// Will translate child child and raise appearing/disappearing events - /// - /// - public override void ApplyPosition(Vector2 currentPosition) - { - CurrentPosition = currentPosition; - - MainThread.BeginInvokeOnMainThread(() => - { - InTransition = !CheckTransitionEnded(); - }); - - Update(); - } - - - protected override bool ScrollToOffset(Vector2 targetOffset, Vector2 velocity, bool animate) { if (ScrollLocked || targetOffset == CurrentSnap) @@ -466,7 +509,7 @@ protected override bool ScrollToOffset(Vector2 targetOffset, Vector2 velocity, b var start = CurrentSnap; var end = new Vector2((float)targetOffset.X, (float)targetOffset.Y); - var atSnapPoint = SnapPoints.Any(x => x.X == CurrentSnap.X && x.Y == CurrentSnap.Y); + //var atSnapPoint = SnapPoints.Any(x => x.X == CurrentSnap.X && x.Y == CurrentSnap.Y); var displacement = start - end; @@ -475,17 +518,18 @@ protected override bool ScrollToOffset(Vector2 targetOffset, Vector2 velocity, b if (displacement != Vector2.Zero) { - if (atSnapPoint && !ThresholdOk(displacement)) - { - //Debug.WriteLine("[CAROUSEL] threshold low"); - return false; - } + //if (atSnapPoint && !ThresholdOk(displacement)) + //{ + // //Debug.WriteLine("[CAROUSEL] threshold low"); + // return false; + //} if (Bounces) { var spring = new Spring((float)(1 * (1 + RubberDamping)), 200, (float)(0.5f * (1 + RubberDamping))); VectorAnimatorSpring.Initialize(end, displacement, velocity, spring, 0.5f); VectorAnimatorSpring.Start(); + _isSnapping = end; } else { @@ -509,8 +553,18 @@ protected override bool ScrollToOffset(Vector2 targetOffset, Vector2 velocity, b if (speed > maxSpeed) speed = maxSpeed; - _animatorRange.Initialize(start, end, (float)speed, Easing.Linear); - _animatorRange.Start(); + if (LinearSpeedMs > 0) + { + var ratio = Math.Abs(end.X - start.X) / CellSize.Pixels.Width; + + speed = ratio * LinearSpeedMs / 1000.0; + } + + //Debug.WriteLine($"Will snap:{start} -> {end}"); + + _isSnapping = end; + AnimatorRange.Initialize(start, end, (float)speed, Easing.Linear); + AnimatorRange.Start(); } } else @@ -537,7 +591,7 @@ protected override bool ScrollToOffset(Vector2 targetOffset, Vector2 velocity, b else if (CanDraw) { - //Debug.WriteLine("[CAROUSEL] setting offset"); + //Debug.WriteLine($"[ScrollToOffset] setting offset {targetOffset}"); ApplyPosition(targetOffset); } @@ -546,6 +600,9 @@ protected override bool ScrollToOffset(Vector2 targetOffset, Vector2 velocity, b return true; } + + private Vector2? _isSnapping; + bool ThresholdOk(Vector2 displacement) { var threshold = 10; @@ -561,9 +618,11 @@ public override void ApplyOptions() if (Parent == null) return; - Viewport = Parent.DrawingRect; + SetupViewport(); - InitializeChildren(); + //Viewport = Parent.DrawingRect; + + //InitializeChildren(); base.ApplyOptions(); } @@ -593,9 +652,14 @@ void Init() OnVectorUpdated = (value) => { ApplyPosition(value); - } + }, + Finished = () => + { + _isSnapping = null; + }, + }; - _animatorRange = new(this) + AnimatorRange = new(this) { OnVectorUpdated = (value) => { @@ -604,7 +668,12 @@ void Init() OnStop = () => { Stopped?.Invoke(this, _appliedPosition); - } + }, + Finished = () => + { + _isSnapping = null; + }, + }; } @@ -692,8 +761,65 @@ protected override void OnChildAdded(SkiaControl child) AdaptChildren(); } + + bool viewportSet; + + protected override void OnLayoutChanged() + { + base.OnLayoutChanged(); + + SetupViewport(); + } + + protected void SetupViewport() + { + if (Parent != null) + { + Viewport = DrawingRect;// Parent.DrawingRect; + + if (!viewportSet)// !CompareRects(Viewport, _lastViewport, 0.5f)) + { + viewportSet = true; + _lastViewport = Viewport; + Init(); + } + + if (DynamicSize && SelectedIndex >= 0) + { + if (!ChildrenInitialized) + { + InitializeChildren(); + } + ApplyDynamicSize(SelectedIndex); + } + else + { + InitializeChildren(); + } + } + } + + protected bool ChildrenInitialized; + private int _MaxIndex; + public int MaxIndex + { + get + { + return _MaxIndex; + } + set + { + if (_MaxIndex != value) + { + _MaxIndex = value; + OnPropertyChanged(); + } + } + } + + /// /// We expect this to be called after this alyout is invalidated /// @@ -707,6 +833,9 @@ public virtual void InitializeChildren() ChildrenInitialized = true; var childrenCount = ChildrenFactory.GetChildrenCount(); + + MaxIndex = childrenCount - 1; + InitializeItemsVisibility(childrenCount, true); var snapPoints = new List(); @@ -726,9 +855,10 @@ public virtual void InitializeChildren() snapPoints.Add(new Vector2(-position.X, -position.Y)); currentPosition += (IsVertical ? cellSize.Height : cellSize.Width); - } + CellSize = ScaledSize.FromUnits(cellSize.Width, cellSize.Height, RenderingScale); + SnapPoints = snapPoints; ContentOffsetBounds = GetContentOffsetBounds(); @@ -743,8 +873,16 @@ public virtual void InitializeChildren() { ApplyIndex(true); } + + OnChildrenInitialized(); + } + + protected virtual void OnChildrenInitialized() + { + } + public ScaledSize CellSize { get; set; } /// /// Set children layout options according to our settings. Not used for template case. @@ -889,6 +1027,9 @@ public bool DynamicSize typeof(SkiaCarousel), false, propertyChanged: NeedApplyOptions); + /// + /// UNIMPLEMENTED YET + /// public bool IsLooped { get { return (bool)GetValue(IsLoopedProperty); } @@ -913,39 +1054,6 @@ public bool IsVertical - bool viewportSet; - - protected override void OnLayoutChanged() - { - base.OnLayoutChanged(); - - if (Parent != null) - { - Viewport = Parent.DrawingRect; - - if (!viewportSet)// !CompareRects(Viewport, _lastViewport, 0.5f)) - { - viewportSet = true; - _lastViewport = Viewport; - Init(); - } - - if (DynamicSize && SelectedIndex >= 0) - { - if (!ChildrenInitialized) - { - InitializeChildren(); - } - ApplyDynamicSize(SelectedIndex); - } - else - { - InitializeChildren(); - } - } - } - - protected virtual void ApplyDynamicSize(int index) { if (ChildrenFactory.TemplatesAvailable) @@ -984,15 +1092,18 @@ protected virtual void OnSelectedIndexChanged(int index) { SelectedIndexChanged?.Invoke(this, index); - if (!LayoutReady) + if (!LayoutReady || _isSnapping != null) return; - ApplyIndex(); if (!ChildrenInitialized) { InitializeChildren(); } + else + { + ApplyIndex(); + } if (DynamicSize && SelectedIndex >= 0) { @@ -1001,6 +1112,42 @@ protected virtual void OnSelectedIndexChanged(int index) } + + public static readonly BindableProperty LinearSpeedMsProperty = BindableProperty.Create( + nameof(LinearSpeedMs), + typeof(double), + typeof(SkiaCarousel), + 0.0); + + /// + /// How long would a whole auto-sliding take, if `Bounces` is `False`. + /// If set (>0) will be used for automatic scrolls instead of using manual velocity. + /// For bouncing carousel + /// + public double LinearSpeedMs + { + get { return (double)GetValue(LinearSpeedMsProperty); } + set { SetValue(LinearSpeedMsProperty, value); } + } + + private int _LastIndex; + public int LastIndex + { + get + { + return _LastIndex; + } + set + { + if (_LastIndex != value) + { + _LastIndex = value; + OnPropertyChanged(); + } + } + } + + public static readonly BindableProperty SelectedIndexProperty = BindableProperty.Create( nameof(SelectedIndex), typeof(int), @@ -1010,6 +1157,7 @@ protected virtual void OnSelectedIndexChanged(int index) { if (b is SkiaCarousel control) { + control.LastIndex = (int)o; control.OnSelectedIndexChanged((int)n); } }); @@ -1080,6 +1228,8 @@ public override ISkiaGestureListener ProcessGestures(SkiaGesturesParameters args { bool passedToChildren = false; + // Debug.WriteLine($"[Carousel] {args.Type}"); + //Super.Log($"[CAROUSEL] {this.Tag} Got {args.Action}.."); //var thisOffset = TranslateInputCoords(apply.childOffset); @@ -1112,14 +1262,14 @@ ISkiaGestureListener PassToChildren() if (!RespondsToGestures) return null; - void ResetPan() { IsUserFocused = true; IsUserPanning = false; - VectorAnimatorSpring?.Stop(); + AnimatorRange.Stop(); + VectorAnimatorSpring?.Stop(); VelocityAccumulator.Clear(); _panningOffset = CurrentPosition; @@ -1128,93 +1278,95 @@ void ResetPan() switch (args.Type) { - case TouchActionResult.Down: + case TouchActionResult.Down: - // if (!IsUserFocused) //first finger down - if (args.Event.NumberOfTouches == 1) //first finger down - { - ResetPan(); - } + // if (!IsUserFocused) //first finger down + if (args.Event.NumberOfTouches == 1) //first finger down + { + ResetPan(); + } - consumed = this; + consumed = this; - break; + break; - case TouchActionResult.Panning when args.Event.NumberOfTouches == 1: + case TouchActionResult.Panning when args.Event.NumberOfTouches == 1: - if (!IsUserPanning) - { - //first pan - if (args.Event.Distance.Total.X == 0 || Math.Abs(args.Event.Distance.Total.Y) > Math.Abs(args.Event.Distance.Total.X) || Math.Abs(args.Event.Distance.Total.X) < 2) - { - return null; - } - } + if (!IsUserPanning) + { + //first pan + if (args.Event.Distance.Total.X == 0 || Math.Abs(args.Event.Distance.Total.Y) > Math.Abs(args.Event.Distance.Total.X) || Math.Abs(args.Event.Distance.Total.X) < 2) + { + return null; + } + } - if (!IsUserFocused) - { - ResetPan(); - } + if (!IsUserFocused) + { + ResetPan(); + } - //todo add direction - //this.IgnoreWrongDirection + //todo add direction + //this.IgnoreWrongDirection - IsUserPanning = true; + IsUserPanning = true; - var x = _panningOffset.X + args.Event.Distance.Delta.X / RenderingScale; - var y = _panningOffset.Y + args.Event.Distance.Delta.Y / RenderingScale; + var x = _panningOffset.X + args.Event.Distance.Delta.X / RenderingScale; + var y = _panningOffset.Y + args.Event.Distance.Delta.Y / RenderingScale; - Vector2 velocity; - float useVelocity = 0; - if (!IsVertical) - { - useVelocity = (float)(args.Event.Distance.Velocity.X / RenderingScale); - velocity = new(useVelocity, 0); - } - else - { - useVelocity = (float)(args.Event.Distance.Velocity.Y / RenderingScale); - velocity = new(0, useVelocity); - } + Vector2 velocity; + float useVelocity = 0; + if (!IsVertical) + { + useVelocity = (float)(args.Event.Distance.Velocity.X / RenderingScale); + velocity = new(useVelocity, 0); + } + else + { + useVelocity = (float)(args.Event.Distance.Velocity.Y / RenderingScale); + velocity = new(0, useVelocity); + } - //record velocity - VelocityAccumulator.CaptureVelocity(velocity); + //record velocity + VelocityAccumulator.CaptureVelocity(velocity); - //saving non clamped - _panningOffset.X = x; - _panningOffset.Y = y; + //saving non clamped + _panningOffset.X = x; + _panningOffset.Y = y; - var clamped = ClampOffset((float)x, (float)y, Bounces); + var clamped = ClampOffset((float)x, (float)y, Bounces); - //Debug.WriteLine($"[CAROUSEL] Panning: {_panningOffset:0} / {clamped:0}"); - ApplyPosition(clamped); + //Debug.WriteLine($"[CAROUSEL] Panning: {_panningOffset:0} / {clamped:0}"); + ApplyPosition(clamped); - consumed = this; - break; + consumed = this; + break; - case TouchActionResult.Up: + case TouchActionResult.Up: + //Debug.WriteLine($"[Carousel] {args.Type} {IsUserFocused} {IsUserPanning} {InTransition}"); - if (IsUserFocused) - { + if (IsUserFocused) + { - if (IsUserPanning) //|| Math.Abs(velocity) > 30) - { - consumed = this; + if (IsUserPanning || InTransition) + { + consumed = this; - var final = VelocityAccumulator.CalculateFinalVelocity(500); + var final = VelocityAccumulator.CalculateFinalVelocity(500); - //animate - CurrentSnap = CurrentPosition; - ScrollToNearestAnchor(CurrentSnap, final); - } + //animate + CurrentSnap = CurrentPosition; - IsUserPanning = false; - IsUserFocused = false; + ScrollToNearestAnchor(CurrentSnap, final); + } - } + IsUserPanning = false; + IsUserFocused = false; + + } - break; + break; } if (consumed != null || IsUserPanning) diff --git a/src/Engine/Controls/Drawer/SkiaDrawer.cs b/src/Engine/Controls/Drawer/SkiaDrawer.cs index 5c8ef310..ed008abd 100644 --- a/src/Engine/Controls/Drawer/SkiaDrawer.cs +++ b/src/Engine/Controls/Drawer/SkiaDrawer.cs @@ -21,10 +21,7 @@ public override void OnWillDisposeWithChildren() public override void ApplyBindingContext() { - if (this.Content != null) - { - Content.BindingContext = this.BindingContext; - } + Content?.SetInheritedBindingContext(this.BindingContext); base.ApplyBindingContext(); } @@ -264,7 +261,7 @@ void Init() ApplyPosition(value); } }; - _animatorRange = new(this) + AnimatorRange = new(this) { OnVectorUpdated = (value) => { @@ -314,28 +311,28 @@ protected virtual Vector2 GetOffsetToHide() switch (this.Direction) { - case DrawerDirection.FromLeft: - if (AmplitudeSize >= 0) - headerSize = amplitudeSize >= 0 ? width - amplitudeSize : width; - return new Vector2((float)(-(width - headerSize)), 0); - - case DrawerDirection.FromRight: - if (AmplitudeSize >= 0) - headerSize = amplitudeSize >= 0 ? width - amplitudeSize : width; - return new Vector2((float)(width - headerSize), 0); - - case DrawerDirection.FromBottom: - if (AmplitudeSize >= 0) - headerSize = amplitudeSize >= 0 ? height - amplitudeSize : height; - return new Vector2(0, (float)(height - headerSize)); - - case DrawerDirection.FromTop: - if (AmplitudeSize >= 0) - headerSize = amplitudeSize >= 0 ? height - amplitudeSize : height; - return new Vector2(0, (float)(-(height - headerSize))); - - default: - return Vector2.Zero; + case DrawerDirection.FromLeft: + if (AmplitudeSize >= 0) + headerSize = amplitudeSize >= 0 ? width - amplitudeSize : width; + return new Vector2((float)(-(width - headerSize)), 0); + + case DrawerDirection.FromRight: + if (AmplitudeSize >= 0) + headerSize = amplitudeSize >= 0 ? width - amplitudeSize : width; + return new Vector2((float)(width - headerSize), 0); + + case DrawerDirection.FromBottom: + if (AmplitudeSize >= 0) + headerSize = amplitudeSize >= 0 ? height - amplitudeSize : height; + return new Vector2(0, (float)(height - headerSize)); + + case DrawerDirection.FromTop: + if (AmplitudeSize >= 0) + headerSize = amplitudeSize >= 0 ? height - amplitudeSize : height; + return new Vector2(0, (float)(-(height - headerSize))); + + default: + return Vector2.Zero; } } @@ -357,13 +354,13 @@ protected override Vector2 GetAutoVelocity(Vector2 displacement) switch (this.Direction) { - case DrawerDirection.FromLeft: - case DrawerDirection.FromRight: - return new Vector2(-velocity * Math.Sign(displacement.X), 0); + case DrawerDirection.FromLeft: + case DrawerDirection.FromRight: + return new Vector2(-velocity * Math.Sign(displacement.X), 0); - case DrawerDirection.FromTop: - case DrawerDirection.FromBottom: - return new Vector2(0, -velocity * Math.Sign(displacement.Y)); + case DrawerDirection.FromTop: + case DrawerDirection.FromBottom: + return new Vector2(0, -velocity * Math.Sign(displacement.Y)); } return base.GetAutoVelocity(displacement); @@ -373,39 +370,39 @@ public override SKRect GetContentOffsetBounds() { switch (this.Direction) { - case DrawerDirection.FromLeft: - return new SKRect( - SnapPoints[1].X, - SnapPoints[0].Y, - SnapPoints[0].X, - SnapPoints[1].Y - ); - - case DrawerDirection.FromRight: - return new SKRect( - SnapPoints[0].X, - SnapPoints[0].Y, - SnapPoints[1].X, - SnapPoints[1].Y - ); - - - case DrawerDirection.FromTop: - return new SKRect( - SnapPoints[0].X, - SnapPoints[1].Y, - SnapPoints[1].X, - SnapPoints[0].Y - ); - - case DrawerDirection.FromBottom: - default: - return new SKRect( - SnapPoints[0].X, - SnapPoints[0].Y, - SnapPoints[1].X, - SnapPoints[1].Y - ); + case DrawerDirection.FromLeft: + return new SKRect( + SnapPoints[1].X, + SnapPoints[0].Y, + SnapPoints[0].X, + SnapPoints[1].Y + ); + + case DrawerDirection.FromRight: + return new SKRect( + SnapPoints[0].X, + SnapPoints[0].Y, + SnapPoints[1].X, + SnapPoints[1].Y + ); + + + case DrawerDirection.FromTop: + return new SKRect( + SnapPoints[0].X, + SnapPoints[1].Y, + SnapPoints[1].X, + SnapPoints[0].Y + ); + + case DrawerDirection.FromBottom: + default: + return new SKRect( + SnapPoints[0].X, + SnapPoints[0].Y, + SnapPoints[1].X, + SnapPoints[1].Y + ); } @@ -606,13 +603,13 @@ ISkiaGestureListener PassToChildren() // if the gesture is not in the header we first will pass it to children, // and process only if children didn't consume it - void ResetPan() { IsUserFocused = true; IsUserPanning = false; + AnimatorRange?.Stop(); VectorAnimatorSpring.Stop(); VelocityAccumulator.Clear(); @@ -635,204 +632,204 @@ void ResetPan() switch (args.Type) { - //--------------------------------------------------------------------------------------------------------- - case TouchActionResult.Tapped: - case TouchActionResult.LongPressing: - //--------------------------------------------------------------------------------------------------------- - - consumed = this; - break; - - //--------------------------------------------------------------------------------------------------------- - case TouchActionResult.Down: - //--------------------------------------------------------------------------------------------------------- - if (args.Event.NumberOfTouches == 1) //first finger down - { - ResetPan(); - } + //--------------------------------------------------------------------------------------------------------- + case TouchActionResult.Tapped: + case TouchActionResult.LongPressing: + //--------------------------------------------------------------------------------------------------------- - consumed = this; + consumed = this; + break; - break; + //--------------------------------------------------------------------------------------------------------- + case TouchActionResult.Down: + //--------------------------------------------------------------------------------------------------------- + if (args.Event.NumberOfTouches == 1) //first finger down + { + ResetPan(); + } - //--------------------------------------------------------------------------------------------------------- - case TouchActionResult.Panning when args.Event.NumberOfTouches == 1: - //--------------------------------------------------------------------------------------------------------- + consumed = this; - var direction = DirectionType.None; - bool lockBounce = false; + break; - // Determine if the gesture is panning towards an existing snap point - if (Direction == DrawerDirection.FromLeft) - { - direction = DirectionType.Horizontal; - if (args.Event.Distance.Delta.X > 0) - // horizontal, lock if panning to right and we are already at SnapPoints[0] - lockBounce = AreVectorsEqual(CurrentPosition, SnapPoints[0], 1); - } - else if (Direction == DrawerDirection.FromRight) - { - direction = DirectionType.Horizontal; - if (args.Event.Distance.Delta.X < 0) - // horizontal, lock if panning to left and we are already at SnapPoints[0] - lockBounce = AreVectorsEqual(CurrentPosition, SnapPoints[0], 1); - } - else if (Direction == DrawerDirection.FromBottom) - { - direction = DirectionType.Vertical; - if (args.Event.Distance.Delta.Y < 0) - // vertical, lock if panning to top and we are already at SnapPoints[0] - lockBounce = AreVectorsEqual(CurrentPosition, SnapPoints[0], 1); - } - else if (Direction == DrawerDirection.FromTop) - { - direction = DirectionType.Vertical; - if (args.Event.Distance.Delta.Y > 0) - // vertical, lock if panning to bottom and we are already at SnapPoints[1] - lockBounce = AreVectorsEqual(CurrentPosition, SnapPoints[1], 1); - } + //--------------------------------------------------------------------------------------------------------- + case TouchActionResult.Panning when args.Event.NumberOfTouches == 1: + //--------------------------------------------------------------------------------------------------------- - if (!IsUserFocused) - { - ResetPan(); - _panningOffset = new(_panningOffset.X - args.Event.Distance.Delta.X / RenderingScale, _panningOffset.Y - args.Event.Distance.Delta.Y / RenderingScale); - } + var direction = DirectionType.None; + bool lockBounce = false; - var x = _panningOffset.X + args.Event.Distance.Delta.X / RenderingScale; - var y = _panningOffset.Y + args.Event.Distance.Delta.Y / RenderingScale; + // Determine if the gesture is panning towards an existing snap point + if (Direction == DrawerDirection.FromLeft) + { + direction = DirectionType.Horizontal; + if (args.Event.Distance.Delta.X > 0) + // horizontal, lock if panning to right and we are already at SnapPoints[0] + lockBounce = AreVectorsEqual(CurrentPosition, SnapPoints[0], 1); + } + else if (Direction == DrawerDirection.FromRight) + { + direction = DirectionType.Horizontal; + if (args.Event.Distance.Delta.X < 0) + // horizontal, lock if panning to left and we are already at SnapPoints[0] + lockBounce = AreVectorsEqual(CurrentPosition, SnapPoints[0], 1); + } + else if (Direction == DrawerDirection.FromBottom) + { + direction = DirectionType.Vertical; + if (args.Event.Distance.Delta.Y < 0) + // vertical, lock if panning to top and we are already at SnapPoints[0] + lockBounce = AreVectorsEqual(CurrentPosition, SnapPoints[0], 1); + } + else if (Direction == DrawerDirection.FromTop) + { + direction = DirectionType.Vertical; + if (args.Event.Distance.Delta.Y > 0) + // vertical, lock if panning to bottom and we are already at SnapPoints[1] + lockBounce = AreVectorsEqual(CurrentPosition, SnapPoints[1], 1); + } + if (!IsUserFocused) + { + ResetPan(); + _panningOffset = new(_panningOffset.X - args.Event.Distance.Delta.X / RenderingScale, _panningOffset.Y - args.Event.Distance.Delta.Y / RenderingScale); + } - if (!IsUserPanning) //for the first panning move only - { - var mainDirection = GetDirectionType(_panningOffset, new Vector2(x, y), 0.9f); + var x = _panningOffset.X + args.Event.Distance.Delta.X / RenderingScale; + var y = _panningOffset.Y + args.Event.Distance.Delta.Y / RenderingScale; - if (direction == DirectionType.None || mainDirection != direction && IgnoreWrongDirection) - { - break; //ignore this gesture - } - IsUserPanning = true; - } + if (!IsUserPanning) //for the first panning move only + { + var mainDirection = GetDirectionType(_panningOffset, new Vector2(x, y), 0.9f); - if (IsUserPanning) - { + if (direction == DirectionType.None || mainDirection != direction && IgnoreWrongDirection) + { + break; //ignore this gesture + } - Vector2 velocity; - float useVelocity = 0; - if (direction == DirectionType.Horizontal) - { - useVelocity = (float)(args.Event.Distance.Velocity.X / RenderingScale); - velocity = new(useVelocity, 0); - y = 0; - } - else - { - useVelocity = (float)(args.Event.Distance.Velocity.Y / RenderingScale); - velocity = new(0, useVelocity); - x = 0; - } - //record velocity - VelocityAccumulator.CaptureVelocity(velocity); + IsUserPanning = true; + } - //saving non clamped - _panningOffset.X = x; - _panningOffset.Y = y; + if (IsUserPanning) + { - // Allow bouncing only if Bounces is true and we are not panning towards an existing snap point - bool shouldBounce = Bounces && !lockBounce; + Vector2 velocity; + float useVelocity = 0; + if (direction == DirectionType.Horizontal) + { + useVelocity = (float)(args.Event.Distance.Velocity.X / RenderingScale); + velocity = new(useVelocity, 0); + y = 0; + } + else + { + useVelocity = (float)(args.Event.Distance.Velocity.Y / RenderingScale); + velocity = new(0, useVelocity); + x = 0; + } + //record velocity + VelocityAccumulator.CaptureVelocity(velocity); - var clamped = ClampOffset((float)x, (float)y, shouldBounce); + //saving non clamped + _panningOffset.X = x; + _panningOffset.Y = y; - if (!Bounces && lockBounce) //if we reached side and boucing is off we cannot drag further so maybe pass pan to parent - { - if (AreEqual(clamped.X, 0, 1) && AreEqual(clamped.Y, 0, 1)) - { - var verticalMove = DrawerDirection.FromBottom; - if (args.Event.Distance.Delta.Y < 0) - verticalMove = DrawerDirection.FromTop; + // Allow bouncing only if Bounces is true and we are not panning towards an existing snap point + bool shouldBounce = Bounces && !lockBounce; - var horizontalMove = DrawerDirection.FromLeft; - if (args.Event.Distance.Delta.X < 0) - horizontalMove = DrawerDirection.FromRight; + var clamped = ClampOffset((float)x, (float)y, shouldBounce); - if ((direction == DirectionType.Vertical && Direction == verticalMove) - || (direction == DirectionType.Horizontal && Direction == horizontalMove)) + if (!Bounces && lockBounce) //if we reached side and boucing is off we cannot drag further so maybe pass pan to parent { - IsUserPanning = false; - return null; + if (AreEqual(clamped.X, 0, 1) && AreEqual(clamped.Y, 0, 1)) + { + var verticalMove = DrawerDirection.FromBottom; + if (args.Event.Distance.Delta.Y < 0) + verticalMove = DrawerDirection.FromTop; + + var horizontalMove = DrawerDirection.FromLeft; + if (args.Event.Distance.Delta.X < 0) + horizontalMove = DrawerDirection.FromRight; + + if ((direction == DirectionType.Vertical && Direction == verticalMove) + || (direction == DirectionType.Horizontal && Direction == horizontalMove)) + { + IsUserPanning = false; + return null; + } + + } } - } - } - - ApplyPosition(clamped); - consumed = this; - //var clamped = ClampOffset((float)x, (float)y, Bounces); + ApplyPosition(clamped); + consumed = this; + //var clamped = ClampOffset((float)x, (float)y, Bounces); - } - else - { - return null; - } + } + else + { + return null; + } - break; + break; - //--------------------------------------------------------------------------------------------------------- - case TouchActionResult.Up: - //--------------------------------------------------------------------------------------------------------- + //--------------------------------------------------------------------------------------------------------- + case TouchActionResult.Up: + //--------------------------------------------------------------------------------------------------------- - direction = DirectionType.None; - var Velocity = Vector2.Zero; + direction = DirectionType.None; + var Velocity = Vector2.Zero; - Velocity = VelocityAccumulator.CalculateFinalVelocity(500); - //Velocity = new((float)(args.Event.Distance.Velocity.X / RenderingScale), (float)(args.Event.Distance.Velocity.Y / RenderingScale)); + Velocity = VelocityAccumulator.CalculateFinalVelocity(500); + //Velocity = new((float)(args.Event.Distance.Velocity.X / RenderingScale), (float)(args.Event.Distance.Velocity.Y / RenderingScale)); - if (IsUserPanning) - { - bool rightDirection = false; + if (IsUserPanning) + { + bool rightDirection = false; - switch (this.Direction) - { - case DrawerDirection.FromLeft: - case DrawerDirection.FromRight: - direction = DirectionType.Horizontal; - if (GetDirectionType(Velocity, direction, 0.9f) == direction) - { - rightDirection = true; - } - Velocity.Y = 0; //be clean - break; - case DrawerDirection.FromBottom: - case DrawerDirection.FromTop: - direction = DirectionType.Vertical; - if (GetDirectionType(Velocity, direction, 0.9f) == direction) - { - rightDirection = true; - } - Velocity.X = 0; //be clean - break; - } + switch (this.Direction) + { + case DrawerDirection.FromLeft: + case DrawerDirection.FromRight: + direction = DirectionType.Horizontal; + if (GetDirectionType(Velocity, direction, 0.9f) == direction) + { + rightDirection = true; + } + Velocity.Y = 0; //be clean + break; + case DrawerDirection.FromBottom: + case DrawerDirection.FromTop: + direction = DirectionType.Vertical; + if (GetDirectionType(Velocity, direction, 0.9f) == direction) + { + rightDirection = true; + } + Velocity.X = 0; //be clean + break; + } - //animate, only but if velociy is according drawer direction - if (!IgnoreWrongDirection) - { - rightDirection = true; - } - if (rightDirection || CheckNeedToSnap()) - { - CurrentSnap = new Vector2((float)TranslationX, (float)TranslationY); - ScrollToNearestAnchor(new Vector2((float)TranslationX, (float)TranslationY), Velocity); + //animate, only but if velociy is according drawer direction + if (!IgnoreWrongDirection) + { + rightDirection = true; + } + if (rightDirection || CheckNeedToSnap()) + { + CurrentSnap = new Vector2((float)TranslationX, (float)TranslationY); + ScrollToNearestAnchor(new Vector2((float)TranslationX, (float)TranslationY), Velocity); - consumed = this; - } + consumed = this; + } - IsUserPanning = false; - IsUserFocused = false; - } + IsUserPanning = false; + IsUserFocused = false; + } - _inContact = false; + _inContact = false; - break; + break; } if (consumed != null || IsUserPanning)// || args.Event.NumberOfTouches > 1) diff --git a/src/Engine/Controls/Layouts/SkiaDecoratedGrid.cs b/src/Engine/Controls/Layouts/SkiaDecoratedGrid.cs index b396d36b..c11f5dbb 100644 --- a/src/Engine/Controls/Layouts/SkiaDecoratedGrid.cs +++ b/src/Engine/Controls/Layouts/SkiaDecoratedGrid.cs @@ -197,8 +197,6 @@ protected override void Draw(SkiaDrawingContext context, SKRect destination, flo { base.Draw(context, destination, scale); - - if (ContainerLines != null) { ContainerLines.Render(context, GetDrawingRectForChildren(Destination, scale), scale); diff --git a/src/Engine/Controls/Navigation/SkiaShell.cs b/src/Engine/Controls/Navigation/SkiaShell.cs index d2b21ee9..8d8a215a 100644 --- a/src/Engine/Controls/Navigation/SkiaShell.cs +++ b/src/Engine/Controls/Navigation/SkiaShell.cs @@ -710,7 +710,7 @@ public virtual async Task PushModalAsync(BindableObject page, try { var content = page as SkiaControl; - modalWrapper.BindingContext = content.BindingContext; + modalWrapper.SetInheritedBindingContext(content.BindingContext); modalWrapper.WrapContent(content); if (modalWrapper.Content is SkiaDrawer drawer) diff --git a/src/Engine/Controls/PlayFrames/SkiaGif.cs b/src/Engine/Controls/PlayFrames/SkiaGif.cs index 913a5c4a..5ce90b4b 100644 --- a/src/Engine/Controls/PlayFrames/SkiaGif.cs +++ b/src/Engine/Controls/PlayFrames/SkiaGif.cs @@ -9,6 +9,8 @@ public class SkiaGif : AnimatedFramesRenderer { public SkiaImage Display { get; protected set; } + //public override bool CanUseCacheDoubleBuffering => false; + /// /// For standalone use /// diff --git a/src/Engine/Controls/PlayFrames/SkiaMediaImage.cs b/src/Engine/Controls/PlayFrames/SkiaMediaImage.cs index d55ebb83..e47274c5 100644 --- a/src/Engine/Controls/PlayFrames/SkiaMediaImage.cs +++ b/src/Engine/Controls/PlayFrames/SkiaMediaImage.cs @@ -3,6 +3,8 @@ namespace DrawnUi.Maui.Controls; public class SkiaMediaImage : SkiaImage { + //public override bool CanUseCacheDoubleBuffering => false; + protected override void OnLayoutChanged() { base.OnLayoutChanged(); diff --git a/src/Engine/Controls/ViewSwitcher/SkiaViewSwitcher.cs b/src/Engine/Controls/ViewSwitcher/SkiaViewSwitcher.cs index db6315a3..4ccca528 100644 --- a/src/Engine/Controls/ViewSwitcher/SkiaViewSwitcher.cs +++ b/src/Engine/Controls/ViewSwitcher/SkiaViewSwitcher.cs @@ -13,14 +13,12 @@ public class SkiaViewSwitcher : SkiaLayout, IDefinesViewport, IVisibilityAware { public override void ApplyBindingContext() { - if (FillGradient != null) FillGradient.BindingContext = BindingContext; foreach (var view in this.Views) { - if (view.BindingContext == null) //do NOT break subview context! - view.BindingContext = BindingContext; + view.SetInheritedBindingContext(BindingContext); } } @@ -910,81 +908,81 @@ void SetNewVisibleViewAsOneVisible() switch (transition) { - case TransitionType.Pop: - easing = Easing.Linear; - //from left to right - translateTo = this.Width; - break; - case TransitionType.Push: - - previousVisibleView.View.ZIndex = -1; - newVisibleView.View.ZIndex = 0; - - easing = Easing.Linear; - //from right to left - translateTo = this.Width; - newVisibleView.View.Opacity = 0.001; - newVisibleView.View.TranslationX = (float)translateTo; - break; - - //this began bugging when a tab child has transforms, - //surprisingly looks like somewhere that canvas is not calling Restore - //when we switch from tab 3 where we have that shape with transforms - //to other tab at the left - case TransitionType.SwitchTabsModern: - - easing = AnimationEasing; - speed = AnimationSpeed; - - newVisibleView.View.ZIndex = 1; - previousVisibleView.View.ZIndex = 0; - - if (selectedIndex > previousVisibleViewIndex) - { - //f===> - translateTo = this.Width; - newVisibleView.View.Opacity = 0.001; - newVisibleView.View.TranslationX = (float)translateTo * 0.75; - //toViewDraw = FadeInFromRight; - } - else - { - // <==== - translateTo = -this.Width; - newVisibleView.View.Opacity = 0.001; - newVisibleView.View.TranslationX = (float)translateTo * 0.75; - //toViewDraw = FadeInFromLeft; - } - - - - break; - - //actually using this for tabs - case TransitionType.SwitchTabs: - //easing = _easing; - easing = AnimationEasing; - speed = AnimationSpeed; - if (selectedIndex > previousVisibleViewIndex) - { - //from right to left - translateTo = this.Width; - } - else - { - //from left to right - translateTo = -this.Width; - } - - newVisibleView.View.Opacity = 1; - newVisibleView.View.TranslationX = (float)translateTo; - break; - - default: - newVisibleView.View.TranslationX = 0; - newVisibleView.View.TranslationY = 0; - newVisibleView.View.Opacity = 1; - break; + case TransitionType.Pop: + easing = Easing.Linear; + //from left to right + translateTo = this.Width; + break; + case TransitionType.Push: + + previousVisibleView.View.ZIndex = -1; + newVisibleView.View.ZIndex = 0; + + easing = Easing.Linear; + //from right to left + translateTo = this.Width; + newVisibleView.View.Opacity = 0.001; + newVisibleView.View.TranslationX = (float)translateTo; + break; + + //this began bugging when a tab child has transforms, + //surprisingly looks like somewhere that canvas is not calling Restore + //when we switch from tab 3 where we have that shape with transforms + //to other tab at the left + case TransitionType.SwitchTabsModern: + + easing = AnimationEasing; + speed = AnimationSpeed; + + newVisibleView.View.ZIndex = 1; + previousVisibleView.View.ZIndex = 0; + + if (selectedIndex > previousVisibleViewIndex) + { + //f===> + translateTo = this.Width; + newVisibleView.View.Opacity = 0.001; + newVisibleView.View.TranslationX = (float)translateTo * 0.75; + //toViewDraw = FadeInFromRight; + } + else + { + // <==== + translateTo = -this.Width; + newVisibleView.View.Opacity = 0.001; + newVisibleView.View.TranslationX = (float)translateTo * 0.75; + //toViewDraw = FadeInFromLeft; + } + + + + break; + + //actually using this for tabs + case TransitionType.SwitchTabs: + //easing = _easing; + easing = AnimationEasing; + speed = AnimationSpeed; + if (selectedIndex > previousVisibleViewIndex) + { + //from right to left + translateTo = this.Width; + } + else + { + //from left to right + translateTo = -this.Width; + } + + newVisibleView.View.Opacity = 1; + newVisibleView.View.TranslationX = (float)translateTo; + break; + + default: + newVisibleView.View.TranslationX = 0; + newVisibleView.View.TranslationY = 0; + newVisibleView.View.Opacity = 1; + break; } ChangeViewVisibility(newVisibleView.View, true); @@ -995,81 +993,81 @@ void SetNewVisibleViewAsOneVisible() //animate switch (transition) { - case TransitionType.Pop: - - Task animateOld1 = previousVisibleView.View.TranslateToAsync(translateTo, 0, 250, Easing.Linear); - Task animateOld2 = previousVisibleView.View.FadeToAsync(0.9, 250, Easing.Linear); - - try - { - var cancelAnimation = new CancellationTokenSource(TimeSpan.FromSeconds(2)); - await Task.WhenAll(animateOld1, animateOld2).WithCancellation(cancelAnimation.Token); - } - catch (Exception e) - { - Debug.WriteLine(e); - } - break; - - case TransitionType.Push: - Task in1 = newVisibleView.View.TranslateToAsync(0, 0, 250, Easing.Linear); - Task in2 = newVisibleView.View.FadeToAsync(1.0, 250, Easing.Linear); - try - { - var cancelAnimation = new CancellationTokenSource(TimeSpan.FromSeconds(2)); - await Task.WhenAll(in1, in2).WithCancellation(cancelAnimation.Token).WithCancellation(cancelAnimation.Token); - } - catch (Exception e) - { - Debug.WriteLine(e); - } - break; - - case TransitionType.SwitchTabsModern: - - Task animateOldM = previousVisibleView.View.TranslateToAsync(-translateTo, 0, (uint)speed, easing); - Task animateNewM = newVisibleView.View.TranslateToAsync(0, 0, (uint)speed, easing); - Task animateNewM1 = newVisibleView.View.FadeToAsync(1.0, (uint)speed, Easing.Linear); - - try - { - var cancelAnimation = - new CancellationTokenSource(TimeSpan.FromSeconds(2)); - - await Task.WhenAll(animateOldM, animateNewM, animateNewM1).WithCancellation(cancelAnimation.Token); - - //var cancelAnimation = new CancellationTokenSource(TimeSpan.FromSeconds(2)); - //await newVisibleView.View.AnimateAsync((value) => - //{ - - // _fadeOpacity = (float)value; - // newVisibleView.View.Repaint(); - - //}, (uint)1000, easing, cancelAnimation); - } - catch (Exception e) - { - Debug.WriteLine(e); - } - break; - - case TransitionType.SwitchTabs: - - Task animateOld = previousVisibleView.View.TranslateToAsync(-translateTo, 0, (uint)speed, easing); - Task animateNew = newVisibleView.View.TranslateToAsync(0, 0, (uint)speed, easing); - - try - { - var cancelAnimation = - new CancellationTokenSource(TimeSpan.FromSeconds(2)); - //PrintDebug(); - await Task.WhenAll(animateOld, animateNew).WithCancellation(cancelAnimation.Token); - } - catch (Exception e) - { - Debug.WriteLine(e); - } - break; + case TransitionType.Pop: + + Task animateOld1 = previousVisibleView.View.TranslateToAsync(translateTo, 0, 250, Easing.Linear); + Task animateOld2 = previousVisibleView.View.FadeToAsync(0.9, 250, Easing.Linear); + + try + { + var cancelAnimation = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await Task.WhenAll(animateOld1, animateOld2).WithCancellation(cancelAnimation.Token); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + break; + + case TransitionType.Push: + Task in1 = newVisibleView.View.TranslateToAsync(0, 0, 250, Easing.Linear); + Task in2 = newVisibleView.View.FadeToAsync(1.0, 250, Easing.Linear); + try + { + var cancelAnimation = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await Task.WhenAll(in1, in2).WithCancellation(cancelAnimation.Token).WithCancellation(cancelAnimation.Token); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + break; + + case TransitionType.SwitchTabsModern: + + Task animateOldM = previousVisibleView.View.TranslateToAsync(-translateTo, 0, (uint)speed, easing); + Task animateNewM = newVisibleView.View.TranslateToAsync(0, 0, (uint)speed, easing); + Task animateNewM1 = newVisibleView.View.FadeToAsync(1.0, (uint)speed, Easing.Linear); + + try + { + var cancelAnimation = + new CancellationTokenSource(TimeSpan.FromSeconds(2)); + + await Task.WhenAll(animateOldM, animateNewM, animateNewM1).WithCancellation(cancelAnimation.Token); + + //var cancelAnimation = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + //await newVisibleView.View.AnimateAsync((value) => + //{ + + // _fadeOpacity = (float)value; + // newVisibleView.View.Repaint(); + + //}, (uint)1000, easing, cancelAnimation); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + break; + + case TransitionType.SwitchTabs: + + Task animateOld = previousVisibleView.View.TranslateToAsync(-translateTo, 0, (uint)speed, easing); + Task animateNew = newVisibleView.View.TranslateToAsync(0, 0, (uint)speed, easing); + + try + { + var cancelAnimation = + new CancellationTokenSource(TimeSpan.FromSeconds(2)); + //PrintDebug(); + await Task.WhenAll(animateOld, animateNew).WithCancellation(cancelAnimation.Token); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + break; } diff --git a/src/Engine/Draw/Base/SkiaControl.Cache.cs b/src/Engine/Draw/Base/SkiaControl.Cache.cs index 7e59f348..128a35c4 100644 --- a/src/Engine/Draw/Base/SkiaControl.Cache.cs +++ b/src/Engine/Draw/Base/SkiaControl.Cache.cs @@ -11,681 +11,740 @@ namespace DrawnUi.Maui.Draw; public partial class SkiaControl { - public static readonly BindableProperty UseCacheProperty = BindableProperty.Create(nameof(UseCache), - typeof(SkiaCacheType), - typeof(SkiaControl), - SkiaCacheType.None, - propertyChanged: NeedDraw); - - /// - /// Never reuse the rendering result. Actually true for ScrollLooped SkiaLayout viewport container to redraw its content several times for creating a looped aspect. - /// - public SkiaCacheType UseCache - { - get { return (SkiaCacheType)GetValue(UseCacheProperty); } - set { SetValue(UseCacheProperty, value); } - } - - - - /// - /// Used by the UseCacheDoubleBuffering process. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public CachedObject RenderObjectPrevious - { - get - { - return _renderObjectPrevious; - } - set - { - RenderObjectNeedsUpdate = false; - if (_renderObjectPrevious != value) - { - var kill = _renderObjectPrevious; - _renderObjectPrevious = value; - if (kill != null && UsingCacheType != SkiaCacheType.Image && UsingCacheType == SkiaCacheType.ImageComposite) - DisposeObject(kill); - } - } - } - CachedObject _renderObjectPrevious; - - - /// - /// The cached representation of the control. Will be used on redraws without calling Paint etc, until the control is requested to be updated. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public CachedObject RenderObject - { - get - { - return _renderObject; - } - set - { - RenderObjectNeedsUpdate = false; - if (_renderObject != value) - { - //lock both RenderObjectPrevious and RenderObject - lock (LockDraw) - { - 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.ImageComposite) - { - RenderObjectPrevious = _renderObject; //send it to back for special cases - } - else - { - DisposeObject(_renderObject); - } - } - _renderObject = value; - OnPropertyChanged(); - - if (value != null) - CreatedCache?.Invoke(this, value); - - Monitor.PulseAll(LockDraw); - } - - } - } - } - CachedObject _renderObject; - - public event EventHandler CreatedCache; - - public void DestroyRenderingObject() - { - RenderObject = null; - } - - protected virtual bool CheckCachedObjectValid(CachedObject cache, SKRect recordingArea, SkiaDrawingContext context) - { - if (cache != null) - { - if (cache.Bounds.Size != recordingArea.Size) - return false; - - //check hardware context maybe changed - if (UsingCacheType == SkiaCacheType.GPU && cache.Surface != null && - cache.Surface.Context != null && - context.Superview?.CanvasView is SkiaViewAccelerated hardware) - { - //hardware context might change if we returned from background.. - if (hardware.GRContext == null || cache.Surface.Context == null - || (int)hardware.GRContext.Handle != (int)cache.Surface.Context.Handle) - { - return false; - } - } - - return true; - } - return false; - } - - public virtual SkiaCacheType UsingCacheType - { - get - { - if (UseCache == SkiaCacheType.GPU && !Super.GpuCacheEnabled) - return SkiaCacheType.Image; - - if (EffectPostRenderer != null && (UseCache == SkiaCacheType.None || UseCache == SkiaCacheType.Operations)) - return SkiaCacheType.Image; - - return UseCache; - } - } - - protected virtual CachedObject CreateRenderingObject( -SkiaDrawingContext context, -SKRect recordingArea, -CachedObject reuseSurfaceFrom, - Action action) - { - if (recordingArea.Height == 0 || recordingArea.Width == 0 || IsDisposed || IsDisposing) - { - return null; - } - - CachedObject renderObject = null; - - try - { - var recordArea = GetCacheArea(recordingArea); - - NeedUpdate = false; //if some child changes this while rendering to cache we will erase resulting RenderObject - - var usingCacheType = UsingCacheType; - - GRContext grContext = null; - - if (IsCacheImage) - { - var width = (int)recordArea.Width; - var height = (int)recordArea.Height; - - bool needCreateSurface = !CheckCachedObjectValid(reuseSurfaceFrom, recordingArea, context) || usingCacheType == SkiaCacheType.GPU; //never reuse GPU surfaces - - SKSurface surface = null; - - if (!needCreateSurface) - { - //reusing existing surface - surface = reuseSurfaceFrom.Surface; - if (surface == null) - { - return null; //would be unexpected - } - if (usingCacheType != SkiaCacheType.ImageComposite) - surface.Canvas.Clear(); - } - else - { - needCreateSurface = true; - var kill = surface; - surface = null; - var cacheSurfaceInfo = new SKImageInfo(width, height); - - if (usingCacheType == SkiaCacheType.GPU) - { - if (context.Superview != null && context.Superview?.CanvasView is SkiaViewAccelerated accelerated - && accelerated.GRContext != null) - { - grContext = accelerated.GRContext; - //hardware accelerated - surface = SKSurface.Create(accelerated.GRContext, - false, - cacheSurfaceInfo); - } - } - if (surface == null) //fallback if gpu failed - { - //non-gpu - surface = SKSurface.Create(cacheSurfaceInfo); - } - - if (kill != surface) - DisposeObject(kill); - - // if (usingCacheType == SkiaCacheType.GPU) - // surface.Canvas.Clear(SKColors.Red); - } - - if (surface == null) - { - return null; //would be totally unexpected - } - - var recordingContext = context.CreateForRecordingImage(surface, recordArea.Size); - - recordingContext.IsRecycled = !needCreateSurface; - - // Translate the canvas to start drawing at (0,0) - - recordingContext.Canvas.Translate(-recordArea.Left, -recordArea.Top); - - // Perform the drawing action - action(recordingContext); - - surface.Canvas.Flush(); - //grContext?.Flush(); - - recordingContext.Canvas.Translate(recordArea.Left, recordArea.Top); - - renderObject = new(usingCacheType, surface, recordArea) - { - SurfaceIsRecycled = recordingContext.IsRecycled - }; - - } - else - if (UsingCacheType == SkiaCacheType.Operations) - { - var cacheRecordingArea = GetCacheRecordingArea(recordArea); - - - using (var recorder = new SKPictureRecorder()) - { - var recordingContext = context.CreateForRecordingOperations(recorder, cacheRecordingArea); - - action(recordingContext); - - // End the recording and obtain the SKPicture - var skPicture = recorder.EndRecording(); - - renderObject = new(SkiaCacheType.Operations, skPicture, recordArea); - } - } - - //else we landed here with no cache type at all.. - } - catch (Exception e) - { - Super.Log(e); - } - - return renderObject; - } - - public Action DelegateDrawCache { get; set; } - - protected virtual void DrawRenderObjectInternal( - CachedObject cache, - SkiaDrawingContext ctx, - SKRect destination) - { - if (DelegateDrawCache != null) - { - DelegateDrawCache(cache, ctx, destination); - } - else - { - DrawRenderObject(cache, ctx, destination); - } - } - - public bool IsCacheImage - { - get - { - var cache = UsingCacheType; - return cache == SkiaCacheType.Image - || cache == SkiaCacheType.GPU - || cache == SkiaCacheType.ImageComposite - || cache == SkiaCacheType.ImageDoubleBuffered; - } - } - - - protected virtual bool UseRenderingObject(SkiaDrawingContext context, SKRect recordArea, float scale) - { - - //lock (LockDraw) - { - var cache = RenderObject; - var cacheType = UsingCacheType; - var cacheOffscreen = RenderObjectPrevious; - - if (RenderObjectPrevious != null && RenderObjectPreviousNeedsUpdate) - { - var kill = RenderObjectPrevious; - RenderObjectPrevious = null; - RenderObjectPreviousNeedsUpdate = false; - if (kill != null) - { - Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => - { - kill.Dispose(); - }); - } - } - - if (cache != null) - { - if (!CheckCachedObjectValid(cache, recordArea, context)) - { - return false; - } - - //draw existing front cache - lock (LockDraw) - { - DrawRenderObjectInternal(cache, context, recordArea); - Monitor.PulseAll(LockDraw); - } - - if (cacheType != SkiaCacheType.ImageDoubleBuffered || !NeedUpdateFrontCache) - return true; - } - - if (cacheType == SkiaCacheType.ImageDoubleBuffered) - { - lock (LockDraw) - { - if (cache == null && cacheOffscreen != null) - { - DrawRenderObjectInternal(cacheOffscreen, context, recordArea); - } - Monitor.PulseAll(LockDraw); - } - - NeedUpdateFrontCache = false; - - //push task to create new cache, will always try to take last from stack: - var args = CreatePaintArguments(); - _offscreenCacheRenderingQueue.Push(() => - { - //will be executed on background thread in parallel - var oldObject = RenderObjectPreparing; - RenderObjectPreparing = CreateRenderingObject(context, recordArea, oldObject, (ctx) => - { - PaintWithEffects(ctx, recordArea, scale, args); - }); - }); - - if (!_processingOffscrenRendering) - { - _processingOffscrenRendering = true; - Task.Run(async () => //100% background thread - { - await ProcessOffscreenCacheRenderingAsync(); - - }).ConfigureAwait(false); - } - - return !NeedUpdateFrontCache; - } - - return false; - } - - } - - private readonly LimitedQueue _offscreenCacheRenderingQueue = new(1); - - - /// - /// Used by the UseCacheDoubleBuffering process. This is the new cache beign created in background. It will be copied to RenderObject when ready. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public CachedObject RenderObjectPreparing - { - get - { - return _renderObjectPreparing; - } - set - { - RenderObjectNeedsUpdate = false; - if (_renderObjectPreparing != value) - { - _renderObjectPreparing = value; - } - } - } - CachedObject _renderObjectPreparing; - - - private bool _processingOffscrenRendering = false; - - protected SemaphoreSlim semaphoreOffsecreenProcess = new(1); - - /// - /// Used by ImageDoubleBuffering cache - /// - protected bool NeedUpdateFrontCache - { - get => _needUpdateFrontCache; - set => _needUpdateFrontCache = value; - } - - - - private bool _RenderObjectPreviousNeedsUpdate; - [EditorBrowsable(EditorBrowsableState.Never)] - public bool RenderObjectPreviousNeedsUpdate - { - get - { - return _RenderObjectPreviousNeedsUpdate; - } - set - { - if (_RenderObjectPreviousNeedsUpdate != value) - { - _RenderObjectPreviousNeedsUpdate = value; - OnPropertyChanged(); - } - } - } - - /// - /// Should delete RenderObject when starting new frame rendering - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public bool RenderObjectNeedsUpdate - { - get - { - return _renderObjectNeedsUpdate; - } - - set - { - if (_renderObjectNeedsUpdate != value) - { - _renderObjectNeedsUpdate = value; - } - } - } - bool _renderObjectNeedsUpdate = true; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public virtual void InvalidateCache() - { - RenderObjectNeedsUpdate = true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public virtual void InvalidateCacheWithPrevious() - { - RenderObjectNeedsUpdate = true; - if (UsingCacheType == SkiaCacheType.ImageComposite) - { - RenderObjectPreviousNeedsUpdate = true; - } - } - - - public async Task ProcessOffscreenCacheRenderingAsync() - { - - await semaphoreOffsecreenProcess.WaitAsync(); - - if (_offscreenCacheRenderingQueue.Count == 0) - return; - - _processingOffscrenRendering = true; - - try - { - Action action = _offscreenCacheRenderingQueue.Pop(); - while (!IsDisposed && !IsDisposing && action != null) - { - try - { - action.Invoke(); - - RenderObject = RenderObjectPreparing; - _renderObjectPreparing = null; - - if (Parent != null && !Parent.UpdateLocked) - { - Parent?.UpdateByChild(this); //repaint us - } - - if (_offscreenCacheRenderingQueue.Count > 0) - action = _offscreenCacheRenderingQueue.Pop(); - else - break; - } - catch (Exception e) - { - Super.Log(e); - } - } - - if (NeedUpdate || RenderObjectNeedsUpdate) //someone changed us while rendering inner content - { - Update(); //kick - } - - } - finally - { - _processingOffscrenRendering = false; - semaphoreOffsecreenProcess.Release(); - } - - } - - /// - /// Used for the Operations cache type to record inside the changed area, if your control is not inside the DrawingRect due to transforms/translations. This is NOT changing the rendering object - /// - protected virtual SKRect GetCacheRecordingArea(SKRect drawingRect) - { - return drawingRect; - } - - /// - /// Normally cache is recorded inside DrawingRect, but you might want to exapnd this to include shadows around, for example. - /// - protected virtual SKRect GetCacheArea(SKRect drawingRect) - { - var pixels = ExpandCacheRecordingArea * RenderingScale; - return new SKRect( - (float)Math.Round(drawingRect.Left - pixels), - (float)Math.Round(drawingRect.Top - pixels), - (float)Math.Round(drawingRect.Right + pixels), - (float)Math.Round(drawingRect.Bottom + pixels)); - } - - /// - /// Returns true if had drawn. - /// - /// - /// - /// - /// - /// - /// - protected virtual bool DrawUsingRenderObject(SkiaDrawingContext context, - float widthRequest, float heightRequest, - SKRect destination, float scale) - { - Arrange(destination, widthRequest, heightRequest, scale); - - bool willDraw = !CheckIsGhost(); - if (willDraw) - { - if (UsingCacheType != SkiaCacheType.None) - { - var recordArea = DrawingRect; - - //paint from cache - if (!UseRenderingObject(context, recordArea, scale)) - { - //record to cache and paint - CreateRenderingObjectAndPaint(context, recordArea, (ctx) => - { - PaintWithEffects(ctx, DrawingRect, scale, CreatePaintArguments()); - }); - } - } - else - { - DrawWithClipAndTransforms(context, DrawingRect, DrawingRect, true, true, (ctx) => - { - PaintWithEffects(ctx, DrawingRect, scale, CreatePaintArguments()); - }); - } - } - - FinalizeDrawingWithRenderObject(context, scale); //NeedUpdate will go false - - return willDraw; - } - - /// - /// This is NOT calling FinalizeDraw()! - /// parameter 'area' Usually is equal to DrawingRect - /// - /// - /// - /// - protected void CreateRenderingObjectAndPaint( - SkiaDrawingContext context, - SKRect recordingArea, - Action action) - { - - if (recordingArea.Width <= 0 || recordingArea.Height <= 0 || float.IsInfinity(recordingArea.Height) || float.IsInfinity(recordingArea.Width) || IsDisposed || IsDisposing) - { - return; - } - - if (RenderObject != null && UsingCacheType != SkiaCacheType.ImageDoubleBuffered) - { - //we might come here with an existing RenderingObject if UseRenderingObject returned False - RenderObject = null; - } - - RenderObjectNeedsUpdate = false; - - var usingCacheType = UsingCacheType; - - CachedObject oldObject = null; //reusing this - if (usingCacheType == SkiaCacheType.ImageDoubleBuffered) - { - oldObject = RenderObject; - } - else - if (usingCacheType == SkiaCacheType.Image - || usingCacheType == SkiaCacheType.ImageComposite) - { - oldObject = RenderObjectPrevious; - } - - var created = CreateRenderingObject(context, recordingArea, oldObject, action); - - if (created == null) - { - return; - } - - if (oldObject != null) - { - if (created.SurfaceIsRecycled) - { - oldObject.Surface = null; - } - if (usingCacheType != SkiaCacheType.ImageDoubleBuffered && usingCacheType != SkiaCacheType.ImageComposite) - { - Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => - { - oldObject.Dispose(); - }); - } - } - - var notValid = RenderObjectNeedsUpdate; - RenderObject = created; - - - if (RenderObject != null) - { - DrawRenderObjectInternal(RenderObject, context, RenderObject.Bounds); - } - else - { - notValid = true; - } - - - if (NeedUpdate || notValid) //someone changed us while rendering inner content - { - InvalidateCache(); - //Update(); - } - } + + private readonly LimitedQueue _offscreenCacheRenderingQueue = new(1); + + public static readonly BindableProperty UseCacheProperty = BindableProperty.Create(nameof(UseCache), + typeof(SkiaCacheType), + typeof(SkiaControl), + SkiaCacheType.None, + propertyChanged: NeedDraw); + + /// + /// Never reuse the rendering result. Actually true for ScrollLooped SkiaLayout viewport container to redraw its content several times for creating a looped aspect. + /// + public SkiaCacheType UseCache + { + get { return (SkiaCacheType)GetValue(UseCacheProperty); } + set { SetValue(UseCacheProperty, value); } + } + + public static readonly BindableProperty AllowCachingProperty = BindableProperty.Create(nameof(AllowCaching), + typeof(bool), + typeof(SkiaControl), + true, + propertyChanged: NeedDraw); + + /// + /// Might want to set this to False for certain cases.. + /// + public bool AllowCaching + { + get { return (bool)GetValue(AllowCachingProperty); } + set { SetValue(AllowCachingProperty, value); } + } + + /// + /// Used by the UseCacheDoubleBuffering process. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public CachedObject RenderObjectPrevious + { + get + { + return _renderObjectPrevious; + } + set + { + RenderObjectNeedsUpdate = false; + if (_renderObjectPrevious != value) + { + var kill = _renderObjectPrevious; + _renderObjectPrevious = value; + if (kill != null + && UsingCacheType != SkiaCacheType.Image + && UsingCacheType == SkiaCacheType.ImageComposite) + DisposeObject(kill); + } + } + } + CachedObject _renderObjectPrevious; + + + /// + /// The cached representation of the control. Will be used on redraws without calling Paint etc, until the control is requested to be updated. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public CachedObject RenderObject + { + get + { + return _renderObject; + } + set + { + RenderObjectNeedsUpdate = false; + if (_renderObject != value) + { + //lock both RenderObjectPrevious and RenderObject + lock (LockDraw) + { + if (_renderObject != null) //if we already have something in actual cache then + { + if (UsesCacheDoubleBuffering + //|| UsingCacheType == SkiaCacheType.Image //to just reuse same surface + || UsingCacheType == SkiaCacheType.ImageComposite) + { + RenderObjectPrevious = _renderObject; //send it to back for special cases + } + else + { + DisposeObject(_renderObject); + } + } + _renderObject = value; + OnPropertyChanged(); + + if (value != null) + CreatedCache?.Invoke(this, value); + + Monitor.PulseAll(LockDraw); + } + + } + } + } + CachedObject _renderObject; + + /// + /// Indended to prohibit background rendering, useful for streaming controls like camera, gif etc. SkiaBackdrop has it set to True as well. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual bool CanUseCacheDoubleBuffering => true; + + /// + /// Read-only computed flag for internal use. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool UsesCacheDoubleBuffering + { + get + { + return CanUseCacheDoubleBuffering + && Super.Multithreaded + //&& Parent is SkiaControl + || UsingCacheType == SkiaCacheType.ImageDoubleBuffered; + } + } + + public event EventHandler CreatedCache; + + public void DestroyRenderingObject() + { + RenderObject = null; + } + + protected virtual bool CheckCachedObjectValid(CachedObject cache, SKRect recordingArea, SkiaDrawingContext context) + { + if (cache != null) + { + if (cache.Bounds.Size != recordingArea.Size) + return false; + + //check hardware context maybe changed + if (UsingCacheType == SkiaCacheType.GPU && cache.Surface != null && + cache.Surface.Context != null && + context.Superview?.CanvasView is SkiaViewAccelerated hardware) + { + //hardware context might change if we returned from background.. + if (hardware.GRContext == null || cache.Surface.Context == null + || (int)hardware.GRContext.Handle != (int)cache.Surface.Context.Handle) + { + return false; + } + } + + return true; + } + return false; + } + + public virtual SkiaCacheType UsingCacheType + { + get + { + if (!AllowCaching) + { + return SkiaCacheType.None; + } + + if (CanUseCacheDoubleBuffering && Super.Multithreaded) + { + //if (Parent is SkiaControl) + { + if (UseCache == SkiaCacheType.None) + return SkiaCacheType.OperationsFull; + } + + if (UseCache == SkiaCacheType.ImageDoubleBuffered || UseCache == SkiaCacheType.GPU) + return SkiaCacheType.Image; + + if (UseCache == SkiaCacheType.ImageComposite) + return SkiaCacheType.Operations; + } + + if (UseCache == SkiaCacheType.GPU && !Super.GpuCacheEnabled) + return SkiaCacheType.Image; + + if (EffectPostRenderer != null && (UseCache == SkiaCacheType.None || UseCache == SkiaCacheType.Operations)) + return SkiaCacheType.Image; + + if (UseCache == SkiaCacheType.None && CanUseCacheDoubleBuffering && Super.Multithreaded && Parent is SkiaControl) + return SkiaCacheType.Operations; + + return UseCache; + } + } + + protected virtual CachedObject CreateRenderingObject( + SkiaDrawingContext context, + SKRect drawingArea, + SKRect recordingArea, + CachedObject reuseSurfaceFrom, + Action action) + { + if (recordingArea.Height == 0 || recordingArea.Width == 0 || IsDisposed || IsDisposing) + { + return null; + } + + CachedObject renderObject = null; + + try + { + var recordArea = GetCacheArea(recordingArea); + + //todo + //if (UsingCacheType == SkiaCacheType.OperationsFull) + //{ + // recordArea = destination; + //} + + NeedUpdate = false; //if some child changes this while rendering to cache we will erase resulting RenderObject + + var usingCacheType = UsingCacheType; + + GRContext grContext = null; + + if (IsCacheImage) + { + var width = (int)recordArea.Width; + var height = (int)recordArea.Height; + + bool needCreateSurface = !CheckCachedObjectValid(reuseSurfaceFrom, recordingArea, context) || usingCacheType == SkiaCacheType.GPU; //never reuse GPU surfaces + + SKSurface surface = null; + + if (!needCreateSurface) + { + //reusing existing surface + surface = reuseSurfaceFrom.Surface; + if (surface == null) + { + return null; //would be unexpected + } + if (usingCacheType != SkiaCacheType.ImageComposite) + surface.Canvas.Clear(); + } + else + { + needCreateSurface = true; + var kill = surface; + surface = null; + var cacheSurfaceInfo = new SKImageInfo(width, height); + + if (usingCacheType == SkiaCacheType.GPU) + { + if (context.Superview != null && context.Superview?.CanvasView is SkiaViewAccelerated accelerated + && accelerated.GRContext != null) + { + grContext = accelerated.GRContext; + //hardware accelerated + surface = SKSurface.Create(accelerated.GRContext, + false, + cacheSurfaceInfo); + } + } + if (surface == null) //fallback if gpu failed + { + //non-gpu + surface = SKSurface.Create(cacheSurfaceInfo); + } + + if (kill != surface) + DisposeObject(kill); + + // if (usingCacheType == SkiaCacheType.GPU) + // surface.Canvas.Clear(SKColors.Red); + } + + if (surface == null) + { + return null; //would be totally unexpected + } + + var recordingContext = context.CreateForRecordingImage(surface, recordArea.Size); + + recordingContext.IsRecycled = !needCreateSurface; + + // Translate the canvas to start drawing at (0,0) + + recordingContext.Canvas.Translate(-recordArea.Left, -recordArea.Top); + + // Perform the drawing action + action(recordingContext); + + surface.Canvas.Flush(); + //grContext?.Flush(); + + recordingContext.Canvas.Translate(recordArea.Left, recordArea.Top); + + renderObject = new(usingCacheType, surface, recordArea, recordArea) + { + SurfaceIsRecycled = recordingContext.IsRecycled + }; + + } + else + if (IsCacheOperations) + { + var cacheRecordingArea = GetCacheRecordingArea(recordingArea); + + using (var recorder = new SKPictureRecorder()) + { + var recordingContext = context.CreateForRecordingOperations(recorder, cacheRecordingArea); + + action(recordingContext); + + // End the recording and obtain the SKPicture + var skPicture = recorder.EndRecording(); + + renderObject = new(UsingCacheType, skPicture, drawingArea, cacheRecordingArea); + } + } + + //else we landed here with no cache type at all.. + } + catch (Exception e) + { + Super.Log(e); + } + + return renderObject; + } + + public Action DelegateDrawCache { get; set; } + + protected virtual void DrawRenderObjectInternal( + CachedObject cache, + SkiaDrawingContext ctx, + SKRect destination) + { + if (DelegateDrawCache != null) + { + DelegateDrawCache(cache, ctx, destination); + } + else + { + DrawRenderObject(cache, ctx, destination); + } + } + + public bool IsCacheImage + { + get + { + var cache = UsingCacheType; + return cache == SkiaCacheType.Image + || cache == SkiaCacheType.GPU + || cache == SkiaCacheType.ImageComposite + || cache == SkiaCacheType.ImageDoubleBuffered; + } + } + + public bool IsCacheOperations + { + get + { + var cache = UsingCacheType; + return cache == SkiaCacheType.Operations + || cache == SkiaCacheType.OperationsFull; + } + } + + + protected virtual bool UseRenderingObject(SkiaDrawingContext context, SKRect drawingArea, SKRect recordArea, float scale) + { + + //lock (LockDraw) + { + var cache = RenderObject; + var cacheOffscreen = RenderObjectPrevious; + + if (RenderObjectPrevious != null && RenderObjectPreviousNeedsUpdate) + { + var kill = RenderObjectPrevious; + RenderObjectPrevious = null; + RenderObjectPreviousNeedsUpdate = false; + if (kill != null) + { + Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => + { + kill.Dispose(); + }); + } + } + + if (cache != null) + { + if (!UsesCacheDoubleBuffering && !CheckCachedObjectValid(cache, recordArea, context)) + { + return false; + } + + //draw existing front cache + lock (LockDraw) + { + DrawRenderObjectInternal(cache, context, drawingArea); + Monitor.PulseAll(LockDraw); + } + + if (!UsesCacheDoubleBuffering || !NeedUpdateFrontCache) + return true; + } + + if (UsesCacheDoubleBuffering) + { + lock (LockDraw) + { + if (cache == null && cacheOffscreen != null) + { + DrawRenderObjectInternal(cacheOffscreen, context, drawingArea); + } + Monitor.PulseAll(LockDraw); + } + + NeedUpdateFrontCache = false; + + var args = CreatePaintArguments(); + PushToOffscreenRendering(() => + { + //will be executed on background thread in parallel + var oldObject = RenderObjectPreparing; + RenderObjectPreparing = CreateRenderingObject(context, drawingArea, recordArea, oldObject, (ctx) => + { + PaintWithEffects(ctx, drawingArea, scale, args); + }); + RenderObject = RenderObjectPreparing; + _renderObjectPreparing = null; + + if (Parent != null && !Parent.UpdateLocked) + { + Parent?.UpdateByChild(this); //repaint us + } + }); + + return !NeedUpdateFrontCache; + } + + return false; + } + + } + + public Action GetOffscreenRenderingAction() + { + var action = _offscreenCacheRenderingQueue.Pop(); + return action; + } + + public void PushToOffscreenRendering(Action action) + { + _offscreenCacheRenderingQueue.Push(action); + Superview?.PushToOffscreenRendering(this); + } + + + + + /// + /// Used by the UseCacheDoubleBuffering process. This is the new cache beign created in background. It will be copied to RenderObject when ready. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public CachedObject RenderObjectPreparing + { + get + { + return _renderObjectPreparing; + } + set + { + RenderObjectNeedsUpdate = false; + if (_renderObjectPreparing != value) + { + _renderObjectPreparing = value; + } + } + } + CachedObject _renderObjectPreparing; + + + /// + /// Used by ImageDoubleBuffering cache + /// + protected bool NeedUpdateFrontCache + { + get => _needUpdateFrontCache; + set => _needUpdateFrontCache = value; + } + + + + private bool _RenderObjectPreviousNeedsUpdate; + [EditorBrowsable(EditorBrowsableState.Never)] + public bool RenderObjectPreviousNeedsUpdate + { + get + { + return _RenderObjectPreviousNeedsUpdate; + } + set + { + if (_RenderObjectPreviousNeedsUpdate != value) + { + _RenderObjectPreviousNeedsUpdate = value; + OnPropertyChanged(); + } + } + } + + /// + /// Should delete RenderObject when starting new frame rendering + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool RenderObjectNeedsUpdate + { + get + { + return _renderObjectNeedsUpdate; + } + + set + { + if (_renderObjectNeedsUpdate != value) + { + _renderObjectNeedsUpdate = value; + } + } + } + bool _renderObjectNeedsUpdate = true; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void InvalidateCache() + { + RenderObjectNeedsUpdate = true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void InvalidateCacheWithPrevious() + { + RenderObjectNeedsUpdate = true; + if (UsingCacheType == SkiaCacheType.ImageComposite) + { + RenderObjectPreviousNeedsUpdate = true; + } + } + + + /// + /// Used for the Operations cache type to record inside the changed area, if your control is not inside the DrawingRect due to transforms/translations. This is NOT changing the rendering object + /// + protected virtual SKRect GetCacheRecordingArea(SKRect drawingRect) + { + return drawingRect; + } + + /// + /// Normally cache is recorded inside DrawingRect, but you might want to exapnd this to include shadows around, for example. + /// + protected virtual SKRect GetCacheArea(SKRect drawingRect) + { + var pixels = ExpandCacheRecordingArea * RenderingScale; + return new SKRect( + (float)Math.Round(drawingRect.Left - pixels), + (float)Math.Round(drawingRect.Top - pixels), + (float)Math.Round(drawingRect.Right + pixels), + (float)Math.Round(drawingRect.Bottom + pixels)); + } + + /// + /// Returns true if had drawn. + /// + /// + /// + /// + /// + /// + /// + protected virtual bool DrawUsingRenderObject(SkiaDrawingContext context, + float widthRequest, float heightRequest, + SKRect destination, + float scale) + { + Arrange(destination, widthRequest, heightRequest, scale); + + bool willDraw = !CheckIsGhost(); + if (willDraw) + { + if (UsingCacheType != SkiaCacheType.None) + { + var drawingArea = DrawingRect; + var recordArea = drawingArea; + if (UsingCacheType == SkiaCacheType.OperationsFull) + { + //recordArea = destination; + recordArea = context.Canvas.LocalClipBounds; + } + + //paint from cache + if (!UseRenderingObject(context, drawingArea, recordArea, scale)) + { + //record to cache and paint + if (UsesCacheDoubleBuffering) + { + var args = CreatePaintArguments(); + PushToOffscreenRendering(() => + { + //will be executed on background thread in parallel + var oldObject = RenderObjectPreparing; + + RenderObjectPreparing = CreateRenderingObject(context, drawingArea, recordArea, oldObject, (ctx) => + { + PaintWithEffects(ctx, drawingArea, scale, args); + }); + RenderObject = RenderObjectPreparing; + _renderObjectPreparing = null; + + if (Parent != null && !Parent.UpdateLocked) + { + Parent?.UpdateByChild(this); //repaint us + } + }); + } + else + CreateRenderingObjectAndPaint(context, drawingArea, recordArea, (ctx) => + { + PaintWithEffects(ctx, DrawingRect, scale, CreatePaintArguments()); + }); + } + } + else + { + DrawWithClipAndTransforms(context, DrawingRect, DrawingRect, true, true, (ctx) => + { + PaintWithEffects(ctx, DrawingRect, scale, CreatePaintArguments()); + }); + } + } + + FinalizeDrawingWithRenderObject(context, scale); //NeedUpdate will go false + + return willDraw; + } + + /// + /// This is NOT calling FinalizeDraw()! + /// parameter 'area' Usually is equal to DrawingRect + /// + /// + /// + /// + protected void CreateRenderingObjectAndPaint( + SkiaDrawingContext context, + SKRect drawingArea, + SKRect recordingArea, + Action action) + { + + if (recordingArea.Width <= 0 || recordingArea.Height <= 0 || float.IsInfinity(recordingArea.Height) || float.IsInfinity(recordingArea.Width) || IsDisposed || IsDisposing) + { + return; + } + + if (RenderObject != null && !UsesCacheDoubleBuffering) + { + //we might come here with an existing RenderingObject if UseRenderingObject returned False + RenderObject = null; + } + + RenderObjectNeedsUpdate = false; + + var usingCacheType = UsingCacheType; + + CachedObject oldObject = null; //reusing this + if (UsesCacheDoubleBuffering) + { + oldObject = RenderObject; + } + else + if (usingCacheType == SkiaCacheType.Image + || usingCacheType == SkiaCacheType.ImageComposite) + { + oldObject = RenderObjectPrevious; + } + + var created = CreateRenderingObject(context, drawingArea, recordingArea, oldObject, action); + + if (created == null) + { + return; + } + + if (oldObject != null) + { + if (created.SurfaceIsRecycled) + { + oldObject.Surface = null; + } + if (!UsesCacheDoubleBuffering && usingCacheType != SkiaCacheType.ImageComposite) + { + Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => + { + oldObject.Dispose(); + }); + } + } + + var notValid = RenderObjectNeedsUpdate; + RenderObject = created; + + + if (RenderObject != null) + { + DrawRenderObjectInternal(RenderObject, context, RenderObject.RecordingArea); + } + else + { + notValid = true; + } + + + if (NeedUpdate || notValid) //someone changed us while rendering inner content + { + InvalidateCache(); + //Update(); + } + } } diff --git a/src/Engine/Draw/Base/SkiaControl.Maui.cs b/src/Engine/Draw/Base/SkiaControl.Maui.cs index 3be26b39..3b5b99a5 100644 --- a/src/Engine/Draw/Base/SkiaControl.Maui.cs +++ b/src/Engine/Draw/Base/SkiaControl.Maui.cs @@ -9,299 +9,300 @@ Normally other partial code definitions should be framework independent. namespace DrawnUi.Maui.Draw { - public partial class SkiaControl : VisualElement, - IVisualTreeElement, - IReloadHandler, - IHotReloadableView - { - public static Color TransparentColor = Colors.Transparent; - public static Color WhiteColor = Colors.White; - public static Color BlackColor = Colors.Black; - public static Color RedColor = Colors.Red; - - public static readonly BindableProperty ClearColorProperty = BindableProperty.Create(nameof(ClearColor), typeof(Color), typeof(SkiaControl), - Colors.Transparent, - propertyChanged: NeedDraw); - public Color ClearColor - { - get { return (Color)GetValue(ClearColorProperty); } - set { SetValue(ClearColorProperty, value); } - } - - protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) - { - base.OnPropertyChanged(propertyName); - - //if (!isApplyingStyle && !string.IsNullOrEmpty(propertyName)) - //{ - // ExplicitPropertiesSet[propertyName] = true; - //} - - #region intercept properties coming from VisualElement.. - - //some VisualElement props will not call this method so we would override them as new - - if (propertyName.IsEither(nameof(ZIndex))) - { - Parent?.InvalidateViewsList(); - Repaint(); - } - else - if (propertyName.IsEither( - nameof(Opacity), - nameof(TranslationX), nameof(TranslationY), - nameof(Rotation), - nameof(ScaleX), nameof(ScaleY) - )) - { - Repaint(); - } - else - if (propertyName.IsEither(nameof(BackgroundColor), - nameof(IsClippedToBounds) - )) - { - Update(); - } - else - if (propertyName.IsEither( - nameof(Padding), - nameof(HorizontalOptions), nameof(VerticalOptions), - nameof(HeightRequest), nameof(WidthRequest), - nameof(MaximumWidthRequest), nameof(MinimumWidthRequest), - nameof(MaximumHeightRequest), nameof(MinimumHeightRequest) - )) - { - InvalidateMeasure(); - } - else - if (propertyName.IsEither(nameof(IsVisible))) - { - OnVisibilityChanged(IsVisible); - - InvalidateMeasure(); - } - else - if (propertyName.IsEither( - nameof(AnchorX), nameof(AnchorY), - nameof(RotationX), nameof(RotationY))) - { - //todo add option not to throw?.. - throw new NotImplementedException("DrawnUi is not using this Maui VisualElement property."); - } - - #endregion - } - - - #region HotReload - - IView IReplaceableView.ReplacedView => - MauiHotReloadHelper.GetReplacedView(this) ?? this; - - IReloadHandler IHotReloadableView.ReloadHandler { get; set; } - - void IHotReloadableView.TransferState(IView newView) - { - //reload the the ViewModel - if (newView is SkiaControl v) - v.BindingContext = BindingContext; - } - - void IHotReloadableView.Reload() - { - InvalidateMeasure(); - } - - #endregion - - #region IVisualTreeElement - - public virtual IReadOnlyList GetVisualChildren() //working fine - { - return Views.Cast().ToList().AsReadOnly(); - - //return Views.Select(x => x as IVisualTreeElement).ToList().AsReadOnly();; - } - - public virtual IVisualTreeElement GetVisualParent() //working fine - { - return Parent as IVisualTreeElement; - } - - #endregion - - #region HOTRELOAD - - /// - /// HOTRELOAD IReloadHandler - /// - public virtual void Reload() - { - InvalidateMeasure(); - } - - public virtual void ReportHotreloadChildAdded(SkiaControl child) - { - if (child == null) - return; - - //this.OnChildAdded(child); - - var children = GetVisualChildren(); - var index = children.FindIndex(child); - - if (index >= 0) - VisualDiagnostics.OnChildAdded(this, child, index); - } - - public virtual void ReportHotreloadChildRemoved(SkiaControl control) - { - if (control == null) - return; - - - var children = GetVisualChildren(); - var index = children.FindIndex(control); - - if (index >= 0) - VisualDiagnostics.OnChildRemoved(this, control, index); - // this.OnChildRemoved(control, index); - } - - - - #endregion - - - public virtual void AddSubView(SkiaControl control) - { - if (control == null) - return; - control.SetParent(this); - - OnChildAdded(control); - - if (Debugger.IsAttached) - Superview?.PostponeExecutionAfterDraw(() => - { - ReportHotreloadChildAdded(control); - }); - } - - public virtual void RemoveSubView(SkiaControl control) - { - if (control == null) - return; - - if (Debugger.IsAttached) - Superview?.PostponeExecutionAfterDraw(() => - { - ReportHotreloadChildRemoved(control); - }); - - try - { - control.SetParent(null); - OnChildRemoved(control); - } - catch (Exception e) - { - Super.Log(e); - } - } - - protected virtual void OnLayoutChanged() - { - LayoutReady = this.Height > 0 && this.Width > 0; - if (LayoutReady) - { - if (!CompareSize(DrawingRect.Size, _lastSize, 1)) - { - _lastSize = DrawingRect.Size; - Frame = new Rect(DrawingRect.Left, DrawingRect.Top, DrawingRect.Width, DrawingRect.Height); - } - } - } - - /// - /// Creates Shader for gradient and sets it to passed SKPaint along with BlendMode - /// - /// - /// - /// - public bool SetupGradient(SKPaint paint, SkiaGradient gradient, SKRect destination) - { - - - if (gradient != null && paint != null) - { - if (paint.Color.Alpha == 0) - { - paint.Color = SKColor.FromHsl(0, 0, 0); - } - - paint.Color = SKColors.White; - paint.BlendMode = gradient.BlendMode; - - var kill = paint.Shader; - paint.Shader = CreateGradient(destination, gradient); - kill?.Dispose(); - - return true; - - //if (LastGradient == null || LastGradient.Gradient != gradient || - // LastGradient.Destination != destination) - //{ - // var kill = LastGradient; - // LastGradient = new() - // { - // Shader = CreateGradient(destination, gradient), - // Destination = destination, - // Gradient = gradient - // }; - // kill?.Dispose(); - //} - - //var old = paint.Shader; - //paint.Shader = LastGradient.Shader; - //if (old != paint.Shader) - //{ - // old?.Dispose(); - //} - - //return true; - } - - return false; - } - - public static SKImageFilter CreateShadow(SkiaShadow shadow, float scale) - { - var colorShadow = shadow.Color; - if (colorShadow.Alpha == 1.0) - { - colorShadow = shadow.Color.WithAlpha((float)shadow.Opacity); - } - if (shadow.ShadowOnly) - { - return SKImageFilter.CreateDropShadowOnly( - (float)(shadow.X * scale), (float)(shadow.Y * scale), - (float)(shadow.Blur * scale), (float)(shadow.Blur * scale), - colorShadow.ToSKColor()); - } - else - { - return SKImageFilter.CreateDropShadow( - (float)(shadow.X * scale), (float)(shadow.Y * scale), - (float)(shadow.Blur * scale), (float)(shadow.Blur * scale), - colorShadow.ToSKColor()); - } - } - - public static float GetDensity() - { - return (float)Super.Screen.Density; - } - } + public partial class SkiaControl : VisualElement, + IVisualTreeElement, + IReloadHandler, + IHotReloadableView + { + public static Color TransparentColor = Colors.Transparent; + public static Color WhiteColor = Colors.White; + public static Color BlackColor = Colors.Black; + public static Color RedColor = Colors.Red; + + public static readonly BindableProperty ClearColorProperty = BindableProperty.Create(nameof(ClearColor), typeof(Color), typeof(SkiaControl), + Colors.Transparent, + propertyChanged: NeedDraw); + public Color ClearColor + { + get { return (Color)GetValue(ClearColorProperty); } + set { SetValue(ClearColorProperty, value); } + } + + protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + base.OnPropertyChanged(propertyName); + + //if (!isApplyingStyle && !string.IsNullOrEmpty(propertyName)) + //{ + // ExplicitPropertiesSet[propertyName] = true; + //} + + #region intercept properties coming from VisualElement.. + + //some VisualElement props will not call this method so we would override them as new + + if (propertyName.IsEither(nameof(ZIndex))) + { + Parent?.InvalidateViewsList(); + Repaint(); + } + else + if (propertyName.IsEither( + nameof(Opacity), + nameof(TranslationX), nameof(TranslationY), + nameof(Rotation), + nameof(ScaleX), nameof(ScaleY) + )) + { + Repaint(); + } + else + if (propertyName.IsEither(nameof(BackgroundColor), + nameof(IsClippedToBounds) + )) + { + Update(); + } + else + if (propertyName.IsEither( + nameof(Padding), + nameof(HorizontalOptions), nameof(VerticalOptions), + nameof(HeightRequest), nameof(WidthRequest), + nameof(MaximumWidthRequest), nameof(MinimumWidthRequest), + nameof(MaximumHeightRequest), nameof(MinimumHeightRequest) + )) + { + InvalidateMeasure(); + } + else + if (propertyName.IsEither(nameof(IsVisible))) + { + OnVisibilityChanged(IsVisible); + + InvalidateMeasure(); + } + else + if (propertyName.IsEither( + nameof(AnchorX), nameof(AnchorY), + nameof(RotationX), nameof(RotationY))) + { + //todo add option not to throw?.. + throw new NotImplementedException("DrawnUi is not using this Maui VisualElement property."); + } + + #endregion + } + + + #region HotReload + + IView IReplaceableView.ReplacedView => + MauiHotReloadHelper.GetReplacedView(this) ?? this; + + IReloadHandler IHotReloadableView.ReloadHandler { get; set; } + + void IHotReloadableView.TransferState(IView newView) + { + //reload the the ViewModel + if (newView is SkiaControl v) + v.BindingContext = BindingContext; + } + + void IHotReloadableView.Reload() + { + InvalidateMeasure(); + } + + #endregion + + #region IVisualTreeElement + + public virtual IReadOnlyList GetVisualChildren() //working fine + { + return Views.Cast().ToList().AsReadOnly(); + + //return Views.Select(x => x as IVisualTreeElement).ToList().AsReadOnly();; + } + + public virtual IVisualTreeElement GetVisualParent() //working fine + { + return Parent as IVisualTreeElement; + } + + #endregion + + #region HOTRELOAD + + /// + /// HOTRELOAD IReloadHandler + /// + public virtual void Reload() + { + InvalidateMeasure(); + } + + public virtual void ReportHotreloadChildAdded(SkiaControl child) + { + if (child == null) + return; + + //this.OnChildAdded(child); + + var children = GetVisualChildren(); + var index = children.FindIndex(child); + + if (index >= 0) + VisualDiagnostics.OnChildAdded(this, child, index); + } + + public virtual void ReportHotreloadChildRemoved(SkiaControl control) + { + if (control == null) + return; + + + var children = GetVisualChildren(); + var index = children.FindIndex(control); + + if (index >= 0) + VisualDiagnostics.OnChildRemoved(this, control, index); + // this.OnChildRemoved(control, index); + } + + + + #endregion + + + public virtual void AddSubView(SkiaControl control) + { + if (control == null) + return; + control.SetParent(this); + + OnChildAdded(control); + + if (Debugger.IsAttached) + Superview?.PostponeExecutionAfterDraw(() => + { + ReportHotreloadChildAdded(control); + }); + } + + public virtual void RemoveSubView(SkiaControl control) + { + if (control == null) + return; + + if (Debugger.IsAttached) + Superview?.PostponeExecutionAfterDraw(() => + { + ReportHotreloadChildRemoved(control); + }); + + try + { + control.SetParent(null); + OnChildRemoved(control); + } + catch (Exception e) + { + Super.Log(e); + } + } + + protected virtual void OnLayoutChanged() + { + var ready = this.Height > 0 && this.Width > 0; + if (ready) + { + if (!CompareSize(DrawingRect.Size, _lastSize, 1)) + { + _lastSize = DrawingRect.Size; + Frame = new Rect(DrawingRect.Left, DrawingRect.Top, DrawingRect.Width, DrawingRect.Height); + } + } + LayoutReady = ready; + } + + /// + /// Creates Shader for gradient and sets it to passed SKPaint along with BlendMode + /// + /// + /// + /// + public bool SetupGradient(SKPaint paint, SkiaGradient gradient, SKRect destination) + { + + + if (gradient != null && paint != null) + { + if (paint.Color.Alpha == 0) + { + paint.Color = SKColor.FromHsl(0, 0, 0); + } + + paint.Color = SKColors.White; + paint.BlendMode = gradient.BlendMode; + + var kill = paint.Shader; + paint.Shader = CreateGradient(destination, gradient); + kill?.Dispose(); + + return true; + + //if (LastGradient == null || LastGradient.Gradient != gradient || + // LastGradient.Destination != destination) + //{ + // var kill = LastGradient; + // LastGradient = new() + // { + // Shader = CreateGradient(destination, gradient), + // Destination = destination, + // Gradient = gradient + // }; + // kill?.Dispose(); + //} + + //var old = paint.Shader; + //paint.Shader = LastGradient.Shader; + //if (old != paint.Shader) + //{ + // old?.Dispose(); + //} + + //return true; + } + + return false; + } + + public static SKImageFilter CreateShadow(SkiaShadow shadow, float scale) + { + var colorShadow = shadow.Color; + if (colorShadow.Alpha == 1.0) + { + colorShadow = shadow.Color.WithAlpha((float)shadow.Opacity); + } + if (shadow.ShadowOnly) + { + return SKImageFilter.CreateDropShadowOnly( + (float)(shadow.X * scale), (float)(shadow.Y * scale), + (float)(shadow.Blur * scale), (float)(shadow.Blur * scale), + colorShadow.ToSKColor()); + } + else + { + return SKImageFilter.CreateDropShadow( + (float)(shadow.X * scale), (float)(shadow.Y * scale), + (float)(shadow.Blur * scale), (float)(shadow.Blur * scale), + colorShadow.ToSKColor()); + } + } + + public static float GetDensity() + { + return (float)Super.Screen.Density; + } + } } diff --git a/src/Engine/Draw/Base/SkiaControl.Shared.cs b/src/Engine/Draw/Base/SkiaControl.Shared.cs index bb08c4c6..f8c145a5 100644 --- a/src/Engine/Draw/Base/SkiaControl.Shared.cs +++ b/src/Engine/Draw/Base/SkiaControl.Shared.cs @@ -27,6577 +27,6587 @@ namespace DrawnUi.Maui.Draw { - [DebuggerDisplay("{DebugString}")] - [ContentProperty("Children")] - public partial class SkiaControl : - IHasAfterEffects, - ISkiaControl - { - public SkiaControl() - { - Init(); - } - - public virtual bool IsVisibleInViewTree() - { - var isVisible = IsVisible && !IsDisposed; - - var parent = this.Parent as SkiaControl; - - if (parent == null) - return isVisible; - - if (isVisible) - return parent.IsVisibleInViewTree(); - - return false; - } - - /// - /// Absolute position in points - /// - /// - public virtual SKPoint GetPositionOnCanvasInPoints(bool useTranslation = true) - { - var position = GetPositionOnCanvas(useTranslation); - - return new(position.X / RenderingScale, position.Y / RenderingScale); - } - - /// - /// Absolute position in points - /// - /// - public virtual SKPoint GetFuturePositionOnCanvasInPoints(bool useTranslation = true) - { - var position = GetFuturePositionOnCanvas(useTranslation); - - return new(position.X / RenderingScale, position.Y / RenderingScale); - } - - /// - /// Absolute position in pixels afetr drawn. - /// - /// - public virtual SKPoint GetPositionOnCanvas(bool useTranslation = true) - { - //Debug.WriteLine($"GetPositionOnCanvas ------------------------START at {LastDrawnAt}"); - - //ignore cache for this specific control only - var position = BuildDrawnOffsetRecursive(LastDrawnAt.Location, this, true, useTranslation); - - //Debug.WriteLine("GetPositionOnCanvas ------------------------END"); - return new(position.X, position.Y); - } - - /// - /// Absolute position in pixels before drawn. - /// - /// - public virtual SKPoint GetFuturePositionOnCanvas(bool useTranslation = true) - { - var position = BuildDrawnOffsetRecursive(DrawingRect.Location, this, true, useTranslation); - return new(position.X, position.Y); - } - - - /// - /// Find drawing position for control accounting for all caches up the rendering tree. - /// - /// - public virtual SKPoint GetSelfDrawingPosition() - { - var position = BuildSelfDrawingPosition(LastDrawnAt.Location, this, true); - - return new(position.X, position.Y); - } - - - - - - public SKPoint BuildSelfDrawingPosition(SKPoint offset, SkiaControl control, bool isChild) - { - if (control == null) - { - return offset; - } - - var drawingOffset = SKPoint.Empty; - if (!isChild) - { - drawingOffset = control.GetPositionOffsetInPixels(true); - drawingOffset.Offset(offset); - } - else - { - drawingOffset = offset; - } - - var parent = control.Parent as SkiaControl; - if (parent == null) - { - return drawingOffset; - } - - return BuildSelfDrawingPosition(drawingOffset, parent, false); - } - - - public SKPoint BuildDrawingOffsetRecursive(SKPoint offset, SkiaControl control, bool ignoreCache, bool useTranslation = true) - { - if (control == null) - { - return offset; - } - - var drawingOffset = control.GetFuturePositionOffsetInPixels(false, ignoreCache); - drawingOffset.Offset(offset); - var parent = control.Parent as SkiaControl; - if (parent == null) - { - drawingOffset.Offset(control.DrawingRect.Location); - return drawingOffset; - } - return BuildDrawingOffsetRecursive(drawingOffset, parent, false, useTranslation); - } - - public SKPoint BuildDrawnOffsetRecursive(SKPoint offset, SkiaControl control, bool ignoreCache, bool useTranslation = true) - { - if (control == null) - { - return offset; - } - - var drawingOffset = control.GetPositionOffsetInPixels(false, ignoreCache); - drawingOffset.Offset(offset); - var parent = control.Parent as SkiaControl; - if (parent == null) - { - drawingOffset.Offset(control.LastDrawnAt.Location); - return drawingOffset; - } - return BuildDrawnOffsetRecursive(drawingOffset, parent, false, useTranslation); - } - - - public virtual string DebugString - { - get - { - return $"{GetType().Name} Tag {Tag}, IsVisible {IsVisible}, Children {Views.Count}, {Width:0.0}x{Height:0.0}dp, DrawingRect: {DrawingRect}"; - } - } - - - - public virtual bool CanDraw - { - get - { - if (!IsVisible || IsDisposed || IsDisposing || SkipRendering) - return false; - - if (Superview != null && !Superview.CanDraw) - { - return false; - } - - return true; - } - } - - - /// - /// Can be set but custom controls while optimizing rendering etc. Will affect CanDraw. - /// - public bool SkipRendering { get; set; } - - protected bool DefaultChildrenCreated { get; set; } - - protected virtual void CreateDefaultContent() - { - - } - /// - /// To create custom content in code-behind. Will be called from OnLayoutChanged if Views.Count == 0. - /// - public Func> CreateChildren - { - get => _createChildren; - set - { - _createChildren = value; - if (value != null) - { - DefaultChildrenCreated = false; - CreateChildrenFromCode(); - } - } - } - - /// - /// Executed when CreateChildren is set - /// - /// - protected virtual void CreateChildrenFromCode() - { - if (this.Views.Count == 0 && !DefaultChildrenCreated) - { - DefaultChildrenCreated = true; - if (CreateChildren != null) - { - try - { - var children = CreateChildren(); - foreach (var skiaControl in children) - { - AddSubView(skiaControl); - } - } - catch (Exception e) - { - Super.Log(e); - } - } - - } - } - - - - /// - /// This actually used by SkiaMauiElement but could be used by other controls. Also might be useful for debugging purposes. - /// - /// - public VisualTreeChain GenerateParentChain() - { - var currentParent = this.Parent as SkiaControl; - - var chain = new VisualTreeChain(this); - - var parents = new List(); - - // Traverse up the parent hierarchy - while (currentParent != null) - { - // Add the current parent to the chain - parents.Add(currentParent); - // Move to the next parent - currentParent = currentParent.Parent as SkiaControl; - } - - parents.Reverse(); - - foreach (var parent in parents) - { - chain.AddNode(parent); - } - - return chain; - } - - /// - /// //todo base. this is actually used by SkiaMauiElement only - /// - /// - public virtual void SetVisualTransform(VisualTransform transform) - { - - } - - /// - /// Apply all postponed invalidation other logic that was postponed until the first draw for optimization. Use this for special code-behind cases, like tests etc, if you cannot wait until the first Draw(). In this version this affects ItemsSource only. - /// - public void CommitInvalidations() - { - foreach (var invalidation in PostponedInvalidations) - { - invalidation.Value.Invoke(); - } - PostponedInvalidations.Clear(); - } - - public virtual void SuperViewChanged() - { - if (Superview != null) - { - CommitInvalidations(); - } - } - - /// - /// Used for optimization process, for example, to avid changing ItemSource several times before the first draw. - /// - /// - /// - public void PostponeInvalidation(string key, Action action) - { - if (Superview == null) - { - PostponedInvalidations[key] = action; - } - else - { - //action.Invoke(); - Superview.PostponeInvalidation(this, action); - } - } - - readonly Dictionary PostponedInvalidations = new(); - - /// - /// Returns rendering scale adapted for another output size, useful for offline rendering - /// - /// - /// - /// - public float GetRenderingScaleFor(float width, float height) - { - var aspectX = width / DrawingRect.Width; - var aspectY = height / DrawingRect.Height; - var scale = Math.Min(aspectX, aspectY) * RenderingScale; - return scale; - } - - public float GetRenderingScaleFor(float measure) - { - var aspectX = measure / DrawingRect.Width; - var scale = aspectX * RenderingScale; - return scale; - } - - /// - /// Creates a new animator, animates from 0 to 1 over a given time, and calls your callback with the current eased value - /// - /// - /// - /// - /// - /// - public Task AnimateAsync(Action callback, - Action callbaclOnCancel = null, - float ms = 250, Easing easing = null, CancellationTokenSource cancel = default) - { - if (easing == null) - { - easing = Easing.Linear; - } - - var animator = new SkiaValueAnimator(this); - - if (cancel == default) - cancel = new CancellationTokenSource(); - var tcs = new TaskCompletionSource(cancel.Token); - - // Update animator parameters - animator.mMinValue = 0; - animator.mMaxValue = 1; - animator.Speed = ms; - animator.Easing = easing; - - animator.OnStop = () => - { - if (!tcs.Task.IsCompleted) - tcs.SetResult(true); - DisposeObject(animator); - }; - animator.OnUpdated = (value) => - { - if (!cancel.IsCancellationRequested) - { - callback?.Invoke(value); - } - else - { - callback?.Invoke(value); - animator.Stop(); - DisposeObject(animator); - } - }; - - animator.Start(); - - return tcs.Task; - } - - - CancellationTokenSource _fadeCancelTokenSource; - /// - /// Fades the view from the current Opacity to end, animator is reused if already running - /// - /// - /// - /// - /// - /// - public Task FadeToAsync(double end, float ms = 250, Easing easing = null, CancellationTokenSource cancel = default) - { - // Cancel previous animation if it exists and is still running. - _fadeCancelTokenSource?.Cancel(); - _fadeCancelTokenSource = new CancellationTokenSource(); - - var startOpacity = this.Opacity; - return AnimateAsync( - (value) => - { - this.Opacity = startOpacity + (end - startOpacity) * value; - //Debug.WriteLine($"[ANIM] Opacity: {this.Opacity}"); - }, - () => - { - this.Opacity = end; - }, - ms, - easing, - _fadeCancelTokenSource); - } - - CancellationTokenSource _scaleCancelTokenSource; - /// - /// Scales the view from the current Scale to x,y, animator is reused if already running - /// - /// - /// - /// - /// - /// - /// - public Task ScaleToAsync(double x, double y, float length = 250, Easing easing = null, CancellationTokenSource cancel = default) - { - // Cancel previous animation if it exists and is still running. - _scaleCancelTokenSource?.Cancel(); - _scaleCancelTokenSource = new CancellationTokenSource(); - - var startScaleX = this.ScaleX; - var startScaleY = this.ScaleY; - - return AnimateAsync(value => - { - this.ScaleX = startScaleX + (x - startScaleX) * value; - this.ScaleY = startScaleY + (y - startScaleY) * value; - }, - () => - { - this.ScaleX = x; - this.ScaleY = y; - }, length, easing, _scaleCancelTokenSource); - } - - - CancellationTokenSource _translateCancelTokenSource; - /// - /// Translates the view from the current position to x,y, animator is reused if already running - /// - /// - /// - /// - /// - /// - /// - public Task TranslateToAsync(double x, double y, uint length = 250, Easing easing = null, CancellationTokenSource cancel = default) - { - // Cancel previous animation if it exists and is still running. - _translateCancelTokenSource?.Cancel(); - _translateCancelTokenSource = new CancellationTokenSource(); - - var startTranslationX = this.TranslationX; - var startTranslationY = this.TranslationY; - - return AnimateAsync(value => - { - this.TranslationX = (float)(startTranslationX + (x - startTranslationX) * value); - this.TranslationY = (float)(startTranslationY + (y - startTranslationY) * value); - }, - () => - { - this.TranslationX = x; - this.TranslationY = y; - }, - length, easing, _translateCancelTokenSource); - } - - CancellationTokenSource _rotateCancelTokenSource; - /// - /// Rotates the view from the current rotation to end, animator is reused if already running - /// - /// - /// - /// - /// - /// - public Task RotateToAsync(double end, uint length = 250, Easing easing = null, CancellationTokenSource cancel = default) - { - // Cancel previous animation if it exists and is still running. - _rotateCancelTokenSource?.Cancel(); - _rotateCancelTokenSource = new CancellationTokenSource(); - - var startRotation = this.Rotation; - - return AnimateAsync(value => - { - this.Rotation = (float)(startRotation + (end - startRotation) * value); - }, - () => - { - this.Rotation = end; - }, - length, easing, _rotateCancelTokenSource); - } - - - public virtual void OnPrintDebug() - { - - } - - public void PrintDebug(string indent = " ") - { - Super.Log($"{indent}└─ {GetType().Name} {Width:0.0}x{Height:0.0} pts ({MeasuredSize.Pixels.Width:0.0}x{MeasuredSize.Pixels.Height:0.0} px)"); - OnPrintDebug(); - foreach (var view in Views) - { - Trace.Write($"{indent} ├─ "); - view.PrintDebug(indent + " │ "); - } - } - - public static readonly BindableProperty DebugRenderingProperty = BindableProperty.Create(nameof(DebugRendering), - typeof(bool), - typeof(SkiaControl), - false, propertyChanged: NeedDraw); - public bool DebugRendering - { - get { return (bool)GetValue(DebugRenderingProperty); } - set { SetValue(DebugRenderingProperty, value); } - } - - float _debugRenderingCurrentOpacity; - - /// - /// When the animator is cancelled if applyEndValueOnStop is true then the end value will be sent to your callback - /// - /// - /// - /// - /// - /// - /// - /// - public Task AnimateRangeAsync(Action callback, double start, double end, double length = 250, Easing easing = null, - CancellationToken cancel = default, - bool applyEndValueOnStop = false) - { - RangeAnimator animator = null; - - var tcs = new TaskCompletionSource(cancel); - - tcs.Task.ContinueWith(task => - { - DisposeObject(animator); - }); - - animator = new RangeAnimator(this) - { - OnStop = () => - { - //if (animator.WasStarted && !cancel.IsCancellationRequested) - { - if (applyEndValueOnStop) - callback?.Invoke(end); - tcs.SetResult(true); - } - } - }; - animator.Start( - (value) => - { - if (!cancel.IsCancellationRequested) - { - callback?.Invoke(value); - } - else - { - animator.Stop(); - } - }, - start, end, (uint)length, easing); - - return tcs.Task; - } - - - - public SKPoint NotValidPoint() - { - return new SKPoint(float.NaN, float.NaN); - } - - public bool PointIsValid(SKPoint point) - { - return !float.IsNaN(point.X) && !float.IsNaN(point.Y); - } - - /// - /// Is set by InvalidateMeasure(); - /// - public SKSize SizeRequest { get; protected set; } - - /// - /// Apply margins to SizeRequest - /// - /// - /// - /// - public float AdaptWidthConstraintToRequest(float widthConstraint, Thickness constraints, double scale) - { - var widthPixels = (float)Math.Round(SizeRequest.Width * scale + constraints.HorizontalThickness); - - if (SizeRequest.Width >= 0) - widthConstraint = widthPixels; - - return widthConstraint; - } - - /// - /// Apply margins to SizeRequest - /// - /// - /// - /// - public float AdaptHeightContraintToRequest(float heightConstraint, Thickness constraints, double scale) - { - var thickness = constraints.VerticalThickness; - - var widthPixels = (float)Math.Round(SizeRequest.Height * scale + thickness); - - if (SizeRequest.Height >= 0) - heightConstraint = widthPixels; - - return heightConstraint; - } - - public virtual MeasuringConstraints GetMeasuringConstraints(MeasureRequest request) - { - var withLock = GetSizeRequest(request.WidthRequest, request.HeightRequest, true); - var margins = GetMarginsInPixels(request.Scale); - - var adaptedWidthConstraint = AdaptWidthConstraintToRequest(withLock.Width, margins, request.Scale); - var adaptedHeightConstraint = AdaptHeightContraintToRequest(withLock.Height, margins, request.Scale); - - var rectForChildrenPixels = GetMeasuringRectForChildren(adaptedWidthConstraint, adaptedHeightConstraint, request.Scale); - - return new MeasuringConstraints - { - Margins = margins, - TotalMargins = GetAllMarginsInPixels(request.Scale), - Request = new(adaptedWidthConstraint, adaptedHeightConstraint), - Content = rectForChildrenPixels - }; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float AdaptConstraintToContentRequest( - float constraintPixels, - double measuredDimension, - double sideConstraintsPixels, - bool autoSize, - double minRequest, double maxRequest, float scale, bool canExpand) - { - - var contentDimension = sideConstraintsPixels + measuredDimension; - - if (autoSize && measuredDimension >= 0 && (canExpand || measuredDimension < constraintPixels) - || float.IsInfinity(constraintPixels)) - { - constraintPixels = (float)contentDimension; - } - - if (minRequest >= 0) - { - var min = double.MinValue; - if (double.IsFinite(minRequest)) - { - min = Math.Round(minRequest * scale); - } - constraintPixels = (float)Math.Max(constraintPixels, min); - } - - if (maxRequest >= 0) - { - var max = double.MaxValue; - if (double.IsFinite(maxRequest)) - { - max = Math.Round(maxRequest * scale); - } - constraintPixels = (float)Math.Min(constraintPixels, max); - } - - return (float)Math.Round(constraintPixels); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float AdaptWidthConstraintToContentRequest(MeasuringConstraints constraints, float contentWidthPixels, bool canExpand) - { - var sideConstraintsPixels = NeedAutoWidth - ? constraints.TotalMargins.HorizontalThickness - : constraints.Margins.HorizontalThickness; - - return AdaptConstraintToContentRequest( - constraints.Request.Width, - contentWidthPixels, - sideConstraintsPixels, - NeedAutoWidth, - MinimumWidthRequest, - MaximumWidthRequest, - RenderingScale, canExpand); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float AdaptHeightConstraintToContentRequest(MeasuringConstraints constraints, - float contentHeightPixels, bool canExpand) - { - var sideConstraintsPixels = NeedAutoHeight - ? constraints.TotalMargins.VerticalThickness - : constraints.Margins.VerticalThickness; - - return AdaptConstraintToContentRequest( - constraints.Request.Height, - contentHeightPixels, - sideConstraintsPixels, - NeedAutoHeight, - MinimumHeightRequest, - MaximumHeightRequest, - RenderingScale, canExpand); - } - - - public float AdaptWidthConstraintToContentRequest(float widthConstraintPixels, - ScaledSize measuredContent, double sideConstraintsPixels) - { - return AdaptConstraintToContentRequest( - widthConstraintPixels, - measuredContent.Pixels.Width, - sideConstraintsPixels, - NeedAutoWidth, - MinimumWidthRequest, - MaximumWidthRequest, - RenderingScale, this.HorizontalOptions.Expands); - } - - - public float AdaptHeightConstraintToContentRequest(float heightConstraintPixels, - ScaledSize measuredContent, - double sideConstraintsPixels) - { - return AdaptConstraintToContentRequest( - heightConstraintPixels, - measuredContent.Pixels.Height, - sideConstraintsPixels, - NeedAutoHeight, - MinimumHeightRequest, - MaximumHeightRequest, - RenderingScale, this.VerticalOptions.Expands); - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SKRect AdaptToContraints(SKRect measuredPixels, - double constraintLeft, - double constraintRight, - double constraintTop, - double constraintBottom) - { - double widthConstraintsPixels = constraintLeft + constraintRight; - double heightConstraintsPixels = constraintTop + constraintBottom; - var outPixels = measuredPixels.Clone(); - - if (NeedAutoWidth) - { - if (outPixels.Width > 0 - && outPixels.Width < widthConstraintsPixels) - { - outPixels.Left -= (float)constraintLeft; - outPixels.Right += (float)constraintRight; - } - } - - if (NeedAutoHeight) - { - if (outPixels.Height > 0 - && outPixels.Height < heightConstraintsPixels) - { - outPixels.Top -= (float)constraintTop; - outPixels.Bottom += (float)constraintBottom; - } - } - - return outPixels; - } - - - - /// - /// In UNITS - /// - /// - /// - /// - protected Size AdaptSizeRequestToContent(double widthRequestPts, double heightRequestPts) - { - if (NeedAutoWidth && ContentSize.Units.Width > 0) - { - widthRequestPts = ContentSize.Units.Width + Padding.Left + Padding.Right; - } - if (NeedAutoHeight && ContentSize.Units.Height > 0) - { - heightRequestPts = ContentSize.Units.Height + Padding.Top + Padding.Bottom; - } - - return new Size(widthRequestPts, heightRequestPts); ; - } - - - - /// - /// Use Superview from public area - /// - /// - public virtual DrawnView GetTopParentView() - { - return GetParentElement(this) as DrawnView; - } - - public static IDrawnBase GetParentElement(IDrawnBase control) - { - if (control != null) - { - if (control is DrawnView) - { - return control; - } - - if (control is SkiaControl skia) - { - return GetParentElement(skia.Parent); - } - } - - return null; - } - - /// - /// To detect if current location is inside Destination - /// - /// - /// - public bool GestureIsInside(TouchActionEventArgs args, float offsetX = 0, float offsetY = 0) - { - - return IsPixelInside((float)args.Location.X + offsetX, (float)args.Location.Y + offsetY); - - // return IsPointInside((float)args.Event.Location.X + offsetX, (float)args.Event.Location.Y + offsetY, (float)RenderingScale); - } - - - /// - /// To detect if a gesture Start point was inside Destination - /// - /// - /// - public bool GestureStartedInside(TouchActionEventArgs args, float offsetX = 0, float offsetY = 0) - { - return IsPixelInside((float)args.StartingLocation.X + offsetX, (float)args.StartingLocation.Y + offsetY); - - // return IsPointInside((float)args.Event.Distance.Start.X + offsetX, (float)args.Event.Distance.Start.Y + offsetY, (float)RenderingScale); - } - - /// - /// Whether the point is inside Destination - /// - /// - /// - /// - /// - public bool IsPointInside(float x, float y, float scale) - { - return IsPointInside(DrawingRect, x, y, scale); - } - - public bool IsPointInside(SKRect rect, float x, float y, float scale) - { - var xx = x * scale; - var yy = y * scale; - bool isInside = rect.ContainsInclusive(xx, yy); - - return isInside; - } - - public bool IsPixelInside(SKRect rect, float x, float y) - { - bool isInside = rect.ContainsInclusive(x, y); - return isInside; - } - - - - /// - /// Whether the pixel is inside Destination - /// - /// - /// - /// - public bool IsPixelInside(float x, float y) - { - bool isInside = DrawingRect.Contains(x, y); - return isInside; - } - - protected virtual bool CheckGestureIsInsideChild(SkiaControl child, TouchActionEventArgs args, float offsetX = 0, float offsetY = 0) - { - return child.CanDraw && child.GestureIsInside(args, offsetX, offsetY); - } - - protected virtual bool CheckGestureIsForChild(SkiaControl child, TouchActionEventArgs args, float offsetX = 0, float offsetY = 0) - { - return child.CanDraw && child.GestureStartedInside(args, offsetX, offsetY); - } - - protected object LockIterateListeners = new(); - - public static readonly BindableProperty LockChildrenGesturesProperty = BindableProperty.Create(nameof(LockChildrenGestures), - typeof(LockTouch), - typeof(SkiaControl), - LockTouch.Disabled); - - /// - /// What gestures are allowed to be passed to children below. - /// If set to Enabled wit, otherwise can be more specific. - /// - public LockTouch LockChildrenGestures - { - get { return (LockTouch)GetValue(LockChildrenGesturesProperty); } - set { SetValue(LockChildrenGesturesProperty, value); } - } - - protected bool CheckChildrenGesturesLocked(TouchActionResult action) - { - switch (LockChildrenGestures) - { - case LockTouch.Enabled: - return true; - - case LockTouch.Disabled: - break; - - case LockTouch.PassTap: - if (action != TouchActionResult.Tapped) - return true; - break; - - case LockTouch.PassTapAndLongPress: - if (action != TouchActionResult.Tapped && action != TouchActionResult.LongPressing) - return true; - break; - } - - return false; - } - - public Dictionary HadInput { get; } = new(128); - - protected TouchActionResult _lastIncomingTouchAction; - - public virtual ISkiaGestureListener OnSkiaGestureEvent(SkiaGesturesParameters args, GestureEventProcessingInfo apply) - { - if (!CanDraw) - return null; - - if (args.Type == _lastIncomingTouchAction && args.Type == TouchActionResult.Up) - { - //a case when we were called for same event by parent and by some top level - //because we previously has input and we got into HadInput to be notified of releases - //so we have set up a filter here - return null; - } - - //todo check latest behaviour! - _lastIncomingTouchAction = args.Type; - - var result = ProcessGestures(args, apply); - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public virtual bool IsGestureForChild(SkiaControlWithRect child, SKPoint point) - { - bool inside = false; - if (child.Control != null && !child.Control.IsDisposing && !child.Control.IsDisposed && - !child.Control.InputTransparent && child.Control.CanDraw) - { - var transformed = child.Control.ApplyTransforms(child.Rect);//todo HitRect - inside = transformed.ContainsInclusive(point.X, point.Y) || child.Control == Superview.FocusedChild; - } - - return inside; - } - - // - // SKPoint childOffset, SKPoint childOffsetDirect, ISkiaGestureListener alreadyConsumed - public static readonly BindableProperty CommandChildTappedProperty = BindableProperty.Create(nameof(CommandChildTapped), typeof(ICommand), - typeof(SkiaControl), - null); - - /// - /// Child was tapped. Will pass the tapped child as parameter. You might want then read child's BindingContext etc.. This works only if your control implements ISkiaGestureListener. - /// - public ICommand CommandChildTapped - { - get { return (ICommand)GetValue(CommandChildTappedProperty); } - set { SetValue(CommandChildTappedProperty, value); } - } - - public virtual ISkiaGestureListener ProcessGestures( - SkiaGesturesParameters args, - GestureEventProcessingInfo apply) - { - if (IsDisposed || IsDisposing) - return null; - - if (Superview == null) - { - //shit happened. we are capturing input but we are not supposed to be on the screen! - Super.Log($"[OnGestureEvent] base captured by unassigned view {this.GetType()} {this.Tag}"); - return null; - } - - if (TouchEffect.LogEnabled) - { - Super.Log($"[BASE] {this.Tag} Got {args.Type}.. {Uid}"); - } - - if (EffectsGestureProcessors.Count > 0) - { - foreach (var effect in EffectsGestureProcessors) - { - effect.ProcessGestures(args, apply); - } - } - - ISkiaGestureListener consumed = null; - ISkiaGestureListener tmpConsumed = apply.alreadyConsumed; - bool manageChildFocus = false; - - if (UsesRenderingTree && RenderTree != null) - { - var thisOffset = TranslateInputCoords(apply.childOffset); - var touchLocationWIthOffset = new SKPoint(args.Event.Location.X + thisOffset.X, args.Event.Location.Y + thisOffset.Y); - - //first process those who already had input - if (HadInput.Count > 0) - { - if ( - args.Type == TouchActionResult.Panning || - args.Type == TouchActionResult.Wheel || - args.Type == TouchActionResult.Up - ) - { - foreach (var hadInput in HadInput.Values) - { - if (!hadInput.CanDraw || hadInput.InputTransparent) - { - continue; - } - consumed = hadInput.OnSkiaGestureEvent(args, - new GestureEventProcessingInfo( - thisOffset, - TranslateInputCoords(apply.childOffsetDirect, false), tmpConsumed)); - - if (consumed != null) - { - if (tmpConsumed == null) - tmpConsumed = consumed; - - if (args.Type != TouchActionResult.Up) - break; - } - } - } - //else - if (HadInput.Count > 0 && args.Type == TouchActionResult.Up) - { - HadInput.Clear(); - } - } - - var hadInputConsumed = consumed; - - //if previously having input didn't keep it - if (consumed == null || args.Type == TouchActionResult.Up) - { - var asSpan = CollectionsMarshal.AsSpan(RenderTree); - for (int i = asSpan.Length - 1; i >= 0; i--) - //for (int i = 0; i < RenderTree.Length; i++) - { - var child = asSpan[i]; - - if (child == Superview.FocusedChild) - manageChildFocus = true; - - ISkiaGestureListener listener = child.Control.GesturesEffect; - if (listener == null && child.Control is ISkiaGestureListener listen) - { - listener = listen; - } - - if (HadInput.Values.Contains(listener) && - ( - args.Type == TouchActionResult.Panning || - args.Type == TouchActionResult.Wheel || - args.Type == TouchActionResult.Up)) - { - continue; - } - - if (listener != null) - { - var forChild = IsGestureForChild(child, touchLocationWIthOffset); - if (forChild) - { - if (args.Type == TouchActionResult.Tapped && CommandChildTapped != null) - { - CommandChildTapped.Execute(child); - } - //Trace.WriteLine($"[HIT] for cell {i} at Y {y:0.0}"); - if (manageChildFocus && listener == Superview.FocusedChild) - { - manageChildFocus = false; - } - - var childOffset = TranslateInputCoords(apply.childOffsetDirect, false); - - if (AddGestures.AttachedListeners.TryGetValue(child.Control, out var effect)) - { - var c = effect.OnSkiaGestureEvent(args, - new GestureEventProcessingInfo( - thisOffset, - childOffset, - apply.alreadyConsumed)); - if (c != null) - { - consumed = effect; - } - } - else - { - var c = listener.OnSkiaGestureEvent(args, - new GestureEventProcessingInfo( - thisOffset, - childOffset, - apply.alreadyConsumed)); - if (c != null) - { - consumed = c; - } - } - - if (consumed != null) - { - if (args.Type != TouchActionResult.Up) - { - HadInput.TryAdd(listener.Uid, consumed); - } - break; - } - } - } - - } - } //end - - if (HadInput.Count > 0 && args.Type == TouchActionResult.Up) - { - HadInput.Clear(); - } - - if (manageChildFocus) - { - Superview.FocusedChild = null; - } - - if (hadInputConsumed != null) - consumed = hadInputConsumed; - } - else - { - //lock (LockIterateListeners) - { - - try - { - if (CheckChildrenGesturesLocked(args.Type)) - return null; - - var point = TranslateInputOffsetToPixels(args.Event.Location, apply.childOffset); - - if (HadInput.Count > 0) - { - if ( - ( - args.Type == TouchActionResult.Panning || - args.Type == TouchActionResult.Wheel || - args.Type == TouchActionResult.Up)) - { - foreach (var hadInput in HadInput.Values) - { - if (!hadInput.CanDraw || hadInput.InputTransparent) - { - continue; - } - consumed = hadInput.OnSkiaGestureEvent(args, - new GestureEventProcessingInfo( - TranslateInputCoords(apply.childOffset, true), - TranslateInputCoords(apply.childOffsetDirect, false), tmpConsumed)); - - if (consumed != null) - { - if (tmpConsumed == null) - tmpConsumed = consumed; - - if (args.Type != TouchActionResult.Up) - break; - } - } - } - //else - if (HadInput.Count > 0 && args.Type == TouchActionResult.Up) - { - HadInput.Clear(); - } - } - - var hadInputConsumed = consumed; - - if (consumed == null || args.Type == TouchActionResult.Up)// !GestureListeners.Contains(consumed)) - foreach (var listener in GestureListeners.GetListeners()) - { - if (!listener.CanDraw || listener.InputTransparent) - continue; - - if (HadInput.Values.Contains(listener) && - ( - args.Type == TouchActionResult.Panning || - args.Type == TouchActionResult.Wheel || - args.Type == TouchActionResult.Up)) - { - continue; - } - - //Debug.WriteLine($"Checking {listener} gestures in {this.Tag} {this}"); - - if (listener == Superview.FocusedChild) - manageChildFocus = true; - - var forChild = IsGestureForChild(listener, point); - - if (TouchEffect.LogEnabled) - { - if (listener is SkiaControl c) - { - Debug.WriteLine($"[BASE] for child {forChild} {c.Tag} at {point.X:0},{point.Y:0} -> {c.HitBoxAuto} "); - } - } - - if (forChild) - { - if (args.Type == TouchActionResult.Tapped && CommandChildTapped != null) - { - CommandChildTapped.Execute(listener); - } - if (manageChildFocus && listener == Superview.FocusedChild) - { - manageChildFocus = false; - } - //Log($"[OnGestureEvent] sent {args.Action} to {listener.Tag}"); - consumed = listener.OnSkiaGestureEvent(args, - new GestureEventProcessingInfo( - TranslateInputCoords(apply.childOffset, true), - TranslateInputCoords(apply.childOffsetDirect, false), - tmpConsumed)); - - if (consumed != null) - { - if (tmpConsumed == null) - tmpConsumed = consumed; - - if (args.Type != TouchActionResult.Up) - { - HadInput.TryAdd(listener.Uid, consumed); - } - break; - } - } - } - - if (HadInput.Count > 0 && args.Type == TouchActionResult.Up) - { - HadInput.Clear(); - } - - if (manageChildFocus) - { - Superview.FocusedChild = null; - } - - if (hadInputConsumed != null) - consumed = hadInputConsumed; - - } - catch (Exception e) - { - Super.Log(e); - } - - - } - } - - return consumed; - } - - - public bool UpdateLocked { get; set; } - - public void LockUpdate(bool value) - { - UpdateLocked = value; - } - - private void Init() - { - NeedMeasure = true; - IsLayoutDirty = true; - - SizeChanged += ViewSizeChanged; - - CalculateMargins(); - 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), - typeof(IDrawnBase), - typeof(SkiaControl), - default(IDrawnBase), - propertyChanged: OnControlParentChanged); - - /// - /// Do not set this directly if you don't know what you are doing, use SetParent() - /// - public new IDrawnBase Parent - { - get { return (IDrawnBase)GetValue(ParentProperty); } - set { SetValue(ParentProperty, value); } - } - - - - #region View - - - public static readonly BindableProperty VerticalOptionsProperty = BindableProperty.Create(nameof(VerticalOptions), - typeof(LayoutOptions), - typeof(SkiaControl), - LayoutOptions.Start, - propertyChanged: NeedInvalidateMeasure); - - public LayoutOptions VerticalOptions - { - get { return (LayoutOptions)GetValue(VerticalOptionsProperty); } - set { SetValue(VerticalOptionsProperty, value); } - } - - public static readonly BindableProperty HorizontalOptionsProperty = BindableProperty.Create(nameof(HorizontalOptions), - typeof(LayoutOptions), - typeof(SkiaControl), - LayoutOptions.Start, - propertyChanged: NeedInvalidateMeasure); - - public LayoutOptions HorizontalOptions - { - get { return (LayoutOptions)GetValue(HorizontalOptionsProperty); } - set { SetValue(HorizontalOptionsProperty, value); } - } - - /// - /// todo override for templated skialayout to use ViewsProvider - /// - /// - protected virtual void OnParentVisibilityChanged(bool newvalue) - { - if (!newvalue) - { - //DestroyRenderingObject(); - } - - Superview?.SetViewTreeVisibilityByParent(this, newvalue); - - if (!newvalue) - { - if (this.UsingCacheType == SkiaCacheType.GPU) - { - RenderObject = null; - } - } - - if (!IsVisible) - { - //though shell not pass - return; - } - - try - { - foreach (var child in Views) - { - child.OnParentVisibilityChanged(newvalue); - } - } - catch (Exception e) - { - Super.Log(e); - } - } - - /// - /// todo override for templated skialayout to use ViewsProvider - /// - /// - public virtual void OnVisibilityChanged(bool newvalue) - { - if (!newvalue) - { - if (this.UsingCacheType == SkiaCacheType.GPU) - { - RenderObject = null; - } - //DestroyRenderingObject(); - } - // need to this to: - // disable child gesture listeners - // pause hidden animations - try - { - var pass = IsVisible && newvalue; - foreach (var child in Views) - { - child.OnParentVisibilityChanged(pass); - } - - Superview?.UpdateRenderingChains(this); - } - catch (Exception e) - { - Super.Log(e); - } - } - - /// - /// Base performs some cleanup actions with Superview - /// - public virtual void OnDisposing() - { - ClippedBy = null; - Disposing?.Invoke(this, null); - Superview?.UnregisterGestureListener(this as ISkiaGestureListener); - Superview?.UnregisterAllAnimatorsByParent(this); - } - - protected object lockPausingAnimators = new(); - - public virtual void PauseAllAnimators() - { - var paused = Superview?.SetPauseStateOfAllAnimatorsByParent(this, true); - } - - public virtual void ResumePausedAnimators() - { - var resumed = Superview?.SetPauseStateOfAllAnimatorsByParent(this, false); - } - - public static readonly BindableProperty IsGhostProperty = BindableProperty.Create(nameof(IsGhost), - typeof(bool), - typeof(SkiaControl), - false, - propertyChanged: NeedDraw); - public bool IsGhost - { - get { return (bool)GetValue(IsGhostProperty); } - set { SetValue(IsGhostProperty, value); } - } - - public static readonly BindableProperty IgnoreChildrenInvalidationsProperty - = BindableProperty.Create(nameof(IgnoreChildrenInvalidations), - typeof(bool), typeof(SkiaControl), - false); - - public bool IgnoreChildrenInvalidations - { - get - { - return (bool)GetValue(IgnoreChildrenInvalidationsProperty); - } - set - { - SetValue(IgnoreChildrenInvalidationsProperty, value); - } - } - - - - - #region FillGradient - - public static readonly BindableProperty FillGradientProperty = BindableProperty.Create(nameof(FillGradient), - typeof(SkiaGradient), typeof(SkiaControl), - null, - propertyChanged: SetupFillGradient); - - private static void SetupFillGradient(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is SkiaControl control) - { - if (newvalue is SkiaGradient gradient) - { - gradient.BindingContext = control.BindingContext; - } - control.Update(); - } - } - - public SkiaGradient FillGradient - { - get { return (SkiaGradient)GetValue(FillGradientProperty); } - set { SetValue(FillGradientProperty, value); } - } - - public bool HasFillGradient - { - get - { - return this.FillGradient != null && this.FillGradient.Type != GradientType.None; - } - } - - - #endregion - - public virtual SKSize GetSizeRequest(float widthConstraint, float heightConstraint, bool insideLayout) - { - widthConstraint *= (float)this.WidthRequestRatio; - heightConstraint *= (float)this.HeightRequestRatio; - - if (LockRatio > 0) - { - var lockValue = (float)SmartMax(widthConstraint, heightConstraint); - return new SKSize(lockValue, lockValue); - } - - if (LockRatio < 0) - { - var lockValue = (float)SmartMin(widthConstraint, heightConstraint); - return new SKSize(lockValue, lockValue); - } - - return new SKSize(widthConstraint, heightConstraint); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static float SmartMax(float a, float b) - { - if (!float.IsFinite(a) || float.IsFinite(b) && b > a) - return b; - return a; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static float SmartMin(float a, float b) - { - if (!float.IsFinite(a) || float.IsFinite(a) && b < a) - return b; - return a; - } - - public static readonly BindableProperty ViewportHeightLimitProperty = BindableProperty.Create( - nameof(ViewportHeightLimit), - typeof(double), - typeof(SkiaControl), - -1.0, propertyChanged: NeedInvalidateViewport); - - /// - /// Will be used inside GetDrawingRectWithMargins to limit the height of the DrawingRect - /// - public double ViewportHeightLimit - { - get { return (double)GetValue(ViewportHeightLimitProperty); } - set { SetValue(ViewportHeightLimitProperty, value); } - } - - public static readonly BindableProperty ViewportWidthLimitProperty = BindableProperty.Create( - nameof(ViewportWidthLimit), - typeof(double), - typeof(SkiaControl), - -1.0, propertyChanged: NeedInvalidateViewport); - - /// - /// Will be used inside GetDrawingRectWithMargins to limit the width of the DrawingRect - /// - public double ViewportWidthLimit - { - get { return (double)GetValue(ViewportWidthLimitProperty); } - set { SetValue(ViewportWidthLimitProperty, value); } - } - - - private double _height = -1; - public new double Height - { - get - { - return _height; - } - set - { - if (_height != value) - { - _height = value; - OnPropertyChanged(); - } - } - } - - private double _width = -1; - public new double Width - { - get - { - return _width; - } - set - { - if (_width != value) - { - _width = value; - OnPropertyChanged(); - } - } - } - - /// - /// Please use ScaleX, ScaleY instead of this maui property - /// - public new double Scale - { - get - { - return Math.Min(ScaleX, ScaleY); - } - set - { - ScaleX = value; - ScaleY = value; - } - } - - - public static readonly BindableProperty TagProperty = BindableProperty.Create(nameof(Tag), - typeof(string), - typeof(SkiaControl), - string.Empty, - propertyChanged: NeedDraw); - public string Tag - { - get { return (string)GetValue(TagProperty); } - set { SetValue(TagProperty, value); } - } - - - public static readonly BindableProperty LockRatioProperty = BindableProperty.Create(nameof(LockRatio), - typeof(double), typeof(SkiaControl), - 0.0, - propertyChanged: NeedInvalidateMeasure); - /// - /// Locks the final size to the min (-1.0 -> 0.0) or max (0.0 -> 1.0) of the provided size. - /// - public double LockRatio - { - get { return (double)GetValue(LockRatioProperty); } - set { SetValue(LockRatioProperty, value); } - } - - public static readonly BindableProperty HeightRequestRatioProperty = BindableProperty.Create( - nameof(HeightRequestRatio), - typeof(double), - typeof(SkiaControl), - 1.0, - propertyChanged: NeedInvalidateMeasure); - - /// - /// HeightRequest Multiplier, default is 1.0 - /// - public double HeightRequestRatio - { - get { return (double)GetValue(HeightRequestRatioProperty); } - set { SetValue(HeightRequestRatioProperty, value); } - } - - public static readonly BindableProperty WidthRequestRatioProperty = BindableProperty.Create( - nameof(WidthRequestRatio), - typeof(double), - typeof(SkiaControl), - 1.0, - propertyChanged: NeedInvalidateMeasure); - - /// - /// WidthRequest Multiplier, default is 1.0 - /// - public double WidthRequestRatio - { - get { return (double)GetValue(WidthRequestRatioProperty); } - set { SetValue(WidthRequestRatioProperty, value); } - } - - public static readonly BindableProperty HorizontalFillRatioProperty = BindableProperty.Create( - nameof(HorizontalFillRatio), - typeof(double), - typeof(SkiaControl), - 1.0, - propertyChanged: NeedInvalidateMeasure); - - public double HorizontalFillRatio - { - get { return (double)GetValue(HorizontalFillRatioProperty); } - set { SetValue(HorizontalFillRatioProperty, value); } - } - - public static readonly BindableProperty VerticalFillRatioProperty = BindableProperty.Create( - nameof(VerticalFillRatio), - typeof(double), - typeof(SkiaControl), - 1.0, - propertyChanged: NeedInvalidateMeasure); - - public double VerticalFillRatio - { - get { return (double)GetValue(VerticalFillRatioProperty); } - set { SetValue(VerticalFillRatioProperty, value); } - } - - public static readonly BindableProperty HorizontalPositionOffsetRatioProperty = BindableProperty.Create( - nameof(HorizontalPositionOffsetRatio), - typeof(double), - typeof(SkiaControl), - 0.0, - propertyChanged: NeedDraw); - - public double HorizontalPositionOffsetRatio - { - get { return (double)GetValue(HorizontalPositionOffsetRatioProperty); } - set { SetValue(HorizontalPositionOffsetRatioProperty, value); } - } - - public static readonly BindableProperty VerticalPositionOffsetRatioProperty = BindableProperty.Create( - nameof(VerticalPositionOffsetRatio), - typeof(double), - typeof(SkiaControl), - 0.0, - propertyChanged: NeedDraw); - - public double VerticalPositionOffsetRatio - { - get { return (double)GetValue(VerticalPositionOffsetRatioProperty); } - set { SetValue(VerticalPositionOffsetRatioProperty, value); } - } - - public static readonly BindableProperty FillBlendModeProperty = BindableProperty.Create(nameof(FillBlendMode), - typeof(SKBlendMode), typeof(SkiaControl), - SKBlendMode.SrcOver, - propertyChanged: NeedDraw); - public SKBlendMode FillBlendMode - { - get { return (SKBlendMode)GetValue(FillBlendModeProperty); } - set { SetValue(FillBlendModeProperty, value); } - } - - /* + [DebuggerDisplay("{DebugString}")] + [ContentProperty("Children")] + public partial class SkiaControl : + IHasAfterEffects, + ISkiaControl + { + public SkiaControl() + { + Init(); + } - disabled this for fps... we can use drawingrect.x and y /renderingscale but OnPropertyChanged calls might slow us down?.. + public virtual bool IsVisibleInViewTree() + { + var isVisible = IsVisible && !IsDisposed; - private double _X; - public double X + var parent = this.Parent as SkiaControl; + + if (parent == null) + return isVisible; + + if (isVisible) + return parent.IsVisibleInViewTree(); + + return false; + } + + /// + /// Absolute position in points + /// + /// + public virtual SKPoint GetPositionOnCanvasInPoints(bool useTranslation = true) + { + var position = GetPositionOnCanvas(useTranslation); + + return new(position.X / RenderingScale, position.Y / RenderingScale); + } + + /// + /// Absolute position in points + /// + /// + public virtual SKPoint GetFuturePositionOnCanvasInPoints(bool useTranslation = true) + { + var position = GetFuturePositionOnCanvas(useTranslation); + + return new(position.X / RenderingScale, position.Y / RenderingScale); + } + + /// + /// Absolute position in pixels afetr drawn. + /// + /// + public virtual SKPoint GetPositionOnCanvas(bool useTranslation = true) + { + //Debug.WriteLine($"GetPositionOnCanvas ------------------------START at {LastDrawnAt}"); + + //ignore cache for this specific control only + var position = BuildDrawnOffsetRecursive(LastDrawnAt.Location, this, true, useTranslation); + + //Debug.WriteLine("GetPositionOnCanvas ------------------------END"); + return new(position.X, position.Y); + } + + /// + /// Absolute position in pixels before drawn. + /// + /// + public virtual SKPoint GetFuturePositionOnCanvas(bool useTranslation = true) + { + var position = BuildDrawnOffsetRecursive(DrawingRect.Location, this, true, useTranslation); + return new(position.X, position.Y); + } + + + /// + /// Find drawing position for control accounting for all caches up the rendering tree. + /// + /// + public virtual SKPoint GetSelfDrawingPosition() + { + var position = BuildSelfDrawingPosition(LastDrawnAt.Location, this, true); + + return new(position.X, position.Y); + } + + + + + + public SKPoint BuildSelfDrawingPosition(SKPoint offset, SkiaControl control, bool isChild) + { + if (control == null) + { + return offset; + } + + var drawingOffset = SKPoint.Empty; + if (!isChild) + { + drawingOffset = control.GetPositionOffsetInPixels(true); + drawingOffset.Offset(offset); + } + else + { + drawingOffset = offset; + } + + var parent = control.Parent as SkiaControl; + if (parent == null) + { + return drawingOffset; + } + + return BuildSelfDrawingPosition(drawingOffset, parent, false); + } + + + public SKPoint BuildDrawingOffsetRecursive(SKPoint offset, SkiaControl control, bool ignoreCache, bool useTranslation = true) + { + if (control == null) + { + return offset; + } + + var drawingOffset = control.GetFuturePositionOffsetInPixels(false, ignoreCache); + drawingOffset.Offset(offset); + var parent = control.Parent as SkiaControl; + if (parent == null) + { + drawingOffset.Offset(control.DrawingRect.Location); + return drawingOffset; + } + return BuildDrawingOffsetRecursive(drawingOffset, parent, false, useTranslation); + } + + public SKPoint BuildDrawnOffsetRecursive(SKPoint offset, SkiaControl control, bool ignoreCache, bool useTranslation = true) + { + if (control == null) + { + return offset; + } + + var drawingOffset = control.GetPositionOffsetInPixels(false, ignoreCache); + drawingOffset.Offset(offset); + var parent = control.Parent as SkiaControl; + if (parent == null) + { + drawingOffset.Offset(control.LastDrawnAt.Location); + return drawingOffset; + } + return BuildDrawnOffsetRecursive(drawingOffset, parent, false, useTranslation); + } + + + public virtual string DebugString { get { - return _X; + return $"{GetType().Name} Tag {Tag}, IsVisible {IsVisible}, Children {Views.Count}, {Width:0.0}x{Height:0.0}dp, DrawingRect: {DrawingRect}"; + } + } + + + + public virtual bool CanDraw + { + get + { + if (!IsVisible || IsDisposed || IsDisposing || SkipRendering) + return false; + + if (Superview != null && !Superview.CanDraw) + { + return false; + } + + return true; } + } + + + /// + /// Can be set but custom controls while optimizing rendering etc. Will affect CanDraw. + /// + public bool SkipRendering { get; set; } + + protected bool DefaultChildrenCreated { get; set; } + + protected virtual void CreateDefaultContent() + { + + } + /// + /// To create custom content in code-behind. Will be called from OnLayoutChanged if Views.Count == 0. + /// + public Func> CreateChildren + { + get => _createChildren; set { - if (_X != value) + _createChildren = value; + if (value != null) + { + DefaultChildrenCreated = false; + CreateChildrenFromCode(); + } + } + } + + /// + /// Executed when CreateChildren is set + /// + /// + protected virtual void CreateChildrenFromCode() + { + if (this.Views.Count == 0 && !DefaultChildrenCreated) + { + DefaultChildrenCreated = true; + if (CreateChildren != null) + { + try + { + var children = CreateChildren(); + foreach (var skiaControl in children) + { + AddSubView(skiaControl); + } + } + catch (Exception e) + { + Super.Log(e); + } + } + + } + } + + + + /// + /// This actually used by SkiaMauiElement but could be used by other controls. Also might be useful for debugging purposes. + /// + /// + public VisualTreeChain GenerateParentChain() + { + var currentParent = this.Parent as SkiaControl; + + var chain = new VisualTreeChain(this); + + var parents = new List(); + + // Traverse up the parent hierarchy + while (currentParent != null) + { + // Add the current parent to the chain + parents.Add(currentParent); + // Move to the next parent + currentParent = currentParent.Parent as SkiaControl; + } + + parents.Reverse(); + + foreach (var parent in parents) + { + chain.AddNode(parent); + } + + return chain; + } + + /// + /// //todo base. this is actually used by SkiaMauiElement only + /// + /// + public virtual void SetVisualTransform(VisualTransform transform) + { + + } + + /// + /// Apply all postponed invalidation other logic that was postponed until the first draw for optimization. Use this for special code-behind cases, like tests etc, if you cannot wait until the first Draw(). In this version this affects ItemsSource only. + /// + public void CommitInvalidations() + { + foreach (var invalidation in PostponedInvalidations) + { + invalidation.Value.Invoke(); + } + PostponedInvalidations.Clear(); + } + + public virtual void SuperViewChanged() + { + if (Superview != null) + { + CommitInvalidations(); + if (Super.Multithreaded && CanUseCacheDoubleBuffering) + Update(); + } + } + + /// + /// Used for optimization process, for example, to avid changing ItemSource several times before the first draw. + /// + /// + /// + public void PostponeInvalidation(string key, Action action) + { + if (Superview == null) + { + PostponedInvalidations[key] = action; + } + else + { + //action.Invoke(); + Superview.PostponeInvalidation(this, action); + } + } + + readonly Dictionary PostponedInvalidations = new(); + + /// + /// Returns rendering scale adapted for another output size, useful for offline rendering + /// + /// + /// + /// + public float GetRenderingScaleFor(float width, float height) + { + var aspectX = width / DrawingRect.Width; + var aspectY = height / DrawingRect.Height; + var scale = Math.Min(aspectX, aspectY) * RenderingScale; + return scale; + } + + public float GetRenderingScaleFor(float measure) + { + var aspectX = measure / DrawingRect.Width; + var scale = aspectX * RenderingScale; + return scale; + } + + /// + /// Creates a new animator, animates from 0 to 1 over a given time, and calls your callback with the current eased value + /// + /// + /// + /// + /// + /// + public Task AnimateAsync(Action callback, + Action callbaclOnCancel = null, + float ms = 250, Easing easing = null, CancellationTokenSource cancel = default) + { + if (easing == null) + { + easing = Easing.Linear; + } + + var animator = new SkiaValueAnimator(this); + + if (cancel == default) + cancel = new CancellationTokenSource(); + var tcs = new TaskCompletionSource(cancel.Token); + + // Update animator parameters + animator.mMinValue = 0; + animator.mMaxValue = 1; + animator.Speed = ms; + animator.Easing = easing; + + animator.OnStop = () => + { + if (!tcs.Task.IsCompleted) + tcs.SetResult(true); + DisposeObject(animator); + }; + animator.OnUpdated = (value) => + { + if (!cancel.IsCancellationRequested) + { + callback?.Invoke(value); + } + else + { + callback?.Invoke(value); + animator.Stop(); + DisposeObject(animator); + } + }; + + animator.Start(); + + return tcs.Task; + } + + + CancellationTokenSource _fadeCancelTokenSource; + /// + /// Fades the view from the current Opacity to end, animator is reused if already running + /// + /// + /// + /// + /// + /// + public Task FadeToAsync(double end, float ms = 250, Easing easing = null, CancellationTokenSource cancel = default) + { + // Cancel previous animation if it exists and is still running. + _fadeCancelTokenSource?.Cancel(); + _fadeCancelTokenSource = new CancellationTokenSource(); + + var startOpacity = this.Opacity; + return AnimateAsync( + (value) => + { + this.Opacity = startOpacity + (end - startOpacity) * value; + //Debug.WriteLine($"[ANIM] Opacity: {this.Opacity}"); + }, + () => + { + this.Opacity = end; + }, + ms, + easing, + _fadeCancelTokenSource); + } + + CancellationTokenSource _scaleCancelTokenSource; + /// + /// Scales the view from the current Scale to x,y, animator is reused if already running + /// + /// + /// + /// + /// + /// + /// + public Task ScaleToAsync(double x, double y, float length = 250, Easing easing = null, CancellationTokenSource cancel = default) + { + // Cancel previous animation if it exists and is still running. + _scaleCancelTokenSource?.Cancel(); + _scaleCancelTokenSource = new CancellationTokenSource(); + + var startScaleX = this.ScaleX; + var startScaleY = this.ScaleY; + + return AnimateAsync(value => + { + this.ScaleX = startScaleX + (x - startScaleX) * value; + this.ScaleY = startScaleY + (y - startScaleY) * value; + }, + () => + { + this.ScaleX = x; + this.ScaleY = y; + }, length, easing, _scaleCancelTokenSource); + } + + + CancellationTokenSource _translateCancelTokenSource; + /// + /// Translates the view from the current position to x,y, animator is reused if already running + /// + /// + /// + /// + /// + /// + /// + public Task TranslateToAsync(double x, double y, uint length = 250, Easing easing = null, CancellationTokenSource cancel = default) + { + // Cancel previous animation if it exists and is still running. + _translateCancelTokenSource?.Cancel(); + _translateCancelTokenSource = new CancellationTokenSource(); + + var startTranslationX = this.TranslationX; + var startTranslationY = this.TranslationY; + + return AnimateAsync(value => + { + this.TranslationX = (float)(startTranslationX + (x - startTranslationX) * value); + this.TranslationY = (float)(startTranslationY + (y - startTranslationY) * value); + }, + () => + { + this.TranslationX = x; + this.TranslationY = y; + }, + length, easing, _translateCancelTokenSource); + } + + CancellationTokenSource _rotateCancelTokenSource; + /// + /// Rotates the view from the current rotation to end, animator is reused if already running + /// + /// + /// + /// + /// + /// + public Task RotateToAsync(double end, uint length = 250, Easing easing = null, CancellationTokenSource cancel = default) + { + // Cancel previous animation if it exists and is still running. + _rotateCancelTokenSource?.Cancel(); + _rotateCancelTokenSource = new CancellationTokenSource(); + + var startRotation = this.Rotation; + + return AnimateAsync(value => + { + this.Rotation = (float)(startRotation + (end - startRotation) * value); + }, + () => + { + this.Rotation = end; + }, + length, easing, _rotateCancelTokenSource); + } + + + public virtual void OnPrintDebug() + { + + } + + public void PrintDebug(string indent = " ") + { + Super.Log($"{indent}└─ {GetType().Name} {Width:0.0}x{Height:0.0} pts ({MeasuredSize.Pixels.Width:0.0}x{MeasuredSize.Pixels.Height:0.0} px)"); + OnPrintDebug(); + foreach (var view in Views) + { + Trace.Write($"{indent} ├─ "); + view.PrintDebug(indent + " │ "); + } + } + + public static readonly BindableProperty DebugRenderingProperty = BindableProperty.Create(nameof(DebugRendering), + typeof(bool), + typeof(SkiaControl), + false, propertyChanged: NeedDraw); + public bool DebugRendering + { + get { return (bool)GetValue(DebugRenderingProperty); } + set { SetValue(DebugRenderingProperty, value); } + } + + float _debugRenderingCurrentOpacity; + + /// + /// When the animator is cancelled if applyEndValueOnStop is true then the end value will be sent to your callback + /// + /// + /// + /// + /// + /// + /// + /// + public Task AnimateRangeAsync(Action callback, double start, double end, double length = 250, Easing easing = null, + CancellationToken cancel = default, + bool applyEndValueOnStop = false) + { + RangeAnimator animator = null; + + var tcs = new TaskCompletionSource(cancel); + + tcs.Task.ContinueWith(task => + { + DisposeObject(animator); + }); + + animator = new RangeAnimator(this) + { + OnStop = () => + { + //if (animator.WasStarted && !cancel.IsCancellationRequested) + { + if (applyEndValueOnStop) + callback?.Invoke(end); + tcs.SetResult(true); + } + } + }; + animator.Start( + (value) => + { + if (!cancel.IsCancellationRequested) + { + callback?.Invoke(value); + } + else + { + animator.Stop(); + } + }, + start, end, (uint)length, easing); + + return tcs.Task; + } + + + + public SKPoint NotValidPoint() + { + return new SKPoint(float.NaN, float.NaN); + } + + public bool PointIsValid(SKPoint point) + { + return !float.IsNaN(point.X) && !float.IsNaN(point.Y); + } + + /// + /// Is set by InvalidateMeasure(); + /// + public SKSize SizeRequest { get; protected set; } + + /// + /// Apply margins to SizeRequest + /// + /// + /// + /// + public float AdaptWidthConstraintToRequest(float widthConstraint, Thickness constraints, double scale) + { + var widthPixels = (float)Math.Round(SizeRequest.Width * scale + constraints.HorizontalThickness); + + if (SizeRequest.Width >= 0) + widthConstraint = widthPixels; + + return widthConstraint; + } + + /// + /// Apply margins to SizeRequest + /// + /// + /// + /// + public float AdaptHeightContraintToRequest(float heightConstraint, Thickness constraints, double scale) + { + var thickness = constraints.VerticalThickness; + + var widthPixels = (float)Math.Round(SizeRequest.Height * scale + thickness); + + if (SizeRequest.Height >= 0) + heightConstraint = widthPixels; + + return heightConstraint; + } + + public virtual MeasuringConstraints GetMeasuringConstraints(MeasureRequest request) + { + var withLock = GetSizeRequest(request.WidthRequest, request.HeightRequest, true); + var margins = GetMarginsInPixels(request.Scale); + + var adaptedWidthConstraint = AdaptWidthConstraintToRequest(withLock.Width, margins, request.Scale); + var adaptedHeightConstraint = AdaptHeightContraintToRequest(withLock.Height, margins, request.Scale); + + var rectForChildrenPixels = GetMeasuringRectForChildren(adaptedWidthConstraint, adaptedHeightConstraint, request.Scale); + + return new MeasuringConstraints + { + Margins = margins, + TotalMargins = GetAllMarginsInPixels(request.Scale), + Request = new(adaptedWidthConstraint, adaptedHeightConstraint), + Content = rectForChildrenPixels + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float AdaptConstraintToContentRequest( + float constraintPixels, + double measuredDimension, + double sideConstraintsPixels, + bool autoSize, + double minRequest, double maxRequest, float scale, bool canExpand) + { + + var contentDimension = sideConstraintsPixels + measuredDimension; + + if (autoSize && measuredDimension >= 0 && (canExpand || measuredDimension < constraintPixels) + || float.IsInfinity(constraintPixels)) + { + constraintPixels = (float)contentDimension; + } + + if (minRequest >= 0) + { + var min = double.MinValue; + if (double.IsFinite(minRequest)) + { + min = Math.Round(minRequest * scale); + } + constraintPixels = (float)Math.Max(constraintPixels, min); + } + + if (maxRequest >= 0) + { + var max = double.MaxValue; + if (double.IsFinite(maxRequest)) + { + max = Math.Round(maxRequest * scale); + } + constraintPixels = (float)Math.Min(constraintPixels, max); + } + + return (float)Math.Round(constraintPixels); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float AdaptWidthConstraintToContentRequest(MeasuringConstraints constraints, float contentWidthPixels, bool canExpand) + { + var sideConstraintsPixels = NeedAutoWidth + ? constraints.TotalMargins.HorizontalThickness + : constraints.Margins.HorizontalThickness; + + return AdaptConstraintToContentRequest( + constraints.Request.Width, + contentWidthPixels, + sideConstraintsPixels, + NeedAutoWidth, + MinimumWidthRequest, + MaximumWidthRequest, + RenderingScale, canExpand); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float AdaptHeightConstraintToContentRequest(MeasuringConstraints constraints, + float contentHeightPixels, bool canExpand) + { + var sideConstraintsPixels = NeedAutoHeight + ? constraints.TotalMargins.VerticalThickness + : constraints.Margins.VerticalThickness; + + return AdaptConstraintToContentRequest( + constraints.Request.Height, + contentHeightPixels, + sideConstraintsPixels, + NeedAutoHeight, + MinimumHeightRequest, + MaximumHeightRequest, + RenderingScale, canExpand); + } + + + public float AdaptWidthConstraintToContentRequest(float widthConstraintPixels, + ScaledSize measuredContent, double sideConstraintsPixels) + { + return AdaptConstraintToContentRequest( + widthConstraintPixels, + measuredContent.Pixels.Width, + sideConstraintsPixels, + NeedAutoWidth, + MinimumWidthRequest, + MaximumWidthRequest, + RenderingScale, this.HorizontalOptions.Expands); + } + + + public float AdaptHeightConstraintToContentRequest(float heightConstraintPixels, + ScaledSize measuredContent, + double sideConstraintsPixels) + { + return AdaptConstraintToContentRequest( + heightConstraintPixels, + measuredContent.Pixels.Height, + sideConstraintsPixels, + NeedAutoHeight, + MinimumHeightRequest, + MaximumHeightRequest, + RenderingScale, this.VerticalOptions.Expands); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SKRect AdaptToContraints(SKRect measuredPixels, + double constraintLeft, + double constraintRight, + double constraintTop, + double constraintBottom) + { + double widthConstraintsPixels = constraintLeft + constraintRight; + double heightConstraintsPixels = constraintTop + constraintBottom; + var outPixels = measuredPixels.Clone(); + + if (NeedAutoWidth) + { + if (outPixels.Width > 0 + && outPixels.Width < widthConstraintsPixels) + { + outPixels.Left -= (float)constraintLeft; + outPixels.Right += (float)constraintRight; + } + } + + if (NeedAutoHeight) + { + if (outPixels.Height > 0 + && outPixels.Height < heightConstraintsPixels) + { + outPixels.Top -= (float)constraintTop; + outPixels.Bottom += (float)constraintBottom; + } + } + + return outPixels; + } + + + + /// + /// In UNITS + /// + /// + /// + /// + protected Size AdaptSizeRequestToContent(double widthRequestPts, double heightRequestPts) + { + if (NeedAutoWidth && ContentSize.Units.Width > 0) + { + widthRequestPts = ContentSize.Units.Width + Padding.Left + Padding.Right; + } + if (NeedAutoHeight && ContentSize.Units.Height > 0) + { + heightRequestPts = ContentSize.Units.Height + Padding.Top + Padding.Bottom; + } + + return new Size(widthRequestPts, heightRequestPts); ; + } + + + + /// + /// Use Superview from public area + /// + /// + public virtual DrawnView GetTopParentView() + { + return GetParentElement(this) as DrawnView; + } + + public static IDrawnBase GetParentElement(IDrawnBase control) + { + if (control != null) + { + if (control is DrawnView) + { + return control; + } + + if (control is SkiaControl skia) + { + return GetParentElement(skia.Parent); + } + } + + return null; + } + + /// + /// To detect if current location is inside Destination + /// + /// + /// + public bool GestureIsInside(TouchActionEventArgs args, float offsetX = 0, float offsetY = 0) + { + + return IsPixelInside((float)args.Location.X + offsetX, (float)args.Location.Y + offsetY); + + // return IsPointInside((float)args.Event.Location.X + offsetX, (float)args.Event.Location.Y + offsetY, (float)RenderingScale); + } + + + /// + /// To detect if a gesture Start point was inside Destination + /// + /// + /// + public bool GestureStartedInside(TouchActionEventArgs args, float offsetX = 0, float offsetY = 0) + { + return IsPixelInside((float)args.StartingLocation.X + offsetX, (float)args.StartingLocation.Y + offsetY); + + // return IsPointInside((float)args.Event.Distance.Start.X + offsetX, (float)args.Event.Distance.Start.Y + offsetY, (float)RenderingScale); + } + + /// + /// Whether the point is inside Destination + /// + /// + /// + /// + /// + public bool IsPointInside(float x, float y, float scale) + { + return IsPointInside(DrawingRect, x, y, scale); + } + + public bool IsPointInside(SKRect rect, float x, float y, float scale) + { + var xx = x * scale; + var yy = y * scale; + bool isInside = rect.ContainsInclusive(xx, yy); + + return isInside; + } + + public bool IsPixelInside(SKRect rect, float x, float y) + { + bool isInside = rect.ContainsInclusive(x, y); + return isInside; + } + + + + /// + /// Whether the pixel is inside Destination + /// + /// + /// + /// + public bool IsPixelInside(float x, float y) + { + bool isInside = DrawingRect.Contains(x, y); + return isInside; + } + + protected virtual bool CheckGestureIsInsideChild(SkiaControl child, TouchActionEventArgs args, float offsetX = 0, float offsetY = 0) + { + return child.CanDraw && child.GestureIsInside(args, offsetX, offsetY); + } + + protected virtual bool CheckGestureIsForChild(SkiaControl child, TouchActionEventArgs args, float offsetX = 0, float offsetY = 0) + { + return child.CanDraw && child.GestureStartedInside(args, offsetX, offsetY); + } + + protected object LockIterateListeners = new(); + + public static readonly BindableProperty LockChildrenGesturesProperty = BindableProperty.Create(nameof(LockChildrenGestures), + typeof(LockTouch), + typeof(SkiaControl), + LockTouch.Disabled); + + /// + /// What gestures are allowed to be passed to children below. + /// If set to Enabled wit, otherwise can be more specific. + /// + public LockTouch LockChildrenGestures + { + get { return (LockTouch)GetValue(LockChildrenGesturesProperty); } + set { SetValue(LockChildrenGesturesProperty, value); } + } + + protected bool CheckChildrenGesturesLocked(TouchActionResult action) + { + switch (LockChildrenGestures) + { + case LockTouch.Enabled: + return true; + + case LockTouch.Disabled: + break; + + case LockTouch.PassTap: + if (action != TouchActionResult.Tapped) + return true; + break; + + case LockTouch.PassTapAndLongPress: + if (action != TouchActionResult.Tapped && action != TouchActionResult.LongPressing) + return true; + break; + } + + return false; + } + + public Dictionary HadInput { get; } = new(128); + + protected TouchActionResult _lastIncomingTouchAction; + + public virtual ISkiaGestureListener OnSkiaGestureEvent(SkiaGesturesParameters args, GestureEventProcessingInfo apply) + { + if (!CanDraw) + return null; + + if (args.Type == _lastIncomingTouchAction && args.Type == TouchActionResult.Up) + { + //a case when we were called for same event by parent and by some top level + //because we previously has input and we got into HadInput to be notified of releases + //so we have set up a filter here + return null; + } + + //todo check latest behaviour! + _lastIncomingTouchAction = args.Type; + + var result = ProcessGestures(args, apply); + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual bool IsGestureForChild(SkiaControlWithRect child, SKPoint point) + { + bool inside = false; + if (child.Control != null && !child.Control.IsDisposing && !child.Control.IsDisposed && + !child.Control.InputTransparent && child.Control.CanDraw) + { + var transformed = child.Control.ApplyTransforms(child.Rect);//todo HitRect + inside = transformed.ContainsInclusive(point.X, point.Y) || child.Control == Superview.FocusedChild; + } + + return inside; + } + + // + // SKPoint childOffset, SKPoint childOffsetDirect, ISkiaGestureListener alreadyConsumed + public static readonly BindableProperty CommandChildTappedProperty = BindableProperty.Create(nameof(CommandChildTapped), typeof(ICommand), + typeof(SkiaControl), + null); + + /// + /// Child was tapped. Will pass the tapped child as parameter. You might want then read child's BindingContext etc.. This works only if your control implements ISkiaGestureListener. + /// + public ICommand CommandChildTapped + { + get { return (ICommand)GetValue(CommandChildTappedProperty); } + set { SetValue(CommandChildTappedProperty, value); } + } + + public virtual ISkiaGestureListener ProcessGestures( + SkiaGesturesParameters args, + GestureEventProcessingInfo apply) + { + if (IsDisposed || IsDisposing) + return null; + + if (Superview == null) + { + //shit happened. we are capturing input but we are not supposed to be on the screen! + Super.Log($"[OnGestureEvent] base captured by unassigned view {this.GetType()} {this.Tag}"); + return null; + } + + if (TouchEffect.LogEnabled) + { + Super.Log($"[BASE] {this.Tag} Got {args.Type}.. {Uid}"); + } + + if (EffectsGestureProcessors.Count > 0) + { + foreach (var effect in EffectsGestureProcessors) + { + effect.ProcessGestures(args, apply); + } + } + + ISkiaGestureListener consumed = null; + ISkiaGestureListener tmpConsumed = apply.alreadyConsumed; + bool manageChildFocus = false; + + if (UsesRenderingTree && RenderTree != null) + { + var thisOffset = TranslateInputCoords(apply.childOffset); + var touchLocationWIthOffset = new SKPoint(args.Event.Location.X + thisOffset.X, args.Event.Location.Y + thisOffset.Y); + + //first process those who already had input + if (HadInput.Count > 0) + { + if ( + args.Type == TouchActionResult.Panning || + args.Type == TouchActionResult.Wheel || + args.Type == TouchActionResult.Up + ) + { + foreach (var hadInput in HadInput.Values) + { + if (!hadInput.CanDraw || hadInput.InputTransparent) + { + continue; + } + consumed = hadInput.OnSkiaGestureEvent(args, + new GestureEventProcessingInfo( + thisOffset, + TranslateInputCoords(apply.childOffsetDirect, false), tmpConsumed)); + + if (consumed != null) + { + if (tmpConsumed == null) + tmpConsumed = consumed; + + if (args.Type != TouchActionResult.Up) + break; + } + } + } + //else + if (HadInput.Count > 0 && args.Type == TouchActionResult.Up) + { + HadInput.Clear(); + } + } + + var hadInputConsumed = consumed; + + //if previously having input didn't keep it + if (consumed == null || args.Type == TouchActionResult.Up) + { + var asSpan = CollectionsMarshal.AsSpan(RenderTree); + for (int i = asSpan.Length - 1; i >= 0; i--) + //for (int i = 0; i < RenderTree.Length; i++) + { + var child = asSpan[i]; + + if (child == Superview.FocusedChild) + manageChildFocus = true; + + ISkiaGestureListener listener = child.Control.GesturesEffect; + if (listener == null && child.Control is ISkiaGestureListener listen) + { + listener = listen; + } + + if (HadInput.Values.Contains(listener) && + ( + args.Type == TouchActionResult.Panning || + args.Type == TouchActionResult.Wheel || + args.Type == TouchActionResult.Up)) + { + continue; + } + + if (listener != null) + { + var forChild = IsGestureForChild(child, touchLocationWIthOffset); + if (forChild) + { + if (args.Type == TouchActionResult.Tapped && CommandChildTapped != null) + { + CommandChildTapped.Execute(child); + } + //Trace.WriteLine($"[HIT] for cell {i} at Y {y:0.0}"); + if (manageChildFocus && listener == Superview.FocusedChild) + { + manageChildFocus = false; + } + + var childOffset = TranslateInputCoords(apply.childOffsetDirect, false); + + if (AddGestures.AttachedListeners.TryGetValue(child.Control, out var effect)) + { + var c = effect.OnSkiaGestureEvent(args, + new GestureEventProcessingInfo( + thisOffset, + childOffset, + apply.alreadyConsumed)); + if (c != null) + { + consumed = effect; + } + } + else + { + var c = listener.OnSkiaGestureEvent(args, + new GestureEventProcessingInfo( + thisOffset, + childOffset, + apply.alreadyConsumed)); + if (c != null) + { + consumed = c; + } + } + + if (consumed != null) + { + if (args.Type != TouchActionResult.Up) + { + HadInput.TryAdd(listener.Uid, consumed); + } + break; + } + } + } + + } + } //end + + if (HadInput.Count > 0 && args.Type == TouchActionResult.Up) + { + HadInput.Clear(); + } + + if (manageChildFocus) + { + Superview.FocusedChild = null; + } + + if (hadInputConsumed != null) + consumed = hadInputConsumed; + } + else + { + //lock (LockIterateListeners) + { + + try + { + if (CheckChildrenGesturesLocked(args.Type)) + return null; + + var point = TranslateInputOffsetToPixels(args.Event.Location, apply.childOffset); + + if (HadInput.Count > 0) + { + if ( + ( + args.Type == TouchActionResult.Panning || + args.Type == TouchActionResult.Wheel || + args.Type == TouchActionResult.Up)) + { + foreach (var hadInput in HadInput.Values) + { + if (!hadInput.CanDraw || hadInput.InputTransparent) + { + continue; + } + consumed = hadInput.OnSkiaGestureEvent(args, + new GestureEventProcessingInfo( + TranslateInputCoords(apply.childOffset, true), + TranslateInputCoords(apply.childOffsetDirect, false), tmpConsumed)); + + if (consumed != null) + { + if (tmpConsumed == null) + tmpConsumed = consumed; + + if (args.Type != TouchActionResult.Up) + break; + } + } + } + //else + if (HadInput.Count > 0 && args.Type == TouchActionResult.Up) + { + HadInput.Clear(); + } + } + + var hadInputConsumed = consumed; + + if (consumed == null || args.Type == TouchActionResult.Up)// !GestureListeners.Contains(consumed)) + foreach (var listener in GestureListeners.GetListeners()) + { + if (!listener.CanDraw || listener.InputTransparent) + continue; + + if (HadInput.Values.Contains(listener) && + ( + args.Type == TouchActionResult.Panning || + args.Type == TouchActionResult.Wheel || + args.Type == TouchActionResult.Up)) + { + continue; + } + + //Debug.WriteLine($"Checking {listener} gestures in {this.Tag} {this}"); + + if (listener == Superview.FocusedChild) + manageChildFocus = true; + + var forChild = IsGestureForChild(listener, point); + + if (TouchEffect.LogEnabled) + { + if (listener is SkiaControl c) + { + Debug.WriteLine($"[BASE] for child {forChild} {c.Tag} at {point.X:0},{point.Y:0} -> {c.HitBoxAuto} "); + } + } + + if (forChild) + { + if (args.Type == TouchActionResult.Tapped && CommandChildTapped != null) + { + CommandChildTapped.Execute(listener); + } + if (manageChildFocus && listener == Superview.FocusedChild) + { + manageChildFocus = false; + } + //Log($"[OnGestureEvent] sent {args.Action} to {listener.Tag}"); + consumed = listener.OnSkiaGestureEvent(args, + new GestureEventProcessingInfo( + TranslateInputCoords(apply.childOffset, true), + TranslateInputCoords(apply.childOffsetDirect, false), + tmpConsumed)); + + if (consumed != null) + { + if (tmpConsumed == null) + tmpConsumed = consumed; + + if (args.Type != TouchActionResult.Up) + { + HadInput.TryAdd(listener.Uid, consumed); + } + break; + } + } + } + + if (HadInput.Count > 0 && args.Type == TouchActionResult.Up) + { + HadInput.Clear(); + } + + if (manageChildFocus) + { + Superview.FocusedChild = null; + } + + if (hadInputConsumed != null) + consumed = hadInputConsumed; + + } + catch (Exception e) + { + Super.Log(e); + } + + + } + } + + return consumed; + } + + + public bool UpdateLocked { get; set; } + + public void LockUpdate(bool value) + { + UpdateLocked = value; + } + + private void Init() + { + NeedMeasure = true; + IsLayoutDirty = true; + + SizeChanged += ViewSizeChanged; + + CalculateMargins(); + 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), + typeof(IDrawnBase), + typeof(SkiaControl), + default(IDrawnBase), + propertyChanged: OnControlParentChanged); + + /// + /// Do not set this directly if you don't know what you are doing, use SetParent() + /// + public new IDrawnBase Parent + { + get { return (IDrawnBase)GetValue(ParentProperty); } + set { SetValue(ParentProperty, value); } + } + + + + #region View + + + public static readonly BindableProperty VerticalOptionsProperty = BindableProperty.Create(nameof(VerticalOptions), + typeof(LayoutOptions), + typeof(SkiaControl), + LayoutOptions.Start, + propertyChanged: NeedInvalidateMeasure); + + public LayoutOptions VerticalOptions + { + get { return (LayoutOptions)GetValue(VerticalOptionsProperty); } + set { SetValue(VerticalOptionsProperty, value); } + } + + public static readonly BindableProperty HorizontalOptionsProperty = BindableProperty.Create(nameof(HorizontalOptions), + typeof(LayoutOptions), + typeof(SkiaControl), + LayoutOptions.Start, + propertyChanged: NeedInvalidateMeasure); + + public LayoutOptions HorizontalOptions + { + get { return (LayoutOptions)GetValue(HorizontalOptionsProperty); } + set { SetValue(HorizontalOptionsProperty, value); } + } + + /// + /// todo override for templated skialayout to use ViewsProvider + /// + /// + protected virtual void OnParentVisibilityChanged(bool newvalue) + { + if (!newvalue) + { + //DestroyRenderingObject(); + } + + Superview?.SetViewTreeVisibilityByParent(this, newvalue); + + if (!newvalue) + { + if (this.UsingCacheType == SkiaCacheType.GPU) + { + RenderObject = null; + } + } + + if (!IsVisible) + { + //though shell not pass + return; + } + + try + { + foreach (var child in Views) + { + child.OnParentVisibilityChanged(newvalue); + } + } + catch (Exception e) + { + Super.Log(e); + } + } + + /// + /// todo override for templated skialayout to use ViewsProvider + /// + /// + public virtual void OnVisibilityChanged(bool newvalue) + { + if (!newvalue) + { + if (this.UsingCacheType == SkiaCacheType.GPU) + { + RenderObject = null; + } + //DestroyRenderingObject(); + } + // need to this to: + // disable child gesture listeners + // pause hidden animations + try + { + var pass = IsVisible && newvalue; + foreach (var child in Views) + { + child.OnParentVisibilityChanged(pass); + } + + Superview?.UpdateRenderingChains(this); + } + catch (Exception e) + { + Super.Log(e); + } + } + + /// + /// Base performs some cleanup actions with Superview + /// + public virtual void OnDisposing() + { + ClippedBy = null; + Disposing?.Invoke(this, null); + Superview?.UnregisterGestureListener(this as ISkiaGestureListener); + Superview?.UnregisterAllAnimatorsByParent(this); + } + + protected object lockPausingAnimators = new(); + + public virtual void PauseAllAnimators() + { + var paused = Superview?.SetPauseStateOfAllAnimatorsByParent(this, true); + } + + public virtual void ResumePausedAnimators() + { + var resumed = Superview?.SetPauseStateOfAllAnimatorsByParent(this, false); + } + + public static readonly BindableProperty IsGhostProperty = BindableProperty.Create(nameof(IsGhost), + typeof(bool), + typeof(SkiaControl), + false, + propertyChanged: NeedDraw); + public bool IsGhost + { + get { return (bool)GetValue(IsGhostProperty); } + set { SetValue(IsGhostProperty, value); } + } + + public static readonly BindableProperty IgnoreChildrenInvalidationsProperty + = BindableProperty.Create(nameof(IgnoreChildrenInvalidations), + typeof(bool), typeof(SkiaControl), + false); + + public bool IgnoreChildrenInvalidations + { + get + { + return (bool)GetValue(IgnoreChildrenInvalidationsProperty); + } + set + { + SetValue(IgnoreChildrenInvalidationsProperty, value); + } + } + + + + + #region FillGradient + + public static readonly BindableProperty FillGradientProperty = BindableProperty.Create(nameof(FillGradient), + typeof(SkiaGradient), typeof(SkiaControl), + null, + propertyChanged: SetupFillGradient); + + private static void SetupFillGradient(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaControl control) + { + if (newvalue is SkiaGradient gradient) + { + gradient.BindingContext = control.BindingContext; + } + control.Update(); + } + } + + public SkiaGradient FillGradient + { + get { return (SkiaGradient)GetValue(FillGradientProperty); } + set { SetValue(FillGradientProperty, value); } + } + + public bool HasFillGradient + { + get + { + return this.FillGradient != null && this.FillGradient.Type != GradientType.None; + } + } + + + #endregion + + public virtual SKSize GetSizeRequest(float widthConstraint, float heightConstraint, bool insideLayout) + { + widthConstraint *= (float)this.WidthRequestRatio; + heightConstraint *= (float)this.HeightRequestRatio; + + if (LockRatio > 0) + { + var lockValue = (float)SmartMax(widthConstraint, heightConstraint); + return new SKSize(lockValue, lockValue); + } + + if (LockRatio < 0) + { + var lockValue = (float)SmartMin(widthConstraint, heightConstraint); + return new SKSize(lockValue, lockValue); + } + + return new SKSize(widthConstraint, heightConstraint); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static float SmartMax(float a, float b) + { + if (!float.IsFinite(a) || float.IsFinite(b) && b > a) + return b; + return a; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static float SmartMin(float a, float b) + { + if (!float.IsFinite(a) || float.IsFinite(a) && b < a) + return b; + return a; + } + + public static readonly BindableProperty ViewportHeightLimitProperty = BindableProperty.Create( + nameof(ViewportHeightLimit), + typeof(double), + typeof(SkiaControl), + -1.0, propertyChanged: NeedInvalidateViewport); + + /// + /// Will be used inside GetDrawingRectWithMargins to limit the height of the DrawingRect + /// + public double ViewportHeightLimit + { + get { return (double)GetValue(ViewportHeightLimitProperty); } + set { SetValue(ViewportHeightLimitProperty, value); } + } + + public static readonly BindableProperty ViewportWidthLimitProperty = BindableProperty.Create( + nameof(ViewportWidthLimit), + typeof(double), + typeof(SkiaControl), + -1.0, propertyChanged: NeedInvalidateViewport); + + /// + /// Will be used inside GetDrawingRectWithMargins to limit the width of the DrawingRect + /// + public double ViewportWidthLimit + { + get { return (double)GetValue(ViewportWidthLimitProperty); } + set { SetValue(ViewportWidthLimitProperty, value); } + } + + + private double _height = -1; + public new double Height + { + get + { + return _height; + } + set + { + if (_height != value) + { + _height = value; + OnPropertyChanged(); + } + } + } + + private double _width = -1; + public new double Width + { + get + { + return _width; + } + set + { + if (_width != value) + { + _width = value; + OnPropertyChanged(); + } + } + } + + /// + /// Please use ScaleX, ScaleY instead of this maui property + /// + public new double Scale + { + get + { + return Math.Min(ScaleX, ScaleY); + } + set + { + ScaleX = value; + ScaleY = value; + } + } + + + public static readonly BindableProperty TagProperty = BindableProperty.Create(nameof(Tag), + typeof(string), + typeof(SkiaControl), + string.Empty, + propertyChanged: NeedDraw); + public string Tag + { + get { return (string)GetValue(TagProperty); } + set { SetValue(TagProperty, value); } + } + + + public static readonly BindableProperty LockRatioProperty = BindableProperty.Create(nameof(LockRatio), + typeof(double), typeof(SkiaControl), + 0.0, + propertyChanged: NeedInvalidateMeasure); + /// + /// Locks the final size to the min (-1.0 -> 0.0) or max (0.0 -> 1.0) of the provided size. + /// + public double LockRatio + { + get { return (double)GetValue(LockRatioProperty); } + set { SetValue(LockRatioProperty, value); } + } + + public static readonly BindableProperty HeightRequestRatioProperty = BindableProperty.Create( + nameof(HeightRequestRatio), + typeof(double), + typeof(SkiaControl), + 1.0, + propertyChanged: NeedInvalidateMeasure); + + /// + /// HeightRequest Multiplier, default is 1.0 + /// + public double HeightRequestRatio + { + get { return (double)GetValue(HeightRequestRatioProperty); } + set { SetValue(HeightRequestRatioProperty, value); } + } + + public static readonly BindableProperty WidthRequestRatioProperty = BindableProperty.Create( + nameof(WidthRequestRatio), + typeof(double), + typeof(SkiaControl), + 1.0, + propertyChanged: NeedInvalidateMeasure); + + /// + /// WidthRequest Multiplier, default is 1.0 + /// + public double WidthRequestRatio + { + get { return (double)GetValue(WidthRequestRatioProperty); } + set { SetValue(WidthRequestRatioProperty, value); } + } + + public static readonly BindableProperty HorizontalFillRatioProperty = BindableProperty.Create( + nameof(HorizontalFillRatio), + typeof(double), + typeof(SkiaControl), + 1.0, + propertyChanged: NeedInvalidateMeasure); + + public double HorizontalFillRatio + { + get { return (double)GetValue(HorizontalFillRatioProperty); } + set { SetValue(HorizontalFillRatioProperty, value); } + } + + public static readonly BindableProperty VerticalFillRatioProperty = BindableProperty.Create( + nameof(VerticalFillRatio), + typeof(double), + typeof(SkiaControl), + 1.0, + propertyChanged: NeedInvalidateMeasure); + + public double VerticalFillRatio + { + get { return (double)GetValue(VerticalFillRatioProperty); } + set { SetValue(VerticalFillRatioProperty, value); } + } + + public static readonly BindableProperty HorizontalPositionOffsetRatioProperty = BindableProperty.Create( + nameof(HorizontalPositionOffsetRatio), + typeof(double), + typeof(SkiaControl), + 0.0, + propertyChanged: NeedDraw); + + public double HorizontalPositionOffsetRatio + { + get { return (double)GetValue(HorizontalPositionOffsetRatioProperty); } + set { SetValue(HorizontalPositionOffsetRatioProperty, value); } + } + + public static readonly BindableProperty VerticalPositionOffsetRatioProperty = BindableProperty.Create( + nameof(VerticalPositionOffsetRatio), + typeof(double), + typeof(SkiaControl), + 0.0, + propertyChanged: NeedDraw); + + public double VerticalPositionOffsetRatio + { + get { return (double)GetValue(VerticalPositionOffsetRatioProperty); } + set { SetValue(VerticalPositionOffsetRatioProperty, value); } + } + + public static readonly BindableProperty FillBlendModeProperty = BindableProperty.Create(nameof(FillBlendMode), + typeof(SKBlendMode), typeof(SkiaControl), + SKBlendMode.SrcOver, + propertyChanged: NeedDraw); + public SKBlendMode FillBlendMode + { + get { return (SKBlendMode)GetValue(FillBlendModeProperty); } + set { SetValue(FillBlendModeProperty, value); } + } + + /* + + disabled this for fps... we can use drawingrect.x and y /renderingscale but OnPropertyChanged calls might slow us down?.. + + private double _X; + public double X + { + get + { + return _X; + } + set + { + if (_X != value) + { + _X = value; + OnPropertyChanged(); + } + } + } + + private double _Y; + public double Y + { + get + { + return _Y; + } + set + { + if (_Y != value) + { + _Y = value; + OnPropertyChanged(); + } + } + } + + */ + + //public static readonly BindableProperty HeightProperty = BindableProperty.Create(nameof(Height), + // typeof(double), typeof(SkiaControl), + // -1.0); + //public double Height + //{ + // get { return (double)GetValue(HeightProperty); } + // set { SetValue(HeightProperty, value); } + //} + + + //public static readonly BindableProperty WidthProperty = BindableProperty.Create(nameof(Width), + // typeof(double), typeof(SkiaControl), + // -1.0); + //public double Width + //{ + // get { return (double)GetValue(WidthProperty); } + // set { SetValue(WidthProperty, value); } + //} + + + /* + + public static readonly BindableProperty OpacityProperty = BindableProperty.Create(nameof(Opacity), + typeof(double), typeof(SkiaControl), + 0.0, + propertyChanged: RedrawCanvas); + public double Opacity + { + get { return (double)GetValue(OpacityProperty); } + set { SetValue(OpacityProperty, value); } + } + */ + + + public static readonly BindableProperty SkewXProperty + = BindableProperty.Create(nameof(SkewX), + typeof(float), typeof(SkiaControl), + 0.0f, propertyChanged: NeedRepaint); + + public float SkewX + { + get + { + return (float)GetValue(SkewXProperty); + } + set + { + SetValue(SkewXProperty, value); + } + } + + public static readonly BindableProperty SkewYProperty + = BindableProperty.Create(nameof(SkewY), + typeof(float), typeof(SkiaControl), + 0.0f, propertyChanged: NeedRepaint); + + public float SkewY + { + get + { + return (float)GetValue(SkewYProperty); + } + set + { + SetValue(SkewYProperty, value); + } + } + + public static readonly BindableProperty CameraAngleXProperty + = BindableProperty.Create(nameof(CameraAngleX), + typeof(float), typeof(SkiaControl), + 0.0f, propertyChanged: NeedRepaint); + + public float CameraAngleX + { + get + { + return (float)GetValue(CameraAngleXProperty); + } + set + { + SetValue(CameraAngleXProperty, value); + } + } + + public static readonly BindableProperty CameraAngleYProperty + = BindableProperty.Create(nameof(CameraAngleY), + typeof(float), typeof(SkiaControl), + 0.0f, propertyChanged: NeedRepaint); + + public float CameraAngleY + { + get + { + return (float)GetValue(CameraAngleYProperty); + } + set + { + SetValue(CameraAngleYProperty, value); + } + } + + public static readonly BindableProperty CameraAngleZProperty + = BindableProperty.Create(nameof(CameraAngleZ), + typeof(float), typeof(SkiaControl), + 0.0f, propertyChanged: NeedRepaint); + + public float CameraAngleZ + { + get + { + return (float)GetValue(CameraAngleZProperty); + } + set + { + SetValue(CameraAngleZProperty, value); + } + } + + public static readonly BindableProperty CameraTranslationZProperty + = BindableProperty.Create(nameof(CameraTranslationZ), + typeof(float), typeof(SkiaControl), + 0.0f, propertyChanged: NeedRepaint); + + public float CameraTranslationZ + { + get + { + return (float)GetValue(CameraTranslationZProperty); + } + set + { + SetValue(CameraTranslationZProperty, value); + } + } + + + + //public new static readonly BindableProperty VerticalOptionsProperty = BindableProperty.Create(nameof(VerticalOptions), + // typeof(LayoutOptions), + // typeof(SkiaControl), + // LayoutOptions.Start, + // propertyChanged: NeedInvalidateMeasure); + + //public new LayoutOptions VerticalOptions + //{ + // get { return (LayoutOptions)GetValue(VerticalOptionsProperty); } + // set { SetValue(VerticalOptionsProperty, value); } + //} + + //public new static readonly BindableProperty HorizontalOptionsProperty = BindableProperty.Create(nameof(HorizontalOptions), + // typeof(LayoutOptions), + // typeof(SkiaControl), + // LayoutOptions.Start, + // propertyChanged: NeedInvalidateMeasure); + + //public new LayoutOptions HorizontalOptions + //{ + // get { return (LayoutOptions)GetValue(HorizontalOptionsProperty); } + // set { SetValue(HorizontalOptionsProperty, value); } + //} + + + + + public static readonly BindableProperty Perspective1Property + = BindableProperty.Create(nameof(Perspective1), + typeof(float), typeof(SkiaControl), + 0.0f, propertyChanged: NeedRepaint); + + public float Perspective1 + { + get + { + return (float)GetValue(Perspective1Property); + } + set + { + SetValue(Perspective1Property, value); + } + } + + public static readonly BindableProperty Perspective2Property + = BindableProperty.Create(nameof(Perspective2), + typeof(float), typeof(SkiaControl), + 0.0f, propertyChanged: NeedRepaint); + + public float Perspective2 + { + get + { + return (float)GetValue(Perspective2Property); + } + set + { + SetValue(Perspective2Property, value); + } + } + + public static readonly BindableProperty TransformPivotPointXProperty + = BindableProperty.Create(nameof(TransformPivotPointX), + typeof(double), typeof(SkiaControl), + 0.5, propertyChanged: NeedRepaint); + public double TransformPivotPointX + { + get + { + return (double)GetValue(TransformPivotPointXProperty); + } + set + { + SetValue(TransformPivotPointXProperty, value); + } + } + + public static readonly BindableProperty TransformPivotPointYProperty + = BindableProperty.Create(nameof(TransformPivotPointY), + typeof(double), typeof(SkiaControl), + 0.5, propertyChanged: NeedRepaint); + + public double TransformPivotPointY + { + get + { + return (double)GetValue(TransformPivotPointYProperty); + } + set + { + SetValue(TransformPivotPointYProperty, value); + } + } + + + 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) + { + if (bindable is SkiaControl control) + { + control.OnParentChanged(newvalue as IDrawnBase, oldvalue as IDrawnBase); + } + } + + public new event EventHandler ParentChanged; + + public static readonly BindableProperty AdjustClippingProperty = BindableProperty.Create( + nameof(AdjustClipping), + typeof(Thickness), + typeof(SkiaControl), + Thickness.Zero, + propertyChanged: NeedInvalidateMeasure); + + public Thickness AdjustClipping + { + get { return (Thickness)GetValue(AdjustClippingProperty); } + set { SetValue(AdjustClippingProperty, value); } + } + + + public static readonly BindableProperty PaddingProperty = BindableProperty.Create(nameof(Padding), typeof(Thickness), + typeof(SkiaControl), Thickness.Zero, + propertyChanged: NeedInvalidateMeasure); + public Thickness Padding + { + get { return (Thickness)GetValue(PaddingProperty); } + set { SetValue(PaddingProperty, value); } + } + + public static readonly BindableProperty MarginProperty = BindableProperty.Create(nameof(Margin), typeof(Thickness), + typeof(SkiaControl), Thickness.Zero, + propertyChanged: NeedInvalidateMeasure); + public Thickness Margin + { + get { return (Thickness)GetValue(MarginProperty); } + set { SetValue(MarginProperty, value); } + } + + public static readonly BindableProperty AddMarginTopProperty = BindableProperty.Create( + nameof(AddMarginTop), + typeof(double), + typeof(SkiaControl), + 0.0, propertyChanged: NeedInvalidateMeasure); + + public double AddMarginTop + { + get { return (double)GetValue(AddMarginTopProperty); } + set { SetValue(AddMarginTopProperty, value); } + } + + public static readonly BindableProperty AddMarginBottomProperty = BindableProperty.Create( + nameof(AddMarginBottom), + typeof(double), + typeof(SkiaControl), + 0.0, propertyChanged: NeedInvalidateMeasure); + + public double AddMarginBottom + { + get { return (double)GetValue(AddMarginBottomProperty); } + set { SetValue(AddMarginBottomProperty, value); } + } + + public static readonly BindableProperty AddMarginLeftProperty = BindableProperty.Create( + nameof(AddMarginLeft), + typeof(double), + typeof(SkiaControl), + 0.0, propertyChanged: NeedInvalidateMeasure); + + public double AddMarginLeft + { + get { return (double)GetValue(AddMarginLeftProperty); } + set { SetValue(AddMarginLeftProperty, value); } + } + + public static readonly BindableProperty AddMarginRightProperty = BindableProperty.Create( + nameof(AddMarginRight), + typeof(double), + typeof(SkiaControl), + 0.0, propertyChanged: NeedInvalidateMeasure); + + public double AddMarginRight + { + get { return (double)GetValue(AddMarginRightProperty); } + set { SetValue(AddMarginRightProperty, value); } + } + + /// + /// Total calculated margins in points + /// + public Thickness Margins + { + get => _margins; + protected set + { + if (value.Equals(_margins)) return; + _margins = value; + OnPropertyChanged(); + } + } + + public static readonly BindableProperty SpacingProperty = BindableProperty.Create(nameof(Spacing), typeof(double), + typeof(SkiaControl), + 8.0, + propertyChanged: NeedInvalidateMeasure); + public double Spacing + { + get { return (double)GetValue(SpacingProperty); } + set { SetValue(SpacingProperty, value); } + } + + public static readonly BindableProperty AddTranslationYProperty = BindableProperty.Create( + nameof(AddTranslationY), + typeof(double), + typeof(SkiaControl), + 0.0, propertyChanged: NeedRepaint); + + public double AddTranslationY + { + get { return (double)GetValue(AddTranslationYProperty); } + set { SetValue(AddTranslationYProperty, value); } + } + + public static readonly BindableProperty AddTranslationXProperty = BindableProperty.Create( + nameof(AddTranslationX), + typeof(double), + typeof(SkiaControl), + 0.0, propertyChanged: NeedRepaint); + + public double AddTranslationX + { + get { return (double)GetValue(AddTranslationXProperty); } + set { SetValue(AddTranslationXProperty, value); } + } + + public static readonly BindableProperty ExpandCacheRecordingAreaProperty + = BindableProperty.Create(nameof(ExpandCacheRecordingArea), + typeof(double), typeof(SkiaControl), + 0.0, propertyChanged: NeedDraw); + /// + /// Normally cache is recorded inside DrawingRect, but you might want to exapnd this to include shadows around, for example. + /// Specify number of points by which you want to expand the recording area. + /// Also you might maybe want to include a bigger area if your control is not inside the DrawingRect due to transforms/translations. + /// Override GetCacheRecordingArea method for a similar action. + /// + public double ExpandCacheRecordingArea + { + get + { + return (double)GetValue(ExpandCacheRecordingAreaProperty); + } + set + { + SetValue(ExpandCacheRecordingAreaProperty, value); + } + } + + /* + public static readonly BindableProperty AlignContentVerticalProperty = BindableProperty.Create(nameof(AlignContentVertical), + typeof(LayoutOptions), + typeof(SkiaControl), + LayoutOptions.Start, + propertyChanged: NeedInvalidateMeasure); + public LayoutOptions AlignContentVertical + { + get { return (LayoutOptions)GetValue(AlignContentVerticalProperty); } + set { SetValue(AlignContentVerticalProperty, value); } + } + + public static readonly BindableProperty AlignContentHorizontalProperty = BindableProperty.Create(nameof(AlignContentHorizontal), + typeof(LayoutOptions), + typeof(SkiaControl), + LayoutOptions.Start, + propertyChanged: NeedInvalidateMeasure); + public LayoutOptions AlignContentHorizontal + { + get { return (LayoutOptions)GetValue(AlignContentHorizontalProperty); } + set { SetValue(AlignContentHorizontalProperty, value); } + } + */ + + public static readonly BindableProperty IsClippedToBoundsProperty = BindableProperty.Create(nameof(IsClippedToBounds), + typeof(bool), typeof(SkiaControl), false, + propertyChanged: NeedInvalidateMeasure); + /// + /// This cuts shadows etc. You might want to enable it for some cases as it speeds up the rendering, it is False by default + /// + public bool IsClippedToBounds + { + get { return (bool)GetValue(IsClippedToBoundsProperty); } + set { SetValue(IsClippedToBoundsProperty, value); } + } + + public static readonly BindableProperty ClipEffectsProperty = BindableProperty.Create(nameof(ClipEffects), + typeof(bool), typeof(SkiaControl), true, + propertyChanged: NeedInvalidateMeasure); + /// + /// This cuts shadows etc + /// + public bool ClipEffects + { + get { return (bool)GetValue(ClipEffectsProperty); } + set { SetValue(ClipEffectsProperty, value); } + } + + public static readonly BindableProperty BindableTriggerProperty = BindableProperty.Create(nameof(BindableTrigger), + typeof(object), typeof(SkiaControl), + null, + propertyChanged: TriggerPropertyChanged); + + private static void TriggerPropertyChanged(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaControl control) + { + control.OnTriggerChanged(); + } + } + + public virtual void OnTriggerChanged() + { + + } + + public object BindableTrigger + { + get { return (object)GetValue(BindableTriggerProperty); } + set { SetValue(BindableTriggerProperty, value); } + } + + public static readonly BindableProperty Value1Property = BindableProperty.Create(nameof(Value1), + typeof(double), typeof(SkiaControl), + 0.0, + propertyChanged: NeedDraw); + public double Value1 + { + get { return (double)GetValue(Value1Property); } + set { SetValue(Value1Property, value); } + } + + public static readonly BindableProperty Value2Property = BindableProperty.Create(nameof(Value2), + typeof(double), typeof(SkiaControl), + 0.0, + propertyChanged: NeedDraw); + public double Value2 + { + get { return (double)GetValue(Value2Property); } + set { SetValue(Value2Property, value); } + } + + public static readonly BindableProperty Value3Property = BindableProperty.Create(nameof(Value3), + typeof(double), typeof(SkiaControl), + 0.0, + propertyChanged: NeedDraw); + public double Value3 + { + get { return (double)GetValue(Value3Property); } + set { SetValue(Value3Property, value); } + } + + //------------------------------------------------------------- + // Value4 + //------------------------------------------------------------- + public static readonly BindableProperty Value4Property = BindableProperty.Create(nameof(Value4), + typeof(double), typeof(SkiaControl), + 0.0, + propertyChanged: NeedDraw); + public double Value4 + { + get { return (double)GetValue(Value4Property); } + set { SetValue(Value4Property, value); } + } + + + public static readonly BindableProperty RenderingScaleProperty = BindableProperty.Create(nameof(RenderingScale), + typeof(float), typeof(SkiaControl), + -1.0f, propertyChanged: NeedUpdateScale); + + private static void NeedUpdateScale(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SkiaControl control) + { + control.OnScaleChanged(); + } + } + + public float RenderingScale + { + get + { + var value = -1f; + try + { + value = (float)GetValue(RenderingScaleProperty); + } + catch (Exception e) + { + Super.Log(e); //catching Nullable object must have a value, is this because of NET8? + } + if (value <= 0) + { + return GetDensity(); + } + return value; + } + set + { + SetValue(RenderingScaleProperty, value); + } + } + + + + //public double RenderingScaleSafe + //{ + // get + // { + // var value = Density; + // if (value == 0) + // value = 1; + + // if (RenderingScale < 0 || RenderingScale == 0) + // { + // return value; + // } + + // return RenderingScale; + // } + //} + + + + #endregion + + + public SKRect RenderedAtDestination { get; set; } + + public virtual void OnScaleChanged() + { + InvalidateMeasure(); + } + + + private Style _currentStyle; + private void SubscribeToStyleProperties() + { + UnsubscribeFromOldStyle(); + + _currentStyle = Style; + + if (_currentStyle != null) + { + + } + } + + private void UnsubscribeFromOldStyle() + { + if (_currentStyle != null) + { + + } + } + + public virtual void SetPropertyValue(BindableProperty property, object value) + { + this.SetValue(property, value); + } + + + public Guid Uid { get; set; } = Guid.NewGuid(); + + //todo check adapt for MAUI + + public static double DegreesToRadians(double value) + { + return ((value * Math.PI) / 180); + } + public static double RadiansToDegrees(double value) + { + return value * 180 / Math.PI; + } + + public static (double X1, double Y1, double X2, double Y2) LinearGradientAngleToPoints(double direction) + { + //adapt to css style + direction -= 90; + + //allow negative angles + if (direction < 0) + direction = 360 + direction; + + if (direction > 360) + direction = 360; + + (double x, double y) PointOfAngle(double a) + { + return (x: Math.Cos(a), y: Math.Sin(a)); + }; + + + + var eps = Math.Pow(2, -52); + var angle = (direction % 360); + var startPoint = PointOfAngle(DegreesToRadians(180 - angle)); + var endPoint = PointOfAngle(DegreesToRadians(360 - angle)); + + if (startPoint.x <= 0 || Math.Abs(startPoint.x) <= eps) + startPoint.x = 0; + + if (startPoint.y <= 0 || Math.Abs(startPoint.y) <= eps) + startPoint.y = 0; + + if (endPoint.x <= 0 || Math.Abs(endPoint.x) <= eps) + endPoint.x = 0; + + if (endPoint.y <= 0 || Math.Abs(endPoint.y) <= eps) + endPoint.y = 0; + + return (startPoint.x, startPoint.y, endPoint.x, endPoint.y); + } + + + /// + /// Dispose with needed delay. + /// + /// + public virtual void DisposeObject(IDisposable disposable) + { + if (disposable != null) + { + if (Superview is DrawnView view) + { + view.ToBeDisposed.Enqueue(disposable); + } + else + { + Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => + { + disposable?.Dispose(); + }); + } + } + } + + private void ViewSizeChanged(object sender, EventArgs e) + { + OnSizeChanged(); + } + + protected virtual void OnSizeChanged() + { + + } + + public Action Clipping { get; set; } + + public SkiaControl ClippedBy { get; set; } + + + /// + /// Optional scene hero control identifier + /// + public string Hero { get; set; } + + public int ContextIndex { get; set; } = -1; + + public bool IsRootView() + { + return this.Parent == null; + } + + /// + /// destination in PIXELS, requests in UNITS + /// + /// + /// + /// + /// + /// + public ScaledSize DefineAvailableSize(SKRect destination, + float widthRequest, float heightRequest, float scale) + { + var rectWidth = destination.Width; + var wants = widthRequest * scale; + if (wants >= 0 && wants < rectWidth) + rectWidth = (int)wants; + + var rectHeight = destination.Height; + wants = heightRequest * scale; + if (wants >= 0 && wants < rectHeight) + rectHeight = (int)wants; + + return ScaledSize.FromPixels(rectWidth, rectHeight, scale); + } + + /// + /// Set this by parent if needed, normally child can detect this itsself. If true will call Arrange when drawing. + /// + public bool IsLayoutDirty + { + get => _isLayoutDirty; + set + { + if (value == _isLayoutDirty) return; + _isLayoutDirty = value; + OnPropertyChanged(); + } + } + + /// + /// destination in PIXELS, requests in UNITS. resulting Destination prop will be filed in PIXELS. + /// Not using Margins nor Padding + /// Children are responsible to apply Padding to their content and to apply Margin to destination when measuring and drawing + /// + /// PIXELS + /// UNITS + /// UNITS + /// + public SKRect CalculateLayout(SKRect destination, float widthRequest, float heightRequest, float scale) + { + //if (widthRequest == 0 || heightRequest == 0) + //{ + // return new SKRect(0, 0, 0, 0); + //} + + var rectAvailable = DefineAvailableSize(destination, widthRequest, heightRequest, scale); + + var useMaxWidth = rectAvailable.Pixels.Width; + var useMaxHeight = rectAvailable.Pixels.Height; + var availableWidth = destination.Width; + var availableHeight = destination.Height; + + var layoutHorizontal = new LayoutOptions(HorizontalOptions.Alignment, HorizontalOptions.Expands); + var layoutVertical = new LayoutOptions(VerticalOptions.Alignment, VerticalOptions.Expands); + + // initial fill + var left = destination.Left; + var top = destination.Top; + var right = 0f; + var bottom = 0f; + + // layoutHorizontal + switch (layoutHorizontal.Alignment) + { + + case LayoutAlignment.Center when float.IsFinite(availableWidth): + { + left += (float)Math.Round(availableWidth / 2.0f - useMaxWidth / 2.0f); + right = left + useMaxWidth; + + if (left < destination.Left) + { + left = destination.Left; + right = left + useMaxWidth; + } + + if (right > destination.Right) + { + right = destination.Right; + } + + break; + } + case LayoutAlignment.End when float.IsFinite(destination.Right): + { + right = destination.Right; + left = right - useMaxWidth; + if (left < destination.Left) + { + left = destination.Left; + } + + break; + } + case LayoutAlignment.Fill: + case LayoutAlignment.Start: + default: + { + right = left + useMaxWidth; + if (right > destination.Right) + { + right = destination.Right; + } + + break; + } + } + + // VerticalOptions + switch (layoutVertical.Alignment) + { + + case LayoutAlignment.Center when float.IsFinite(availableHeight): + { + top += (float)Math.Round(availableHeight / 2.0f - useMaxHeight / 2.0f); + bottom = top + useMaxHeight; + + if (top < destination.Top) + { + top = destination.Top; + bottom = top + useMaxHeight; + } + + else if (bottom > destination.Bottom) + { + bottom = destination.Bottom; + top = bottom - useMaxHeight; + } + + break; + } + case LayoutAlignment.End when float.IsFinite(destination.Bottom): + { + bottom = destination.Bottom; + top = bottom - useMaxHeight; + if (top < destination.Top) + { + top = destination.Top; + } + + break; + } + case LayoutAlignment.Start: + case LayoutAlignment.Fill: + default: + + bottom = top + useMaxHeight; + if (bottom > destination.Bottom) + { + bottom = destination.Bottom; + } + break; + + } + + var layout = new SKRect(left, top, right, bottom); + + var offsetX = 0f; + var offsetY = 0f; + if (float.IsFinite(availableHeight)) + { + offsetY = (float)VerticalPositionOffsetRatio * layout.Height; + } + if (float.IsFinite(availableWidth)) + { + offsetX = (float)HorizontalPositionOffsetRatio * layout.Width; + } + + layout.Offset(offsetX, offsetY); + + return layout; + } + + /* + public SKRect CalculateLayout(SKRect destination, float widthRequest, float heightRequest, float scale) + { + if (widthRequest == 0 || heightRequest == 0) + { + return new SKRect(0, 0, 0, 0); + } + + var rectAvailable = DefineAvailableSize(destination, widthRequest, heightRequest, scale); + + var availableWidth = destination.Width; + var availableHeight = destination.Height; + + var layoutHorizontal = new LayoutOptions(HorizontalOptions.Alignment, HorizontalOptions.Expands); + var layoutVertical = new LayoutOptions(VerticalOptions.Alignment, HorizontalOptions.Expands); + + + //initial fill + var left = destination.Left; + var top = destination.Top; + var right = left + rectAvailable.Pixels.Width; + var bottom = top + rectAvailable.Pixels.Height; + + //layoutHorizontal + if (layoutHorizontal.Alignment == LayoutAlignment.Center && float.IsFinite(availableWidth)) + { + //center + left += (availableWidth - rectAvailable.Pixels.Width) / 2.0f; + right = left + rectAvailable.Pixels.Width; + + if (left < destination.Left) + { + left = (float)(destination.Left); + right = left + rectAvailable.Pixels.Width; + } + + if (right > destination.Right) + { + right = (float)(destination.Right); + } + + } + else + if (layoutHorizontal.Alignment == LayoutAlignment.End) + { + //end + right = destination.Right; + left = right - rectAvailable.Pixels.Width; + if (left < destination.Left) + { + left = (float)(destination.Left); + } + } + else + { + //start or fill + right = left + rectAvailable.Pixels.Width; + if (right > destination.Right) + { + right = (float)(destination.Right); + } + + + } + + //VerticalOptions + if (layoutVertical.Alignment == LayoutAlignment.Center) + { + //center + top += availableHeight / 2.0f - rectAvailable.Pixels.Height / 2.0f; + bottom = top + rectAvailable.Pixels.Height; + if (top < destination.Top) + { + top = (float)(destination.Top); + bottom = top + rectAvailable.Pixels.Height; + } + else + if (bottom > destination.Bottom) + { + bottom = (float)(destination.Bottom); + top = bottom - rectAvailable.Pixels.Height; + } + } + else + if (layoutVertical.Alignment == LayoutAlignment.End && double.IsFinite(destination.Bottom)) + { + //end + bottom = destination.Bottom; + top = bottom - rectAvailable.Pixels.Height; + if (top < destination.Top) + { + top = (float)(destination.Top); + } + + } + else + { + //start or fill + bottom = top + rectAvailable.Pixels.Height; + if (bottom > destination.Bottom) + { + bottom = (float)(destination.Bottom); + } + + } + + var ret = new SKRect((float)left, (float)top, (float)right, (float)bottom); + + //Debug.WriteLine($"[Layout] '{Tag}' {ret.Left - destination.Left:0.0}-{destination.Right - ret.Right:0.0} "); + + return ret; + } + */ + + + private ScaledSize _contentSize = new(); + public ScaledSize ContentSize + { + get + { + return _contentSize; + } + protected set + { + if (_contentSize != value) + { + _contentSize = value; + OnPropertyChanged(); + } + } + } + + + protected bool WasMeasured; + + protected virtual void OnDrawingSizeChanged() + { + + } + + protected virtual void AdaptCachedLayout(SKRect destination, float scale) + { + //adapt cache to current request + var newDestination = ArrangedDestination; + newDestination.Offset(destination.Left, destination.Top); + + Destination = newDestination; + DrawingRect = GetDrawingRectWithMargins(newDestination, scale); + + IsLayoutDirty = false; + } + + private SKRect _lastArrangedFor = new(); + private float _lastArrangedWidth; + private float _lastArrangedHeight; + + public float _lastMeasuredForWidth { get; protected set; } + public float _lastMeasuredForHeight { get; protected set; } + + /// + /// This is the destination in PIXELS with margins applied, using this to paint background. Since we enabled subpixel drawing (for smooth scroll etc) expect this to have non-rounded values, use CompareRects and similar for comparison. + /// + public SKRect DrawingRect { get; set; } + + /// + /// Overriding VisualElement property, use DrawingRect instead. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public new Rect Bounds + { + get + { + return DrawingRect.ToMauiRectangle(); + } + private set + { + throw new NotImplementedException("Use DrawingRect instead."); + } + } + + /// + /// ISkiaGestureListener impl + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual bool HitIsInside(float x, float y) + { + var hitbox = HitBoxAuto; + + //if (UsingCacheType != SkiaCacheType.None && RenderObject != null) + //{ + // var offsetCacheX = Math.Abs(DrawingRect.Left - RenderObject.Bounds.Left); + // var offsetCacheY = Math.Abs(DrawingRect.Top - RenderObject.Bounds.Top); + + // hitbox.Offset(offsetCacheX,offsetCacheY); + //} + + return hitbox.ContainsInclusive(x, y); ; + } + + /// + /// This can be absolutely false if we are inside a cached + /// rendering object parent that already moved somewhere. + /// So coords will be of the moment we were first drawn, + /// while if cached parent moved, our coords might differ. + /// todo detect if parent is cached somewhere and offset hotbox by cached parent movement offset... + /// todo think about it baby =) meanwhile just do not set gestures below cached level + /// + public virtual SKRect HitBoxAuto + { + get + { + var moved = ApplyTransforms(DrawingRect); + + return moved; + } + } + + public virtual bool IsGestureForChild(ISkiaGestureListener listener, SKPoint point) + { + return IsGestureForChild(listener, point.X, point.Y); + } + + public virtual bool IsGestureForChild(ISkiaGestureListener listener, float x, float y) + { + var hit = listener.HitIsInside(x, y); + return hit; + } + + public SKRect ApplyTransforms(SKRect rect) + { + //Debug.WriteLine($"[Transforming] {rect}"); + + return new SKRect(rect.Left + (float)(UseTranslationX * RenderingScale), + rect.Top + (float)(UseTranslationY * RenderingScale), + rect.Right + (float)(UseTranslationX * RenderingScale), + rect.Bottom + (float)(UseTranslationY * RenderingScale)); + } + + public virtual SKPoint TranslateInputDirectOffsetToPoints(PointF location, SKPoint childOffsetDirect) + { + var thisOffset1 = TranslateInputCoords(childOffsetDirect, false); + //apply touch coords + var x1 = location.X + thisOffset1.X; + var y1 = location.Y + thisOffset1.Y; + //convert to points + return new SKPoint(x1 / RenderingScale, y1 / RenderingScale); + } + + public virtual SKPoint TranslateInputOffsetToPixels(PointF location, SKPoint childOffset) + { + var thisOffset = TranslateInputCoords(childOffset); + return new SKPoint(location.X + thisOffset.X, location.Y + thisOffset.Y); + } + + /// + /// Use this to consume gestures in your control only, + /// do not use result for passing gestures below + /// + /// + /// + public virtual SKPoint TranslateInputCoords(SKPoint childOffset, bool accountForCache = true) + { + var thisOffset = new SKPoint(-(float)(UseTranslationX * RenderingScale), -(float)(UseTranslationY * RenderingScale)); + + //inside a cached object coordinates are frozen at the moment the snapshot was taken + //so we must offset the coordinates to match the current drawing rect + if (accountForCache) + { + /* + if (UsingCacheType == SkiaCacheType.ImageComposite) + { + if (RenderObjectPrevious != null) + { + thisOffset.Offset(RenderObjectPrevious.TranslateInputCoords(LastDrawnAt)); + } + else + if (RenderObject != null) + { + thisOffset.Offset(RenderObject.TranslateInputCoords(LastDrawnAt)); + } + } + else + */ + { + if (RenderObject != null) + { + thisOffset.Offset(RenderObject.TranslateInputCoords(LastDrawnAt)); + } + else + if (RenderObjectPrevious != null) + { + thisOffset.Offset(RenderObjectPrevious.TranslateInputCoords(LastDrawnAt)); + } + } + } + thisOffset.Offset(childOffset); + + //layout is different from real drawing area + var displaced = LastDrawnAt.Location - DrawingRect.Location; + thisOffset.Offset(displaced); + + return thisOffset; + } + + public virtual SKPoint CalculatePositionOffset(bool cacheOnly = false, + bool ignoreCache = false, + bool useTranlsation = true) + { + var thisOffset = SKPoint.Empty; + if (!cacheOnly && useTranlsation) + { + thisOffset = new SKPoint((float)(UseTranslationX * RenderingScale), (float)(UseTranslationY * RenderingScale)); + } + //inside a cached object coordinates are frozen at the moment the snapshot was taken + //so we must offset the coordinates to match the current drawing rect + if (!ignoreCache && UsingCacheType != SkiaCacheType.None) + { + if (RenderObject != null) + { + thisOffset.Offset(RenderObject.CalculatePositionOffset(LastDrawnAt.Location)); + } + else + if (UsesCacheDoubleBuffering && RenderObjectPrevious != null) + { + thisOffset.Offset(RenderObjectPrevious.CalculatePositionOffset(LastDrawnAt.Location)); + } + //Debug.WriteLine($"[CalculatePositionOffset] was cached!"); + } + + //Debug.WriteLine($"[CalculatePositionOffset] {this} {cacheOnly} returned {thisOffset}"); + + return thisOffset; + } + + public virtual SKPoint CalculateFuturePositionOffset(bool cacheOnly = false, + bool ignoreCache = false, + bool useTranlsation = true) + { + var thisOffset = SKPoint.Empty; + if (!cacheOnly && useTranlsation) + { + thisOffset = new SKPoint((float)(UseTranslationX * RenderingScale), (float)(UseTranslationY * RenderingScale)); + } + //inside a cached object coordinates are frozen at the moment the snapshot was taken + //so we must offset the coordinates to match the current drawing rect + if (!ignoreCache && UsingCacheType != SkiaCacheType.None) + { + if (RenderObject != null) + { + thisOffset.Offset(RenderObject.CalculatePositionOffset(DrawingRect.Location)); + } + else + if (UsesCacheDoubleBuffering && RenderObjectPrevious != null) + { + thisOffset.Offset(RenderObjectPrevious.CalculatePositionOffset(DrawingRect.Location)); + } + } + return thisOffset; + } + + long _layoutChanged = 0; + + public SKRect ArrangedDestination { get; protected set; } + + private SKSize _lastSize; + + + public event EventHandler LayoutIsReady; + public event EventHandler Disposing; + + /// + /// Layout was changed with dimensions above zero. Rather a helper method, can you more generic OnLayoutChanged(). + /// + protected virtual void OnLayoutReady() + { + IsLayoutReady = true; + LayoutIsReady?.Invoke(this, null); + } + + public bool IsLayoutReady { get; protected set; } + + public bool LayoutReady + { + get + { + return _layoutReady; + } + + protected set + { + if (_layoutReady != value) + { + _layoutReady = value; + OnPropertyChanged(); + if (value) + { + OnLayoutReady(); + } + } + } + } + bool _layoutReady; + + public bool CheckIsGhost() + { + return IsGhost || Destination == SKRect.Empty; + } + + /// + /// destination in PIXELS, requests in UNITS. resulting Destination prop will be filed in PIXELS. + /// DrawUsingRenderObject wil call this among others.. + /// + /// PIXELS + /// UNITS + /// UNITS + /// + public virtual void Arrange(SKRect destination, float widthRequest, float heightRequest, float scale) + { + + + if (!PreArrange(destination, widthRequest, heightRequest, scale)) + { + DrawingRect = SKRect.Empty; + return; + } + + var width = (HorizontalOptions.Alignment == LayoutAlignment.Fill && WidthRequest < 0) ? -1 : MeasuredSize.Units.Width; + var height = (VerticalOptions.Alignment == LayoutAlignment.Fill && HeightRequest < 0) ? -1 : MeasuredSize.Units.Height; + + PostArrange(destination, width, height, scale); + } + + protected virtual void PostArrange(SKRect destination, float widthRequest, float heightRequest, float scale) + { + SKRect arrangingFor = new(0, 0, destination.Width, destination.Height); + + if (!IsLayoutDirty && + (ViewportHeightLimit != _arrangedViewportHeightLimit || + ViewportWidthLimit != _arrangedViewportWidthLimit || + scale != _lastArrangedForScale || + !CompareRects(arrangingFor, _lastArrangedFor, 0.5f) || + !AreEqual(_lastArrangedHeight, heightRequest, 0.5f) || + !AreEqual(_lastArrangedWidth, widthRequest, 0.5f))) + { + IsLayoutDirty = true; + } + + if (!IsLayoutDirty) + { + AdaptCachedLayout(destination, scale); + return; + } + + //var oldDestination = Destination; + var layout = CalculateLayout(arrangingFor, widthRequest, heightRequest, scale); + bool layoutChanged = false; + if (!CompareRects(layout, ArrangedDestination, 0.5f)) + { + layoutChanged = true; + } + + var oldDrawingRect = this.DrawingRect; + + //save to cache + ArrangedDestination = layout; + //ArrangedDrawingRect = GetDrawingRectWithMargins(layout, scale); + + AdaptCachedLayout(destination, scale); + + _arrangedViewportHeightLimit = ViewportHeightLimit; + _arrangedViewportWidthLimit = ViewportWidthLimit; + _lastArrangedFor = arrangingFor; + _lastArrangedForScale = scale; + _lastArrangedHeight = heightRequest; + _lastArrangedWidth = widthRequest; + + if (!AreEqual(oldDrawingRect.Height, DrawingRect.Height, 0.5) + || !AreEqual(oldDrawingRect.Width, DrawingRect.Width, 0.5)) + { + OnDrawingSizeChanged(); + layoutChanged = true; + } + + if (layoutChanged) + OnLayoutChanged(); + + IsLayoutDirty = false; + } + + /// + /// PIXELS + /// + /// + /// + /// + /// + /// + public ScaledSize MeasureChild(SkiaControl child, double availableWidth, double availableHeight, float scale) + { + if (child == null) + { + return ScaledSize.Default; + } + + child.OnBeforeMeasure(); //could set IsVisible or whatever inside + + if (!child.CanDraw) + return ScaledSize.Default; //child set himself invisible + + return child.Measure((float)availableWidth, (float)availableHeight, scale); + } + + /// + /// Measuring as absolute layout for passed children + /// + /// + /// + /// + /// + protected virtual ScaledSize MeasureContent( + IEnumerable children, + SKRect rectForChildrenPixels, + float scale) + { + var maxHeight = -1.0f; + var maxWidth = -1.0f; + + List fill = new(); + var autosize = this.NeedAutoSize; + var hadFixedSize = false; + + void PostProcessMeasuredChild(ScaledSize measured, SkiaControl child, bool ignoreFill) + { + if (!measured.IsEmpty) + { + var measuredHeight = measured.Pixels.Height; + var measuredWidth = measured.Pixels.Width; + + if (child.ViewportHeightLimit >= 0) + { + float mHeight = (float)(child.ViewportHeightLimit * scale); + if (measuredHeight > mHeight) + { + float excessHeight = measuredHeight - mHeight; + measuredHeight -= excessHeight; + } + } + + if (child.ViewportWidthLimit >= 0) + { + float mWidth = (float)(child.ViewportWidthLimit * scale); + if (measuredWidth > mWidth) + { + float excessWidth = measuredWidth - mWidth; + measuredWidth -= excessWidth; + } + } + + if (ignoreFill) + { + if (measuredWidth > maxWidth && (child.HorizontalOptions.Alignment != LayoutAlignment.Fill || child.WidthRequest >= 0)) + maxWidth = measuredWidth; + + if (measuredHeight > maxHeight && (child.VerticalOptions.Alignment != LayoutAlignment.Fill || child.HeightRequest >= 0)) + maxHeight = measuredHeight; + } + else + { + if (measuredWidth > maxWidth) + maxWidth = measuredWidth; + + if (measuredHeight > maxHeight) + maxHeight = measuredHeight; + } + + } + } + + bool heightCut = false, widthCut = false; + + //PASS 1 + foreach (var child in children) + { + if (child == null) + continue; + + child.OnBeforeMeasure(); //could set IsVisible or whatever inside + + if (autosize && + (child.HorizontalOptions.Alignment == LayoutAlignment.Fill + && child.VerticalOptions.Alignment == LayoutAlignment.Fill) + || (!autosize && (child.HorizontalOptions.Alignment == LayoutAlignment.Fill || child.VerticalOptions.Alignment == LayoutAlignment.Fill)) + ) + { + fill.Add(child); //todo not very correct for the case just 1 dimension is Fill and other one may by bigger that other children! + continue; + } + + hadFixedSize = true; + var measured = MeasureChild(child, rectForChildrenPixels.Width, rectForChildrenPixels.Height, scale); + PostProcessMeasuredChild(measured, child, true); + + widthCut |= measured.WidthCut; + heightCut |= measured.HeightCut; + } + + //PASS 2 for thoses with Fill + foreach (var child in fill) + { + ScaledSize measured; + if (!hadFixedSize) //we had only children with fill so no fixed dimensions yet + { + measured = MeasureChild(child, rectForChildrenPixels.Width, rectForChildrenPixels.Height, scale); + PostProcessMeasuredChild(measured, child, false); + } + else + { + var provideWidth = rectForChildrenPixels.Width; + if (NeedAutoWidth && maxWidth >= 0) + { + provideWidth = maxWidth; + } + var provideHeight = rectForChildrenPixels.Height; + if (NeedAutoHeight && maxHeight >= 0) + { + provideHeight = maxHeight; + } + measured = MeasureChild(child, provideWidth, provideHeight, scale); + if (maxHeight == 0 || maxWidth == 0) + { + PostProcessMeasuredChild(measured, child, false); + } + } + + widthCut |= measured.WidthCut; + heightCut |= measured.HeightCut; + } + + if (HorizontalOptions.Alignment == LayoutAlignment.Fill && WidthRequest < 0) + { + maxWidth = rectForChildrenPixels.Width; + } + if (VerticalOptions.Alignment == LayoutAlignment.Fill && HeightRequest < 0) + { + maxHeight = rectForChildrenPixels.Height; + } + + return ScaledSize.FromPixels(maxWidth, maxHeight, widthCut, heightCut, scale); + } + + /// + /// This is to be called by layouts to propagate their binding context to children. + /// By overriding this method any child could deny a new context or use any other custom logic. + /// To force new context for child parent would set child's BindingContext directly skipping the use of this method. + /// + /// + public virtual void SetInheritedBindingContext(object context) + { + BindingContext ??= context; //only if existing is null + } + + /// + /// https://github.com/taublast/DrawnUi.Maui/issues/92#issuecomment-2408805077 + /// + public virtual void ApplyBindingContext() + { + + foreach (var content in this.Views) + { + content.SetInheritedBindingContext(BindingContext); + } + + foreach (var content in this.VisualEffects) + { + content.Attach(this); + } + + if (FillGradient != null) + FillGradient.BindingContext = BindingContext; + } + + protected bool BindingContextWasSet { get; set; } + /// + /// First Maui will apply bindings to your controls, then it would call OnBindingContextChanged, so beware on not to break bindings. + /// + protected override void OnBindingContextChanged() + { + BindingContextWasSet = true; + + try + { + InvalidateCacheWithPrevious(); + + //InvalidateViewsList(); //we might get different ZIndex which is bindable.. + + ApplyBindingContext(); + + //will apply to maui prps like styles, triggers etc + base.OnBindingContextChanged(); + + } + catch (Exception e) + { + Super.Log(e); + } + } + + public struct ParentMeasureRequest + { + public IDrawnBase Parent { get; set; } + public float WidthRequest { get; set; } + public float HeightRequest { get; set; } + } + + /// + /// + /// + /// + /// + /// + /// + public virtual ScaledSize Measure(float widthConstraint, float heightConstraint, float scale) + { + if (IsDisposed || IsDisposing) + return ScaledSize.Default; + + if (IsMeasuring) //basically we need this for cache double buffering to avoid conflicts with background thread + { + NeedRemeasuring = true; + return MeasuredSize; + } + + try + { + IsMeasuring = true; + + RenderingScale = scale; + + var request = CreateMeasureRequest(widthConstraint, heightConstraint, scale); + //if (request.IsSame) + //{ + // return MeasuredSize; + //} + + if (!this.CanDraw || request.WidthRequest == 0 || request.HeightRequest == 0) + { + InvalidateCacheWithPrevious(); + + return SetMeasuredAsEmpty(request.Scale); + } + + var constraints = GetMeasuringConstraints(request); + + ContentSize = MeasureAbsolute(constraints.Content, scale); + + return SetMeasuredAdaptToContentSize(constraints, scale); + } + finally + { + IsMeasuring = false; + } + + } + + protected virtual ScaledSize SetMeasuredAsEmpty(float scale) + { + return SetMeasured(0, 0, false, false, scale); + } + + public virtual ScaledSize SetMeasuredAdaptToContentSize(MeasuringConstraints constraints, + float scale) + { + var contentWidth = NeedAutoWidth ? ContentSize.Pixels.Width : SmartMax(ContentSize.Pixels.Width, constraints.Request.Width); + var contentHeight = NeedAutoHeight ? ContentSize.Pixels.Height : SmartMax(ContentSize.Pixels.Height, constraints.Request.Height); + + var width = AdaptWidthConstraintToContentRequest(constraints, contentWidth, HorizontalOptions.Expands); + var height = AdaptHeightConstraintToContentRequest(constraints, contentHeight, VerticalOptions.Expands); + + var widthCut = ContentSize.Pixels.Width > width || ContentSize.WidthCut; + var heighCut = ContentSize.Pixels.Height > height || ContentSize.HeightCut; + + SKSize size = new(width, height); + + var invalid = !CompareSize(size, MeasuredSize.Pixels, 0); + if (invalid) + { + InvalidateCacheWithPrevious(); + } + + return SetMeasured(size.Width, size.Height, widthCut, heighCut, scale); + } + + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SKSize GetSizeInPoints(SKSize size, float scale) + { + var width = float.PositiveInfinity; + var height = float.PositiveInfinity; + if (double.IsFinite(size.Width) && size.Width >= 0) + { + width = size.Width / scale; + } + if (double.IsFinite(size.Height) && size.Height >= 0) + { + height = size.Height / scale; + } + return new SKSize(width, height); + } + + protected virtual MeasureRequest CreateMeasureRequest(float widthConstraint, float heightConstraint, float scale) + { + + RenderingScale = scale; + + //LastMeasureRequest = new() + //{ + // Parent = this.Parent, + // WidthRequest = widthConstraint, + // HeightRequest = heightConstraint, + //}; + + if (HorizontalFillRatio != 1 && double.IsFinite(widthConstraint) && widthConstraint > 0) + { + widthConstraint *= (float)HorizontalFillRatio; + } + if (VerticalFillRatio != 1 && double.IsFinite(heightConstraint) && heightConstraint > 0) + { + heightConstraint *= (float)VerticalFillRatio; + } + + if (LockRatio < 0) + { + var size = Math.Min(heightConstraint, widthConstraint); + size *= (float)-LockRatio; + heightConstraint = size; + widthConstraint = size; + } + else + if (LockRatio > 0) + { + var size = Math.Max(heightConstraint, widthConstraint); + size *= (float)LockRatio; + heightConstraint = size; + widthConstraint = size; + } + + var isSame = + !NeedMeasure + && _lastMeasuredForScale == scale + && AreEqual(_lastMeasuredForHeight, heightConstraint, 1) + && AreEqual(_lastMeasuredForWidth, widthConstraint, 1); + + if (!isSame) + { + _lastMeasuredForWidth = widthConstraint; + _lastMeasuredForHeight = heightConstraint; + _lastMeasuredForScale = scale; + } + + return new MeasureRequest(widthConstraint, heightConstraint, scale) + { + IsSame = isSame + }; + } + + public SKRect GetMeasuringRectForChildren(float widthConstraint, float heightConstraint, double scale) + { + var constraintLeft = (Padding.Left + Margins.Left) * scale; + var constraintRight = (Padding.Right + Margins.Right) * scale; + var constraintTop = (Padding.Top + Margins.Top) * scale; + var constraintBottom = (Padding.Bottom + Margins.Bottom) * scale; + + //SKRect rectForChild = new SKRect(0 + (float)constraintLeft, + // 0 + (float)constraintTop, + // widthConstraint - (float)constraintRight, + // heightConstraint - (float)constraintBottom); + + SKRect rectForChild = new SKRect(0, 0, + (float)Math.Round(widthConstraint - (float)(constraintRight + constraintLeft)), + (float)Math.Round(heightConstraint - (float)(constraintBottom + constraintTop))); + + + return rectForChild; + } + + public virtual ScaledSize MeasureAbsolute(SKRect rectForChildrenPixels, float scale) + { + return MeasureAbsoluteBase(rectForChildrenPixels, scale); + } + + /// + /// Base method, not aware of any views provider, not virtual, silly measuring Children. + /// + /// + /// + /// + protected ScaledSize MeasureAbsoluteBase(SKRect rectForChildrenPixels, float scale) + { + if (Views.Count > 0) + { + var maxHeight = 0.0f; + var maxWidth = 0.0f; + + var children = Views;//GetOrderedSubviews(); + return MeasureContent(children, rectForChildrenPixels, scale); + } + //empty container + else + if (NeedAutoHeight || NeedAutoWidth) + { + return ScaledSize.CreateEmpty(scale); + //return SetMeasured(0, 0, scale); + } + + return ScaledSize.FromPixels(rectForChildrenPixels.Width, rectForChildrenPixels.Height, scale); + } + + + public static SKRect ContractPixelsRect(SKRect rect, float scale, Thickness amount) + { + return new SKRect( + rect.Left + (float)Math.Round((float)amount.Left * scale), + rect.Top + (float)Math.Round((float)amount.Top * scale), + rect.Right - (float)Math.Round((float)amount.Right * scale), + rect.Bottom - (float)Math.Round((float)amount.Bottom * scale) + ); + } + + + public SKRect GetDrawingRectForChildren(SKRect destination, double scale) + { + //var constraintLeft = (Padding.Left + Margins.Left) * scale; + //var constraintRight = (Padding.Right + Margins.Right) * scale; + //var constraintTop = (Padding.Top + Margins.Top) * scale; + //var constraintBottom = (Padding.Bottom + Margins.Bottom) * scale; + + var constraintLeft = (Padding.Left + Margins.Left) * scale; + var constraintRight = (Padding.Right + Margins.Right) * scale; + var constraintTop = (Padding.Top + Margins.Top) * scale; + var constraintBottom = (Padding.Bottom + Margins.Bottom) * scale; + + + SKRect rectForChild = new SKRect( + (float)Math.Round(destination.Left + (float)constraintLeft), + (float)Math.Round(destination.Top + (float)constraintTop), + (float)Math.Round(destination.Right - (float)constraintRight), + (float)Math.Round(destination.Bottom - (float)constraintBottom) + ); + + return rectForChild; + } + + public virtual SKRect GetDrawingRectWithMargins(SKRect destination, double scale) + { + + + var constraintLeft = (float)Math.Round(Margins.Left * scale); + var constraintRight = (float)Math.Round(Margins.Right * scale); + var constraintTop = (float)Math.Round(Margins.Top * scale); + var constraintBottom = (float)Math.Round(Margins.Bottom * scale); + + SKRect rectForChild; + + rectForChild = new SKRect( + (destination.Left + constraintLeft), + (destination.Top + constraintTop), + (destination.Right - constraintRight), + (destination.Bottom - constraintBottom) + ); + + // Apply ViewportHeightLimit if it's set + if (ViewportHeightLimit >= 0) + { + float maxHeight = (float)Math.Round(ViewportHeightLimit * scale); + if (rectForChild.Height > maxHeight) + { + float excessHeight = rectForChild.Height - maxHeight; + rectForChild.Bottom -= excessHeight; + } + } + + // Apply ViewportWidthLimit if it's set + if (ViewportWidthLimit >= 0) + { + float maxWidth = (float)Math.Round(ViewportWidthLimit * scale); + if (rectForChild.Width > maxWidth) + { + float excessWidth = rectForChild.Width - maxWidth; + rectForChild.Right -= excessWidth; + } + } + + return rectForChild; + } + + + protected object lockMeasured = new(); + + bool debugMe; + + /// + /// Flag for internal use, maynly used to avoid conflicts between measuring on ui-thread and in background. If true, measure will return last measured value. + /// + public bool IsMeasuring { get; protected internal set; } + + protected object LockMeasure = new(); + + /// + /// Parameters in PIXELS. sets IsLayoutDirty = true; + /// + /// + /// + /// + /// + protected virtual ScaledSize SetMeasured(float width, float height, bool widthCut, bool heightCut, float scale) + { + lock (lockMeasured) + { + WasMeasured = true; + + NeedMeasure = false; + + IsLayoutDirty = true; + + if (double.IsFinite(height) && !double.IsNaN(height)) + { + Height = (height / scale) - (Margins.Top + Margins.Bottom); + } + else + { + height = -1; + Height = height; + } + + if (double.IsFinite(width) && !double.IsNaN(width)) + { + Width = (width / scale) - (Margins.Left + Margins.Right); + } + else + { + width = -1; + Width = width; + } + + MeasuredSize = ScaledSize.FromPixels(width, height, widthCut, heightCut, scale); + + //SetValueCore(RenderingScaleProperty, scale, SetValueFlags.None); + + OnMeasured(); + + return MeasuredSize; + } + } + + protected virtual void OnMeasured() + { + //Debug.WriteLine($"[MEASURED] {this.GetType().Name} {this.Tag} "); + Measured?.Invoke(this, MeasuredSize); + } + + /// + /// UNITS + /// + public event EventHandler Measured; + + + public ScaledSize MeasuredSize { get; set; } = new(); + + + public virtual bool NeedAutoSize + { + get + { + return NeedAutoHeight || NeedAutoWidth; + } + } + + public bool NeedAutoHeight + { + get + { + return LockRatio == 0 && VerticalOptions.Alignment != LayoutAlignment.Fill && SizeRequest.Height < 0; + } + } + public bool NeedAutoWidth + { + get + { + return LockRatio == 0 && HorizontalOptions.Alignment != LayoutAlignment.Fill && SizeRequest.Width < 0; + } + } + + + private bool _isDisposed; + + public bool IsDisposed + { + get + { + return _isDisposed; + } + protected set + { + if (value != _isDisposed) + { + _isDisposed = value; + OnPropertyChanged(); + } + } + } + + private bool _isDisposing; + + public bool IsDisposing + { + get + { + return _isDisposing; + } + protected set + { + if (value != _isDisposing) + { + _isDisposing = value; + OnPropertyChanged(); + } + } + } + + + + /// + /// Developer can use this to mark control as to be disposed by parent custom controls + /// + public bool NeedDispose { get; set; } + + public TChild FindView(string tag) where TChild : SkiaControl + { + if (this.Tag == tag && this is TChild) + return this as TChild; + + var found = Views.FirstOrDefault(x => x.Tag == tag) as TChild; + if (found == null) + { + //go sub level + foreach (var view in Views) + { + found = view.FindView(tag); + if (found != null) + { + return found; + } + } + } + return found; + } + + public TChild FindView() where TChild : SkiaControl + { + + var found = Views.FirstOrDefault(x => x is TChild) as TChild; + if (found == null) + { + //go sub level + foreach (var view in Views) + { + found = view.FindView(); + if (found != null) + { + return found; + } + } + } + return found; + } + + public SkiaControl FindViewByTag(string tag) + { + if (this.Tag == tag) + return this; + + var found = Views.FirstOrDefault(x => x.Tag == tag); + if (found == null) + { + //go sub level + foreach (var view in Views) + { + found = view.FindViewByTag(tag); + if (found != null) + { + return found; + } + } + } + return found; + } + + /// + /// Avoid setting parent to null before calling this, or set SuperView prop manually for proper cleanup of animations and gestures if any used + /// + public void Dispose() + { + if (IsDisposed) + return; + + OnWillDisposeWithChildren(); + + IsDisposed = true; + + //for the double buffering case it's safer to delay + Tasks.StartDelayed(TimeSpan.FromSeconds(1), () => + { + + RenderObject = null; + + PaintSystem?.Dispose(); + + _lastAnimatorManager = null; + + DisposeChildren(); + + RenderTree?.Clear(); + + GestureListeners?.Clear(); + + VisualEffects?.Clear(); + + OnDisposing(); + + Parent = null; + + Superview = null; + + LastGradient?.Dispose(); + LastGradient = null; + + LastShadow?.Dispose(); + LastShadow = null; + + CustomizeLayerPaint = null; + + RenderObjectPreparing?.Dispose(); + RenderObjectPreparing = null; + + clipPreviousCachePath?.Dispose(); + PaintErase?.Dispose(); + + RenderObjectPrevious?.Dispose(); + RenderObjectPrevious = null; + + _paintWithOpacity?.Dispose(); + _paintWithEffects?.Dispose(); + _preparedClipBounds?.Dispose(); + + EffectColorFilter = null; + EffectImageFilter = null; + EffectRenderers = null; + EffectsState = null; + EffectsGestureProcessors = null; + EffectPostRenderer = null; + }); + } + + protected SKPaint PaintErase = new() + { + Color = SKColors.Transparent, + BlendMode = SKBlendMode.Src + }; + + + public static long GetNanoseconds() + { + double timestamp = Stopwatch.GetTimestamp(); + double nanoseconds = 1_000_000_000.0 * timestamp / Stopwatch.Frequency; + return (long)nanoseconds; + + //double nano = 10000L * Stopwatch.GetTimestamp(); + //nano /= TimeSpan.TicksPerMillisecond; + //nano *= 100L; + //return (long)nano; + } + + + public virtual void OnBeforeMeasure() + { + + } + + public virtual void OptionalOnBeforeDrawing() + { + Superview?.UpdateRenderingChains(this); + } + + + /// + /// do not ever erase background + /// + public bool IsOverlay { get; set; } + + + /// + /// Executed after the rendering + /// + public List PostAnimators { get; } = new(); //to be renamed to post-effects + + + public static readonly BindableProperty UpdateWhenReturnedFromBackgroundProperty = BindableProperty.Create(nameof(UpdateWhenReturnedFromBackground), + typeof(bool), + typeof(SkiaControl), + false); + public bool UpdateWhenReturnedFromBackground + { + get { return (bool)GetValue(UpdateWhenReturnedFromBackgroundProperty); } + set { SetValue(UpdateWhenReturnedFromBackgroundProperty, value); } + } + + public virtual void OnSuperviewShouldRenderChanged(bool state) + { + if (UpdateWhenReturnedFromBackground) + { + Update(); + } + + try + { + foreach (var view in Views.ToList()) + { + view.OnSuperviewShouldRenderChanged(state); + } + } + catch (Exception e) + { + Super.Log(e); + } + } + + /// + /// Normally get a a Measure by parent then parent calls Draw and we can apply the measure result. + /// But in a case we have measured us ourselves inside PreArrange etc we must call ApplyMeasureResult because this would happen after the Draw and not before. + /// + public virtual void ApplyMeasureResult() + { + } + + /// + /// Returns false if should not render + /// + /// + public virtual bool PreArrange(SKRect destination, float widthRequest, float heightRequest, float scale) + { + if (!CanDraw) + return false; + + if (WillInvalidateMeasure) + { + WillInvalidateMeasure = false; + InvalidateMeasureInternal(); + } + + if (NeedMeasure) + { + //self measuring + var adjustedDestination = CalculateLayout(destination, widthRequest, heightRequest, scale); + ArrangedDestination = adjustedDestination; + Measure(adjustedDestination.Width, adjustedDestination.Height, scale); + ApplyMeasureResult(); + } + else + { + _lastArrangedInside = destination; + } + + return true; + } + + protected bool IsRendering { get; set; } + + public virtual void Render(SkiaDrawingContext context, + SKRect destination, + float scale) + { + if (IsDisposing || IsDisposed) + return; + + IsRendering = true; + + Superview = context.Superview; + RenderingScale = scale; + NeedUpdate = false; + + OnBeforeDrawing(context, destination, scale); + + if (WillInvalidateMeasure) + { + WillInvalidateMeasure = false; + InvalidateMeasureInternal(); + } + + if (RenderObjectNeedsUpdate) + { + //disposal etc inside setter + RenderObject = null; + } + + if ( + //RenderedAtDestination.Width != destination.Width || + //RenderedAtDestination.Height != destination.Height || + !CompareRectsSize(RenderedAtDestination, destination, 0.5f) || + RenderingScale != scale) + { + //NeedMeasure = true; + RenderedAtDestination = destination; + } + + if (RenderedAtDestination != SKRect.Empty) + { + //moved to superview + //ExecuteAnimators(context.FrameTimeNanos);// Super.GetNanoseconds() + + Draw(context, destination, scale); + } + + //UpdateLocked = false; + + OnAfterDrawing(context, destination, scale); + + Rendered?.Invoke(this, null); + + IsRendering = false; + } + + public event EventHandler Rendered; + + /// + /// Lock between replacing and using RenderObject + /// + protected object LockDraw = new(); + + /// + /// Creating new cache lock + /// + protected object LockRenderObject = new(); + + public virtual object CreatePaintArguments() + { + return null; + } + + protected virtual void OnBeforeDrawing(SkiaDrawingContext context, + SKRect destination, + float scale) + { + InvalidatedParent = false; + _invalidatedParentPostponed = false; + + if (EffectsState != null) + { + foreach (var stateEffect in EffectsState) + { + stateEffect?.UpdateState(); + } + } + } + + protected virtual void OnAfterDrawing(SkiaDrawingContext context, + SKRect destination, + float scale) + { + if (_invalidatedParentPostponed) + { + InvalidatedParent = false; + InvalidateParent(); + } + + if (UsingCacheType == SkiaCacheType.None) + NeedUpdate = false; //otherwise CreateRenderingObject will set this to false + + //trying to find exact location on the canvas + + LastDrawnAt = DrawingRect; + + X = LastDrawnAt.Location.X / scale; + Y = LastDrawnAt.Location.Y / scale; + + ExecutePostAnimators(context, scale); + + if (NeedRemeasuring) + { + NeedRemeasuring = false; + } + + if (UsesCacheDoubleBuffering + && RenderObject != null) + { + if (!CompareRectsSize(DrawingRect, RenderObject.Bounds, 0.5f)) + { + InvalidateMeasure(); + } + } + else + if (UsingCacheType == SkiaCacheType.ImageComposite + && RenderObjectPrevious != null) + { + if (!CompareRectsSize(DrawingRect, RenderObjectPrevious.Bounds, 0.5f)) + { + InvalidateMeasure(); + } + } + + } + + + protected virtual void Draw(SkiaDrawingContext context, SKRect destination, float scale) + { + if (IsDisposing || IsDisposed) + return; + + DrawUsingRenderObject(context, + SizeRequest.Width, SizeRequest.Height, + destination, scale); + } + + //public new static readonly BindableProperty XProperty + // = BindableProperty.Create(nameof(X), + // typeof(double), typeof(SkiaControl), + // 0.0f); + + + private double _X; + /// + /// Absolute position obtained after this control was drawn on the Canvas, this is not relative to parent control. + /// + public new double X + { + get + { + return _X; + } + protected set + { + if (_X != value) + { + _X = value; + OnPropertyChanged(); + } + } + } + + private double _Y; + /// + /// Absolute position obtained after this control was drawn on the Canvas, this is not relative to parent control. + /// + public new double Y + { + get + { + return _Y; + } + protected set + { + if (_Y != value) + { + _Y = value; + OnPropertyChanged(); + } + } + } + + + /// + /// Execute post drawing operations, like post-animators etc + /// + /// + /// + protected void FinalizeDrawingWithRenderObject(SkiaDrawingContext context, double scale) + { + + + } + + public SKPoint GetPositionOffsetInPoints() + { + var thisOffset = CalculatePositionOffset(); + var x = (thisOffset.X) / RenderingScale; + var y = (thisOffset.Y) / RenderingScale; + return new((float)x, (float)y); + } + + public SKPoint GetPositionOffsetInPixels(bool cacheOnly = false, bool ignoreCache = false, bool useTranslation = true) + { + var thisOffset = CalculatePositionOffset(cacheOnly, ignoreCache, useTranslation); + var x = (thisOffset.X); + var y = (thisOffset.Y); + return new((float)x, (float)y); + } + + public SKPoint GetFuturePositionOffsetInPixels(bool cacheOnly = false, bool ignoreCache = false, bool useTranslation = true) + { + var thisOffset = CalculateFuturePositionOffset(cacheOnly, ignoreCache, useTranslation); + var x = (thisOffset.X); + var y = (thisOffset.Y); + return new((float)x, (float)y); + } + + public SKPoint GetOffsetInsideControlInPoints(PointF location, SKPoint childOffset) + { + var thisOffset = TranslateInputCoords(childOffset, false); + var x = (location.X + thisOffset.X) / RenderingScale; + var y = (location.Y + thisOffset.Y) / RenderingScale; + var insideX = x - X; + var insideY = y - Y; + return new((float)insideX, (float)insideY); + } + + public SKPoint GetOffsetInsideControlInPixels(PointF location, SKPoint childOffset) + { + var thisOffset = TranslateInputCoords(childOffset, false); + var x = location.X + thisOffset.X; + var y = location.Y + thisOffset.Y; + var insideX = x - X * RenderingScale; + var insideY = y - Y * RenderingScale; + return new((float)insideX, (float)insideY); + } + + /// + /// Location on the canvas after last drawing completed + /// + public SKRect LastDrawnAt { get; protected set; } + + public void ExecutePostAnimators(SkiaDrawingContext context, double scale) + { + try + { + if (PostAnimators.Count == 0 || IsDisposing || IsDisposed) + { + return; + } + + //Debug.WriteLine($"[ExecutePostAnimators] {Tag} {PostAnimators.Count} effects"); + + foreach (var effect in PostAnimators.ToList()) + { + //if (effect.IsRunning) + { + if (effect.Render(this, context, scale)) + { + Repaint(); + } + } + } + } + catch (Exception e) + { + Super.Log(e); + } + + } + + #region CACHE + + /// + /// Base method will call RenderViewsList. + /// Return number of drawn views. + /// + /// + /// + /// + /// + /// + protected virtual int DrawViews(SkiaDrawingContext context, SKRect destination, float scale, bool debug = false) + { + var children = GetOrderedSubviews(); + + return RenderViewsList((IList)children, context, destination, scale, debug); + } + + + + + /// + /// Just make us repaint to apply new transforms etc + /// + public virtual void Repaint() + { + if (IsDisposing || NeedUpdate || + Superview == null + || IsParentIndependent + || IsDisposed || Parent == null) + return; + + if (!Parent.UpdateLocked) + { + Parent?.UpdateByChild(this); + } + } + + protected SKPaint _paintWithEffects = null; + protected SKPaint _paintWithOpacity = null; + + SKPath _preparedClipBounds = null; + + private IAnimatorsManager _lastAnimatorManager; + + private Func> _createChildren; + + /// + /// Can customize the SKPaint used for painting the object + /// + public Action CustomizeLayerPaint { get; set; } + +#if SKIA3 + public Sk3dView Helper3d; +#else + public SK3dView Helper3d; +#endif + + + + public void DrawWithClipAndTransforms( + SkiaDrawingContext ctx, + SKRect destination, + SKRect transformsArea, + bool useOpacity, + bool useClipping, + Action draw) + { + bool isClipping = (WillClipBounds || Clipping != null || ClippedBy != null) && useClipping; + + + if (isClipping) + { + + _preparedClipBounds ??= new SKPath(); + _preparedClipBounds.Reset(); + if (ClippedBy != null) + { + ClippedBy.CreateClip(null, true, _preparedClipBounds); + } + else + { + _preparedClipBounds.AddRect(destination); + Clipping?.Invoke(_preparedClipBounds, destination); + } + } + + bool applyOpacity = useOpacity && Opacity < 1; + bool needTransform = HasTransform; + + if (applyOpacity || isClipping || needTransform || CustomizeLayerPaint != null) + { + _paintWithOpacity ??= new SKPaint + { + IsAntialias = IsDistorted, + FilterQuality = IsDistorted ? SKFilterQuality.Medium : SKFilterQuality.None + }; + + _paintWithOpacity.Color = SKColors.White.WithAlpha((byte)(0xFF * Opacity)); + + var restore = 0; + + if (applyOpacity || CustomizeLayerPaint != null) + { + CustomizeLayerPaint?.Invoke(_paintWithOpacity, destination); + restore = ctx.Canvas.SaveLayer(_paintWithOpacity); + } + else + { + restore = ctx.Canvas.Save(); + } + + if (needTransform) + { + ApplyTransforms(ctx, transformsArea); + } + + if (isClipping) + { + + //ctx.Canvas.ClipPath(_preparedClipBounds, SKClipOperation.Intersect, false); + ClipSmart(ctx.Canvas, _preparedClipBounds); + } + + draw(ctx); + + ctx.Canvas.RestoreToCount(restore); + } + else + { + draw(ctx); + } + } + + protected virtual void ApplyTransforms(SkiaDrawingContext ctx, SKRect destination) + { + var moveX = (int)Math.Round(UseTranslationX * RenderingScale); + var moveY = (int)Math.Round(UseTranslationY * RenderingScale); + + float pivotX = (float)(destination.Left + destination.Width * TransformPivotPointX); + float pivotY = (float)(destination.Top + destination.Height * TransformPivotPointY); + + var centerX = moveX + destination.Left + destination.Width * TransformPivotPointX; + var centerY = moveY + destination.Top + destination.Height * TransformPivotPointY; + + var skewX = SkewX > 0 ? (float)Math.Tan(Math.PI * SkewX / 180f) : 0f; + var skewY = SkewY > 0 ? (float)Math.Tan(Math.PI * SkewY / 180f) : 0f; + + if (Rotation != 0) + { + ctx.Canvas.RotateDegrees((float)Rotation, (float)centerX, (float)centerY); + } + + var matrixTransforms = new SKMatrix + { + TransX = moveX, + TransY = moveY, + Persp0 = Perspective1, + Persp1 = Perspective2, + SkewX = skewX, + SkewY = skewY, + Persp2 = 1, + ScaleX = (float)ScaleX, + ScaleY = (float)ScaleY + }; + + var drawingMatrix = SKMatrix.CreateTranslation((float)-pivotX, (float)-pivotY).PostConcat(matrixTransforms); + + if (CameraAngleX != 0 || CameraAngleY != 0 || CameraAngleZ != 0) + { + Helper3d ??= new(); +#if SKIA3 + Helper3d.Reset(); + Helper3d.RotateXDegrees(CameraAngleX); + Helper3d.RotateYDegrees(CameraAngleY); + Helper3d.RotateZDegrees(CameraAngleZ); + if (CameraTranslationZ != 0) + Helper3d.Translate(0, 0, CameraTranslationZ); + + drawingMatrix = drawingMatrix.PostConcat(Helper3d.GetMatrix()); +#else + Helper3d.Save(); + Helper3d.RotateXDegrees(CameraAngleX); + Helper3d.RotateYDegrees(CameraAngleY); + Helper3d.RotateZDegrees(CameraAngleZ); + if (CameraTranslationZ != 0) Helper3d.TranslateZ(CameraTranslationZ); + drawingMatrix = drawingMatrix.PostConcat(Helper3d.Matrix); + Helper3d.Restore(); +#endif + } + + drawingMatrix = drawingMatrix.PostConcat(SKMatrix.CreateTranslation(pivotX, pivotY)) + .PostConcat(ctx.Canvas.TotalMatrix); + + 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 from ShouldClipAntialiased + /// + /// + /// + /// + public virtual void ClipSmart(SKCanvas canvas, SKPath path, SKClipOperation operation = SKClipOperation.Intersect) + { + + canvas.ClipPath(path, operation, ShouldClipAntialiased); + } + + /// + /// This is not a static bindable property. Can be set manually or by control, for example SkiaShape sets this to true for non-rectangular shapes, or rounded corners.. + /// + public bool ShouldClipAntialiased { get; set; } + + public virtual bool NeedMeasure + { + get => _needMeasure; + set + { + if (value == _needMeasure) return; + _needMeasure = value; + + if (value) + { + IsLayoutDirty = true; + InvalidateCacheWithPrevious(); + } + //OnPropertyChanged(); disabled atm + } + } + private bool _needMeasure = true; + + + + /// + /// If attached to a SuperView and rendering is in progress will run after it. Run now otherwise. + /// + /// + protected void SafePostAction(Action action) + { + var super = this.Superview; + if (super != null) + { + Superview.PostponeExecutionBeforeDraw(() => + { + action(); + }); + + Repaint(); + } + else + { + action(); + } + } + + /// + /// If attached to a SuperView will run only after draw to avoid memory access conflicts. If not attached will run after 3 secs.. + /// + /// + protected void SafeAction(Action action) + { + var super = this.Superview; + if (super == null || !Superview.IsRendering) + { + Tasks.StartDelayed(TimeSpan.FromSeconds(3), action); + } + else + Superview.PostponeExecutionAfterDraw(action); + } + + /* + public async Task ProcessOffscreenCacheRenderingAsync() + { + + await semaphoreOffsecreenProcess.WaitAsync(); + + _processingOffscrenRendering = true; + + try + { + Action action = _offscreenCacheRenderingQueue.Pop(); + if (!IsDisposed && !IsDisposing && action != null) + { + try + { + action.Invoke(); + + RenderObject = RenderObjectPreparing; + _renderObjectPreparing = null; + + Repaint(); + } + catch (Exception e) + { + Super.Log(e); + } + } + } + finally + { + _processingOffscrenRendering = false; + semaphoreOffsecreenProcess.Release(); + + if (NeedUpdate || _offscreenCacheRenderingQueue.Count > 0) //someone changed us while rendering inner content + { + Update(); //kick + } + } + + } + */ + + + protected bool NeedRemeasuring; + + + + + + + protected virtual void PaintWithEffects( + SkiaDrawingContext ctx, SKRect destination, float scale, object arguments) + { + if (IsDisposed || IsDisposing) + return; + + void draw(SkiaDrawingContext context) + { + Paint(context, destination, scale, arguments); + } + + if (!DisableEffects && VisualEffects.Count > 0) + { + if (_paintWithEffects == null) + { + _paintWithEffects = new() + { + IsAntialias = true, + FilterQuality = SKFilterQuality.Medium + }; + + } + + var effectColor = EffectColorFilter; + var effectImage = EffectImageFilter; + + if (effectImage != null) + _paintWithEffects.ImageFilter = effectImage.CreateFilter(destination); + else + _paintWithEffects.ImageFilter = null;//will be disposed internally by effect + + if (effectColor != null) + _paintWithEffects.ColorFilter = effectColor.CreateFilter(destination); + else + _paintWithEffects.ColorFilter = null; + + var restore = ctx.Canvas.SaveLayer(_paintWithEffects); + + bool hasDrawnControl = false; + + var renderers = EffectRenderers; + + if (renderers.Count > 0) + { + foreach (var effect in renderers) + { + var chainedEffectResult = effect.Draw(destination, ctx, draw); + if (chainedEffectResult.DrawnControl) + hasDrawnControl = true; + } + } + + if (!hasDrawnControl) + { + draw(ctx); + } + + ctx.Canvas.RestoreToCount(restore); + } + else + { + draw(ctx); + } + } + + + /// + /// This is the main drawing routine you should override to draw something. + /// Base one paints background color inside DrawingRect that was defined by Arrange inside base.Draw. + /// Pass arguments if you want to use some time-frozen data for painting at any time from any thread.. + /// + /// + /// + /// + protected virtual void Paint(SkiaDrawingContext ctx, SKRect destination, float scale, object arguments) + { + if (destination.Width == 0 || destination.Height == 0 || IsDisposing || IsDisposed) + return; + + PaintTintBackground(ctx.Canvas, destination); + + WasDrawn = true; + } + + 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 + { + get + { + return _wasDrawn; + } + set + { + if (_wasDrawn != value) + { + _wasDrawn = value; + OnPropertyChanged(); + if (value) + WasFirstTimeDrawn?.Invoke(this, null); + } + } + } + + public event EventHandler WasFirstTimeDrawn; + + /// + /// Create this control clip for painting content. + /// Pass arguments if you want to use some time-frozen data for painting at any time from any thread.. + /// + /// + /// + public virtual SKPath CreateClip(object arguments, bool usePosition, SKPath path = null) + { + path ??= new SKPath(); + + if (usePosition) + { + path.AddRect(DrawingRect); + } + else + { + path.AddRect(new(0, 0, DrawingRect.Width, DrawingRect.Height)); + } + return path; + } + + + public static SKColor DebugRenderingColor = SKColor.Parse("#66FFFF00"); + + public double UseTranslationY + { + get + { + return TranslationY + AddTranslationY; + } + } + + public double UseTranslationX + { + get + { + return TranslationX + AddTranslationX; + } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public bool HasTransform + { + get + { + return + UseTranslationY != 0 || UseTranslationX != 0 + || ScaleY != 1f || ScaleX != 1f + || Perspective1 != 0f || Perspective2 != 0f + || SkewX != 0 || SkewY != 0 + || Rotation != 0 + || CameraAngleX != 0 || CameraAngleY != 0 || CameraAngleZ != 0; + } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsDistorted + { + get + { + return + Rotation != 0 || ScaleY != 1f || ScaleX != 1f + || Perspective1 != 0f || Perspective2 != 0f + || SkewX != 0 || SkewY != 0 + || CameraAngleX != 0 || CameraAngleY != 0 || CameraAngleZ != 0; + } + } + + + /// + /// Drawing cache, applying clip and transforms as well + /// + /// + /// + /// + public virtual void DrawRenderObject(CachedObject cache, + SkiaDrawingContext ctx, + SKRect destination) + { + + //lock (LockDraw) + { + DrawWithClipAndTransforms(ctx, destination, destination, true, true, (ctx) => + { + + if (EffectPostRenderer != null) + { + EffectPostRenderer.Render(ctx, destination); + } + else + { + if (_paintWithOpacity == null) + { + _paintWithOpacity = new SKPaint(); + } + + _paintWithOpacity.Color = SKColors.White; + _paintWithOpacity.IsAntialias = true; + _paintWithOpacity.FilterQuality = SKFilterQuality.Medium; + + cache.Draw(ctx.Canvas, destination, _paintWithOpacity); + } + }); + + } + + } + + /// + /// Use to render Absolute layout. Base method is not supporting templates, override it to implemen your logic. + /// Returns number of drawn children. + /// + /// + /// + /// + /// + /// + protected virtual int RenderViewsList(IEnumerable skiaControls, + SkiaDrawingContext context, + SKRect destination, float scale, + bool debug = false) + { + var count = 0; + + List tree = new(); + + //todo + //var visibleArea = GetOnScreenVisibleArea(); + + foreach (var child in skiaControls) + { + if (child != null) + { + child.OptionalOnBeforeDrawing(); //could set IsVisible or whatever inside + if (child.CanDraw) //still visible + { + child.Render(context, destination, scale); + + tree.Add(new SkiaControlWithRect(child, + child.LastDrawnAt, + child.LastDrawnAt, count)); + + count++; + } + } + } + + RenderTree = tree; + _builtRenderTreeStamp = _measuredStamp; + + return count; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual bool UsesRenderingTree + { + get + { + return true; + } + } + + /// + /// Rect is real drawing position + /// + /// + /// + /// + public record SkiaControlWithRect(SkiaControl Control, + SKRect Rect, + SKRect HitRect, + int Index); + + protected long _measuredStamp; + + protected long _builtRenderTreeStamp; + + + /// + /// Last rendered controls tree. Used by gestures etc.. + /// + public List RenderTree { get; protected set; } + + #endregion + public bool Invalidated { get; set; } = true; + + /// + /// For internal use, set by Update method + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool NeedUpdate + { + get + { + return _needUpdate; + } + set + { + if (_needUpdate != value) + { + _needUpdate = value; + + if (value) + InvalidateCache(); + } + } + } + bool _needUpdate; + + + + DrawnView _superview; + /// + /// Our canvas + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public DrawnView Superview + { + get + { + if (_superview == null) + { + var value = GetTopParentView(); + if (value != _superview) + { + _superview = value; + OnPropertyChanged(); + SuperViewChanged(); + } + } + return _superview; + } + set + { + if (value != _superview) + { + _superview = value; + OnPropertyChanged(); + SuperViewChanged(); + } + } + } + + /// + /// For virtualization + /// + /// + public virtual ScaledRect GetOnScreenVisibleArea(float inflateByPixels = 0) + { + if (this.UsingCacheType != SkiaCacheType.None) + { + //we are going to cache our children so they all must draw + //regardless of the fact they might still be offscreen + var inflated = DrawingRect; + inflated.Inflate(inflateByPixels, inflateByPixels); + + return ScaledRect.FromPixels(inflated, RenderingScale); + } + + //go up the tree to find the screen area or some parent will override this + if (Parent != null) + { + return Parent.GetOnScreenVisibleArea(inflateByPixels); + } + + if (Superview != null) + { + return Superview.GetOnScreenVisibleArea(inflateByPixels); + } + + var inflated2 = Destination; + inflated2.Inflate(inflateByPixels, inflateByPixels); + return ScaledRect.FromPixels(inflated2, RenderingScale); + } + + bool _lastUpdatedVisibility; + + + + /// + /// Used to check whether to apply IsClippedToBounds property + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual bool WillClipBounds + { + get + { + return IsClippedToBounds || (UsingCacheType != SkiaCacheType.None && !IsCacheOperations); + } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual bool WillClipEffects + { + get + { + return ClipEffects; + } + } + + + protected virtual void UpdateInternal() + { + if (IsDisposing || IsDisposed) + return; + + NeedUpdateFrontCache = true; + NeedUpdate = true; + + if (UpdateLocked) + return; + + if (IsParentIndependent) + return; + + Parent?.UpdateByChild(this); + } + + /// + /// Main method to invalidate cache and invoke rendering + /// + public virtual void Update() + { + InvalidateCache(); + + UpdateInternal(); + + Updated?.Invoke(this, null); + } + + /// + /// Triggered by Update method + /// + public event EventHandler Updated; + + public static MemoryStream StreamFromString(string value) + { + return new MemoryStream(Encoding.UTF8.GetBytes(value ?? "")); + } + + public static int DeviceUnitsToPixels(double units) + { + return (int)(units * GetDensity()); + } + + 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; } + + private bool _IsRenderingWithComposition; + /// + /// Internal flag indicating that the current frame will use cache composition, old cache will be reused, only dirty children will be redrawn over it + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsRenderingWithComposition + { + get + { + return _IsRenderingWithComposition; + } + protected set + { + if (_IsRenderingWithComposition != value) + { + _IsRenderingWithComposition = value; + OnPropertyChanged(); + } + } + } + + private SKPath clipPreviousCachePath = new(); + + /// + /// Pixels, if you see no Scale parameter + /// + /// + /// + public virtual void PaintTintBackground(SKCanvas canvas, SKRect destination) + { + if (BackgroundColor != null && BackgroundColor != TransparentColor) + { + if (PaintSystem == null) + { + PaintSystem = new SKPaint(); + } + PaintSystem.Style = SKPaintStyle.StrokeAndFill; + PaintSystem.Color = BackgroundColor.ToSKColor(); + PaintSystem.BlendMode = this.FillBlendMode; + + SetupGradient(PaintSystem, FillGradient, destination); + + //clip upon ImageComposite + if (IsRenderingWithComposition) + { + var previousCache = RenderObjectPrevious; + var offset = new SKPoint(this.DrawingRect.Left - previousCache.Bounds.Left, DrawingRect.Top - previousCache.Bounds.Top); + clipPreviousCachePath.Reset(); + + foreach (var dirtyChild in DirtyChildrenInternal) + { + var clip = dirtyChild.DrawingRect; + clip.Offset(offset); + clipPreviousCachePath.AddRect(clip); + } + + var saved = canvas.Save(); + canvas.ClipPath(clipPreviousCachePath, SKClipOperation.Intersect, false); //we have rectangles, no need to antialiase + canvas.DrawRect(destination, PaintSystem); + canvas.RestoreToCount(saved); + } + else + { + canvas.DrawRect(destination, PaintSystem); + } + } + + } + + protected SKPath CombineClipping(SKPath add, SKPath path) + { + if (path == null) + return add; + if (add != null) + return add.Op(path, SKPathOp.Intersect); + return null; + } + + protected void ActionWithClipping(SKRect viewport, SKCanvas canvas, Action draw) + { + var clip = new SKPath(); + { + clip.MoveTo(viewport.Left, viewport.Top); + clip.LineTo(viewport.Right, viewport.Top); + clip.LineTo(viewport.Right, viewport.Bottom); + clip.LineTo(viewport.Left, viewport.Bottom); + clip.MoveTo(viewport.Left, viewport.Top); + clip.Close(); + } + + var saved = canvas.Save(); + + ClipSmart(canvas, clip); + + draw(); + + canvas.RestoreToCount(saved); + } + + + + + /// + /// Summing up Margins and AddMargin.. properties + /// + public virtual void CalculateMargins() + { + //use Margin property as starting point + //if specific margin is set (>=0) apply + //to final Thickness + var margin = Margin; + + margin.Left += AddMarginLeft; + margin.Right += AddMarginRight; + margin.Top += AddMarginTop; + margin.Bottom += AddMarginBottom; + + Margins = margin; + } + + public virtual Thickness GetAllMarginsInPixels(float scale) + { + var constraintLeft = Math.Round((Margins.Left + Padding.Left) * scale); + var constraintRight = Math.Round((Margins.Right + Padding.Right) * scale); + var constraintTop = Math.Round((Margins.Top + Padding.Top) * scale); + var constraintBottom = Math.Round((Margins.Bottom + Padding.Bottom) * scale); + return new(constraintLeft, constraintTop, constraintRight, constraintBottom); + } + + public virtual Thickness GetMarginsInPixels(float scale) + { + var constraintLeft = Math.Round((Margins.Left) * scale); + var constraintRight = Math.Round((Margins.Right) * scale); + var constraintTop = Math.Round((Margins.Top) * scale); + var constraintBottom = Math.Round((Margins.Bottom) * scale); + return new(constraintLeft, constraintTop, constraintRight, constraintBottom); + } + + ///// + ///// Main method to call when dimensions changed + ///// + //public virtual void InvalidateMeasure() + //{ + // if (!IsDisposed) + // { + // CalculateMargins(); + // CalculateSizeRequest(); + + // InvalidateWithChildren(); + // InvalidateParent(); + + // Update(); + // } + //} + + public virtual void InvalidateMeasureInternal() + { + CalculateMargins(); + CalculateSizeRequest(); + InvalidateWithChildren(); + InvalidateParent(); + } + + protected virtual void CalculateSizeRequest() + { + SizeRequest = GetSizeRequest((float)WidthRequest, (float)HeightRequest, false); + } + + /// + /// Will invoke InvalidateInternal on controls and subviews + /// + /// + public virtual void InvalidateChildrenTree(SkiaControl control) + { + if (control != null) + { + control.NeedMeasure = true; + + foreach (var view in control.Views.ToList()) + { + InvalidateChildrenTree(view as SkiaControl); + } + } + } + + public virtual void InvalidateChildrenTree() + { + foreach (var view in Views.ToList()) + { + InvalidateChildrenTree(view as SkiaControl); + } + } + + public virtual void InvalidateWithChildren() + { + UpdateLocked = true; + + foreach (var view in Views) //will crash? why adapter nor used?? + { + InvalidateChildren(view as SkiaControl); + } + + UpdateLocked = false; + + InvalidateInternal(); + } + + /// + /// Will invoke InvalidateInternal on controls and subviews + /// + /// + void InvalidateChildren(SkiaControl control) + { + if (control != null) + { + control.InvalidateInternal(); + + foreach (var view in control.Views.ToList()) + { + InvalidateChildren(view as SkiaControl); + } + } + } + + protected bool WillInvalidateMeasure { get; set; } + + protected static void NeedInvalidateMeasure(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaControl control) + { + control.InvalidateMeasure(); + //control.PostponeInvalidation(nameof(InvalidateMeasure), control.InvalidateMeasure); + } + } + + + protected static void NeedDraw(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaControl control) + { + control.Update(); + //control.PostponeInvalidation(nameof(Update), control.Update); + } + } + + /// + /// Just make us repaint to apply new transforms etc + /// + protected static void NeedRepaint(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaControl control) + { + control.Repaint(); + //control.PostponeInvalidation(nameof(Repaint), control.Repaint); + } + } + + protected override void InvalidateMeasure() + { + InvalidateMeasureInternal(); + + Update(); + } + + protected static void NeedInvalidateViewport(BindableObject bindable, object oldvalue, object newvalue) + { + + var control = bindable as SkiaControl; + { + if (control != null && !control.IsDisposed) + { + control.InvalidateViewport(); + } + } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + /// + /// Is using ItemTemplate or no + /// + public virtual bool IsTemplated + { + get + { + return (this.ItemTemplate != null || ItemTemplateType != null); + } + } + + public virtual void OnItemTemplateChanged() + { + + } + + public virtual object CreateContentFromTemplate() + { + if (ItemTemplateType != null) + { + return Activator.CreateInstance(ItemTemplateType); + } + return ItemTemplate.CreateContent(); + } + + protected static void ItemTemplateChanged(BindableObject bindable, object oldvalue, object newvalue) + { + + var control = bindable as SkiaControl; + { + if (control != null && !control.IsDisposed) + { + control.OnItemTemplateChanged(); + } + } + } + static object lockViews = new(); + + + + + #region Animation + + //public async Task PlayRippleAnimationAsync(Color color, double x, double y, bool removePrevious = true) + //{ + // var animation = new AfterEffectRipple() + // { + // X = x, + // Y = y, + // Color = color.ToSKColor(), + // }; + + // await PlayAnimation(animation, 350, null, removePrevious); + //} + + + public IAnimatorsManager GetAnimatorsManager() + { + return GetTopParentView() as IAnimatorsManager; + + //var parent = this.Parent; + + //if (parent is IAnimatorsManager manager) + //{ + // return manager; + //} + + //if (parent is SkiaControl control) + // return control.GetAnimatorsManager(); + + //return null; + } + + public bool RegisterAnimator(ISkiaAnimator animator) + { + var top = GetAnimatorsManager(); + if (top != null) + { + _lastAnimatorManager = top; + top.AddAnimator(animator); + Repaint(); + return true; + } + return false; + } + + public void UnregisterAnimator(Guid uid) + { + var top = GetAnimatorsManager(); + if (top == null) + { + top = _lastAnimatorManager; + } + top?.RemoveAnimator(uid); + } + + public IEnumerable UnregisterAllAnimatorsByType(Type type) + { + if (Superview != null) + return Superview.UnregisterAllAnimatorsByType(type); + + return Array.Empty(); + } + + /// + /// Expecting input coordinates in POINTs and relative to control coordinates. Use GetOffsetInsideControlInPoints to help. + /// + /// + /// + /// + /// + public async void PlayRippleAnimation(Color color, double x, double y, bool removePrevious = true) + { + if (removePrevious) + { + //UnregisterAllAnimatorsByType(typeof(RippleAnimator)); + } + + //Debug.WriteLine($"[RIPPLE] start play for '{Tag}'"); + var animation = new RippleAnimator(this) + { + Color = color.ToSKColor(), + X = x, + Y = y + }; + animation.Start(); + } + + public async void PlayShimmerAnimation(Color color, float shimmerWidth = 50, float shimmerAngle = 45, int speedMs = 1000, bool removePrevious = true) + { + //Debug.WriteLine($"[SHIMMER] start play for '{Tag}'"); + if (removePrevious) + { + //UnregisterAllAnimatorsByType(typeof(ShimmerAnimator)); + } + var animation = new ShimmerAnimator(this) + { + Color = color.ToSKColor(), + ShimmerWidth = shimmerWidth, + ShimmerAngle = shimmerAngle, + Speed = speedMs + }; + animation.Start(); + } + + #endregion + + + #region PAINT HELPERS + + public SKShader CreateGradient(SKRect destination, SkiaGradient gradient) + { + if (gradient != null && gradient.Type != GradientType.None) + { + var colors = new List(); + foreach (var color in gradient.Colors.ToList()) + { + var usingColor = color; + if (gradient.Light < 1.0) + { + usingColor = usingColor.MakeDarker(100 - gradient.Light * 100); + } + else if (gradient.Light > 1.0) + { + usingColor = usingColor.MakeLighter(gradient.Light * 100 - 100); + } + + var newAlpha = usingColor.Alpha * gradient.Opacity; + usingColor = usingColor.WithAlpha(newAlpha); + colors.Add(usingColor.ToSKColor()); + } + + float[] colorPositions = null; + if (gradient.ColorPositions?.Count == colors.Count) + { + colorPositions = gradient.ColorPositions.Select(x => (float)x).ToArray(); + } + + switch (gradient.Type) + { + case GradientType.Sweep: + + //float sweep = (float)Value3;//((float)this.Variable1 % (float)this.Variable2 / 100F) * 360.0F; + + return SKShader.CreateSweepGradient( + new SKPoint(destination.Left + destination.Width / 2.0f, + destination.Top + destination.Height / 2.0f), + colors.ToArray(), + colorPositions, + gradient.TileMode, (float)Value1, (float)(Value1 + Value2)); + + case GradientType.Circular: + return SKShader.CreateRadialGradient( + new SKPoint(destination.Left + destination.Width / 2.0f, + destination.Top + destination.Height / 2.0f), + Math.Max(destination.Width, destination.Height) / 2.0f, + colors.ToArray(), + colorPositions, + gradient.TileMode); + + case GradientType.Linear: + default: + return SKShader.CreateLinearGradient( + new SKPoint(destination.Left + destination.Width * gradient.StartXRatio, + destination.Top + destination.Height * gradient.StartYRatio), + new SKPoint(destination.Left + destination.Width * gradient.EndXRatio, + destination.Top + destination.Height * gradient.EndYRatio), + colors.ToArray(), + colorPositions, + gradient.TileMode); + break; + } + + } + + return null; + } + + protected CachedGradient LastGradient; + + protected CachedShadow LastShadow; + + + + + /// + /// Creates and sets an ImageFilter for SKPaint + /// + /// + /// + public bool SetupShadow(SKPaint paint, SkiaShadow shadow, float scale) + { + if (shadow != null && paint != null) + { + + if (LastShadow == null || LastShadow.Shadow != shadow || + LastShadow.Scale != scale) + { + var kill = LastShadow; + LastShadow = new() + { + Filter = CreateShadow(shadow, scale), + Scale = scale, + Shadow = shadow + }; + kill?.Dispose(); + } + + var old = paint.ImageFilter; + paint.ImageFilter = LastShadow.Filter; + if (old != paint.ImageFilter) + { + old?.Dispose(); + } + + return true; + } + + return false; + } + + #endregion + + + #region CHILDREN VIEWS + + private List _orderedChildren; + + public List GetOrderedSubviews(bool recalculate = false) + { + if (_orderedChildren == null || recalculate) + { + _orderedChildren = Views.OrderBy(x => x.ZIndex).ToList(); + } + return _orderedChildren; + } + + + public IReadOnlyList GetUnorderedSubviews(bool recalculate = false) + { + return Views; + } + + public List Views { get; } = new(); + + public virtual void DisposeChildren() + { + foreach (var child in Views.ToList()) + { + RemoveSubView(child); + child.Dispose(); + } + Views.Clear(); + Invalidate(); + } + + /// + /// The OnDisposing might come with a delay to avoid disposing resources at use. + /// This method will be called without delay when Dispose() is invoked. Disposed will set to True and for Views their OnWillDisposeWithChildren will be called. + /// + public virtual void OnWillDisposeWithChildren() + { + IsDisposing = true; + + foreach (var child in Views.ToList()) + { + if (child == null) + continue; + + child.OnWillDisposeWithChildren(); + } + } + + public virtual void ClearChildren() + { + foreach (var child in Views.ToList()) + { + if (child == null) + continue; + + RemoveSubView(child); + } + + Views.Clear(); + + Invalidate(); + } + + public virtual void InvalidateViewsList() + { + _orderedChildren = null; + //NeedMeasure = true; + } + + protected virtual void OnChildAdded(SkiaControl child) + { + Invalidate(); + } + + protected override void OnChildRemoved(Element child, int oldLogicalIndex) + { + //base.OnChildRemoved(child, oldLogicalIndex); + } + + protected override void OnChildAdded(Element child) + { + //base.OnChildAdded(child); + } + + protected virtual void OnChildRemoved(SkiaControl child) + { + Invalidate(); + } + + public DateTime GestureListenerRegistrationTime { get; set; } + + public void RegisterGestureListener(ISkiaGestureListener gestureListener) + { + lock (LockIterateListeners) + { + gestureListener.GestureListenerRegistrationTime = DateTime.UtcNow; + GestureListeners.Add(gestureListener); + //Debug.WriteLine($"Added {gestureListener} to gestures of {this.Tag} {this}"); + } + } + + public void UnregisterGestureListener(ISkiaGestureListener gestureListener) + { + lock (LockIterateListeners) + { + HadInput.Remove(gestureListener.Uid); + GestureListeners.Remove(gestureListener); + //Debug.WriteLine($"Removed {gestureListener} from gestures of {this.Tag} {this}"); + } + } + + /// + /// Children we should check for touch hits + /// + //public SortedSet GestureListeners { get; } = new(new DescendingZIndexGestureListenerComparer()); + + public SortedGestureListeners GestureListeners { get; } = new(); + + public virtual void OnParentChanged(IDrawnBase newvalue, IDrawnBase oldvalue) + { + if (newvalue != null && newvalue is SkiaControl control) + { + Superview = control.Superview; + } + + if (newvalue != null) + Update(); + + ParentChanged?.Invoke(this, Parent); + } + + static object lockParent = new(); + + public virtual void SetParent(IDrawnBase parent) + { + //lock (lockParent) + { + if (Parent == parent) + return; + + var iAmGestureListener = this as ISkiaGestureListener; + + //clear previous + if (Parent is IDrawnBase oldParent) + { + //kill gestures + if (iAmGestureListener != null) + { + oldParent.UnregisterGestureListener(iAmGestureListener); + } + + //fill animations + Superview?.UnregisterAllAnimatorsByParent(this); + + oldParent.Views.Remove(this); + + if (oldParent is SkiaControl skiaParent) + { + skiaParent.InvalidateViewsList(); + } + } + + if (parent == null) + { + Parent = null; + //todo maybe enable to avoid potential memory leaks + //todo investigate perf when enabled + //BindingContext = null; + return; + } + + parent.Views.Add(this); + if (parent is SkiaControl skiaParent2) + { + skiaParent2.InvalidateViewsList(); + } + + Parent = parent; + + if (iAmGestureListener != null) + { + parent.RegisterGestureListener(iAmGestureListener); + } + + if (parent is IDrawnBase control) + { + SetInheritedBindingContext(control.BindingContext); + } + + InvalidateInternal(); + } + + } + + + #endregion + + public virtual void RegisterGestureListenersTree(SkiaControl control) + { + if (control.Parent == null) + return; + + if (control is ISkiaGestureListener listener) + { + control.Parent.RegisterGestureListener(listener); + } + foreach (var view in Views) + { + view.RegisterGestureListenersTree(view); + } + } + + public virtual void UnregisterGestureListenersTree(SkiaControl control) + { + if (control.Parent == null) + return; + + if (control is ISkiaGestureListener listener) + { + control.Parent.UnregisterGestureListener(listener); + } + foreach (var view in Views) + { + view.UnregisterGestureListenersTree(view); + } + } + + + #region Children + + + public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), + typeof(DataTemplate), null, + propertyChanged: ItemTemplateChanged); + /// + /// Kind of BindableLayout.DrawnTemplate + /// + public DataTemplate ItemTemplate + { + get { return (DataTemplate)GetValue(ItemTemplateProperty); } + set { SetValue(ItemTemplateProperty, value); } + } + + public static readonly BindableProperty ItemTemplateTypeProperty = BindableProperty.Create( + nameof(ItemTemplateType), + typeof(Type), + typeof(SkiaControl), + null + , propertyChanged: ItemTemplateChanged); + + /// + /// ItemTemplate alternative for faster creation + /// + public Type ItemTemplateType + { + get { return (Type)GetValue(ItemTemplateTypeProperty); } + set { SetValue(ItemTemplateTypeProperty, value); } + } + + + #region SELECTABLE TEMPLATES + + + public static readonly BindableProperty TemplatesProperty = BindableProperty.Create( + nameof(Templates), + typeof(IList), + typeof(SkiaControl), + defaultValueCreator: (instance) => + { + var created = new ObservableCollection(); + ItemTemplateChanged(instance, null, created); + return created; + }, + validateValue: (bo, v) => v is IList, + propertyChanged: ItemTemplateChanged, + coerceValue: CoerceTemplates); + + public IList Templates + { + get => (IList)GetValue(TemplatesProperty); + set => SetValue(TemplatesProperty, value); + } + + private static object CoerceTemplates(BindableObject bindable, object value) + { + if (!(value is ReadOnlyCollection readonlyCollection)) + { + return value; + } + + return new ReadOnlyCollection( + readonlyCollection.ToList()); + } + + private void OnSkiaPropertyTemplateCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (sender is SkiaControl control) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + control.OnItemTemplateChanged(); + break; + + case NotifyCollectionChangedAction.Reset: + case NotifyCollectionChangedAction.Remove: + control.OnItemTemplateChanged(); + break; + } + } + } + + + + + #endregion + + public static readonly BindableProperty ChildrenProperty = BindableProperty.Create( + nameof(Children), + typeof(IList), + typeof(SkiaControl), + defaultValueCreator: (instance) => + { + var created = new ObservableCollection(); + ChildrenPropertyChanged(instance, null, created); + return created; + }, + validateValue: (bo, v) => v is IList, + propertyChanged: ChildrenPropertyChanged); + + public IList Children + { + get => (IList)GetValue(ChildrenProperty); + set => SetValue(ChildrenProperty, value); + } + + protected void AddOrRemoveView(SkiaControl subView, bool add) + { + if (subView != null) + { + if (add) + { + AddSubView(subView); + } + else + { + RemoveSubView(subView); + } + + } + } + + public virtual void SetChildren(IEnumerable views) + { + ClearChildren(); + + if (views == null) + return; + + foreach (var child in views) + { + AddOrRemoveView(child, true); + } + } + + private static void ChildrenPropertyChanged(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaControl skiaControl) + { + + var enumerableChildren = (IEnumerable)newvalue; + + if (oldvalue != null) + { + var oldViews = (IEnumerable)oldvalue; + + if (oldvalue is INotifyCollectionChanged oldCollection) + { + oldCollection.CollectionChanged -= skiaControl.OnChildrenCollectionChanged; + } + + foreach (var subView in oldViews) + { + skiaControl.AddOrRemoveView(subView, false); + } + } + + if (skiaControl.ItemTemplate == null) { - _X = value; - OnPropertyChanged(); + skiaControl.SetChildren(enumerableChildren); + } + + //foreach (var subView in enumerableChildren) + //{ + // subView.SetParent(skiaControl); + //} + + if (newvalue is INotifyCollectionChanged newCollection) + { + newCollection.CollectionChanged -= skiaControl.OnChildrenCollectionChanged; + newCollection.CollectionChanged += skiaControl.OnChildrenCollectionChanged; } + + skiaControl.Update(); } + } - private double _Y; - public double Y + public bool HasItemTemplate { get { - return _Y; - } - set - { - if (_Y != value) - { - _Y = value; - OnPropertyChanged(); - } + return ItemTemplate != null; } } - */ - - //public static readonly BindableProperty HeightProperty = BindableProperty.Create(nameof(Height), - // typeof(double), typeof(SkiaControl), - // -1.0); - //public double Height - //{ - // get { return (double)GetValue(HeightProperty); } - // set { SetValue(HeightProperty, value); } - //} - + private void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (HasItemTemplate) + return; - //public static readonly BindableProperty WidthProperty = BindableProperty.Create(nameof(Width), - // typeof(double), typeof(SkiaControl), - // -1.0); - //public double Width - //{ - // get { return (double)GetValue(WidthProperty); } - // set { SetValue(WidthProperty, value); } - //} + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (SkiaControl newChildren in e.NewItems) + { + AddOrRemoveView(newChildren, true); + } + break; + case NotifyCollectionChangedAction.Reset: + case NotifyCollectionChangedAction.Remove: + foreach (SkiaControl oldChildren in e.OldItems ?? Array.Empty()) + { + AddOrRemoveView(oldChildren, false); + } - /* + break; + } - public static readonly BindableProperty OpacityProperty = BindableProperty.Create(nameof(Opacity), - typeof(double), typeof(SkiaControl), - 0.0, - propertyChanged: RedrawCanvas); - public double Opacity - { - get { return (double)GetValue(OpacityProperty); } - set { SetValue(OpacityProperty, value); } + Update(); } - */ + #endregion - public static readonly BindableProperty SkewXProperty - = BindableProperty.Create(nameof(SkewX), - typeof(float), typeof(SkiaControl), - 0.0f, propertyChanged: NeedRepaint); - - public float SkewX - { - get - { - return (float)GetValue(SkewXProperty); - } - set - { - SetValue(SkewXProperty, value); - } - } - - public static readonly BindableProperty SkewYProperty - = BindableProperty.Create(nameof(SkewY), - typeof(float), typeof(SkiaControl), - 0.0f, propertyChanged: NeedRepaint); - - public float SkewY - { - get - { - return (float)GetValue(SkewYProperty); - } - set - { - SetValue(SkewYProperty, value); - } - } - - public static readonly BindableProperty CameraAngleXProperty - = BindableProperty.Create(nameof(CameraAngleX), - typeof(float), typeof(SkiaControl), - 0.0f, propertyChanged: NeedRepaint); - - public float CameraAngleX - { - get - { - return (float)GetValue(CameraAngleXProperty); - } - set - { - SetValue(CameraAngleXProperty, value); - } - } - - public static readonly BindableProperty CameraAngleYProperty - = BindableProperty.Create(nameof(CameraAngleY), - typeof(float), typeof(SkiaControl), - 0.0f, propertyChanged: NeedRepaint); - - public float CameraAngleY - { - get - { - return (float)GetValue(CameraAngleYProperty); - } - set - { - SetValue(CameraAngleYProperty, value); - } - } - - public static readonly BindableProperty CameraAngleZProperty - = BindableProperty.Create(nameof(CameraAngleZ), - typeof(float), typeof(SkiaControl), - 0.0f, propertyChanged: NeedRepaint); - - public float CameraAngleZ - { - get - { - return (float)GetValue(CameraAngleZProperty); - } - set - { - SetValue(CameraAngleZProperty, value); - } - } - - public static readonly BindableProperty CameraTranslationZProperty - = BindableProperty.Create(nameof(CameraTranslationZ), - typeof(float), typeof(SkiaControl), - 0.0f, propertyChanged: NeedRepaint); - - public float CameraTranslationZ - { - get - { - return (float)GetValue(CameraTranslationZProperty); - } - set - { - SetValue(CameraTranslationZProperty, value); - } - } - - - - //public new static readonly BindableProperty VerticalOptionsProperty = BindableProperty.Create(nameof(VerticalOptions), - // typeof(LayoutOptions), - // typeof(SkiaControl), - // LayoutOptions.Start, - // propertyChanged: NeedInvalidateMeasure); - - //public new LayoutOptions VerticalOptions - //{ - // get { return (LayoutOptions)GetValue(VerticalOptionsProperty); } - // set { SetValue(VerticalOptionsProperty, value); } - //} - - //public new static readonly BindableProperty HorizontalOptionsProperty = BindableProperty.Create(nameof(HorizontalOptions), - // typeof(LayoutOptions), - // typeof(SkiaControl), - // LayoutOptions.Start, - // propertyChanged: NeedInvalidateMeasure); - - //public new LayoutOptions HorizontalOptions - //{ - // get { return (LayoutOptions)GetValue(HorizontalOptionsProperty); } - // set { SetValue(HorizontalOptionsProperty, value); } - //} - - - - - public static readonly BindableProperty Perspective1Property - = BindableProperty.Create(nameof(Perspective1), - typeof(float), typeof(SkiaControl), - 0.0f, propertyChanged: NeedRepaint); - - public float Perspective1 - { - get - { - return (float)GetValue(Perspective1Property); - } - set - { - SetValue(Perspective1Property, value); - } - } - - public static readonly BindableProperty Perspective2Property - = BindableProperty.Create(nameof(Perspective2), - typeof(float), typeof(SkiaControl), - 0.0f, propertyChanged: NeedRepaint); - - public float Perspective2 - { - get - { - return (float)GetValue(Perspective2Property); - } - set - { - SetValue(Perspective2Property, value); - } - } - - public static readonly BindableProperty TransformPivotPointXProperty - = BindableProperty.Create(nameof(TransformPivotPointX), - typeof(double), typeof(SkiaControl), - 0.5, propertyChanged: NeedRepaint); - public double TransformPivotPointX - { - get - { - return (double)GetValue(TransformPivotPointXProperty); - } - set - { - SetValue(TransformPivotPointXProperty, value); - } - } - - public static readonly BindableProperty TransformPivotPointYProperty - = BindableProperty.Create(nameof(TransformPivotPointY), - typeof(double), typeof(SkiaControl), - 0.5, propertyChanged: NeedRepaint); - - public double TransformPivotPointY - { - get - { - return (double)GetValue(TransformPivotPointYProperty); - } - set - { - SetValue(TransformPivotPointYProperty, value); - } - } - - - 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) - { - if (bindable is SkiaControl control) - { - control.OnParentChanged(newvalue as IDrawnBase, oldvalue as IDrawnBase); - } - } - - public new event EventHandler ParentChanged; - - public static readonly BindableProperty AdjustClippingProperty = BindableProperty.Create( - nameof(AdjustClipping), - typeof(Thickness), - typeof(SkiaControl), - Thickness.Zero, - propertyChanged: NeedInvalidateMeasure); - - public Thickness AdjustClipping - { - get { return (Thickness)GetValue(AdjustClippingProperty); } - set { SetValue(AdjustClippingProperty, value); } - } - - - public static readonly BindableProperty PaddingProperty = BindableProperty.Create(nameof(Padding), typeof(Thickness), - typeof(SkiaControl), Thickness.Zero, - propertyChanged: NeedInvalidateMeasure); - public Thickness Padding - { - get { return (Thickness)GetValue(PaddingProperty); } - set { SetValue(PaddingProperty, value); } - } - - public static readonly BindableProperty MarginProperty = BindableProperty.Create(nameof(Margin), typeof(Thickness), - typeof(SkiaControl), Thickness.Zero, - propertyChanged: NeedInvalidateMeasure); - public Thickness Margin - { - get { return (Thickness)GetValue(MarginProperty); } - set { SetValue(MarginProperty, value); } - } - - public static readonly BindableProperty AddMarginTopProperty = BindableProperty.Create( - nameof(AddMarginTop), - typeof(double), - typeof(SkiaControl), - 0.0, propertyChanged: NeedInvalidateMeasure); - - public double AddMarginTop - { - get { return (double)GetValue(AddMarginTopProperty); } - set { SetValue(AddMarginTopProperty, value); } - } - - public static readonly BindableProperty AddMarginBottomProperty = BindableProperty.Create( - nameof(AddMarginBottom), - typeof(double), - typeof(SkiaControl), - 0.0, propertyChanged: NeedInvalidateMeasure); - - public double AddMarginBottom - { - get { return (double)GetValue(AddMarginBottomProperty); } - set { SetValue(AddMarginBottomProperty, value); } - } - - public static readonly BindableProperty AddMarginLeftProperty = BindableProperty.Create( - nameof(AddMarginLeft), - typeof(double), - typeof(SkiaControl), - 0.0, propertyChanged: NeedInvalidateMeasure); - - public double AddMarginLeft - { - get { return (double)GetValue(AddMarginLeftProperty); } - set { SetValue(AddMarginLeftProperty, value); } - } - - public static readonly BindableProperty AddMarginRightProperty = BindableProperty.Create( - nameof(AddMarginRight), - typeof(double), - typeof(SkiaControl), - 0.0, propertyChanged: NeedInvalidateMeasure); - - public double AddMarginRight - { - get { return (double)GetValue(AddMarginRightProperty); } - set { SetValue(AddMarginRightProperty, value); } - } - - /// - /// Total calculated margins in points - /// - public Thickness Margins - { - get => _margins; - protected set - { - if (value.Equals(_margins)) return; - _margins = value; - OnPropertyChanged(); - } - } - - public static readonly BindableProperty SpacingProperty = BindableProperty.Create(nameof(Spacing), typeof(double), - typeof(SkiaControl), - 8.0, - propertyChanged: NeedInvalidateMeasure); - public double Spacing - { - get { return (double)GetValue(SpacingProperty); } - set { SetValue(SpacingProperty, value); } - } - - public static readonly BindableProperty AddTranslationYProperty = BindableProperty.Create( - nameof(AddTranslationY), - typeof(double), - typeof(SkiaControl), - 0.0, propertyChanged: NeedRepaint); - - public double AddTranslationY - { - get { return (double)GetValue(AddTranslationYProperty); } - set { SetValue(AddTranslationYProperty, value); } - } - - public static readonly BindableProperty AddTranslationXProperty = BindableProperty.Create( - nameof(AddTranslationX), - typeof(double), - typeof(SkiaControl), - 0.0, propertyChanged: NeedRepaint); - - public double AddTranslationX - { - get { return (double)GetValue(AddTranslationXProperty); } - set { SetValue(AddTranslationXProperty, value); } - } - - public static readonly BindableProperty ExpandCacheRecordingAreaProperty - = BindableProperty.Create(nameof(ExpandCacheRecordingArea), - typeof(double), typeof(SkiaControl), - 0.0, propertyChanged: NeedDraw); - /// - /// Normally cache is recorded inside DrawingRect, but you might want to exapnd this to include shadows around, for example. - /// Specify number of points by which you want to expand the recording area. - /// Also you might maybe want to include a bigger area if your control is not inside the DrawingRect due to transforms/translations. - /// Override GetCacheRecordingArea method for a similar action. - /// - public double ExpandCacheRecordingArea - { - get - { - return (double)GetValue(ExpandCacheRecordingAreaProperty); - } - set - { - SetValue(ExpandCacheRecordingAreaProperty, value); - } - } - - /* - public static readonly BindableProperty AlignContentVerticalProperty = BindableProperty.Create(nameof(AlignContentVertical), - typeof(LayoutOptions), - typeof(SkiaControl), - LayoutOptions.Start, - propertyChanged: NeedInvalidateMeasure); - public LayoutOptions AlignContentVertical - { - get { return (LayoutOptions)GetValue(AlignContentVerticalProperty); } - set { SetValue(AlignContentVerticalProperty, value); } - } - public static readonly BindableProperty AlignContentHorizontalProperty = BindableProperty.Create(nameof(AlignContentHorizontal), - typeof(LayoutOptions), - typeof(SkiaControl), - LayoutOptions.Start, - propertyChanged: NeedInvalidateMeasure); - public LayoutOptions AlignContentHorizontal - { - get { return (LayoutOptions)GetValue(AlignContentHorizontalProperty); } - set { SetValue(AlignContentHorizontalProperty, value); } - } - */ + public AddGestures.GestureListener GesturesEffect { get; set; } - public static readonly BindableProperty IsClippedToBoundsProperty = BindableProperty.Create(nameof(IsClippedToBounds), - typeof(bool), typeof(SkiaControl), false, - propertyChanged: NeedInvalidateMeasure); - /// - /// This cuts shadows etc. You might want to enable it for some cases as it speeds up the rendering, it is False by default - /// - public bool IsClippedToBounds - { - get { return (bool)GetValue(IsClippedToBoundsProperty); } - set { SetValue(IsClippedToBoundsProperty, value); } - } - - public static readonly BindableProperty ClipEffectsProperty = BindableProperty.Create(nameof(ClipEffects), - typeof(bool), typeof(SkiaControl), true, - propertyChanged: NeedInvalidateMeasure); - /// - /// This cuts shadows etc - /// - public bool ClipEffects - { - get { return (bool)GetValue(ClipEffectsProperty); } - set { SetValue(ClipEffectsProperty, value); } - } - - public static readonly BindableProperty BindableTriggerProperty = BindableProperty.Create(nameof(BindableTrigger), - typeof(object), typeof(SkiaControl), - null, - propertyChanged: TriggerPropertyChanged); - - private static void TriggerPropertyChanged(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is SkiaControl control) - { - control.OnTriggerChanged(); - } - } - - public virtual void OnTriggerChanged() - { - - } - - public object BindableTrigger - { - get { return (object)GetValue(BindableTriggerProperty); } - set { SetValue(BindableTriggerProperty, value); } - } - - public static readonly BindableProperty Value1Property = BindableProperty.Create(nameof(Value1), - typeof(double), typeof(SkiaControl), - 0.0, - propertyChanged: NeedDraw); - public double Value1 - { - get { return (double)GetValue(Value1Property); } - set { SetValue(Value1Property, value); } - } - - public static readonly BindableProperty Value2Property = BindableProperty.Create(nameof(Value2), - typeof(double), typeof(SkiaControl), - 0.0, - propertyChanged: NeedDraw); - public double Value2 - { - get { return (double)GetValue(Value2Property); } - set { SetValue(Value2Property, value); } - } - - public static readonly BindableProperty Value3Property = BindableProperty.Create(nameof(Value3), - typeof(double), typeof(SkiaControl), - 0.0, - propertyChanged: NeedDraw); - public double Value3 - { - get { return (double)GetValue(Value3Property); } - set { SetValue(Value3Property, value); } - } - - //------------------------------------------------------------- - // Value4 - //------------------------------------------------------------- - public static readonly BindableProperty Value4Property = BindableProperty.Create(nameof(Value4), - typeof(double), typeof(SkiaControl), - 0.0, - propertyChanged: NeedDraw); - public double Value4 - { - get { return (double)GetValue(Value4Property); } - set { SetValue(Value4Property, value); } - } - - - public static readonly BindableProperty RenderingScaleProperty = BindableProperty.Create(nameof(RenderingScale), - typeof(float), typeof(SkiaControl), - -1.0f, propertyChanged: NeedUpdateScale); - - private static void NeedUpdateScale(BindableObject bindable, object oldValue, object newValue) - { - if (bindable is SkiaControl control) - { - control.OnScaleChanged(); - } - } - - public float RenderingScale - { - get - { - var value = -1f; - try - { - value = (float)GetValue(RenderingScaleProperty); - } - catch (Exception e) - { - Super.Log(e); //catching Nullable object must have a value, is this because of NET8? - } - if (value <= 0) - { - return GetDensity(); - } - return value; - } - set - { - SetValue(RenderingScaleProperty, value); - } - } - - - - //public double RenderingScaleSafe - //{ - // get - // { - // var value = Density; - // if (value == 0) - // value = 1; - - // if (RenderingScale < 0 || RenderingScale == 0) - // { - // return value; - // } - - // return RenderingScale; - // } - //} - - - - #endregion - - - public SKRect RenderedAtDestination { get; set; } - - public virtual void OnScaleChanged() - { - InvalidateMeasure(); - } - - - private Style _currentStyle; - private void SubscribeToStyleProperties() - { - UnsubscribeFromOldStyle(); - - _currentStyle = Style; - - if (_currentStyle != null) - { - - } - } - - private void UnsubscribeFromOldStyle() - { - if (_currentStyle != null) - { - - } - } - - public virtual void SetPropertyValue(BindableProperty property, object value) - { - this.SetValue(property, value); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (float X, float Y) RescaleAspect(float width, float height, SKRect dest, TransformAspect stretch) + { + float aspectX = 1; + float aspectY = 1; + var s1 = dest.Width / width; + var s2 = dest.Height / height; - public Guid Uid { get; set; } = Guid.NewGuid(); + switch (stretch) + { + case TransformAspect.None: + break; + + case TransformAspect.Fit: + aspectX = dest.Width < width ? dest.Width / width : 1; + aspectY = dest.Height < height ? dest.Height / height : 1; + break; + + case TransformAspect.Fill: + aspectX = width < dest.Width ? s1 : 1; + aspectY = height < dest.Height ? s2 : 1; + break; + + case TransformAspect.AspectFit: + aspectX = Math.Min(s1, s2); + aspectY = aspectX; + break; + + case TransformAspect.AspectFill: + aspectX = width < dest.Width ? Math.Max(s1, s2) : 1; + aspectY = aspectX; + break; + + case TransformAspect.Cover: + aspectX = s1; + aspectY = s2; + break; + + case TransformAspect.AspectCover: + aspectX = Math.Max(s1, s2); + aspectY = aspectX; + break; + + case TransformAspect.AspectFitFill: + var imageAspect = width / height; + var viewportAspect = dest.Width / dest.Height; + + if (imageAspect > viewportAspect) // Image is wider than viewport + { + aspectX = dest.Width / width; + aspectY = aspectX; + } + else // Image is taller than viewport + { + aspectY = dest.Height / height; + aspectX = aspectY; + } + break; + } - //todo check adapt for MAUI + return (aspectX, aspectY); + } - public static double DegreesToRadians(double value) - { - return ((value * Math.PI) / 180); - } - public static double RadiansToDegrees(double value) - { - return value * 180 / Math.PI; - } - - public static (double X1, double Y1, double X2, double Y2) LinearGradientAngleToPoints(double direction) - { - //adapt to css style - direction -= 90; - - //allow negative angles - if (direction < 0) - direction = 360 + direction; - - if (direction > 360) - direction = 360; - - (double x, double y) PointOfAngle(double a) - { - return (x: Math.Cos(a), y: Math.Sin(a)); - }; + #region HELPERS - var eps = Math.Pow(2, -52); - var angle = (direction % 360); - var startPoint = PointOfAngle(DegreesToRadians(180 - angle)); - var endPoint = PointOfAngle(DegreesToRadians(360 - angle)); + public static Random Random = new Random(); + protected double _arrangedViewportHeightLimit; + protected double _arrangedViewportWidthLimit; + protected float _lastMeasuredForScale; + private bool _isLayoutDirty; + private Thickness _margins; + private SKRect _lastArrangedInside; + private double _lastArrangedForScale; + private bool _needUpdateFrontCache; - if (startPoint.x <= 0 || Math.Abs(startPoint.x) <= eps) - startPoint.x = 0; - if (startPoint.y <= 0 || Math.Abs(startPoint.y) <= eps) - startPoint.y = 0; - - if (endPoint.x <= 0 || Math.Abs(endPoint.x) <= eps) - endPoint.x = 0; - - if (endPoint.y <= 0 || Math.Abs(endPoint.y) <= eps) - endPoint.y = 0; - - return (startPoint.x, startPoint.y, endPoint.x, endPoint.y); - } - - - /// - /// Dispose with needed delay. - /// - /// - public virtual void DisposeObject(IDisposable disposable) - { - if (disposable != null) - { - if (Superview is DrawnView view) - { - view.ToBeDisposed.Enqueue(disposable); - } - else - { - Tasks.StartDelayed(TimeSpan.FromSeconds(3.5), () => - { - disposable?.Dispose(); - }); - } - } - } - - private void ViewSizeChanged(object sender, EventArgs e) - { - OnSizeChanged(); - } - - protected virtual void OnSizeChanged() - { - - } - - public Action Clipping { get; set; } - - public SkiaControl ClippedBy { get; set; } - - - /// - /// Optional scene hero control identifier - /// - public string Hero { get; set; } - - public int ContextIndex { get; set; } = -1; - - public bool IsRootView() - { - return this.Parent == null; - } - - /// - /// destination in PIXELS, requests in UNITS - /// - /// - /// - /// - /// - /// - public ScaledSize DefineAvailableSize(SKRect destination, - float widthRequest, float heightRequest, float scale) - { - var rectWidth = destination.Width; - var wants = widthRequest * scale; - if (wants >= 0 && wants < rectWidth) - rectWidth = (int)wants; - - var rectHeight = destination.Height; - wants = heightRequest * scale; - if (wants >= 0 && wants < rectHeight) - rectHeight = (int)wants; - - return ScaledSize.FromPixels(rectWidth, rectHeight, scale); - } - - /// - /// Set this by parent if needed, normally child can detect this itsself. If true will call Arrange when drawing. - /// - public bool IsLayoutDirty - { - get => _isLayoutDirty; - set - { - if (value == _isLayoutDirty) return; - _isLayoutDirty = value; - OnPropertyChanged(); - } - } - - /// - /// destination in PIXELS, requests in UNITS. resulting Destination prop will be filed in PIXELS. - /// Not using Margins nor Padding - /// Children are responsible to apply Padding to their content and to apply Margin to destination when measuring and drawing - /// - /// PIXELS - /// UNITS - /// UNITS - /// - public SKRect CalculateLayout(SKRect destination, float widthRequest, float heightRequest, float scale) - { - //if (widthRequest == 0 || heightRequest == 0) - //{ - // return new SKRect(0, 0, 0, 0); - //} - - var rectAvailable = DefineAvailableSize(destination, widthRequest, heightRequest, scale); - - var useMaxWidth = rectAvailable.Pixels.Width; - var useMaxHeight = rectAvailable.Pixels.Height; - var availableWidth = destination.Width; - var availableHeight = destination.Height; - - var layoutHorizontal = new LayoutOptions(HorizontalOptions.Alignment, HorizontalOptions.Expands); - var layoutVertical = new LayoutOptions(VerticalOptions.Alignment, VerticalOptions.Expands); - - // initial fill - var left = destination.Left; - var top = destination.Top; - var right = 0f; - var bottom = 0f; - - // layoutHorizontal - switch (layoutHorizontal.Alignment) - { - - case LayoutAlignment.Center when float.IsFinite(availableWidth): - { - left += (float)Math.Round(availableWidth / 2.0f - useMaxWidth / 2.0f); - right = left + useMaxWidth; - - if (left < destination.Left) - { - left = destination.Left; - right = left + useMaxWidth; - } - - if (right > destination.Right) - { - right = destination.Right; - } - - break; - } - case LayoutAlignment.End when float.IsFinite(destination.Right): - { - right = destination.Right; - left = right - useMaxWidth; - if (left < destination.Left) - { - left = destination.Left; - } - - break; - } - case LayoutAlignment.Fill: - case LayoutAlignment.Start: - default: - { - right = left + useMaxWidth; - if (right > destination.Right) - { - right = destination.Right; - } - - break; - } - } - - // VerticalOptions - switch (layoutVertical.Alignment) - { - - case LayoutAlignment.Center when float.IsFinite(availableHeight): - { - top += (float)Math.Round(availableHeight / 2.0f - useMaxHeight / 2.0f); - bottom = top + useMaxHeight; - - if (top < destination.Top) - { - top = destination.Top; - bottom = top + useMaxHeight; - } - - else if (bottom > destination.Bottom) - { - bottom = destination.Bottom; - top = bottom - useMaxHeight; - } - - break; - } - case LayoutAlignment.End when float.IsFinite(destination.Bottom): - { - bottom = destination.Bottom; - top = bottom - useMaxHeight; - if (top < destination.Top) - { - top = destination.Top; - } - - break; - } - case LayoutAlignment.Start: - case LayoutAlignment.Fill: - default: - - bottom = top + useMaxHeight; - if (bottom > destination.Bottom) - { - bottom = destination.Bottom; - } - break; - - } - - var layout = new SKRect(left, top, right, bottom); - - var offsetX = 0f; - var offsetY = 0f; - if (float.IsFinite(availableHeight)) - { - offsetY = (float)VerticalPositionOffsetRatio * layout.Height; - } - if (float.IsFinite(availableWidth)) - { - offsetX = (float)HorizontalPositionOffsetRatio * layout.Width; - } - - layout.Offset(offsetX, offsetY); - - return layout; - } - - /* - public SKRect CalculateLayout(SKRect destination, float widthRequest, float heightRequest, float scale) + public static Color GetRandomColor() { - if (widthRequest == 0 || heightRequest == 0) - { - return new SKRect(0, 0, 0, 0); - } - - var rectAvailable = DefineAvailableSize(destination, widthRequest, heightRequest, scale); + byte r = (byte)Random.Next(256); + byte g = (byte)Random.Next(256); + byte b = (byte)Random.Next(256); - var availableWidth = destination.Width; - var availableHeight = destination.Height; + return Color.FromRgb(r, g, b); + } - var layoutHorizontal = new LayoutOptions(HorizontalOptions.Alignment, HorizontalOptions.Expands); - var layoutVertical = new LayoutOptions(VerticalOptions.Alignment, HorizontalOptions.Expands); + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + //public static bool CompareRects(SKRect a, SKRect b, float precision) + //{ + // return + // Math.Abs(a.Left - b.Left) <= precision + // && Math.Abs(a.Top - b.Top) <= precision + // && Math.Abs(a.Right - b.Right) <= precision + // && Math.Abs(a.Bottom - b.Bottom) <= precision; + //} + + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + //public static bool CompareRectsSize(SKRect a, SKRect b, float precision) + //{ + // return + // Math.Abs(a.Width - b.Width) <= precision + // && Math.Abs(a.Height - b.Height) <= precision; + //} + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CompareFloats(float a, float b, float precision) + { + return Math.Abs(a - b) <= precision; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CompareDoubles(double a, double b, double precision) + { + return Math.Abs(a - b) <= precision; + } - //initial fill - var left = destination.Left; - var top = destination.Top; - var right = left + rectAvailable.Pixels.Width; - var bottom = top + rectAvailable.Pixels.Height; - //layoutHorizontal - if (layoutHorizontal.Alignment == LayoutAlignment.Center && float.IsFinite(availableWidth)) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CompareRects(SKRect a, SKRect b, float precision) + { + if ((float.IsInfinity(a.Left) && float.IsInfinity(b.Left)) || Math.Abs(a.Left - b.Left) <= precision) { - //center - left += (availableWidth - rectAvailable.Pixels.Width) / 2.0f; - right = left + rectAvailable.Pixels.Width; - - if (left < destination.Left) - { - left = (float)(destination.Left); - right = left + rectAvailable.Pixels.Width; - } - - if (right > destination.Right) + if ((float.IsInfinity(a.Top) && float.IsInfinity(b.Top)) || Math.Abs(a.Top - b.Top) <= precision) { - right = (float)(destination.Right); + if ((float.IsInfinity(a.Right) && float.IsInfinity(b.Right)) || Math.Abs(a.Right - b.Right) <= precision) + { + if ((float.IsInfinity(a.Bottom) && float.IsInfinity(b.Bottom)) || Math.Abs(a.Bottom - b.Bottom) <= precision) + { + return true; + } + } } - } - else - if (layoutHorizontal.Alignment == LayoutAlignment.End) + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CompareRectsSize(SKRect a, SKRect b, float precision) + { + if ((float.IsInfinity(a.Width) && float.IsInfinity(b.Width)) || (Math.Abs(a.Width - b.Width) <= precision)) { - //end - right = destination.Right; - left = right - rectAvailable.Pixels.Width; - if (left < destination.Left) + if ((float.IsInfinity(a.Height) && float.IsInfinity(b.Height)) || (Math.Abs(a.Height - b.Height) <= precision)) { - left = (float)(destination.Left); + return true; } } - else + return false; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CompareSize(SKSize a, SKSize b, float precision) + { + if ((float.IsInfinity(a.Width) && float.IsInfinity(b.Width)) || Math.Abs(a.Width - b.Width) <= precision) { - //start or fill - right = left + rectAvailable.Pixels.Width; - if (right > destination.Right) + if ((float.IsInfinity(a.Height) && float.IsInfinity(b.Height)) || Math.Abs(a.Height - b.Height) <= precision) { - right = (float)(destination.Right); + return true; } + } + return false; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AreEqual(double v1, double v2, double precision) + { + if (double.IsInfinity(v1) && double.IsInfinity(v2) || Math.Abs(v1 - v2) <= precision) + { + return true; } + return false; + } - //VerticalOptions - if (layoutVertical.Alignment == LayoutAlignment.Center) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AreEqual(float v1, float v2, float precision) + { + if (float.IsInfinity(v1) && float.IsInfinity(v2) || Math.Abs(v1 - v2) <= precision) { - //center - top += availableHeight / 2.0f - rectAvailable.Pixels.Height / 2.0f; - bottom = top + rectAvailable.Pixels.Height; - if (top < destination.Top) - { - top = (float)(destination.Top); - bottom = top + rectAvailable.Pixels.Height; - } - else - if (bottom > destination.Bottom) - { - bottom = (float)(destination.Bottom); - top = bottom - rectAvailable.Pixels.Height; - } + return true; } - else - if (layoutVertical.Alignment == LayoutAlignment.End && double.IsFinite(destination.Bottom)) + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AreVectorsEqual(Vector2 v1, Vector2 v2, float precision) + { + if ((float.IsInfinity(v1.X) && float.IsInfinity(v2.X)) || Math.Abs(v1.X - v2.X) <= precision) { - //end - bottom = destination.Bottom; - top = bottom - rectAvailable.Pixels.Height; - if (top < destination.Top) + if ((float.IsInfinity(v1.Y) && float.IsInfinity(v2.Y)) || Math.Abs(v1.Y - v2.Y) <= precision) { - top = (float)(destination.Top); + return true; } + } + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DirectionType GetDirectionType(Vector2 velocity, DirectionType defaultDirection, float ratio) + { + float x = Math.Abs(velocity.X); + float y = Math.Abs(velocity.Y); + + if (x > y && x / y > ratio) + { + //Debug.WriteLine($"[DirectionType] H {x:0.0},{y:0.0} = {x / y:0.00}"); + return DirectionType.Horizontal; + } + else if (y > x && y / x >= ratio) + { + return DirectionType.Vertical; } else { - //start or fill - bottom = top + rectAvailable.Pixels.Height; - if (bottom > destination.Bottom) - { - bottom = (float)(destination.Bottom); - } + return defaultDirection; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DirectionType GetDirectionType(Vector2 start, Vector2 end, float ratio) + { + // Calculate the absolute differences between the X and Y coordinates + float x = Math.Abs(end.X - start.X); + float y = Math.Abs(end.Y - start.Y); + // Compare the differences to determine the dominant direction + if (x > y && x / y > ratio) + { + return DirectionType.Horizontal; + } + else if (y > x && y / x >= ratio) + { + return DirectionType.Vertical; + } + else + { + return DirectionType.None; // The direction is neither horizontal nor vertical (the vectors are equal or diagonally aligned) } + } - var ret = new SKRect((float)left, (float)top, (float)right, (float)bottom); - //Debug.WriteLine($"[Layout] '{Tag}' {ret.Left - destination.Left:0.0}-{destination.Right - ret.Right:0.0} "); - return ret; + /// + /// Ported from Avalonia: AreClose - Returns whether or not two floats are "close". That is, whether or + /// not they are within epsilon of each other. + /// + /// The first float to compare. + /// The second float to compare. + public static bool AreClose(float value1, float value2) + { + //in case they are Infinities (then epsilon check does not work) + if (value1 == value2) return true; + float eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0f) * float.Epsilon; + float delta = value1 - value2; + return (-eps < delta) && (eps > delta); } - */ - private ScaledSize _contentSize = new(); - public ScaledSize ContentSize - { - get - { - return _contentSize; - } - protected set - { - if (_contentSize != value) - { - _contentSize = value; - OnPropertyChanged(); - } - } - } - - - protected bool WasMeasured; - - protected virtual void OnDrawingSizeChanged() - { - - } - - protected virtual void AdaptCachedLayout(SKRect destination, float scale) - { - //adapt cache to current request - var newDestination = ArrangedDestination;//.Clone(); - newDestination.Offset(destination.Left, destination.Top); - - Destination = newDestination; - DrawingRect = GetDrawingRectWithMargins(newDestination, scale); - - IsLayoutDirty = false; - } - - private SKRect _lastArrangedFor = new(); - private float _lastArrangedWidth; - private float _lastArrangedHeight; - - public float _lastMeasuredForWidth { get; protected set; } - public float _lastMeasuredForHeight { get; protected set; } - - /// - /// This is the destination in PIXELS with margins applied, using this to paint background. Since we enabled subpixel drawing (for smooth scroll etc) expect this to have non-rounded values, use CompareRects and similar for comparison. - /// - public SKRect DrawingRect { get; set; } - - /// - /// Overriding VisualElement property, use DrawingRect instead. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public new Rect Bounds - { - get - { - return DrawingRect.ToMauiRectangle(); - } - private set - { - throw new NotImplementedException("Use DrawingRect instead."); - } - } - - /// - /// ISkiaGestureListener impl - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public virtual bool HitIsInside(float x, float y) - { - var hitbox = HitBoxAuto; - - //if (UsingCacheType != SkiaCacheType.None && RenderObject != null) - //{ - // var offsetCacheX = Math.Abs(DrawingRect.Left - RenderObject.Bounds.Left); - // var offsetCacheY = Math.Abs(DrawingRect.Top - RenderObject.Bounds.Top); - - // hitbox.Offset(offsetCacheX,offsetCacheY); - //} - - return hitbox.ContainsInclusive(x, y); ; - } - - /// - /// This can be absolutely false if we are inside a cached - /// rendering object parent that already moved somewhere. - /// So coords will be of the moment we were first drawn, - /// while if cached parent moved, our coords might differ. - /// todo detect if parent is cached somewhere and offset hotbox by cached parent movement offset... - /// todo think about it baby =) meanwhile just do not set gestures below cached level - /// - public virtual SKRect HitBoxAuto - { - get - { - var moved = ApplyTransforms(DrawingRect); - - return moved; - } - } - - public virtual bool IsGestureForChild(ISkiaGestureListener listener, SKPoint point) - { - return IsGestureForChild(listener, point.X, point.Y); - } - - public virtual bool IsGestureForChild(ISkiaGestureListener listener, float x, float y) - { - var hit = listener.HitIsInside(x, y); - return hit; - } - - public SKRect ApplyTransforms(SKRect rect) - { - //Debug.WriteLine($"[Transforming] {rect}"); - - return new SKRect(rect.Left + (float)(UseTranslationX * RenderingScale), - rect.Top + (float)(UseTranslationY * RenderingScale), - rect.Right + (float)(UseTranslationX * RenderingScale), - rect.Bottom + (float)(UseTranslationY * RenderingScale)); - } - - public virtual SKPoint TranslateInputDirectOffsetToPoints(PointF location, SKPoint childOffsetDirect) - { - var thisOffset1 = TranslateInputCoords(childOffsetDirect, false); - //apply touch coords - var x1 = location.X + thisOffset1.X; - var y1 = location.Y + thisOffset1.Y; - //convert to points - return new SKPoint(x1 / RenderingScale, y1 / RenderingScale); - } - - public virtual SKPoint TranslateInputOffsetToPixels(PointF location, SKPoint childOffset) - { - var thisOffset = TranslateInputCoords(childOffset); - return new SKPoint(location.X + thisOffset.X, location.Y + thisOffset.Y); - } - - /// - /// Use this to consume gestures in your control only, - /// do not use result for passing gestures below - /// - /// - /// - public virtual SKPoint TranslateInputCoords(SKPoint childOffset, bool accountForCache = true) - { - var thisOffset = new SKPoint(-(float)(UseTranslationX * RenderingScale), -(float)(UseTranslationY * RenderingScale)); - - //inside a cached object coordinates are frozen at the moment the snapshot was taken - //so we must offset the coordinates to match the current drawing rect - if (accountForCache) - { - /* - if (UsingCacheType == SkiaCacheType.ImageComposite) - { - if (RenderObjectPrevious != null) - { - thisOffset.Offset(RenderObjectPrevious.TranslateInputCoords(LastDrawnAt)); - } - else - if (RenderObject != null) - { - thisOffset.Offset(RenderObject.TranslateInputCoords(LastDrawnAt)); - } - } - else - */ - { - if (RenderObject != null) - { - thisOffset.Offset(RenderObject.TranslateInputCoords(LastDrawnAt)); - } - else - if (RenderObjectPrevious != null) - { - thisOffset.Offset(RenderObjectPrevious.TranslateInputCoords(LastDrawnAt)); - } - } - } - thisOffset.Offset(childOffset); - - //layout is different from real drawing area - var displaced = LastDrawnAt.Location - DrawingRect.Location; - thisOffset.Offset(displaced); - - return thisOffset; - } - - public virtual SKPoint CalculatePositionOffset(bool cacheOnly = false, - bool ignoreCache = false, - bool useTranlsation = true) - { - var thisOffset = SKPoint.Empty; - if (!cacheOnly && useTranlsation) - { - thisOffset = new SKPoint((float)(UseTranslationX * RenderingScale), (float)(UseTranslationY * RenderingScale)); - } - //inside a cached object coordinates are frozen at the moment the snapshot was taken - //so we must offset the coordinates to match the current drawing rect - if (!ignoreCache && UsingCacheType != SkiaCacheType.None) - { - if (RenderObject != null) - { - thisOffset.Offset(RenderObject.CalculatePositionOffset(LastDrawnAt.Location)); - } - else - if (UsingCacheType == SkiaCacheType.ImageDoubleBuffered && RenderObjectPrevious != null) - { - thisOffset.Offset(RenderObjectPrevious.CalculatePositionOffset(LastDrawnAt.Location)); - } - //Debug.WriteLine($"[CalculatePositionOffset] was cached!"); - } - - //Debug.WriteLine($"[CalculatePositionOffset] {this} {cacheOnly} returned {thisOffset}"); - - return thisOffset; - } - - public virtual SKPoint CalculateFuturePositionOffset(bool cacheOnly = false, - bool ignoreCache = false, - bool useTranlsation = true) - { - var thisOffset = SKPoint.Empty; - if (!cacheOnly && useTranlsation) - { - thisOffset = new SKPoint((float)(UseTranslationX * RenderingScale), (float)(UseTranslationY * RenderingScale)); - } - //inside a cached object coordinates are frozen at the moment the snapshot was taken - //so we must offset the coordinates to match the current drawing rect - if (!ignoreCache && UsingCacheType != SkiaCacheType.None) - { - if (RenderObject != null) - { - thisOffset.Offset(RenderObject.CalculatePositionOffset(DrawingRect.Location)); - } - else - if (UsingCacheType == SkiaCacheType.ImageDoubleBuffered && RenderObjectPrevious != null) - { - thisOffset.Offset(RenderObjectPrevious.CalculatePositionOffset(DrawingRect.Location)); - } - } - return thisOffset; - } - - long _layoutChanged = 0; - - public SKRect ArrangedDestination { get; protected set; } - - private SKSize _lastSize; - - - public event EventHandler LayoutIsReady; - public event EventHandler Disposing; - - /// - /// Layout was changed with dimensions above zero. Rather a helper method, can you more generic OnLayoutChanged(). - /// - protected virtual void OnLayoutReady() - { - IsLayoutReady = true; - LayoutIsReady?.Invoke(this, null); - } - - public bool IsLayoutReady { get; protected set; } - - public bool LayoutReady - { - get - { - return _layoutReady; - } - - protected set - { - if (_layoutReady != value) - { - _layoutReady = value; - OnPropertyChanged(); - if (value) - { - OnLayoutReady(); - } - } - } - } - bool _layoutReady; - - public bool CheckIsGhost() - { - return IsGhost || Destination == SKRect.Empty; - } - - /// - /// destination in PIXELS, requests in UNITS. resulting Destination prop will be filed in PIXELS. - /// DrawUsingRenderObject wil call this among others.. - /// - /// PIXELS - /// UNITS - /// UNITS - /// - public virtual void Arrange(SKRect destination, float widthRequest, float heightRequest, float scale) - { - - - if (!PreArrange(destination, widthRequest, heightRequest, scale)) - { - DrawingRect = SKRect.Empty; - return; - } - - var width = (HorizontalOptions.Alignment == LayoutAlignment.Fill && WidthRequest < 0) ? -1 : MeasuredSize.Units.Width; - var height = (VerticalOptions.Alignment == LayoutAlignment.Fill && HeightRequest < 0) ? -1 : MeasuredSize.Units.Height; - - PostArrange(destination, width, height, scale); - } - - protected virtual void PostArrange(SKRect destination, float widthRequest, float heightRequest, float scale) - { - SKRect arrangingFor = new(0, 0, destination.Width, destination.Height); - - if (!IsLayoutDirty && - (ViewportHeightLimit != _arrangedViewportHeightLimit || - ViewportWidthLimit != _arrangedViewportWidthLimit || - scale != _lastArrangedForScale || - !CompareRects(arrangingFor, _lastArrangedFor, 0.5f) || - !AreEqual(_lastArrangedHeight, heightRequest, 0.5f) || - !AreEqual(_lastArrangedWidth, widthRequest, 0.5f))) - { - IsLayoutDirty = true; - } - - if (!IsLayoutDirty) - { - AdaptCachedLayout(destination, scale); - return; - } - - //var oldDestination = Destination; - var layout = CalculateLayout(arrangingFor, widthRequest, heightRequest, scale); - bool layoutChanged = false; - if (!CompareRects(layout, ArrangedDestination, 0.5f)) - { - layoutChanged = true; - } - - var oldDrawingRect = this.DrawingRect; - - //save to cache - ArrangedDestination = layout; - //ArrangedDrawingRect = GetDrawingRectWithMargins(layout, scale); - - AdaptCachedLayout(destination, scale); - - _arrangedViewportHeightLimit = ViewportHeightLimit; - _arrangedViewportWidthLimit = ViewportWidthLimit; - _lastArrangedFor = arrangingFor; - _lastArrangedForScale = scale; - _lastArrangedHeight = heightRequest; - _lastArrangedWidth = widthRequest; - - if (!AreEqual(oldDrawingRect.Height, DrawingRect.Height, 0.5) - || !AreEqual(oldDrawingRect.Width, DrawingRect.Width, 0.5)) - { - OnDrawingSizeChanged(); - layoutChanged = true; - } - - if (layoutChanged) - OnLayoutChanged(); - - IsLayoutDirty = false; - } - - /// - /// PIXELS - /// - /// - /// - /// - /// - /// - public ScaledSize MeasureChild(SkiaControl child, double availableWidth, double availableHeight, float scale) - { - if (child == null) - { - return ScaledSize.Default; - } - - child.OnBeforeMeasure(); //could set IsVisible or whatever inside - - if (!child.CanDraw) - return ScaledSize.Default; //child set himself invisible - - return child.Measure((float)availableWidth, (float)availableHeight, scale); - } - - /// - /// Measuring as absolute layout for passed children - /// - /// - /// - /// - /// - protected virtual ScaledSize MeasureContent( - IEnumerable children, - SKRect rectForChildrenPixels, - float scale) - { - var maxHeight = -1.0f; - var maxWidth = -1.0f; - - List fill = new(); - var autosize = this.NeedAutoSize; - var hadFixedSize = false; - - void PostProcessMeasuredChild(ScaledSize measured, SkiaControl child, bool ignoreFill) - { - if (!measured.IsEmpty) - { - var measuredHeight = measured.Pixels.Height; - var measuredWidth = measured.Pixels.Width; - - if (child.ViewportHeightLimit >= 0) - { - float mHeight = (float)(child.ViewportHeightLimit * scale); - if (measuredHeight > mHeight) - { - float excessHeight = measuredHeight - mHeight; - measuredHeight -= excessHeight; - } - } - - if (child.ViewportWidthLimit >= 0) - { - float mWidth = (float)(child.ViewportWidthLimit * scale); - if (measuredWidth > mWidth) - { - float excessWidth = measuredWidth - mWidth; - measuredWidth -= excessWidth; - } - } - - if (ignoreFill) - { - if (measuredWidth > maxWidth && (child.HorizontalOptions.Alignment != LayoutAlignment.Fill || child.WidthRequest >= 0)) - maxWidth = measuredWidth; - - if (measuredHeight > maxHeight && (child.VerticalOptions.Alignment != LayoutAlignment.Fill || child.HeightRequest >= 0)) - maxHeight = measuredHeight; - } - else - { - if (measuredWidth > maxWidth) - maxWidth = measuredWidth; - - if (measuredHeight > maxHeight) - maxHeight = measuredHeight; - } - - } - } - - bool heightCut = false, widthCut = false; - - //PASS 1 - foreach (var child in children) - { - if (child == null) - continue; - - child.OnBeforeMeasure(); //could set IsVisible or whatever inside - - if (autosize && - (child.HorizontalOptions.Alignment == LayoutAlignment.Fill - && child.VerticalOptions.Alignment == LayoutAlignment.Fill) - || (!autosize && (child.HorizontalOptions.Alignment == LayoutAlignment.Fill || child.VerticalOptions.Alignment == LayoutAlignment.Fill)) - ) - { - fill.Add(child); //todo not very correct for the case just 1 dimension is Fill and other one may by bigger that other children! - continue; - } - - hadFixedSize = true; - var measured = MeasureChild(child, rectForChildrenPixels.Width, rectForChildrenPixels.Height, scale); - PostProcessMeasuredChild(measured, child, true); - - widthCut |= measured.WidthCut; - heightCut |= measured.HeightCut; - } - - //PASS 2 for thoses with Fill - foreach (var child in fill) - { - ScaledSize measured; - if (!hadFixedSize) //we had only children with fill so no fixed dimensions yet - { - measured = MeasureChild(child, rectForChildrenPixels.Width, rectForChildrenPixels.Height, scale); - PostProcessMeasuredChild(measured, child, false); - } - else - { - var provideWidth = rectForChildrenPixels.Width; - if (NeedAutoWidth && maxWidth >= 0) - { - provideWidth = maxWidth; - } - var provideHeight = rectForChildrenPixels.Height; - if (NeedAutoHeight && maxHeight >= 0) - { - provideHeight = maxHeight; - } - measured = MeasureChild(child, provideWidth, provideHeight, scale); - if (maxHeight == 0 || maxWidth == 0) - { - PostProcessMeasuredChild(measured, child, false); - } - } - - widthCut |= measured.WidthCut; - heightCut |= measured.HeightCut; - } - - if (HorizontalOptions.Alignment == LayoutAlignment.Fill && WidthRequest < 0) - { - maxWidth = rectForChildrenPixels.Width; - } - if (VerticalOptions.Alignment == LayoutAlignment.Fill && HeightRequest < 0) - { - maxHeight = rectForChildrenPixels.Height; - } - - return ScaledSize.FromPixels(maxWidth, maxHeight, widthCut, heightCut, scale); - } - - public virtual void ApplyBindingContext() - { - - foreach (var content in this.Views) - { - content.BindingContext = BindingContext; - } - - foreach (var content in this.VisualEffects) - { - content.Attach(this); - } - - if (FillGradient != null) - FillGradient.BindingContext = BindingContext; - - } - - protected bool BindingContextWasSet { get; set; } - /// - /// First Maui will apply bindings to your controls, then it would call OnBindingContextChanged, so beware on not to break bindings. - /// - protected override void OnBindingContextChanged() - { - BindingContextWasSet = true; - - try - { - InvalidateCacheWithPrevious(); - - //InvalidateViewsList(); //we might get different ZIndex which is bindable.. - - ApplyBindingContext(); - - //will apply to maui prps like styles, triggers etc - base.OnBindingContextChanged(); - - } - catch (Exception e) - { - Super.Log(e); - } - } - - public struct ParentMeasureRequest - { - public IDrawnBase Parent { get; set; } - public float WidthRequest { get; set; } - public float HeightRequest { get; set; } - } - - /// - /// - /// - /// - /// - /// - /// - public virtual ScaledSize Measure(float widthConstraint, float heightConstraint, float scale) - { - if (IsDisposed || IsDisposing) - return ScaledSize.Default; - - if (IsMeasuring) //basically we need this for cache double buffering to avoid conflicts with background thread - { - NeedRemeasuring = true; - return MeasuredSize; - } - - try - { - IsMeasuring = true; - - RenderingScale = scale; - - var request = CreateMeasureRequest(widthConstraint, heightConstraint, scale); - //if (request.IsSame) - //{ - // return MeasuredSize; - //} - - if (!this.CanDraw || request.WidthRequest == 0 || request.HeightRequest == 0) - { - InvalidateCacheWithPrevious(); - - return SetMeasuredAsEmpty(request.Scale); - } - - var constraints = GetMeasuringConstraints(request); - - ContentSize = MeasureAbsolute(constraints.Content, scale); - - return SetMeasuredAdaptToContentSize(constraints, scale); - } - finally - { - IsMeasuring = false; - } - - } - - protected virtual ScaledSize SetMeasuredAsEmpty(float scale) - { - return SetMeasured(0, 0, false, false, scale); - } - - public virtual ScaledSize SetMeasuredAdaptToContentSize(MeasuringConstraints constraints, - float scale) - { - var contentWidth = NeedAutoWidth ? ContentSize.Pixels.Width : SmartMax(ContentSize.Pixels.Width, constraints.Request.Width); - var contentHeight = NeedAutoHeight ? ContentSize.Pixels.Height : SmartMax(ContentSize.Pixels.Height, constraints.Request.Height); - - var width = AdaptWidthConstraintToContentRequest(constraints, contentWidth, HorizontalOptions.Expands); - var height = AdaptHeightConstraintToContentRequest(constraints, contentHeight, VerticalOptions.Expands); - - var widthCut = ContentSize.Pixels.Width > width || ContentSize.WidthCut; - var heighCut = ContentSize.Pixels.Height > height || ContentSize.HeightCut; - - SKSize size = new(width, height); - - var invalid = !CompareSize(size, MeasuredSize.Pixels, 0); - if (invalid) - { - InvalidateCacheWithPrevious(); - } - - return SetMeasured(size.Width, size.Height, widthCut, heighCut, scale); - } - - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static SKSize GetSizeInPoints(SKSize size, float scale) - { - var width = float.PositiveInfinity; - var height = float.PositiveInfinity; - if (double.IsFinite(size.Width) && size.Width >= 0) - { - width = size.Width / scale; - } - if (double.IsFinite(size.Height) && size.Height >= 0) - { - height = size.Height / scale; - } - return new SKSize(width, height); - } - - protected virtual MeasureRequest CreateMeasureRequest(float widthConstraint, float heightConstraint, float scale) - { - - RenderingScale = scale; - - //LastMeasureRequest = new() - //{ - // Parent = this.Parent, - // WidthRequest = widthConstraint, - // HeightRequest = heightConstraint, - //}; - - if (HorizontalFillRatio != 1 && double.IsFinite(widthConstraint) && widthConstraint > 0) - { - widthConstraint *= (float)HorizontalFillRatio; - } - if (VerticalFillRatio != 1 && double.IsFinite(heightConstraint) && heightConstraint > 0) - { - heightConstraint *= (float)VerticalFillRatio; - } - - if (LockRatio < 0) - { - var size = Math.Min(heightConstraint, widthConstraint); - size *= (float)-LockRatio; - heightConstraint = size; - widthConstraint = size; - } - else - if (LockRatio > 0) - { - var size = Math.Max(heightConstraint, widthConstraint); - size *= (float)LockRatio; - heightConstraint = size; - widthConstraint = size; - } - - var isSame = - !NeedMeasure - && _lastMeasuredForScale == scale - && AreEqual(_lastMeasuredForHeight, heightConstraint, 1) - && AreEqual(_lastMeasuredForWidth, widthConstraint, 1); - - if (!isSame) - { - _lastMeasuredForWidth = widthConstraint; - _lastMeasuredForHeight = heightConstraint; - _lastMeasuredForScale = scale; - } - - return new MeasureRequest(widthConstraint, heightConstraint, scale) - { - IsSame = isSame - }; - } - - public SKRect GetMeasuringRectForChildren(float widthConstraint, float heightConstraint, double scale) - { - var constraintLeft = (Padding.Left + Margins.Left) * scale; - var constraintRight = (Padding.Right + Margins.Right) * scale; - var constraintTop = (Padding.Top + Margins.Top) * scale; - var constraintBottom = (Padding.Bottom + Margins.Bottom) * scale; - - //SKRect rectForChild = new SKRect(0 + (float)constraintLeft, - // 0 + (float)constraintTop, - // widthConstraint - (float)constraintRight, - // heightConstraint - (float)constraintBottom); - - SKRect rectForChild = new SKRect(0, 0, - (float)Math.Round(widthConstraint - (float)(constraintRight + constraintLeft)), - (float)Math.Round(heightConstraint - (float)(constraintBottom + constraintTop))); - - - return rectForChild; - } - - public virtual ScaledSize MeasureAbsolute(SKRect rectForChildrenPixels, float scale) - { - return MeasureAbsoluteBase(rectForChildrenPixels, scale); - } - - /// - /// Base method, not aware of any views provider, not virtual, silly measuring Children. - /// - /// - /// - /// - protected ScaledSize MeasureAbsoluteBase(SKRect rectForChildrenPixels, float scale) - { - if (Views.Count > 0) - { - var maxHeight = 0.0f; - var maxWidth = 0.0f; - - var children = Views;//GetOrderedSubviews(); - return MeasureContent(children, rectForChildrenPixels, scale); - } - //empty container - else - if (NeedAutoHeight || NeedAutoWidth) - { - return ScaledSize.CreateEmpty(scale); - //return SetMeasured(0, 0, scale); - } - - return ScaledSize.FromPixels(rectForChildrenPixels.Width, rectForChildrenPixels.Height, scale); - } - - - public static SKRect ContractPixelsRect(SKRect rect, float scale, Thickness amount) - { - return new SKRect( - rect.Left + (float)Math.Round((float)amount.Left * scale), - rect.Top + (float)Math.Round((float)amount.Top * scale), - rect.Right - (float)Math.Round((float)amount.Right * scale), - rect.Bottom - (float)Math.Round((float)amount.Bottom * scale) - ); - } - - - public SKRect GetDrawingRectForChildren(SKRect destination, double scale) - { - //var constraintLeft = (Padding.Left + Margins.Left) * scale; - //var constraintRight = (Padding.Right + Margins.Right) * scale; - //var constraintTop = (Padding.Top + Margins.Top) * scale; - //var constraintBottom = (Padding.Bottom + Margins.Bottom) * scale; - - var constraintLeft = (Padding.Left + Margins.Left) * scale; - var constraintRight = (Padding.Right + Margins.Right) * scale; - var constraintTop = (Padding.Top + Margins.Top) * scale; - var constraintBottom = (Padding.Bottom + Margins.Bottom) * scale; - - - SKRect rectForChild = new SKRect( - (float)Math.Round(destination.Left + (float)constraintLeft), - (float)Math.Round(destination.Top + (float)constraintTop), - (float)Math.Round(destination.Right - (float)constraintRight), - (float)Math.Round(destination.Bottom - (float)constraintBottom) - ); - - return rectForChild; - } - - public virtual SKRect GetDrawingRectWithMargins(SKRect destination, double scale) - { - - - var constraintLeft = (float)Math.Round(Margins.Left * scale); - var constraintRight = (float)Math.Round(Margins.Right * scale); - var constraintTop = (float)Math.Round(Margins.Top * scale); - var constraintBottom = (float)Math.Round(Margins.Bottom * scale); - - SKRect rectForChild; - - rectForChild = new SKRect( - (destination.Left + constraintLeft), - (destination.Top + constraintTop), - (destination.Right - constraintRight), - (destination.Bottom - constraintBottom) - ); - - // Apply ViewportHeightLimit if it's set - if (ViewportHeightLimit >= 0) - { - float maxHeight = (float)Math.Round(ViewportHeightLimit * scale); - if (rectForChild.Height > maxHeight) - { - float excessHeight = rectForChild.Height - maxHeight; - rectForChild.Bottom -= excessHeight; - } - } - - // Apply ViewportWidthLimit if it's set - if (ViewportWidthLimit >= 0) - { - float maxWidth = (float)Math.Round(ViewportWidthLimit * scale); - if (rectForChild.Width > maxWidth) - { - float excessWidth = rectForChild.Width - maxWidth; - rectForChild.Right -= excessWidth; - } - } - - return rectForChild; - } - - - protected object lockMeasured = new(); - - bool debugMe; - - /// - /// Flag for internal use, maynly used to avoid conflicts between measuring on ui-thread and in background. If true, measure will return last measured value. - /// - public bool IsMeasuring { get; protected internal set; } - - protected object LockMeasure = new(); - - /// - /// Parameters in PIXELS. sets IsLayoutDirty = true; - /// - /// - /// - /// - /// - protected virtual ScaledSize SetMeasured(float width, float height, bool widthCut, bool heightCut, float scale) - { - lock (lockMeasured) - { - WasMeasured = true; - - NeedMeasure = false; - - IsLayoutDirty = true; - - if (double.IsFinite(height) && !double.IsNaN(height)) - { - Height = (height / scale) - (Margins.Top + Margins.Bottom); - } - else - { - height = -1; - Height = height; - } - - if (double.IsFinite(width) && !double.IsNaN(width)) - { - Width = (width / scale) - (Margins.Left + Margins.Right); - } - else - { - width = -1; - Width = width; - } - - MeasuredSize = ScaledSize.FromPixels(width, height, widthCut, heightCut, scale); - - //SetValueCore(RenderingScaleProperty, scale, SetValueFlags.None); - - OnMeasured(); - - return MeasuredSize; - } - } - - protected virtual void OnMeasured() - { - //Debug.WriteLine($"[MEASURED] {this.GetType().Name} {this.Tag} "); - Measured?.Invoke(this, MeasuredSize); - } - - /// - /// UNITS - /// - public event EventHandler Measured; - - - public ScaledSize MeasuredSize { get; set; } = new(); - - - public virtual bool NeedAutoSize - { - get - { - return NeedAutoHeight || NeedAutoWidth; - } - } - - public bool NeedAutoHeight - { - get - { - return LockRatio == 0 && VerticalOptions.Alignment != LayoutAlignment.Fill && SizeRequest.Height < 0; - } - } - public bool NeedAutoWidth - { - get - { - return LockRatio == 0 && HorizontalOptions.Alignment != LayoutAlignment.Fill && SizeRequest.Width < 0; - } - } - - - private bool _isDisposed; - - public bool IsDisposed - { - get - { - return _isDisposed; - } - protected set - { - if (value != _isDisposed) - { - _isDisposed = value; - OnPropertyChanged(); - } - } - } - - private bool _isDisposing; - - public bool IsDisposing - { - get - { - return _isDisposing; - } - protected set - { - if (value != _isDisposing) - { - _isDisposing = value; - OnPropertyChanged(); - } - } - } - - - - /// - /// Developer can use this to mark control as to be disposed by parent custom controls - /// - public bool NeedDispose { get; set; } - - public TChild FindView(string tag) where TChild : SkiaControl - { - if (this.Tag == tag && this is TChild) - return this as TChild; - - var found = Views.FirstOrDefault(x => x.Tag == tag) as TChild; - if (found == null) - { - //go sub level - foreach (var view in Views) - { - found = view.FindView(tag); - if (found != null) - { - return found; - } - } - } - return found; - } - - public TChild FindView() where TChild : SkiaControl - { - - var found = Views.FirstOrDefault(x => x is TChild) as TChild; - if (found == null) - { - //go sub level - foreach (var view in Views) - { - found = view.FindView(); - if (found != null) - { - return found; - } - } - } - return found; - } - - public SkiaControl FindViewByTag(string tag) - { - if (this.Tag == tag) - return this; - - var found = Views.FirstOrDefault(x => x.Tag == tag); - if (found == null) - { - //go sub level - foreach (var view in Views) - { - found = view.FindViewByTag(tag); - if (found != null) - { - return found; - } - } - } - return found; - } - - /// - /// Avoid setting parent to null before calling this, or set SuperView prop manually for proper cleanup of animations and gestures if any used - /// - public void Dispose() - { - if (IsDisposed) - return; - - OnWillDisposeWithChildren(); - - IsDisposed = true; - - //for the double buffering case it's safer to delay - Tasks.StartDelayed(TimeSpan.FromSeconds(1), () => - { - - RenderObject = null; - - PaintSystem?.Dispose(); - - _lastAnimatorManager = null; - - DisposeChildren(); - - RenderTree?.Clear(); - - GestureListeners?.Clear(); - - VisualEffects?.Clear(); - - OnDisposing(); - - Parent = null; - - Superview = null; - - LastGradient?.Dispose(); - LastGradient = null; - - LastShadow?.Dispose(); - LastShadow = null; - - CustomizeLayerPaint = null; - - RenderObjectPreparing?.Dispose(); - RenderObjectPreparing = null; - - clipPreviousCachePath?.Dispose(); - PaintErase?.Dispose(); - - RenderObjectPrevious?.Dispose(); - RenderObjectPrevious = null; - - _paintWithOpacity?.Dispose(); - _paintWithEffects?.Dispose(); - _preparedClipBounds?.Dispose(); - - EffectColorFilter = null; - EffectImageFilter = null; - EffectRenderers = null; - EffectsState = null; - EffectsGestureProcessors = null; - EffectPostRenderer = null; - }); - } - - protected SKPaint PaintErase = new() - { - Color = SKColors.Transparent, - BlendMode = SKBlendMode.Src - }; - - - public static long GetNanoseconds() - { - double timestamp = Stopwatch.GetTimestamp(); - double nanoseconds = 1_000_000_000.0 * timestamp / Stopwatch.Frequency; - return (long)nanoseconds; - - //double nano = 10000L * Stopwatch.GetTimestamp(); - //nano /= TimeSpan.TicksPerMillisecond; - //nano *= 100L; - //return (long)nano; - } - - - public virtual void OnBeforeMeasure() - { - - } - - public virtual void OptionalOnBeforeDrawing() - { - Superview?.UpdateRenderingChains(this); - } - - - /// - /// do not ever erase background - /// - public bool IsOverlay { get; set; } - - - /// - /// Executed after the rendering - /// - public List PostAnimators { get; } = new(); //to be renamed to post-effects + /// + /// Ported from Avalonia: AreClose - Returns whether or not two doubles are "close". That is, whether or + /// not they are within epsilon of each other. + /// + /// The first double to compare. + /// The second double to compare. + public static bool AreClose(double value1, double value2) + { + //in case they are Infinities (then epsilon check does not work) + if (value1 == value2) return true; + double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon; + double delta = value1 - value2; + return (-eps < delta) && (eps > delta); + } + /// + /// Avalonia: IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1), + /// but this is faster. + /// + /// The double to compare to 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsOne(double value) + { + return Math.Abs(value - 1.0) < 10.0 * double.Epsilon; + } - public static readonly BindableProperty UpdateWhenReturnedFromBackgroundProperty = BindableProperty.Create(nameof(UpdateWhenReturnedFromBackground), - typeof(bool), - typeof(SkiaControl), - false); - public bool UpdateWhenReturnedFromBackground - { - get { return (bool)GetValue(UpdateWhenReturnedFromBackgroundProperty); } - set { SetValue(UpdateWhenReturnedFromBackgroundProperty, value); } - } - - public virtual void OnSuperviewShouldRenderChanged(bool state) - { - if (UpdateWhenReturnedFromBackground) - { - Update(); - } - - try - { - foreach (var view in Views.ToList()) - { - view.OnSuperviewShouldRenderChanged(state); - } - } - catch (Exception e) - { - Super.Log(e); - } - } - - /// - /// Normally get a a Measure by parent then parent calls Draw and we can apply the measure result. - /// But in a case we have measured us ourselves inside PreArrange etc we must call ApplyMeasureResult because this would happen after the Draw and not before. - /// - public virtual void ApplyMeasureResult() - { - } - - /// - /// Returns false if should not render - /// - /// - public virtual bool PreArrange(SKRect destination, float widthRequest, float heightRequest, float scale) - { - if (!CanDraw) - return false; - - if (WillInvalidateMeasure) - { - WillInvalidateMeasure = false; - InvalidateMeasureInternal(); - } - - if (NeedMeasure) - { - //self measuring - var adjustedDestination = CalculateLayout(destination, widthRequest, heightRequest, scale); - ArrangedDestination = adjustedDestination; - Measure(adjustedDestination.Width, adjustedDestination.Height, scale); - ApplyMeasureResult(); - } - else - { - _lastArrangedInside = destination; - } - - return true; - } - - protected bool IsRendering { get; set; } - - public virtual void Render(SkiaDrawingContext context, - SKRect destination, - float scale) - { - if (IsDisposing || IsDisposed) - return; - - IsRendering = true; - - Superview = context.Superview; - RenderingScale = scale; - NeedUpdate = false; - - OnBeforeDrawing(context, destination, scale); - - if (WillInvalidateMeasure) - { - WillInvalidateMeasure = false; - InvalidateMeasureInternal(); - } - - if (RenderObjectNeedsUpdate) - { - //disposal etc inside setter - RenderObject = null; - } - - if ( - //RenderedAtDestination.Width != destination.Width || - //RenderedAtDestination.Height != destination.Height || - !CompareRectsSize(RenderedAtDestination, destination, 0.5f) || - RenderingScale != scale) - { - //NeedMeasure = true; - RenderedAtDestination = destination; - } - - if (RenderedAtDestination != SKRect.Empty) - { - //moved to superview - //ExecuteAnimators(context.FrameTimeNanos);// Super.GetNanoseconds() - - Draw(context, destination, scale); - } - - //UpdateLocked = false; - - OnAfterDrawing(context, destination, scale); - - Rendered?.Invoke(this, null); - - IsRendering = false; - } - - public event EventHandler Rendered; - - /// - /// Lock between replacing and using RenderObject - /// - protected object LockDraw = new(); - - /// - /// Creating new cache lock - /// - protected object LockRenderObject = new(); - - public virtual object CreatePaintArguments() - { - return null; - } - - protected virtual void OnBeforeDrawing(SkiaDrawingContext context, - SKRect destination, - float scale) - { - InvalidatedParent = false; - _invalidatedParentPostponed = false; - - if (EffectsState != null) - { - foreach (var stateEffect in EffectsState) - { - stateEffect?.UpdateState(); - } - } - } - - protected virtual void OnAfterDrawing(SkiaDrawingContext context, - SKRect destination, - float scale) - { - if (_invalidatedParentPostponed) - { - InvalidatedParent = false; - InvalidateParent(); - } - - if (UsingCacheType == SkiaCacheType.None) - NeedUpdate = false; //otherwise CreateRenderingObject will set this to false - - //trying to find exact location on the canvas - - LastDrawnAt = DrawingRect; - - X = LastDrawnAt.Location.X / scale; - Y = LastDrawnAt.Location.Y / scale; - - ExecutePostAnimators(context, scale); - - if (NeedRemeasuring) - { - NeedRemeasuring = false; - } - - if (UsingCacheType == SkiaCacheType.ImageDoubleBuffered - && RenderObject != null) - { - if (!CompareRectsSize(DrawingRect, RenderObject.Bounds, 0.5f)) - { - InvalidateMeasure(); - } - } - else - if (UsingCacheType == SkiaCacheType.ImageComposite - && RenderObjectPrevious != null) - { - if (!CompareRectsSize(DrawingRect, RenderObjectPrevious.Bounds, 0.5f)) - { - InvalidateMeasure(); - } - } - - } - - - - protected virtual void Draw(SkiaDrawingContext context, SKRect destination, float scale) - { - if (IsDisposing || IsDisposed) - return; - - DrawUsingRenderObject(context, - SizeRequest.Width, SizeRequest.Height, - destination, scale); - } - - //public new static readonly BindableProperty XProperty - // = BindableProperty.Create(nameof(X), - // typeof(double), typeof(SkiaControl), - // 0.0f); - - - private double _X; - /// - /// Absolute position obtained after this control was drawn on the Canvas, this is not relative to parent control. - /// - public new double X - { - get - { - return _X; - } - protected set - { - if (_X != value) - { - _X = value; - OnPropertyChanged(); - } - } - } - - private double _Y; - /// - /// Absolute position obtained after this control was drawn on the Canvas, this is not relative to parent control. - /// - public new double Y - { - get - { - return _Y; - } - protected set - { - if (_Y != value) - { - _Y = value; - OnPropertyChanged(); - } - } - } - - - /// - /// Execute post drawing operations, like post-animators etc - /// - /// - /// - protected void FinalizeDrawingWithRenderObject(SkiaDrawingContext context, double scale) - { - - - } - - public SKPoint GetPositionOffsetInPoints() - { - var thisOffset = CalculatePositionOffset(); - var x = (thisOffset.X) / RenderingScale; - var y = (thisOffset.Y) / RenderingScale; - return new((float)x, (float)y); - } - - public SKPoint GetPositionOffsetInPixels(bool cacheOnly = false, bool ignoreCache = false, bool useTranslation = true) - { - var thisOffset = CalculatePositionOffset(cacheOnly, ignoreCache, useTranslation); - var x = (thisOffset.X); - var y = (thisOffset.Y); - return new((float)x, (float)y); - } - - public SKPoint GetFuturePositionOffsetInPixels(bool cacheOnly = false, bool ignoreCache = false, bool useTranslation = true) - { - var thisOffset = CalculateFuturePositionOffset(cacheOnly, ignoreCache, useTranslation); - var x = (thisOffset.X); - var y = (thisOffset.Y); - return new((float)x, (float)y); - } - - public SKPoint GetOffsetInsideControlInPoints(PointF location, SKPoint childOffset) - { - var thisOffset = TranslateInputCoords(childOffset, false); - var x = (location.X + thisOffset.X) / RenderingScale; - var y = (location.Y + thisOffset.Y) / RenderingScale; - var insideX = x - X; - var insideY = y - Y; - return new((float)insideX, (float)insideY); - } - - public SKPoint GetOffsetInsideControlInPixels(PointF location, SKPoint childOffset) - { - var thisOffset = TranslateInputCoords(childOffset, false); - var x = location.X + thisOffset.X; - var y = location.Y + thisOffset.Y; - var insideX = x - X * RenderingScale; - var insideY = y - Y * RenderingScale; - return new((float)insideX, (float)insideY); - } - - /// - /// Location on the canvas after last drawing completed - /// - public SKRect LastDrawnAt { get; protected set; } - - public void ExecutePostAnimators(SkiaDrawingContext context, double scale) - { - try - { - if (PostAnimators.Count == 0 || IsDisposing || IsDisposed) - { - return; - } - - //Debug.WriteLine($"[ExecutePostAnimators] {Tag} {PostAnimators.Count} effects"); - - foreach (var effect in PostAnimators.ToList()) - { - //if (effect.IsRunning) - { - if (effect.Render(this, context, scale)) - { - Repaint(); - } - } - } - } - catch (Exception e) - { - Super.Log(e); - } - - } - - #region CACHE - - /// - /// Base method will call RenderViewsList. - /// Return number of drawn views. - /// - /// - /// - /// - /// - /// - protected virtual int DrawViews(SkiaDrawingContext context, SKRect destination, float scale, bool debug = false) - { - var children = GetOrderedSubviews(); - - return RenderViewsList((IList)children, context, destination, scale, debug); - } - - - - - /// - /// Just make us repaint to apply new transforms etc - /// - public virtual void Repaint() - { - if (IsDisposing || NeedUpdate || - Superview == null - || IsParentIndependent - || IsDisposed || Parent == null) - return; - - if (!Parent.UpdateLocked) - { - Parent?.UpdateByChild(this); - } - } - - protected SKPaint _paintWithEffects = null; - protected SKPaint _paintWithOpacity = null; - - SKPath _preparedClipBounds = null; - - private IAnimatorsManager _lastAnimatorManager; - - private Func> _createChildren; - - /// - /// Can customize the SKPaint used for painting the object - /// - public Action CustomizeLayerPaint { get; set; } - -#if SKIA3 - public Sk3dView Helper3d; -#else - public SK3dView Helper3d; -#endif - public void DrawWithClipAndTransforms( - SkiaDrawingContext ctx, - SKRect destination, - SKRect transformsArea, - bool useOpacity, - bool useClipping, - Action draw) - { - bool isClipping = (WillClipBounds || Clipping != null || ClippedBy != null) && useClipping; - - if (isClipping) - { - _preparedClipBounds ??= new SKPath(); - _preparedClipBounds.Reset(); - if (ClippedBy != null) - { - ClippedBy.CreateClip(null, true, _preparedClipBounds); - } - else - { - _preparedClipBounds.AddRect(destination); - Clipping?.Invoke(_preparedClipBounds, destination); - } - } - - bool applyOpacity = useOpacity && Opacity < 1; - bool needTransform = HasTransform; - - if (applyOpacity || isClipping || needTransform || CustomizeLayerPaint != null) - { - _paintWithOpacity ??= new SKPaint - { - IsAntialias = IsDistorted, - FilterQuality = IsDistorted ? SKFilterQuality.Medium : SKFilterQuality.None - }; - - _paintWithOpacity.Color = SKColors.White.WithAlpha((byte)(0xFF * Opacity)); - - var restore = 0; - - if (applyOpacity || CustomizeLayerPaint != null) - { - CustomizeLayerPaint?.Invoke(_paintWithOpacity, destination); - restore = ctx.Canvas.SaveLayer(_paintWithOpacity); - } - else - { - restore = ctx.Canvas.Save(); - } - - if (needTransform) - { - ApplyTransforms(ctx, transformsArea); - } - - if (isClipping) - { - //ctx.Canvas.ClipPath(_preparedClipBounds, SKClipOperation.Intersect, false); - ClipSmart(ctx.Canvas, _preparedClipBounds); - } - - draw(ctx); - - ctx.Canvas.RestoreToCount(restore); - } - else - { - draw(ctx); - } - } - - protected virtual void ApplyTransforms(SkiaDrawingContext ctx, SKRect destination) - { - var moveX = (int)Math.Round(UseTranslationX * RenderingScale); - var moveY = (int)Math.Round(UseTranslationY * RenderingScale); - - float pivotX = (float)(destination.Left + destination.Width * TransformPivotPointX); - float pivotY = (float)(destination.Top + destination.Height * TransformPivotPointY); - - var centerX = moveX + destination.Left + destination.Width * TransformPivotPointX; - var centerY = moveY + destination.Top + destination.Height * TransformPivotPointY; - - var skewX = SkewX > 0 ? (float)Math.Tan(Math.PI * SkewX / 180f) : 0f; - var skewY = SkewY > 0 ? (float)Math.Tan(Math.PI * SkewY / 180f) : 0f; - - if (Rotation != 0) - { - ctx.Canvas.RotateDegrees((float)Rotation, (float)centerX, (float)centerY); - } - - var matrixTransforms = new SKMatrix - { - TransX = moveX, - TransY = moveY, - Persp0 = Perspective1, - Persp1 = Perspective2, - SkewX = skewX, - SkewY = skewY, - Persp2 = 1, - ScaleX = (float)ScaleX, - ScaleY = (float)ScaleY - }; - - var drawingMatrix = SKMatrix.CreateTranslation((float)-pivotX, (float)-pivotY).PostConcat(matrixTransforms); - - if (CameraAngleX != 0 || CameraAngleY != 0 || CameraAngleZ != 0) - { - Helper3d ??= new(); -#if SKIA3 - Helper3d.Reset(); - Helper3d.RotateXDegrees(CameraAngleX); - Helper3d.RotateYDegrees(CameraAngleY); - Helper3d.RotateZDegrees(CameraAngleZ); - if (CameraTranslationZ != 0) - Helper3d.Translate(0, 0, CameraTranslationZ); + #endregion + } - drawingMatrix = drawingMatrix.PostConcat(Helper3d.GetMatrix()); -#else - Helper3d.Save(); - Helper3d.RotateXDegrees(CameraAngleX); - Helper3d.RotateYDegrees(CameraAngleY); - Helper3d.RotateZDegrees(CameraAngleZ); - if (CameraTranslationZ != 0) Helper3d.TranslateZ(CameraTranslationZ); - drawingMatrix = drawingMatrix.PostConcat(Helper3d.Matrix); - Helper3d.Restore(); -#endif - } - - drawingMatrix = drawingMatrix.PostConcat(SKMatrix.CreateTranslation(pivotX, pivotY)) - .PostConcat(ctx.Canvas.TotalMatrix); - - 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 from ShouldClipAntialiased - /// - /// - /// - /// - public virtual void ClipSmart(SKCanvas canvas, SKPath path, SKClipOperation operation = SKClipOperation.Intersect) - { - canvas.ClipPath(path, operation, ShouldClipAntialiased); - } - - /// - /// This is not a static bindable property. Can be set manually or by control, for example SkiaShape sets this to true for non-rectangular shapes, or rounded corners.. - /// - public bool ShouldClipAntialiased { get; set; } - - public virtual bool NeedMeasure - { - get => _needMeasure; - set - { - if (value == _needMeasure) return; - _needMeasure = value; - - if (value) - { - IsLayoutDirty = true; - InvalidateCacheWithPrevious(); - } - //OnPropertyChanged(); disabled atm - } - } - private bool _needMeasure = true; - - - - /// - /// If attached to a SuperView and rendering is in progress will run after it. Run now otherwise. - /// - /// - protected void SafePostAction(Action action) - { - var super = this.Superview; - if (super != null) - { - Superview.PostponeExecutionBeforeDraw(() => - { - action(); - }); - - Repaint(); - } - else - { - action(); - } - } - - /// - /// If attached to a SuperView will run only after draw to avoid memory access conflicts. If not attached will run after 3 secs.. - /// - /// - protected void SafeAction(Action action) - { - var super = this.Superview; - if (super == null || !Superview.IsRendering) - { - Tasks.StartDelayed(TimeSpan.FromSeconds(3), action); - } - else - Superview.PostponeExecutionAfterDraw(action); - } - - /* - public async Task ProcessOffscreenCacheRenderingAsync() + public static class Snapping + { + /// + /// Used by the layout system to round a position translation value applying scale and initial anchor. Pass POINTS only, it wont do its job when receiving pixels! + /// + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float SnapPointsToPixel(float initialPosition, float translation, double scale) { + // Scale the initial and the translation + float scaledInitial = initialPosition * (float)scale; + float scaledTranslation = translation * (float)scale; - await semaphoreOffsecreenProcess.WaitAsync(); + // Add the scaled initial to the scaled translation + float scaledTotal = scaledInitial + scaledTranslation; - _processingOffscrenRendering = true; + // Round to the nearest integer (you could also use Floor or Ceiling or Round, play with it + float snappedTotal = (float)Math.Round(scaledTotal); - try - { - Action action = _offscreenCacheRenderingQueue.Pop(); - if (!IsDisposed && !IsDisposing && action != null) - { - try - { - action.Invoke(); + // Subtract the scaled initial position to get the snapped, scaled translation + float snappedScaledTranslation = snappedTotal - scaledInitial; - RenderObject = RenderObjectPreparing; - _renderObjectPreparing = null; + // Divide by scale to get back to your original units + float snappedTranslation = snappedScaledTranslation / (float)scale; - Repaint(); - } - catch (Exception e) - { - Super.Log(e); - } - } - } - finally - { - _processingOffscrenRendering = false; - semaphoreOffsecreenProcess.Release(); + return snappedTranslation; + } - if (NeedUpdate || _offscreenCacheRenderingQueue.Count > 0) //someone changed us while rendering inner content - { - Update(); //kick - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float SnapPixelsToPixel(float initialPosition, float translation) + { + // Sum the initial position and the translation + float total = initialPosition + translation; + // Round to the nearest integer to snap to the nearest pixel + float snappedTotal = (float)Math.Round(total); + + // Find out how much we've adjusted the translation by + float snappedTranslation = snappedTotal - initialPosition; + + return snappedTranslation; } - */ - protected bool NeedRemeasuring; - - - - - - - protected virtual void PaintWithEffects( - SkiaDrawingContext ctx, SKRect destination, float scale, object arguments) - { - if (IsDisposed || IsDisposing) - return; - - void draw(SkiaDrawingContext context) - { - Paint(context, destination, scale, arguments); - } - - if (!DisableEffects && VisualEffects.Count > 0) - { - if (_paintWithEffects == null) - { - _paintWithEffects = new() - { - IsAntialias = true, - FilterQuality = SKFilterQuality.Medium - }; - - } - - var effectColor = EffectColorFilter; - var effectImage = EffectImageFilter; - - if (effectImage != null) - _paintWithEffects.ImageFilter = effectImage.CreateFilter(destination); - else - _paintWithEffects.ImageFilter = null;//will be disposed internally by effect - - if (effectColor != null) - _paintWithEffects.ColorFilter = effectColor.CreateFilter(destination); - else - _paintWithEffects.ColorFilter = null; - - var restore = ctx.Canvas.SaveLayer(_paintWithEffects); - - bool hasDrawnControl = false; - - var renderers = EffectRenderers; - - if (renderers.Count > 0) - { - foreach (var effect in renderers) - { - var chainedEffectResult = effect.Draw(destination, ctx, draw); - if (chainedEffectResult.DrawnControl) - hasDrawnControl = true; - } - } - - if (!hasDrawnControl) - { - draw(ctx); - } - - ctx.Canvas.RestoreToCount(restore); - } - else - { - draw(ctx); - } - } - - - /// - /// This is the main drawing routine you should override to draw something. - /// Base one paints background color inside DrawingRect that was defined by Arrange inside base.Draw. - /// Pass arguments if you want to use some time-frozen data for painting at any time from any thread.. - /// - /// - /// - /// - protected virtual void Paint(SkiaDrawingContext ctx, SKRect destination, float scale, object arguments) - { - if (destination.Width == 0 || destination.Height == 0 || IsDisposing || IsDisposed) - return; - - PaintTintBackground(ctx.Canvas, destination); - - WasDrawn = true; - } - - 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 - { - get - { - return _wasDrawn; - } - set - { - if (_wasDrawn != value) - { - _wasDrawn = value; - OnPropertyChanged(); - if (value) - WasFirstTimeDrawn?.Invoke(this, null); - } - } - } - - public event EventHandler WasFirstTimeDrawn; - - /// - /// Create this control clip for painting content. - /// Pass arguments if you want to use some time-frozen data for painting at any time from any thread.. - /// - /// - /// - public virtual SKPath CreateClip(object arguments, bool usePosition, SKPath path = null) - { - path ??= new SKPath(); - - if (usePosition) - { - path.AddRect(DrawingRect); - } - else - { - path.AddRect(new(0, 0, DrawingRect.Width, DrawingRect.Height)); - } - return path; - } - - - public static SKColor DebugRenderingColor = SKColor.Parse("#66FFFF00"); - - public double UseTranslationY - { - get - { - return TranslationY + AddTranslationY; - } - } - - public double UseTranslationX - { - get - { - return TranslationX + AddTranslationX; - } - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public bool HasTransform - { - get - { - return - UseTranslationY != 0 || UseTranslationX != 0 - || ScaleY != 1f || ScaleX != 1f - || Perspective1 != 0f || Perspective2 != 0f - || SkewX != 0 || SkewY != 0 - || Rotation != 0 - || CameraAngleX != 0 || CameraAngleY != 0 || CameraAngleZ != 0; - } - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public bool IsDistorted - { - get - { - return - Rotation != 0 || ScaleY != 1f || ScaleX != 1f - || Perspective1 != 0f || Perspective2 != 0f - || SkewX != 0 || SkewY != 0 - || CameraAngleX != 0 || CameraAngleY != 0 || CameraAngleZ != 0; - } - } - - - /// - /// Drawing cache, applying clip and transforms as well - /// - /// - /// - /// - public virtual void DrawRenderObject(CachedObject cache, - SkiaDrawingContext ctx, - SKRect destination) - { - - //lock (LockDraw) - { - DrawWithClipAndTransforms(ctx, destination, destination, true, true, (ctx) => - { - if (_paintWithOpacity == null) - { - _paintWithOpacity = new SKPaint(); - } - - _paintWithOpacity.Color = SKColors.White; - _paintWithOpacity.IsAntialias = true; - _paintWithOpacity.FilterQuality = SKFilterQuality.Medium; - - if (EffectPostRenderer != null) - { - EffectPostRenderer.Render(ctx, destination); - } - else - { - cache.Draw(ctx.Canvas, destination, _paintWithOpacity); - } - }); - - } - - } - - /// - /// Use to render Absolute layout. Base method is not supporting templates, override it to implemen your logic. - /// Returns number of drawn children. - /// - /// - /// - /// - /// - /// - protected virtual int RenderViewsList(IEnumerable skiaControls, - SkiaDrawingContext context, - SKRect destination, float scale, - bool debug = false) - { - var count = 0; - - List tree = new(); - - //todo - //var visibleArea = GetOnScreenVisibleArea(); - - foreach (var child in skiaControls) - { - if (child != null) - { - child.OptionalOnBeforeDrawing(); //could set IsVisible or whatever inside - if (child.CanDraw) //still visible - { - child.Render(context, destination, scale); - - tree.Add(new SkiaControlWithRect(child, - child.LastDrawnAt, - child.LastDrawnAt, count)); - - count++; - } - } - } - - RenderTree = tree; - _builtRenderTreeStamp = _measuredStamp; - - return count; - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public virtual bool UsesRenderingTree - { - get - { - return true; - } - } - - /// - /// Rect is real drawing position - /// - /// - /// - /// - public record SkiaControlWithRect(SkiaControl Control, - SKRect Rect, - SKRect HitRect, - int Index); - - protected long _measuredStamp; - - protected long _builtRenderTreeStamp; - - - /// - /// Last rendered controls tree. Used by gestures etc.. - /// - public List RenderTree { get; protected set; } - - #endregion - public bool Invalidated { get; set; } = true; - - /// - /// For internal use, set by Update method - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public bool NeedUpdate - { - get - { - return _needUpdate; - } - set - { - if (_needUpdate != value) - { - _needUpdate = value; - - if (value) - InvalidateCache(); - } - } - } - bool _needUpdate; - - - - DrawnView _superview; - /// - /// Our canvas - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public DrawnView Superview - { - get - { - if (_superview == null) - { - var value = GetTopParentView(); - if (value != _superview) - { - _superview = value; - OnPropertyChanged(); - SuperViewChanged(); - } - } - return _superview; - } - set - { - if (value != _superview) - { - _superview = value; - OnPropertyChanged(); - SuperViewChanged(); - } - } - } - - /// - /// For virtualization - /// - /// - public virtual ScaledRect GetOnScreenVisibleArea(float inflateByPixels = 0) - { - if (this.UsingCacheType != SkiaCacheType.None) - { - //we are going to cache our children so they all must draw - //regardless of the fact they might still be offscreen - var inflated = DrawingRect; - inflated.Inflate(inflateByPixels, inflateByPixels); - - return ScaledRect.FromPixels(inflated, RenderingScale); - } - - //go up the tree to find the screen area or some parent will override this - if (Parent != null) - { - return Parent.GetOnScreenVisibleArea(inflateByPixels); - } - - if (Superview != null) - { - return Superview.GetOnScreenVisibleArea(inflateByPixels); - } - - var inflated2 = Destination; - inflated2.Inflate(inflateByPixels, inflateByPixels); - return ScaledRect.FromPixels(inflated2, RenderingScale); - } - - bool _lastUpdatedVisibility; - - - - /// - /// Used to check whether to apply IsClippedToBounds property - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public virtual bool WillClipBounds - { - get - { - return IsClippedToBounds || - (UsingCacheType != SkiaCacheType.None && UsingCacheType != SkiaCacheType.Operations); - } - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public virtual bool WillClipEffects - { - get - { - return ClipEffects; - } - } - - - protected virtual void UpdateInternal() - { - if (IsDisposing || IsDisposed) - return; - - NeedUpdateFrontCache = true; - NeedUpdate = true; - - if (UpdateLocked) - return; - - if (IsParentIndependent) - return; - - Parent?.UpdateByChild(this); - } - - /// - /// Main method to invalidate cache and invoke rendering - /// - public virtual void Update() - { - InvalidateCache(); - - UpdateInternal(); - - Updated?.Invoke(this, null); - } - - /// - /// Triggered by Update method - /// - public event EventHandler Updated; - - public static MemoryStream StreamFromString(string value) - { - return new MemoryStream(Encoding.UTF8.GetBytes(value ?? "")); - } - - public static int DeviceUnitsToPixels(double units) - { - return (int)(units * GetDensity()); - } - - 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; } - - private bool _IsRenderingWithComposition; - /// - /// Internal flag indicating that the current frame will use cache composition, old cache will be reused, only dirty children will be redrawn over it - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public bool IsRenderingWithComposition - { - get - { - return _IsRenderingWithComposition; - } - protected set - { - if (_IsRenderingWithComposition != value) - { - _IsRenderingWithComposition = value; - OnPropertyChanged(); - } - } - } - - private SKPath clipPreviousCachePath = new(); - - /// - /// Pixels, if you see no Scale parameter - /// - /// - /// - public virtual void PaintTintBackground(SKCanvas canvas, SKRect destination) - { - if (BackgroundColor != null && BackgroundColor != TransparentColor) - { - if (PaintSystem == null) - { - PaintSystem = new SKPaint(); - } - PaintSystem.Style = SKPaintStyle.StrokeAndFill; - PaintSystem.Color = BackgroundColor.ToSKColor(); - PaintSystem.BlendMode = this.FillBlendMode; - - SetupGradient(PaintSystem, FillGradient, destination); - - //clip upon ImageComposite - if (IsRenderingWithComposition) - { - var previousCache = RenderObjectPrevious; - var offset = new SKPoint(this.DrawingRect.Left - previousCache.Bounds.Left, DrawingRect.Top - previousCache.Bounds.Top); - clipPreviousCachePath.Reset(); - - foreach (var dirtyChild in DirtyChildrenInternal) - { - var clip = dirtyChild.DrawingRect; - clip.Offset(offset); - clipPreviousCachePath.AddRect(clip); - } - - var saved = canvas.Save(); - canvas.ClipPath(clipPreviousCachePath, SKClipOperation.Intersect, false); //we have rectangles, no need to antialiase - canvas.DrawRect(destination, PaintSystem); - canvas.RestoreToCount(saved); - } - else - { - canvas.DrawRect(destination, PaintSystem); - } - } - - } - - protected SKPath CombineClipping(SKPath add, SKPath path) - { - if (path == null) - return add; - if (add != null) - return add.Op(path, SKPathOp.Intersect); - return null; - } - - protected void ActionWithClipping(SKRect viewport, SKCanvas canvas, Action draw) - { - var clip = new SKPath(); - { - clip.MoveTo(viewport.Left, viewport.Top); - clip.LineTo(viewport.Right, viewport.Top); - clip.LineTo(viewport.Right, viewport.Bottom); - clip.LineTo(viewport.Left, viewport.Bottom); - clip.MoveTo(viewport.Left, viewport.Top); - clip.Close(); - } - - var saved = canvas.Save(); - - ClipSmart(canvas, clip); - - draw(); - - canvas.RestoreToCount(saved); - } - - - - - /// - /// Summing up Margins and AddMargin.. properties - /// - public virtual void CalculateMargins() - { - //use Margin property as starting point - //if specific margin is set (>=0) apply - //to final Thickness - var margin = Margin; - - margin.Left += AddMarginLeft; - margin.Right += AddMarginRight; - margin.Top += AddMarginTop; - margin.Bottom += AddMarginBottom; - - Margins = margin; - } - - public virtual Thickness GetAllMarginsInPixels(float scale) - { - var constraintLeft = Math.Round((Margins.Left + Padding.Left) * scale); - var constraintRight = Math.Round((Margins.Right + Padding.Right) * scale); - var constraintTop = Math.Round((Margins.Top + Padding.Top) * scale); - var constraintBottom = Math.Round((Margins.Bottom + Padding.Bottom) * scale); - return new(constraintLeft, constraintTop, constraintRight, constraintBottom); - } - - public virtual Thickness GetMarginsInPixels(float scale) - { - var constraintLeft = Math.Round((Margins.Left) * scale); - var constraintRight = Math.Round((Margins.Right) * scale); - var constraintTop = Math.Round((Margins.Top) * scale); - var constraintBottom = Math.Round((Margins.Bottom) * scale); - return new(constraintLeft, constraintTop, constraintRight, constraintBottom); - } - - ///// - ///// Main method to call when dimensions changed - ///// - //public virtual void InvalidateMeasure() - //{ - // if (!IsDisposed) - // { - // CalculateMargins(); - // CalculateSizeRequest(); - - // InvalidateWithChildren(); - // InvalidateParent(); - - // Update(); - // } - //} - - public virtual void InvalidateMeasureInternal() - { - CalculateMargins(); - CalculateSizeRequest(); - InvalidateWithChildren(); - InvalidateParent(); - } - - protected virtual void CalculateSizeRequest() - { - SizeRequest = GetSizeRequest((float)WidthRequest, (float)HeightRequest, false); - } - - /// - /// Will invoke InvalidateInternal on controls and subviews - /// - /// - public virtual void InvalidateChildrenTree(SkiaControl control) - { - if (control != null) - { - control.NeedMeasure = true; - - foreach (var view in control.Views.ToList()) - { - InvalidateChildrenTree(view as SkiaControl); - } - } - } - - public virtual void InvalidateChildrenTree() - { - foreach (var view in Views.ToList()) - { - InvalidateChildrenTree(view as SkiaControl); - } - } - - public virtual void InvalidateWithChildren() - { - UpdateLocked = true; - - foreach (var view in Views) //will crash? why adapter nor used?? - { - InvalidateChildren(view as SkiaControl); - } - - UpdateLocked = false; - - InvalidateInternal(); - } - - /// - /// Will invoke InvalidateInternal on controls and subviews - /// - /// - void InvalidateChildren(SkiaControl control) - { - if (control != null) - { - control.InvalidateInternal(); - - foreach (var view in control.Views.ToList()) - { - InvalidateChildren(view as SkiaControl); - } - } - } - - protected bool WillInvalidateMeasure { get; set; } - - protected static void NeedInvalidateMeasure(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is SkiaControl control) - { - control.InvalidateMeasure(); - //control.PostponeInvalidation(nameof(InvalidateMeasure), control.InvalidateMeasure); - } - } - - - protected static void NeedDraw(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is SkiaControl control) - { - control.Update(); - //control.PostponeInvalidation(nameof(Update), control.Update); - } - } - - /// - /// Just make us repaint to apply new transforms etc - /// - protected static void NeedRepaint(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is SkiaControl control) - { - control.Repaint(); - //control.PostponeInvalidation(nameof(Repaint), control.Repaint); - } - } - - protected override void InvalidateMeasure() - { - InvalidateMeasureInternal(); - - Update(); - } - - protected static void NeedInvalidateViewport(BindableObject bindable, object oldvalue, object newvalue) - { - - var control = bindable as SkiaControl; - { - if (control != null && !control.IsDisposed) - { - control.InvalidateViewport(); - } - } - } - - [EditorBrowsable(EditorBrowsableState.Never)] - /// - /// Is using ItemTemplate or no - /// - public virtual bool IsTemplated - { - get - { - return (this.ItemTemplate != null || ItemTemplateType != null); - } - } - - public virtual void OnItemTemplateChanged() - { - - } - - public virtual object CreateContentFromTemplate() - { - if (ItemTemplateType != null) - { - return Activator.CreateInstance(ItemTemplateType); - } - return ItemTemplate.CreateContent(); - } - - protected static void ItemTemplateChanged(BindableObject bindable, object oldvalue, object newvalue) - { - - var control = bindable as SkiaControl; - { - if (control != null && !control.IsDisposed) - { - control.OnItemTemplateChanged(); - } - } - } - static object lockViews = new(); - - - - - #region Animation - - //public async Task PlayRippleAnimationAsync(Color color, double x, double y, bool removePrevious = true) - //{ - // var animation = new AfterEffectRipple() - // { - // X = x, - // Y = y, - // Color = color.ToSKColor(), - // }; - - // await PlayAnimation(animation, 350, null, removePrevious); - //} - - - public IAnimatorsManager GetAnimatorsManager() - { - return GetTopParentView() as IAnimatorsManager; - - //var parent = this.Parent; - - //if (parent is IAnimatorsManager manager) - //{ - // return manager; - //} - - //if (parent is SkiaControl control) - // return control.GetAnimatorsManager(); - - //return null; - } - - public bool RegisterAnimator(ISkiaAnimator animator) - { - var top = GetAnimatorsManager(); - if (top != null) - { - _lastAnimatorManager = top; - top.AddAnimator(animator); - Repaint(); - return true; - } - return false; - } - - public void UnregisterAnimator(Guid uid) - { - var top = GetAnimatorsManager(); - if (top == null) - { - top = _lastAnimatorManager; - } - top?.RemoveAnimator(uid); - } - - public IEnumerable UnregisterAllAnimatorsByType(Type type) - { - if (Superview != null) - return Superview.UnregisterAllAnimatorsByType(type); - - return Array.Empty(); - } - - /// - /// Expecting input coordinates in POINTs and relative to control coordinates. Use GetOffsetInsideControlInPoints to help. - /// - /// - /// - /// - /// - public async void PlayRippleAnimation(Color color, double x, double y, bool removePrevious = true) - { - if (removePrevious) - { - //UnregisterAllAnimatorsByType(typeof(RippleAnimator)); - } - - //Debug.WriteLine($"[RIPPLE] start play for '{Tag}'"); - var animation = new RippleAnimator(this) - { - Color = color.ToSKColor(), - X = x, - Y = y - }; - animation.Start(); - } - - public async void PlayShimmerAnimation(Color color, float shimmerWidth = 50, float shimmerAngle = 45, int speedMs = 1000, bool removePrevious = true) - { - //Debug.WriteLine($"[SHIMMER] start play for '{Tag}'"); - if (removePrevious) - { - //UnregisterAllAnimatorsByType(typeof(ShimmerAnimator)); - } - var animation = new ShimmerAnimator(this) - { - Color = color.ToSKColor(), - ShimmerWidth = shimmerWidth, - ShimmerAngle = shimmerAngle, - Speed = speedMs - }; - animation.Start(); - } - - #endregion - - - #region PAINT HELPERS - - public SKShader CreateGradient(SKRect destination, SkiaGradient gradient) - { - if (gradient != null && gradient.Type != GradientType.None) - { - var colors = new List(); - foreach (var color in gradient.Colors.ToList()) - { - var usingColor = color; - if (gradient.Light < 1.0) - { - usingColor = usingColor.MakeDarker(100 - gradient.Light * 100); - } - else if (gradient.Light > 1.0) - { - usingColor = usingColor.MakeLighter(gradient.Light * 100 - 100); - } - - var newAlpha = usingColor.Alpha * gradient.Opacity; - usingColor = usingColor.WithAlpha(newAlpha); - colors.Add(usingColor.ToSKColor()); - } - - float[] colorPositions = null; - if (gradient.ColorPositions?.Count == colors.Count) - { - colorPositions = gradient.ColorPositions.Select(x => (float)x).ToArray(); - } - - switch (gradient.Type) - { - case GradientType.Sweep: - - //float sweep = (float)Value3;//((float)this.Variable1 % (float)this.Variable2 / 100F) * 360.0F; - - return SKShader.CreateSweepGradient( - new SKPoint(destination.Left + destination.Width / 2.0f, - destination.Top + destination.Height / 2.0f), - colors.ToArray(), - colorPositions, - gradient.TileMode, (float)Value1, (float)(Value1 + Value2)); - - case GradientType.Circular: - return SKShader.CreateRadialGradient( - new SKPoint(destination.Left + destination.Width / 2.0f, - destination.Top + destination.Height / 2.0f), - Math.Max(destination.Width, destination.Height) / 2.0f, - colors.ToArray(), - colorPositions, - gradient.TileMode); - - case GradientType.Linear: - default: - return SKShader.CreateLinearGradient( - new SKPoint(destination.Left + destination.Width * gradient.StartXRatio, - destination.Top + destination.Height * gradient.StartYRatio), - new SKPoint(destination.Left + destination.Width * gradient.EndXRatio, - destination.Top + destination.Height * gradient.EndYRatio), - colors.ToArray(), - colorPositions, - gradient.TileMode); - break; - } - - } - - return null; - } - - protected CachedGradient LastGradient; - - protected CachedShadow LastShadow; - - - - - /// - /// Creates and sets an ImageFilter for SKPaint - /// - /// - /// - public bool SetupShadow(SKPaint paint, SkiaShadow shadow, float scale) - { - if (shadow != null && paint != null) - { - - if (LastShadow == null || LastShadow.Shadow != shadow || - LastShadow.Scale != scale) - { - var kill = LastShadow; - LastShadow = new() - { - Filter = CreateShadow(shadow, scale), - Scale = scale, - Shadow = shadow - }; - kill?.Dispose(); - } - - var old = paint.ImageFilter; - paint.ImageFilter = LastShadow.Filter; - if (old != paint.ImageFilter) - { - old?.Dispose(); - } - - return true; - } - - return false; - } - - #endregion - - - #region CHILDREN VIEWS - - private List _orderedChildren; - - public List GetOrderedSubviews(bool recalculate = false) - { - if (_orderedChildren == null || recalculate) - { - _orderedChildren = Views.OrderBy(x => x.ZIndex).ToList(); - } - return _orderedChildren; - } - - - public IReadOnlyList GetUnorderedSubviews(bool recalculate = false) - { - return Views; - } - - public List Views { get; } = new(); - - public virtual void DisposeChildren() - { - foreach (var child in Views.ToList()) - { - RemoveSubView(child); - child.Dispose(); - } - Views.Clear(); - Invalidate(); - } - - /// - /// The OnDisposing might come with a delay to avoid disposing resources at use. - /// This method will be called without delay when Dispose() is invoked. Disposed will set to True and for Views their OnWillDisposeWithChildren will be called. - /// - public virtual void OnWillDisposeWithChildren() - { - IsDisposing = true; - - foreach (var child in Views.ToList()) - { - if (child == null) - continue; - - child.OnWillDisposeWithChildren(); - } - } - - public virtual void ClearChildren() - { - foreach (var child in Views.ToList()) - { - if (child == null) - continue; - - RemoveSubView(child); - } - - Views.Clear(); - - Invalidate(); - } - - public virtual void InvalidateViewsList() - { - _orderedChildren = null; - //NeedMeasure = true; - } - - protected virtual void OnChildAdded(SkiaControl child) - { - Invalidate(); - } - - protected override void OnChildRemoved(Element child, int oldLogicalIndex) - { - //base.OnChildRemoved(child, oldLogicalIndex); - } - - protected override void OnChildAdded(Element child) - { - //base.OnChildAdded(child); - } - - protected virtual void OnChildRemoved(SkiaControl child) - { - Invalidate(); - } - - public DateTime GestureListenerRegistrationTime { get; set; } - - public void RegisterGestureListener(ISkiaGestureListener gestureListener) - { - lock (LockIterateListeners) - { - gestureListener.GestureListenerRegistrationTime = DateTime.UtcNow; - GestureListeners.Add(gestureListener); - //Debug.WriteLine($"Added {gestureListener} to gestures of {this.Tag} {this}"); - } - } - - public void UnregisterGestureListener(ISkiaGestureListener gestureListener) - { - lock (LockIterateListeners) - { - HadInput.Remove(gestureListener.Uid); - GestureListeners.Remove(gestureListener); - //Debug.WriteLine($"Removed {gestureListener} from gestures of {this.Tag} {this}"); - } - } - - /// - /// Children we should check for touch hits - /// - //public SortedSet GestureListeners { get; } = new(new DescendingZIndexGestureListenerComparer()); - - public SortedGestureListeners GestureListeners { get; } = new(); - - public virtual void OnParentChanged(IDrawnBase newvalue, IDrawnBase oldvalue) - { - if (newvalue != null && newvalue is SkiaControl control) - { - Superview = control.Superview; - } - - if (newvalue != null) - Update(); - - ParentChanged?.Invoke(this, Parent); - } - - static object lockParent = new(); - - public virtual void SetParent(IDrawnBase parent) - { - //lock (lockParent) - { - if (Parent == parent) - return; - - var iAmGestureListener = this as ISkiaGestureListener; - - //clear previous - if (Parent is IDrawnBase oldParent) - { - //kill gestures - if (iAmGestureListener != null) - { - oldParent.UnregisterGestureListener(iAmGestureListener); - } - - //fill animations - Superview?.UnregisterAllAnimatorsByParent(this); - - oldParent.Views.Remove(this); - - if (oldParent is SkiaControl skiaParent) - { - skiaParent.InvalidateViewsList(); - } - } - - if (parent == null) - { - Parent = null; - //BindingContext = null; - - //this.SizeChanged -= OnFormsSizeChanged; - return; - } - - //if (!parent.Views.Contains(this)) //Slow A - { - parent.Views.Add(this); - if (parent is SkiaControl skiaParent) - { - skiaParent.InvalidateViewsList(); - } - } - //else - //{ - // var stop = 1; - //} - - Parent = parent; - - if (iAmGestureListener != null) - { - parent.RegisterGestureListener(iAmGestureListener); - } - - if (parent is IDrawnBase control) - { - if (BindingContext == null) - BindingContext = control.BindingContext; - } - - InvalidateInternal(); - } - - } - - - #endregion - - public virtual void RegisterGestureListenersTree(SkiaControl control) - { - if (control.Parent == null) - return; - - if (control is ISkiaGestureListener listener) - { - control.Parent.RegisterGestureListener(listener); - } - foreach (var view in Views) - { - view.RegisterGestureListenersTree(view); - } - } - - public virtual void UnregisterGestureListenersTree(SkiaControl control) - { - if (control.Parent == null) - return; - - if (control is ISkiaGestureListener listener) - { - control.Parent.UnregisterGestureListener(listener); - } - foreach (var view in Views) - { - view.UnregisterGestureListenersTree(view); - } - } - - - #region Children - - - public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), - typeof(DataTemplate), null, - propertyChanged: ItemTemplateChanged); - /// - /// Kind of BindableLayout.DrawnTemplate - /// - public DataTemplate ItemTemplate - { - get { return (DataTemplate)GetValue(ItemTemplateProperty); } - set { SetValue(ItemTemplateProperty, value); } - } - - public static readonly BindableProperty ItemTemplateTypeProperty = BindableProperty.Create( - nameof(ItemTemplateType), - typeof(Type), - typeof(SkiaControl), - null - , propertyChanged: ItemTemplateChanged); - - /// - /// ItemTemplate alternative for faster creation - /// - public Type ItemTemplateType - { - get { return (Type)GetValue(ItemTemplateTypeProperty); } - set { SetValue(ItemTemplateTypeProperty, value); } - } - - - #region SELECTABLE TEMPLATES - - - public static readonly BindableProperty TemplatesProperty = BindableProperty.Create( - nameof(Templates), - typeof(IList), - typeof(SkiaControl), - defaultValueCreator: (instance) => - { - var created = new ObservableCollection(); - ItemTemplateChanged(instance, null, created); - return created; - }, - validateValue: (bo, v) => v is IList, - propertyChanged: ItemTemplateChanged, - coerceValue: CoerceTemplates); - - public IList Templates - { - get => (IList)GetValue(TemplatesProperty); - set => SetValue(TemplatesProperty, value); - } - - private static object CoerceTemplates(BindableObject bindable, object value) - { - if (!(value is ReadOnlyCollection readonlyCollection)) - { - return value; - } - - return new ReadOnlyCollection( - readonlyCollection.ToList()); - } - - private void OnSkiaPropertyTemplateCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (sender is SkiaControl control) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - control.OnItemTemplateChanged(); - break; - - case NotifyCollectionChangedAction.Reset: - case NotifyCollectionChangedAction.Remove: - control.OnItemTemplateChanged(); - break; - } - } - } - - - - - #endregion - - public static readonly BindableProperty ChildrenProperty = BindableProperty.Create( - nameof(Children), - typeof(IList), - typeof(SkiaControl), - defaultValueCreator: (instance) => - { - var created = new ObservableCollection(); - ChildrenPropertyChanged(instance, null, created); - return created; - }, - validateValue: (bo, v) => v is IList, - propertyChanged: ChildrenPropertyChanged); - - public IList Children - { - get => (IList)GetValue(ChildrenProperty); - set => SetValue(ChildrenProperty, value); - } - - protected void AddOrRemoveView(SkiaControl subView, bool add) - { - if (subView != null) - { - if (add) - { - AddSubView(subView); - } - else - { - RemoveSubView(subView); - } - - } - } - - public virtual void SetChildren(IEnumerable views) - { - ClearChildren(); - - if (views == null) - return; - - foreach (var child in views) - { - AddOrRemoveView(child, true); - } - } - - private static void ChildrenPropertyChanged(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is SkiaControl skiaControl) - { - - var enumerableChildren = (IEnumerable)newvalue; - - if (oldvalue != null) - { - var oldViews = (IEnumerable)oldvalue; - - if (oldvalue is INotifyCollectionChanged oldCollection) - { - oldCollection.CollectionChanged -= skiaControl.OnChildrenCollectionChanged; - } - - foreach (var subView in oldViews) - { - skiaControl.AddOrRemoveView(subView, false); - } - } - - if (skiaControl.ItemTemplate == null) - { - skiaControl.SetChildren(enumerableChildren); - } - - //foreach (var subView in enumerableChildren) - //{ - // subView.SetParent(skiaControl); - //} - - if (newvalue is INotifyCollectionChanged newCollection) - { - newCollection.CollectionChanged -= skiaControl.OnChildrenCollectionChanged; - newCollection.CollectionChanged += skiaControl.OnChildrenCollectionChanged; - } - - skiaControl.Update(); - } - - } - - public bool HasItemTemplate - { - get - { - return ItemTemplate != null; - } - } - - private void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (HasItemTemplate) - return; - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - foreach (SkiaControl newChildren in e.NewItems) - { - AddOrRemoveView(newChildren, true); - } - break; - - case NotifyCollectionChangedAction.Reset: - case NotifyCollectionChangedAction.Remove: - foreach (SkiaControl oldChildren in e.OldItems ?? Array.Empty()) - { - AddOrRemoveView(oldChildren, false); - } - - break; - } - - Update(); - } - - #endregion - - - public AddGestures.GestureListener GesturesEffect { get; set; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (float X, float Y) RescaleAspect(float width, float height, SKRect dest, TransformAspect stretch) - { - float aspectX = 1; - float aspectY = 1; - - var s1 = dest.Width / width; - var s2 = dest.Height / height; - - switch (stretch) - { - case TransformAspect.None: - break; - - case TransformAspect.Fit: - aspectX = dest.Width < width ? dest.Width / width : 1; - aspectY = dest.Height < height ? dest.Height / height : 1; - break; - - case TransformAspect.Fill: - aspectX = width < dest.Width ? s1 : 1; - aspectY = height < dest.Height ? s2 : 1; - break; - - case TransformAspect.AspectFit: - aspectX = Math.Min(s1, s2); - aspectY = aspectX; - break; - - case TransformAspect.AspectFill: - aspectX = width < dest.Width ? Math.Max(s1, s2) : 1; - aspectY = aspectX; - break; - - case TransformAspect.Cover: - aspectX = s1; - aspectY = s2; - break; - - case TransformAspect.AspectCover: - aspectX = Math.Max(s1, s2); - aspectY = aspectX; - break; - - case TransformAspect.AspectFitFill: - var imageAspect = width / height; - var viewportAspect = dest.Width / dest.Height; - - if (imageAspect > viewportAspect) // Image is wider than viewport - { - aspectX = dest.Width / width; - aspectY = aspectX; - } - else // Image is taller than viewport - { - aspectY = dest.Height / height; - aspectX = aspectY; - } - break; - } - - return (aspectX, aspectY); - } - - #region HELPERS - - - - public static Random Random = new Random(); - protected double _arrangedViewportHeightLimit; - protected double _arrangedViewportWidthLimit; - protected float _lastMeasuredForScale; - private bool _isLayoutDirty; - private Thickness _margins; - private SKRect _lastArrangedInside; - private double _lastArrangedForScale; - private bool _needUpdateFrontCache; - - - public static Color GetRandomColor() - { - byte r = (byte)Random.Next(256); - byte g = (byte)Random.Next(256); - byte b = (byte)Random.Next(256); - - return Color.FromRgb(r, g, b); - } - - //[MethodImpl(MethodImplOptions.AggressiveInlining)] - //public static bool CompareRects(SKRect a, SKRect b, float precision) - //{ - // return - // Math.Abs(a.Left - b.Left) <= precision - // && Math.Abs(a.Top - b.Top) <= precision - // && Math.Abs(a.Right - b.Right) <= precision - // && Math.Abs(a.Bottom - b.Bottom) <= precision; - //} - - //[MethodImpl(MethodImplOptions.AggressiveInlining)] - //public static bool CompareRectsSize(SKRect a, SKRect b, float precision) - //{ - // return - // Math.Abs(a.Width - b.Width) <= precision - // && Math.Abs(a.Height - b.Height) <= precision; - //} - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool CompareFloats(float a, float b, float precision) - { - return Math.Abs(a - b) <= precision; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool CompareDoubles(double a, double b, double precision) - { - return Math.Abs(a - b) <= precision; - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool CompareRects(SKRect a, SKRect b, float precision) - { - if ((float.IsInfinity(a.Left) && float.IsInfinity(b.Left)) || Math.Abs(a.Left - b.Left) <= precision) - { - if ((float.IsInfinity(a.Top) && float.IsInfinity(b.Top)) || Math.Abs(a.Top - b.Top) <= precision) - { - if ((float.IsInfinity(a.Right) && float.IsInfinity(b.Right)) || Math.Abs(a.Right - b.Right) <= precision) - { - if ((float.IsInfinity(a.Bottom) && float.IsInfinity(b.Bottom)) || Math.Abs(a.Bottom - b.Bottom) <= precision) - { - return true; - } - } - } - } - return false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool CompareRectsSize(SKRect a, SKRect b, float precision) - { - if ((float.IsInfinity(a.Width) && float.IsInfinity(b.Width)) || (Math.Abs(a.Width - b.Width) <= precision)) - { - if ((float.IsInfinity(a.Height) && float.IsInfinity(b.Height)) || (Math.Abs(a.Height - b.Height) <= precision)) - { - return true; - } - } - return false; - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool CompareSize(SKSize a, SKSize b, float precision) - { - if ((float.IsInfinity(a.Width) && float.IsInfinity(b.Width)) || Math.Abs(a.Width - b.Width) <= precision) - { - if ((float.IsInfinity(a.Height) && float.IsInfinity(b.Height)) || Math.Abs(a.Height - b.Height) <= precision) - { - return true; - } - } - return false; - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AreEqual(double v1, double v2, double precision) - { - if (double.IsInfinity(v1) && double.IsInfinity(v2) || Math.Abs(v1 - v2) <= precision) - { - return true; - } - return false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AreEqual(float v1, float v2, float precision) - { - if (float.IsInfinity(v1) && float.IsInfinity(v2) || Math.Abs(v1 - v2) <= precision) - { - return true; - } - return false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AreVectorsEqual(Vector2 v1, Vector2 v2, float precision) - { - if ((float.IsInfinity(v1.X) && float.IsInfinity(v2.X)) || Math.Abs(v1.X - v2.X) <= precision) - { - if ((float.IsInfinity(v1.Y) && float.IsInfinity(v2.Y)) || Math.Abs(v1.Y - v2.Y) <= precision) - { - return true; - } - } - return false; - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static DirectionType GetDirectionType(Vector2 velocity, DirectionType defaultDirection, float ratio) - { - float x = Math.Abs(velocity.X); - float y = Math.Abs(velocity.Y); - - if (x > y && x / y > ratio) - { - //Debug.WriteLine($"[DirectionType] H {x:0.0},{y:0.0} = {x / y:0.00}"); - return DirectionType.Horizontal; - } - else if (y > x && y / x >= ratio) - { - return DirectionType.Vertical; - } - else - { - return defaultDirection; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static DirectionType GetDirectionType(Vector2 start, Vector2 end, float ratio) - { - // Calculate the absolute differences between the X and Y coordinates - float x = Math.Abs(end.X - start.X); - float y = Math.Abs(end.Y - start.Y); - - // Compare the differences to determine the dominant direction - if (x > y && x / y > ratio) - { - return DirectionType.Horizontal; - } - else if (y > x && y / x >= ratio) - { - return DirectionType.Vertical; - } - else - { - return DirectionType.None; // The direction is neither horizontal nor vertical (the vectors are equal or diagonally aligned) - } - } - - - - /// - /// Ported from Avalonia: AreClose - Returns whether or not two floats are "close". That is, whether or - /// not they are within epsilon of each other. - /// - /// The first float to compare. - /// The second float to compare. - public static bool AreClose(float value1, float value2) - { - //in case they are Infinities (then epsilon check does not work) - if (value1 == value2) return true; - float eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0f) * float.Epsilon; - float delta = value1 - value2; - return (-eps < delta) && (eps > delta); - } - - - /// - /// Ported from Avalonia: AreClose - Returns whether or not two doubles are "close". That is, whether or - /// not they are within epsilon of each other. - /// - /// The first double to compare. - /// The second double to compare. - public static bool AreClose(double value1, double value2) - { - //in case they are Infinities (then epsilon check does not work) - if (value1 == value2) return true; - double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon; - double delta = value1 - value2; - return (-eps < delta) && (eps > delta); - } - - /// - /// Avalonia: IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1), - /// but this is faster. - /// - /// The double to compare to 1. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsOne(double value) - { - return Math.Abs(value - 1.0) < 10.0 * double.Epsilon; - } - - - - - #endregion - } - - public static class Snapping - { - /// - /// Used by the layout system to round a position translation value applying scale and initial anchor. Pass POINTS only, it wont do its job when receiving pixels! - /// - /// - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float SnapPointsToPixel(float initialPosition, float translation, double scale) - { - // Scale the initial and the translation - float scaledInitial = initialPosition * (float)scale; - float scaledTranslation = translation * (float)scale; - - // Add the scaled initial to the scaled translation - float scaledTotal = scaledInitial + scaledTranslation; - - // Round to the nearest integer (you could also use Floor or Ceiling or Round, play with it - float snappedTotal = (float)Math.Round(scaledTotal); - - // Subtract the scaled initial position to get the snapped, scaled translation - float snappedScaledTranslation = snappedTotal - scaledInitial; - - // Divide by scale to get back to your original units - float snappedTranslation = snappedScaledTranslation / (float)scale; - - return snappedTranslation; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float SnapPixelsToPixel(float initialPosition, float translation) - { - // Sum the initial position and the translation - float total = initialPosition + translation; - - // Round to the nearest integer to snap to the nearest pixel - float snappedTotal = (float)Math.Round(total); - - // Find out how much we've adjusted the translation by - float snappedTranslation = snappedTotal - initialPosition; - - return snappedTranslation; - } - - - - } + + } } \ No newline at end of file diff --git a/src/Engine/Draw/Cache/SkiaCacheType.cs b/src/Engine/Draw/Cache/SkiaCacheType.cs index 9664fa8a..3e346eb4 100644 --- a/src/Engine/Draw/Cache/SkiaCacheType.cs +++ b/src/Engine/Draw/Cache/SkiaCacheType.cs @@ -14,6 +14,13 @@ public enum SkiaCacheType /// Operations, + /// + /// Create and reuse SKPicture all over the canvas ignoring clipping. + /// Try this first for labels, svg etc. + /// Do not use this when dropping shadows or with other effects, better use Bitmap. + /// + OperationsFull, + /// /// Will use simple SKBitmap cache type, will not use hardware acceleration. /// Slower but will work for sizes bigger than graphics memory if needed. diff --git a/src/Engine/Draw/Cache/SkiaRenderObject.cs b/src/Engine/Draw/Cache/SkiaRenderObject.cs index ef247ea5..f2ecb58e 100644 --- a/src/Engine/Draw/Cache/SkiaRenderObject.cs +++ b/src/Engine/Draw/Cache/SkiaRenderObject.cs @@ -47,6 +47,13 @@ public void Draw(SKCanvas canvas, SKRect destination, SKPaint paint) var x = (float)Math.Round(destination.Left - Bounds.Left); var y = (float)Math.Round(destination.Top - Bounds.Top); + if (Type == SkiaCacheType.OperationsFull) + { + var stop = 1; + //x = (float)Math.Round(RecordingArea.Left); + //y = (float)Math.Round(RecordingArea.Top); + } + canvas.DrawPicture(Picture, x, y, paint); } else @@ -67,19 +74,23 @@ public void Draw(SKCanvas canvas, SKRect destination, SKPaint paint) } } - public CachedObject(SkiaCacheType type, SKPicture picture, SKRect bounds) + public CachedObject(SkiaCacheType type, SKPicture picture, SKRect bounds, SKRect recordingArea) { Type = type; Bounds = bounds; + RecordingArea = recordingArea; Picture = picture; } - public CachedObject(SkiaCacheType type, SKSurface surface, SKRect bounds) + public CachedObject(SkiaCacheType type, SKSurface surface, SKRect bounds, SKRect recordingArea) { Type = type; Surface = surface; Bounds = bounds; + RecordingArea = recordingArea; Image = surface.Snapshot(); + + } /// @@ -93,6 +104,8 @@ public CachedObject(SkiaCacheType type, SKSurface surface, SKRect bounds) public SKRect Bounds { get; set; } + public SKRect RecordingArea { get; set; } + public SkiaCacheType Type { get; protected set; } public void Dispose() diff --git a/src/Engine/Draw/Layout/ContentLayout.cs b/src/Engine/Draw/Layout/ContentLayout.cs index 95b216f7..b883a759 100644 --- a/src/Engine/Draw/Layout/ContentLayout.cs +++ b/src/Engine/Draw/Layout/ContentLayout.cs @@ -8,307 +8,304 @@ namespace DrawnUi.Maui.Draw; public partial class ContentLayout : SkiaControl, IVisibilityAware, ISkiaGestureListener { - public override void Invalidate() - { - base.Invalidate(); - - Update(); - } - - public override void OnWillDisposeWithChildren() - { - base.OnWillDisposeWithChildren(); - - Content?.Dispose(); - } - - public virtual bool OnFocusChanged(bool focus) - { - return false; - } - - public virtual void OnAppeared() - { - if (Content is IVisibilityAware aware) - { - aware.OnAppeared(); - } - } - - public virtual void OnDisappeared() - { - if (Content is IVisibilityAware aware) - { - aware.OnDisappeared(); - } - } - - public virtual void OnDisappearing() - { - if (Content is IVisibilityAware aware) - { - aware.OnDisappearing(); - } - - } - - public virtual void OnAppearing() - { - if (Content is IVisibilityAware aware) - { - aware.OnAppearing(); - } - } - - - protected bool IsContentActive - { - get - { - return Content != null && Content.IsVisible; - } - } - - - public override ScaledSize Measure(float widthConstraint, float heightConstraint, float scale) - { - if (IsMeasuring) - { - NeedRemeasuring = true; - return MeasuredSize; - } - - //background measuring or invisible or self measure from draw because layout will never pass -1 - if (!CanDraw || widthConstraint < 0 || heightConstraint < 0) - { - return MeasuredSize; - } - - try - { - IsMeasuring = true; - - var request = CreateMeasureRequest(widthConstraint, heightConstraint, scale); - if (request.IsSame) - { - return MeasuredSize; - } - - if (request.WidthRequest == 0 || request.HeightRequest == 0) - { - InvalidateCacheWithPrevious(); - - return SetMeasuredAsEmpty(request.Scale); - } - - var constraints = GetMeasuringConstraints(request); - - var children = new List { Content }; - ContentSize = MeasureContent(children, constraints.Content, scale); - - return SetMeasuredAdaptToContentSize(constraints, scale); - } - finally - { - IsMeasuring = false; - } - - - } - - public ScaledRect Viewport { get; protected set; } = new(); - - //public override void Arrange(SKRect destination, float widthRequest, float heightRequest, float scale) - //{ - // if (!CanDraw) - // return; - - // if (NeedMeasure) - // { - // var adjustedDestination = CalculateLayout(destination, SizeRequest.Width, SizeRequest.Height, scale); - // Measure(adjustedDestination.Width, adjustedDestination.Height, scale); - // IsLayoutDirty = true; - // } - - // base.Arrange(destination, MeasuredSize.Units.Width, MeasuredSize.Units.Height, scale); - //} - - - - /// - /// In PIXELS - /// - /// - /// - protected virtual SKRect GetContentAvailableRect(SKRect destination) - { - return destination; - } - - - public override ScaledRect GetOnScreenVisibleArea(float inflateByPixels = 0) - { - var inflated = DrawingRect; - inflated.Inflate(inflateByPixels, inflateByPixels); - return ScaledRect.FromPixels(inflated, RenderingScale); - } - - protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float scale, object arguments) - { - base.Paint(ctx, destination, scale, arguments); - - DrawViews(ctx, destination, scale); - } - - - protected override int DrawViews(SkiaDrawingContext context, SKRect destination, float scale, - bool debug = false) - { - if (!IsContentActive || destination.Width <= 0 || destination.Height <= 0) - { - return 0; - } - var drawViews = new List { Content }; - - return RenderViewsList(drawViews, context, destination, scale); - } - - - public SKRect ContentAvailableSpace { get; protected set; } - - public override void SetChildren(IEnumerable views) - { - //do not use subviews as we are using Content property for this control - - return; - } - - public override void ApplyBindingContext() - { - base.ApplyBindingContext(); - - if (this.Content != null) - { - Content.BindingContext = BindingContext; - } - } - - protected virtual void SetContent(SkiaControl view) - { - var oldContent = Views.FirstOrDefault(x => x == Content); - if (view != oldContent) - { - if (oldContent != null) - { - RemoveSubView(oldContent); - } - if (view != null) - { - AddSubView(view); - } - } - } - - - protected override void CreateChildrenFromCode() - { - if (Content == null && !DefaultChildrenCreated) - { - DefaultChildrenCreated = true; - if (CreateChildren != null) - { - try - { - var children = CreateChildren(); - Content = children.FirstOrDefault(); - } - catch (Exception e) - { - Trace.WriteLine(e); - } - } - } - } - - #region PROPERTIES - - - private static void OnReplaceContent(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is ContentLayout control) - { - control.SetContent(newvalue as SkiaControl); - } - } - - public static readonly BindableProperty ContentProperty = BindableProperty.Create( - nameof(Content), - typeof(SkiaControl), typeof(ContentLayout), - null, - propertyChanged: OnReplaceContent); - - public SkiaControl Content - { - get { return (SkiaControl)GetValue(ContentProperty); } - set { SetValue(ContentProperty, value); } - } - - public static readonly BindableProperty OrientationProperty = BindableProperty.Create(nameof(Orientation), typeof(ScrollOrientation), typeof(ContentLayout), - ScrollOrientation.Vertical, - propertyChanged: NeedDraw); - /// - /// Gets or sets the scrolling direction of the ScrollView. This is a bindable property. - /// - public ScrollOrientation Orientation - { - get { return (ScrollOrientation)GetValue(OrientationProperty); } - set { SetValue(OrientationProperty, value); } - } - - public static readonly BindableProperty ScrollTypeProperty = BindableProperty.Create(nameof(ViewportScrollType), typeof(ViewportScrollType), typeof(ContentLayout), - ViewportScrollType.Scrollable, - propertyChanged: NeedDraw); - /// - /// Gets or sets the scrolling direction of the ScrollView. This is a bindable property. - /// - public ViewportScrollType ScrollType - { - get { return (ViewportScrollType)GetValue(ScrollTypeProperty); } - set { SetValue(ScrollTypeProperty, value); } - } - - public static readonly BindableProperty VirtualizationProperty = BindableProperty.Create( - nameof(Virtualisation), - typeof(VirtualisationType), - typeof(ContentLayout), - VirtualisationType.Enabled, - propertyChanged: NeedInvalidateMeasure); - - /// - /// Default is Enabled, children get the visible viewport area for rendering and can virtualize. - /// - public VirtualisationType Virtualisation - { - get { return (VirtualisationType)GetValue(VirtualizationProperty); } - set { SetValue(VirtualizationProperty, value); } - } - - - - - #endregion - - - - protected override void OnPropertyChanged([CallerMemberName] string propertyName = "") - { - base.OnPropertyChanged(propertyName); - - if (propertyName == nameof(Orientation)) - { - Invalidate(); - } - } + public override void Invalidate() + { + base.Invalidate(); + + Update(); + } + + public override void OnWillDisposeWithChildren() + { + base.OnWillDisposeWithChildren(); + + Content?.Dispose(); + } + + public virtual bool OnFocusChanged(bool focus) + { + return false; + } + + public virtual void OnAppeared() + { + if (Content is IVisibilityAware aware) + { + aware.OnAppeared(); + } + } + + public virtual void OnDisappeared() + { + if (Content is IVisibilityAware aware) + { + aware.OnDisappeared(); + } + } + + public virtual void OnDisappearing() + { + if (Content is IVisibilityAware aware) + { + aware.OnDisappearing(); + } + + } + + public virtual void OnAppearing() + { + if (Content is IVisibilityAware aware) + { + aware.OnAppearing(); + } + } + + + protected bool IsContentActive + { + get + { + return Content != null && Content.IsVisible; + } + } + + + public override ScaledSize Measure(float widthConstraint, float heightConstraint, float scale) + { + if (IsMeasuring) + { + NeedRemeasuring = true; + return MeasuredSize; + } + + //background measuring or invisible or self measure from draw because layout will never pass -1 + if (!CanDraw || widthConstraint < 0 || heightConstraint < 0) + { + return MeasuredSize; + } + + try + { + IsMeasuring = true; + + var request = CreateMeasureRequest(widthConstraint, heightConstraint, scale); + if (request.IsSame) + { + return MeasuredSize; + } + + if (request.WidthRequest == 0 || request.HeightRequest == 0) + { + InvalidateCacheWithPrevious(); + + return SetMeasuredAsEmpty(request.Scale); + } + + var constraints = GetMeasuringConstraints(request); + + var children = new List { Content }; + ContentSize = MeasureContent(children, constraints.Content, scale); + + return SetMeasuredAdaptToContentSize(constraints, scale); + } + finally + { + IsMeasuring = false; + } + + + } + + public ScaledRect Viewport { get; protected set; } = new(); + + //public override void Arrange(SKRect destination, float widthRequest, float heightRequest, float scale) + //{ + // if (!CanDraw) + // return; + + // if (NeedMeasure) + // { + // var adjustedDestination = CalculateLayout(destination, SizeRequest.Width, SizeRequest.Height, scale); + // Measure(adjustedDestination.Width, adjustedDestination.Height, scale); + // IsLayoutDirty = true; + // } + + // base.Arrange(destination, MeasuredSize.Units.Width, MeasuredSize.Units.Height, scale); + //} + + + + /// + /// In PIXELS + /// + /// + /// + protected virtual SKRect GetContentAvailableRect(SKRect destination) + { + return destination; + } + + + public override ScaledRect GetOnScreenVisibleArea(float inflateByPixels = 0) + { + var inflated = DrawingRect; + inflated.Inflate(inflateByPixels, inflateByPixels); + return ScaledRect.FromPixels(inflated, RenderingScale); + } + + protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float scale, object arguments) + { + base.Paint(ctx, destination, scale, arguments); + + DrawViews(ctx, destination, scale); + } + + + protected override int DrawViews(SkiaDrawingContext context, SKRect destination, float scale, + bool debug = false) + { + if (!IsContentActive || destination.Width <= 0 || destination.Height <= 0) + { + return 0; + } + var drawViews = new List { Content }; + + return RenderViewsList(drawViews, context, destination, scale); + } + + + public SKRect ContentAvailableSpace { get; protected set; } + + public override void SetChildren(IEnumerable views) + { + //do not use subviews as we are using Content property for this control + + return; + } + + public override void ApplyBindingContext() + { + base.ApplyBindingContext(); + + Content?.SetInheritedBindingContext(BindingContext); + } + + protected virtual void SetContent(SkiaControl view) + { + var oldContent = Views.FirstOrDefault(x => x == Content); + if (view != oldContent) + { + if (oldContent != null) + { + RemoveSubView(oldContent); + } + if (view != null) + { + AddSubView(view); + } + } + } + + + protected override void CreateChildrenFromCode() + { + if (Content == null && !DefaultChildrenCreated) + { + DefaultChildrenCreated = true; + if (CreateChildren != null) + { + try + { + var children = CreateChildren(); + Content = children.FirstOrDefault(); + } + catch (Exception e) + { + Trace.WriteLine(e); + } + } + } + } + + #region PROPERTIES + + + private static void OnReplaceContent(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is ContentLayout control) + { + control.SetContent(newvalue as SkiaControl); + } + } + + public static readonly BindableProperty ContentProperty = BindableProperty.Create( + nameof(Content), + typeof(SkiaControl), typeof(ContentLayout), + null, + propertyChanged: OnReplaceContent); + + public SkiaControl Content + { + get { return (SkiaControl)GetValue(ContentProperty); } + set { SetValue(ContentProperty, value); } + } + + public static readonly BindableProperty OrientationProperty = BindableProperty.Create(nameof(Orientation), typeof(ScrollOrientation), typeof(ContentLayout), + ScrollOrientation.Vertical, + propertyChanged: NeedDraw); + /// + /// Gets or sets the scrolling direction of the ScrollView. This is a bindable property. + /// + public ScrollOrientation Orientation + { + get { return (ScrollOrientation)GetValue(OrientationProperty); } + set { SetValue(OrientationProperty, value); } + } + + public static readonly BindableProperty ScrollTypeProperty = BindableProperty.Create(nameof(ViewportScrollType), typeof(ViewportScrollType), typeof(ContentLayout), + ViewportScrollType.Scrollable, + propertyChanged: NeedDraw); + /// + /// Gets or sets the scrolling direction of the ScrollView. This is a bindable property. + /// + public ViewportScrollType ScrollType + { + get { return (ViewportScrollType)GetValue(ScrollTypeProperty); } + set { SetValue(ScrollTypeProperty, value); } + } + + public static readonly BindableProperty VirtualizationProperty = BindableProperty.Create( + nameof(Virtualisation), + typeof(VirtualisationType), + typeof(ContentLayout), + VirtualisationType.Enabled, + propertyChanged: NeedInvalidateMeasure); + + /// + /// Default is Enabled, children get the visible viewport area for rendering and can virtualize. + /// + public VirtualisationType Virtualisation + { + get { return (VirtualisationType)GetValue(VirtualizationProperty); } + set { SetValue(VirtualizationProperty, value); } + } + + + + + #endregion + + + + protected override void OnPropertyChanged([CallerMemberName] string propertyName = "") + { + base.OnPropertyChanged(propertyName); + + if (propertyName == nameof(Orientation)) + { + Invalidate(); + } + } diff --git a/src/Engine/Draw/Layout/SnappingLayout.cs b/src/Engine/Draw/Layout/SnappingLayout.cs index 0fb5df32..d925c9bf 100644 --- a/src/Engine/Draw/Layout/SnappingLayout.cs +++ b/src/Engine/Draw/Layout/SnappingLayout.cs @@ -129,7 +129,7 @@ public virtual Vector2 SelectNextAnchor(Vector2 origin, Vector2 velocity) } /// - /// Part of the distance between snap points the velocity need to cover to trigger going to the next snap point + /// 0.2 - Part of the distance between snap points the velocity need to cover to trigger going to the next snap point. NOT a bindable property (yet). /// public double SnapDistanceRatio { get; set; } = 0.2; @@ -146,28 +146,20 @@ public virtual void ScrollToNearestAnchor(Vector2 location, Vector2 velocity) Vector2 projectionAnchor = SelectNextAnchor(origin, velocity); // Calculate the distance between the origin and the projectionAnchor - float distance = Vector2.Distance(origin, projectionAnchor); + //float distance = Vector2.Distance(origin, projectionAnchor); // Calculate the projected position along the direction of the velocity - Vector2 projectedPosition = origin + velocity; + //Vector2 projectedPosition = origin + velocity; // Calculate the distance to the anchor in the opposite direction - Vector2 oppositeAnchor = SelectNextAnchor(origin, -velocity); - float oppositeDistance = Vector2.Distance(origin, oppositeAnchor); + //Vector2 oppositeAnchor = SelectNextAnchor(origin, -velocity); + //float oppositeDistance = Vector2.Distance(origin, oppositeAnchor); - Vector2 targetAnchor; - if (Vector2.Distance(origin, projectedPosition) <= distance * SnapDistanceRatio && oppositeDistance < distance) - { - targetAnchor = oppositeAnchor; - } - else - { - targetAnchor = projectionAnchor; - } + //var targetAnchor = projectionAnchor; - if (Vector2.Distance(location, targetAnchor) >= 0.5) //todo move threshold to options + if (Vector2.Distance(location, projectionAnchor) >= 0.5) //todo move threshold to options { - ScrollToOffset(targetAnchor, velocity, CanAnimate); + ScrollToOffset(projectionAnchor, velocity, CanAnimate); } UpdateReportedPosition(); @@ -185,7 +177,7 @@ public virtual bool CanAnimate protected SpringWithVelocityVectorAnimator VectorAnimatorSpring; - protected RangeVectorAnimator _animatorRange; + protected RangeVectorAnimator AnimatorRange; private Vector2 _currentPosition; protected Vector2 CurrentSnap { get; set; } = new(-1, -1); @@ -251,8 +243,9 @@ protected virtual bool ScrollToOffset(Vector2 targetOffset, Vector2 velocity, bo { speed *= (Math.Abs(end.X - start.X) / Height); } - _animatorRange.Initialize(start, end, (float)speed, Easing.CubicInOut); - _animatorRange.Start(); + + AnimatorRange.Initialize(start, end, (float)speed, Easing.CubicInOut); + AnimatorRange.Start(); } } @@ -407,6 +400,18 @@ public bool Bounces } + public static readonly BindableProperty TransitionDirectionProperty = BindableProperty.Create( + nameof(TransitionDirection), + typeof(LinearDirectionType), + typeof(SnappingLayout), + LinearDirectionType.Forward, BindingMode.OneWayToSource); + + public LinearDirectionType TransitionDirection + { + get { return (LinearDirectionType)GetValue(TransitionDirectionProperty); } + set { SetValue(TransitionDirectionProperty, value); } + } + public static readonly BindableProperty InTransitionProperty = BindableProperty.Create( nameof(InTransition), typeof(bool), diff --git a/src/Engine/Draw/Scroll/ScrollToIndexOrder.cs b/src/Engine/Draw/Scroll/ScrollToIndexOrder.cs new file mode 100644 index 00000000..4aa5aebe --- /dev/null +++ b/src/Engine/Draw/Scroll/ScrollToIndexOrder.cs @@ -0,0 +1,111 @@ +using System.Numerics; + +namespace DrawnUi.Maui.Draw; + +public struct ScrollToIndexOrder +{ + public static ScrollToIndexOrder Default => new() + { + Index = -1 + }; + public bool IsSet + { + get + { + return Index >= 0; + } + } + public bool Animated { get; set; } + public float MaxTimeSecs { get; set; } + public RelativePositionType RelativePosition { get; set; } + public int Index { get; set; } +} + +public class VelocityAccumulator +{ + private List<(Vector2 velocity, DateTime time)> velocities = new List<(Vector2 velocity, DateTime time)>(); + private const double Threshold = 10.0; // Minimum significant movement + private const int MaxSampleSize = 5; // Number of samples for weighted average + private const int ConsiderationTimeframeMs = 150; // Timeframe in ms for velocity consideration + + public void Clear() + { + velocities.Clear(); + } + + public void CaptureVelocity(Vector2 velocity) + { + var now = DateTime.UtcNow; + if (velocities.Count == MaxSampleSize) velocities.RemoveAt(0); + velocities.Add((velocity, now)); + } + + public Vector2 CalculateFinalVelocity(float clampAbsolute = 0) + { + var now = DateTime.UtcNow; + var relevantVelocities = velocities.Where(v => (now - v.time).TotalMilliseconds <= ConsiderationTimeframeMs).ToList(); + if (!relevantVelocities.Any()) return Vector2.Zero; + + // Calculate weighted average for both X and Y components + float weightedSumX = relevantVelocities.Select((v, i) => v.velocity.X * (i + 1)).Sum(); + float weightedSumY = relevantVelocities.Select((v, i) => v.velocity.Y * (i + 1)).Sum(); + var weightSum = Enumerable.Range(1, relevantVelocities.Count).Sum(); + + if (clampAbsolute != 0) + { + return new Vector2(Math.Clamp(weightedSumX / weightSum, -clampAbsolute, clampAbsolute), + Math.Clamp(weightedSumY / weightSum, -clampAbsolute, clampAbsolute)); + } + + return new Vector2(weightedSumX / weightSum, weightedSumY / weightSum); + } +} + +public struct ScrollToPointOrder +{ + public bool IsValid + { + get + { + return !float.IsNaN(Location.X) && !float.IsNaN(Location.Y); ; + } + } + + public static ScrollToPointOrder NotValid => new() + { + Location = new SKPoint(float.NaN, float.NaN) + }; + + + public static ScrollToPointOrder ToPoint(SKPoint point, bool animated) + { + return new() + { + Location = point, + Animated = animated + }; + } + + public static ScrollToPointOrder ToCoords(float x, float y, bool animated) + { + return new() + { + Location = new SKPoint(x, y), + Animated = animated + }; + } + + public static ScrollToPointOrder ToCoords(float x, float y, float maxTimeSecs) + { + return new() + { + Location = new SKPoint(x, y), + MaxTimeSecs = maxTimeSecs + }; + } + + public bool Animated { get; set; } + public SKPoint Location { get; set; } + public float MaxTimeSecs { get; set; } + +} \ No newline at end of file diff --git a/src/Engine/Draw/Scroll/SkiaScroll.Scrolling.cs b/src/Engine/Draw/Scroll/SkiaScroll.Scrolling.cs index b72dcc4f..07c33f9b 100644 --- a/src/Engine/Draw/Scroll/SkiaScroll.Scrolling.cs +++ b/src/Engine/Draw/Scroll/SkiaScroll.Scrolling.cs @@ -5,446 +5,340 @@ namespace DrawnUi.Maui.Draw; -public class VelocityAccumulator -{ - private List<(Vector2 velocity, DateTime time)> velocities = new List<(Vector2 velocity, DateTime time)>(); - private const double Threshold = 10.0; // Minimum significant movement - private const int MaxSampleSize = 5; // Number of samples for weighted average - private const int ConsiderationTimeframeMs = 150; // Timeframe in ms for velocity consideration - - public void Clear() - { - velocities.Clear(); - } - - public void CaptureVelocity(Vector2 velocity) - { - var now = DateTime.UtcNow; - if (velocities.Count == MaxSampleSize) velocities.RemoveAt(0); - velocities.Add((velocity, now)); - } - - public Vector2 CalculateFinalVelocity(float clampAbsolute = 0) - { - var now = DateTime.UtcNow; - var relevantVelocities = velocities.Where(v => (now - v.time).TotalMilliseconds <= ConsiderationTimeframeMs).ToList(); - if (!relevantVelocities.Any()) return Vector2.Zero; - - // Calculate weighted average for both X and Y components - float weightedSumX = relevantVelocities.Select((v, i) => v.velocity.X * (i + 1)).Sum(); - float weightedSumY = relevantVelocities.Select((v, i) => v.velocity.Y * (i + 1)).Sum(); - var weightSum = Enumerable.Range(1, relevantVelocities.Count).Sum(); - - if (clampAbsolute != 0) - { - return new Vector2(Math.Clamp(weightedSumX / weightSum, -clampAbsolute, clampAbsolute), - Math.Clamp(weightedSumY / weightSum, -clampAbsolute, clampAbsolute)); - } - - return new Vector2(weightedSumX / weightSum, weightedSumY / weightSum); - } -} -public struct ScrollToIndexOrder -{ - public static ScrollToIndexOrder Default => new() - { - Index = -1 - }; - public bool IsSet - { - get - { - return Index >= 0; - } - } - public bool Animated { get; set; } - public float MaxTimeSecs { get; set; } - public RelativePositionType RelativePosition { get; set; } - public int Index { get; set; } -} -public struct ScrollToPointOrder + +public partial class SkiaScroll { - public bool IsValid - { - get - { - return !float.IsNaN(Location.X) && !float.IsNaN(Location.Y); ; - } - } - - public static ScrollToPointOrder NotValid => new() - { - Location = new SKPoint(float.NaN, float.NaN) - }; - - - public static ScrollToPointOrder ToPoint(SKPoint point, bool animated) - { - return new() - { - Location = point, - Animated = animated - }; - } - - public static ScrollToPointOrder ToCoords(float x, float y, bool animated) - { - return new() - { - Location = new SKPoint(x, y), - Animated = animated - }; - } - - public static ScrollToPointOrder ToCoords(float x, float y, float maxTimeSecs) - { - return new() - { - Location = new SKPoint(x, y), - MaxTimeSecs = maxTimeSecs - }; - } - - public bool Animated { get; set; } - public SKPoint Location { get; set; } - public float MaxTimeSecs { get; set; } -} + public float ViewportOffsetY + { + get + { + return _orderedOffsetY; + } + set + { + if (_orderedOffsetY != value) + { + _orderedOffsetY = value; + if (!NeedUpdate) + Update(); + OnPropertyChanged(); + } + } + } + + protected float _orderedOffsetY; + + + public float ViewportOffsetX + { + get + { + return _viewportOffsetX; + } + + set + { + if (_viewportOffsetX != value) + { + _viewportOffsetX = value; + if (!NeedUpdate) + Update(); + OnPropertyChanged(); + } + } + } + float _viewportOffsetX; + + protected virtual void InitializeViewport(float scale) + { + _loadMoreTriggeredAt = 0; + + ContentOffsetBounds = GetContentOffsetBounds(); + + HasContentToScroll = ptsContentHeight > Viewport.Units.Height || ptsContentWidth > Viewport.Units.Width; + + _scrollMinX = ContentOffsetBounds.Left; + if (_scrollMinX >= 0) + { + ViewportOffsetX = 0; + } + _scrollMaxX = 0; + + _scrollMinY = ContentOffsetBounds.Top; + if (_scrollMinY >= 0) + { + ViewportOffsetY = 0; + } + _scrollMaxY = 0; + + ViewportReady = true; + onceAfterInitializeViewport = true; + } + + bool onceAfterInitializeViewport; + + public bool ViewportReady { get; protected set; } + + protected virtual void InitializeScroller(float scale) + { + if (_vectorAnimatorBounceY == null) + { + _vectorAnimatorBounceY = new(this) + { + OnStart = () => + { + + }, + OnStop = () => + { + UpdateLoadingLock(false); + IsSnapping = false; + }, + OnUpdated = (value) => + { + ViewportOffsetY = (float)value; //not clamped + } + }; + + _vectorAnimatorBounceX = new(this) + { + OnStart = () => + { + + }, + OnStop = () => + { + UpdateLoadingLock(false); + IsSnapping = false; + }, + OnUpdated = (value) => + { + ViewportOffsetX = (float)value; //not clamped + } + }; + + _animatorFlingX = new(this) + { + OnStart = () => + { + //_isSnapping = false; + OnScrollerStarted(); + }, + OnStop = () => + { + //_isSnapping = false; + OnScrollerStopped(); + }, + OnUpdated = (value) => + { + var clamped = ClampOffset((float)value, 0); + ViewportOffsetX = clamped.X; + + OnScrollerUpdated(); + } + }; + + _animatorFlingY = new(this) + { + OnStart = () => + { + //_isSnapping = false; + OnScrollerStarted(); + }, + OnStop = () => + { + //_isSnapping = false; + OnScrollerStopped(); + }, + OnUpdated = (value) => + { + var clamped = ClampOffset(0, (float)value); + ViewportOffsetY = clamped.Y; + + OnScrollerUpdated(); + } + }; + + _scrollerX = new(this) + { + OnStop = () => + { + IsSnapping = false; + //SkiaImageLoadingManager.Instance.IsLoadingLocked = false; + } + }; + + _scrollerY = new(this) + { + OnStop = () => + { + IsSnapping = false; + //SkiaImageLoadingManager.Instance.IsLoadingLocked = false; + } + }; + } + + if (_vectorAnimatorBounceY.IsRunning) + { + _vectorAnimatorBounceY.Stop(); + } + if (_vectorAnimatorBounceX.IsRunning) + { + _vectorAnimatorBounceX.Stop(); + } + + SetDetectIndexChildPoint(TrackIndexPosition); + + this.UpdateVisibleIndex(); + + ExecuteDelayedScrollOrders(); + + if (CheckNeedToSnap()) + Snap(0); + } + + /// + /// Use Range scroller, offset in Units + /// + /// + /// + public void ScrollToX(float offset, bool animate) + { + + if (animate) + { + _scrollerX.Start( + (value) => + { + ViewportOffsetX = (float)value; + }, + InternalViewportOffset.Units.X, offset, (uint)ScrollingSpeedMs, ScrollingEasing); + } + else + { + ViewportOffsetX = offset; + IsSnapping = false; + } + } + + + + /// + /// Use Range scroller, offset in Units + /// + /// + /// + protected void ScrollToY(float offset, bool animate) + { + + if (animate) + { + _scrollerY.Start( + (value) => + { + ViewportOffsetY = (float)value; + }, + InternalViewportOffset.Units.Y, offset, (uint)ScrollingSpeedMs, ScrollingEasing); + } + else + { + ViewportOffsetY = offset; + IsSnapping = false; + } + } + + protected virtual void OnScrollerStarted() + { + UpdateLoadingLock(new Vector2( + _animatorFlingX.Parameters.InitialVelocity, + _animatorFlingY.Parameters.InitialVelocity) + ); + } + + protected virtual void OnScrollerUpdated() + { + UpdateLoadingLock(new Vector2( + _animatorFlingX.CurrentVelocity, + _animatorFlingY.CurrentVelocity)); + } + + + protected virtual void OnScrollerStopped() + { + //Super.Log("OnScrollerStopped.."); + + UpdateLoadingLock(false); + + if (CheckNeedToSnap()) + { + Snap(SystemAnimationTimeSecs); + } + else + { + //scroll ended prematurely by our intent because it would end past the bounds + if (Bounces) + { + + void BounceIfNeeded(ScrollFlingAnimator animator) + { + if (animator.SelfFinished) + { + var remainingVelocity = animator.Parameters.VelocityAt(animator.Speed); + + var velocity = remainingVelocity; + + if (Math.Abs(remainingVelocity) > MaxBounceVelocity) + { + velocity = Math.Sign(remainingVelocity) * MaxBounceVelocity; + } + + var swipeThreshold = ThesholdSwipeOnUp * RenderingScale; + if (Math.Abs(velocity) > swipeThreshold) + { + if (animator == _animatorFlingY) + { + BounceY((float)ViewportOffsetY, _axis.Y, velocity); + } + else + if (animator == _animatorFlingX) + { + BounceX((float)ViewportOffsetX, _axis.X, velocity); + } + } + + } + } + + if (_changeSpeed != null) + { + BounceIfNeeded(_animatorFlingY); + BounceIfNeeded(_animatorFlingX); + } + + } + } + } + + + + public virtual void ExecuteDelayedScrollOrders() + { + if (OrderedScrollToIndex.IsSet) + { + ExecuteScrollToIndexOrder(); + } + else + { + ExecuteScrollToOrder(); + } + } -public partial class SkiaScroll -{ - public float ViewportOffsetY - { - get - { - return _orderedOffsetY; - } - - set - { - if (_orderedOffsetY != value) - { - _orderedOffsetY = value; - if (!NeedUpdate) - Update(); - OnPropertyChanged(); - } - } - } - - protected float _orderedOffsetY; - - - public float ViewportOffsetX - { - get - { - return _viewportOffsetX; - } - - set - { - if (_viewportOffsetX != value) - { - _viewportOffsetX = value; - if (!NeedUpdate) - Update(); - OnPropertyChanged(); - } - } - } - float _viewportOffsetX; - - protected virtual void InitializeViewport(float scale) - { - _loadMoreTriggeredAt = 0; - - ContentOffsetBounds = GetContentOffsetBounds(); - - HasContentToScroll = ptsContentHeight > Viewport.Units.Height || ptsContentWidth > Viewport.Units.Width; - - _scrollMinX = ContentOffsetBounds.Left; - if (_scrollMinX >= 0) - { - ViewportOffsetX = 0; - } - _scrollMaxX = 0; - - _scrollMinY = ContentOffsetBounds.Top; - if (_scrollMinY >= 0) - { - ViewportOffsetY = 0; - } - _scrollMaxY = 0; - - ViewportReady = true; - onceAfterInitializeViewport = true; - } - - bool onceAfterInitializeViewport; - - public bool ViewportReady { get; protected set; } - - protected virtual void InitializeScroller(float scale) - { - if (_vectorAnimatorBounceY == null) - { - _vectorAnimatorBounceY = new(this) - { - OnStart = () => - { - - }, - OnStop = () => - { - UpdateLoadingLock(false); - IsSnapping = false; - }, - OnUpdated = (value) => - { - ViewportOffsetY = (float)value; //not clamped - } - }; - - _vectorAnimatorBounceX = new(this) - { - OnStart = () => - { - - }, - OnStop = () => - { - UpdateLoadingLock(false); - IsSnapping = false; - }, - OnUpdated = (value) => - { - ViewportOffsetX = (float)value; //not clamped - } - }; - - _animatorFlingX = new(this) - { - OnStart = () => - { - //_isSnapping = false; - OnScrollerStarted(); - }, - OnStop = () => - { - //_isSnapping = false; - OnScrollerStopped(); - }, - OnUpdated = (value) => - { - var clamped = ClampOffset((float)value, 0); - ViewportOffsetX = clamped.X; - - OnScrollerUpdated(); - } - }; - - _animatorFlingY = new(this) - { - OnStart = () => - { - //_isSnapping = false; - OnScrollerStarted(); - }, - OnStop = () => - { - //_isSnapping = false; - OnScrollerStopped(); - }, - OnUpdated = (value) => - { - var clamped = ClampOffset(0, (float)value); - ViewportOffsetY = clamped.Y; - - OnScrollerUpdated(); - } - }; - - _scrollerX = new(this) - { - OnStop = () => - { - IsSnapping = false; - //SkiaImageLoadingManager.Instance.IsLoadingLocked = false; - } - }; - - _scrollerY = new(this) - { - OnStop = () => - { - IsSnapping = false; - //SkiaImageLoadingManager.Instance.IsLoadingLocked = false; - } - }; - } - - if (_vectorAnimatorBounceY.IsRunning) - { - _vectorAnimatorBounceY.Stop(); - } - if (_vectorAnimatorBounceX.IsRunning) - { - _vectorAnimatorBounceX.Stop(); - } - - SetDetectIndexChildPoint(TrackIndexPosition); - - this.UpdateVisibleIndex(); - - ExecuteDelayedScrollOrders(); - - if (CheckNeedToSnap()) - Snap(0); - } - - /// - /// Use Range scroller, offset in Units - /// - /// - /// - public void ScrollToX(float offset, bool animate) - { - - if (animate) - { - _scrollerX.Start( - (value) => - { - ViewportOffsetX = (float)value; - }, - InternalViewportOffset.Units.X, offset, (uint)ScrollingSpeedMs, ScrollingEasing); - } - else - { - ViewportOffsetX = offset; - IsSnapping = false; - } - } - - - - /// - /// Use Range scroller, offset in Units - /// - /// - /// - protected void ScrollToY(float offset, bool animate) - { - - if (animate) - { - _scrollerY.Start( - (value) => - { - ViewportOffsetY = (float)value; - }, - InternalViewportOffset.Units.Y, offset, (uint)ScrollingSpeedMs, ScrollingEasing); - } - else - { - ViewportOffsetY = offset; - IsSnapping = false; - } - } - - protected virtual void OnScrollerStarted() - { - UpdateLoadingLock(new Vector2( - _animatorFlingX.Parameters.InitialVelocity, - _animatorFlingY.Parameters.InitialVelocity) - ); - } - - protected virtual void OnScrollerUpdated() - { - UpdateLoadingLock(new Vector2( - _animatorFlingX.CurrentVelocity, - _animatorFlingY.CurrentVelocity)); - } - - - protected virtual void OnScrollerStopped() - { - //Super.Log("OnScrollerStopped.."); - - UpdateLoadingLock(false); - - if (CheckNeedToSnap()) - { - Snap(SystemAnimationTimeSecs); - } - else - { - //scroll ended prematurely by our intent because it would end past the bounds - if (Bounces) - { - - void BounceIfNeeded(ScrollFlingAnimator animator) - { - if (animator.SelfFinished) - { - var remainingVelocity = animator.Parameters.VelocityAt(animator.Speed); - - var velocity = remainingVelocity; - - if (Math.Abs(remainingVelocity) > MaxBounceVelocity) - { - velocity = Math.Sign(remainingVelocity) * MaxBounceVelocity; - } - - var swipeThreshold = ThesholdSwipeOnUp * RenderingScale; - if (Math.Abs(velocity) > swipeThreshold) - { - if (animator == _animatorFlingY) - { - BounceY((float)ViewportOffsetY, _axis.Y, velocity); - } - else - if (animator == _animatorFlingX) - { - BounceX((float)ViewportOffsetX, _axis.X, velocity); - } - } - - } - } - - if (_changeSpeed != null) - { - BounceIfNeeded(_animatorFlingY); - BounceIfNeeded(_animatorFlingX); - } - - } - } - } - - - - public virtual void ExecuteDelayedScrollOrders() - { - if (OrderedScrollToIndex.IsSet) - { - ExecuteScrollToIndexOrder(); - } - else - { - ExecuteScrollToOrder(); - } - } - - - /* + /* basic concept: @@ -464,63 +358,63 @@ otherwise we apply simple clamp */ - //deceleration slow 0.999 - // deceleration normal 0.998 - // deceleration fast 0.99 - - - protected enum GesturesLogicState - { - None, - Began, - Changed, - Ended, - Canceled, - } - - void BounceX(float offsetFrom, float offsetTo, float velocity) - { - //Super.Log($"[SCROLL] {this.Tag} *BOUNCE* to {offsetTo.Y} v {velocity.Y}.."); - - var displacement = offsetFrom - offsetTo; - - //Debug.WriteLine($"[BOUNCE] {offsetFrom} - {offsetTo} with {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(); - } - else - { - IsSnapping = false; - } - } - - void BounceY(float offsetFrom, float offsetTo, float velocity) - { - //Super.Log($"[SCROLL] {this.Tag} *BOUNCE* to {offsetTo.Y} v {velocity.Y}.."); - - var displacement = offsetFrom - offsetTo; - - //Debug.WriteLine($"[BOUNCE] {offsetFrom} - {offsetTo} with {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(); - } - else - { - IsSnapping = false; - } - } - - /* + //deceleration slow 0.999 + // deceleration normal 0.998 + // deceleration fast 0.99 + + + protected enum GesturesLogicState + { + None, + Began, + Changed, + Ended, + Canceled, + } + + void BounceX(float offsetFrom, float offsetTo, float velocity) + { + //Super.Log($"[SCROLL] {this.Tag} *BOUNCE* to {offsetTo.Y} v {velocity.Y}.."); + + var displacement = offsetFrom - offsetTo; + + //Debug.WriteLine($"[BOUNCE] {offsetFrom} - {offsetTo} with {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(); + } + else + { + IsSnapping = false; + } + } + + void BounceY(float offsetFrom, float offsetTo, float velocity) + { + //Super.Log($"[SCROLL] {this.Tag} *BOUNCE* to {offsetTo.Y} v {velocity.Y}.."); + + var displacement = offsetFrom - offsetTo; + + //Debug.WriteLine($"[BOUNCE] {offsetFrom} - {offsetTo} with {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(); + } + else + { + IsSnapping = false; + } + } + + /* void Bounce(Vector2 offsetFrom, Vector2 offsetTo, Vector2 velocity) { //Super.Log($"[SCROLL] {this.Tag} *BOUNCE* to {offsetTo.Y} v {velocity.Y}.."); @@ -542,296 +436,296 @@ void Bounce(Vector2 offsetFrom, Vector2 offsetTo, Vector2 velocity) } */ - /// - /// This uses whole viewport size, do not use this for snapping - /// - /// - /// - /// - /// - public static SKPoint GetClosestSidePoint(SKPoint overscrollPoint, SKRect contentRect, SKSize viewportSize) - { - SKPoint closestPoint = new SKPoint(); - - // The overscrollPoint represents the negative of the content offset, so we need to reverse it for calculation - SKPoint contentOffset = new SKPoint(-overscrollPoint.X, -overscrollPoint.Y); - - var width = contentRect.Width - viewportSize.Width; - if (width < 0) - width = 0; - - if (contentOffset.X < 0) //scrolling to right - closestPoint.X = contentRect.Left; - else - if (contentOffset.X > 0) //scrolling to left - closestPoint.X = width; - else - closestPoint.X = contentOffset.X; - - var height = contentRect.Height - viewportSize.Height; - if (height < 0) - height = 0; - - if (contentOffset.Y < 0) //scrolling to bottom - closestPoint.Y = contentRect.Top; - else - if (contentOffset.Y > 0) //scrolling to top - closestPoint.Y = height; - else - closestPoint.Y = contentOffset.Y; - - // Reverse the offset back to the overscroll representation for the result - closestPoint.X = -closestPoint.X; - closestPoint.Y = -closestPoint.Y; - - return closestPoint; - } - - - public static SKPoint ClosestPoint(SKRect rect, SKPoint point) - { - SKPoint result = point; - - if (!rect.ContainsInclusive(point)) - { - if (point.X < rect.Left) - result.X = rect.Left; - else if (point.X > rect.Right) - result.X = rect.Right; - - if (point.Y < rect.Top) - result.Y = rect.Top; - else if (point.Y > rect.Bottom) - result.Y = rect.Bottom; - } - - return result; - } - - protected virtual bool OffsetOk(Vector2 offset) - { - if (offset.Y >= ContentOffsetBounds.Top && offset.Y <= ContentOffsetBounds.Bottom - && offset.X >= ContentOffsetBounds.Left && offset.X <= ContentOffsetBounds.Right) - return true; - - return false; - } - - - public bool OverScrolled - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return OverscrollDistance != Vector2.Zero; - } - } - - - protected float ptsContentWidth; - protected float ptsContentHeight; - - /// - /// There are the bounds the scroll offset can go to.. This is NOT the bounds for the whole content. - /// - public SKRect GetContentOffsetBounds() - { - ptsContentWidth = ContentSize.Units.Width; - ptsContentHeight = ContentSize.Units.Height; - - if (Orientation == ScrollOrientation.Vertical) - { - ptsContentHeight += HeaderSize.Units.Height + FooterSize.Units.Height + (float)ContentOffset; - } - - if (Orientation == ScrollOrientation.Horizontal) - { - ptsContentWidth += HeaderSize.Units.Width + FooterSize.Units.Width + (float)ContentOffset; - } - - var width = ptsContentWidth - Viewport.Units.Width; - var height = ptsContentHeight - Viewport.Units.Height; - - if (height < 0) - height = 0; - - if (width < 0) - width = 0; - - var rect = new SKRect(-width, -height, 0, 0); - - return rect; - } - - public Vector2 CalculateOverscrollDistance(float x, float y) - { - float overscrollX = 0f; - float overscrollY = 0f; - - if (x > _scrollMaxX) - { - overscrollX = x - _scrollMaxX; - } - else if (x < _scrollMinX) - { - overscrollX = -(_scrollMinX - x); - } - - if (y > _scrollMaxY) - { - overscrollY = y - _scrollMaxY; - } - else if (y < _scrollMinY) - { - overscrollY = -(_scrollMinY - y); - } - - //Debug.WriteLine($"[OVERSCROLL] {overscrollY}"); - - return new Vector2(overscrollX, overscrollY); - } - - protected double _minVelocity = 1.5; - - private float _DecelerationRatio = 0.002f; - public float DecelerationRatio - { - get - { - return _DecelerationRatio; - } - set - { - if (_DecelerationRatio != value) - { - _DecelerationRatio = value; - OnPropertyChanged(); - } - } - } - - public void UpdateFriction() - { - var friction = FrictionScrolled; - if (friction < 0.1) - { - //silent clamp - friction = 0.1f; - } - - DecelerationRatio = (float)friction / 100f; // 0.2 => 0.002 - } - - public virtual bool StartToFlingFrom(ScrollFlingAnimator animator, float from, float velocity) - { - var contentOffset = from; - - animator.Initialize(contentOffset, velocity, 1f - DecelerationRatio); - - if (PrepareToFlingAfterInitialized(animator)) - { - animator.RunAsync(null).ConfigureAwait(false); - return true; - } - - return false; - } - - protected virtual async Task FlingFrom(ScrollFlingAnimator animator, float from, float velocity) - { - //todo - add cancellation support - - // Trace.WriteLine($"[FLING] velocity {velocity}"); - - var contentOffset = from;// new float((float)ViewportOffsetX, (float)ViewportOffsetY); - - animator.Initialize(contentOffset, velocity, 1f - DecelerationRatio); - - return await FlingAfterInitialized(animator); - } - - protected virtual async Task FlingToAuto(ScrollFlingAnimator animator, float from, float to, float changeSpeedSecs = 0) - { - var velocity = animator.Parameters.VelocityToZero(from, to, changeSpeedSecs); - - animator.Initialize(from, velocity, 1f - DecelerationRatio); - - if (changeSpeedSecs > 0) - animator.Speed = changeSpeedSecs; - - return await FlingAfterInitialized(animator); - } - - protected virtual async Task FlingTo(ScrollFlingAnimator animator, float from, float to, float timeSeconds) - { - var velocity = animator.Parameters.VelocityTo(from, to, timeSeconds); - - animator.Initialize(from, velocity, 1f - DecelerationRatio); - - animator.Speed = timeSeconds; - - return await FlingAfterInitialized(animator); - } - - protected virtual bool PrepareToFlingAfterInitialized(ScrollFlingAnimator animator) - { - var destination = animator.Parameters.Destination; - bool offsetOk = true; - - var destinationPoint = SKPoint.Empty; - if (animator == _animatorFlingX) - { - destinationPoint = new SKPoint(destination, 0); - offsetOk = OffsetOk(new(destination, 0)); - } - else - if (animator == _animatorFlingY) - { - destinationPoint = new SKPoint(0, destination); - offsetOk = OffsetOk(new(0, destination)); - } - - _changeSpeed = null; - - if (!offsetOk) //detected that scroll will end past the bounds - { - var contentRect = new SKRect(0, 0, ptsContentWidth, ptsContentHeight); - var closestPoint = GetClosestSidePoint(destinationPoint, contentRect, Viewport.Units.Size); - - if (animator == _animatorFlingX) - { - _axis = _axis with { X = closestPoint.X }; - _changeSpeed = animator.Parameters.DurationToValue(closestPoint.X); - animator.Speed = _changeSpeed.Value; - } - else - if (animator == _animatorFlingY) - { - _axis = _axis with { Y = closestPoint.Y }; - _changeSpeed = animator.Parameters.DurationToValue(closestPoint.Y); - animator.Speed = _changeSpeed.Value; - } - } + /// + /// This uses whole viewport size, do not use this for snapping + /// + /// + /// + /// + /// + public static SKPoint GetClosestSidePoint(SKPoint overscrollPoint, SKRect contentRect, SKSize viewportSize) + { + SKPoint closestPoint = new SKPoint(); + + // The overscrollPoint represents the negative of the content offset, so we need to reverse it for calculation + SKPoint contentOffset = new SKPoint(-overscrollPoint.X, -overscrollPoint.Y); + + var width = contentRect.Width - viewportSize.Width; + if (width < 0) + width = 0; + + if (contentOffset.X < 0) //scrolling to right + closestPoint.X = contentRect.Left; + else + if (contentOffset.X > 0) //scrolling to left + closestPoint.X = width; + else + closestPoint.X = contentOffset.X; + + var height = contentRect.Height - viewportSize.Height; + if (height < 0) + height = 0; + + if (contentOffset.Y < 0) //scrolling to bottom + closestPoint.Y = contentRect.Top; + else + if (contentOffset.Y > 0) //scrolling to top + closestPoint.Y = height; + else + closestPoint.Y = contentOffset.Y; + + // Reverse the offset back to the overscroll representation for the result + closestPoint.X = -closestPoint.X; + closestPoint.Y = -closestPoint.Y; + + return closestPoint; + } + + + public static SKPoint ClosestPoint(SKRect rect, SKPoint point) + { + SKPoint result = point; + + if (!rect.ContainsInclusive(point)) + { + if (point.X < rect.Left) + result.X = rect.Left; + else if (point.X > rect.Right) + result.X = rect.Right; + + if (point.Y < rect.Top) + result.Y = rect.Top; + else if (point.Y > rect.Bottom) + result.Y = rect.Bottom; + } + + return result; + } + + protected virtual bool OffsetOk(Vector2 offset) + { + if (offset.Y >= ContentOffsetBounds.Top && offset.Y <= ContentOffsetBounds.Bottom + && offset.X >= ContentOffsetBounds.Left && offset.X <= ContentOffsetBounds.Right) + return true; + + return false; + } + + + public bool OverScrolled + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return OverscrollDistance != Vector2.Zero; + } + } + + + protected float ptsContentWidth; + protected float ptsContentHeight; + + /// + /// There are the bounds the scroll offset can go to.. This is NOT the bounds for the whole content. + /// + public SKRect GetContentOffsetBounds() + { + ptsContentWidth = ContentSize.Units.Width; + ptsContentHeight = ContentSize.Units.Height; + + if (Orientation == ScrollOrientation.Vertical) + { + ptsContentHeight += HeaderSize.Units.Height + FooterSize.Units.Height + (float)ContentOffset; + } + + if (Orientation == ScrollOrientation.Horizontal) + { + ptsContentWidth += HeaderSize.Units.Width + FooterSize.Units.Width + (float)ContentOffset; + } + + var width = ptsContentWidth - Viewport.Units.Width; + var height = ptsContentHeight - Viewport.Units.Height; + + if (height < 0) + height = 0; + + if (width < 0) + width = 0; + + var rect = new SKRect(-width, -height, 0, 0); + + return rect; + } + + public Vector2 CalculateOverscrollDistance(float x, float y) + { + float overscrollX = 0f; + float overscrollY = 0f; + + if (x > _scrollMaxX) + { + overscrollX = x - _scrollMaxX; + } + else if (x < _scrollMinX) + { + overscrollX = -(_scrollMinX - x); + } + + if (y > _scrollMaxY) + { + overscrollY = y - _scrollMaxY; + } + else if (y < _scrollMinY) + { + overscrollY = -(_scrollMinY - y); + } + + //Debug.WriteLine($"[OVERSCROLL] {overscrollY}"); + + return new Vector2(overscrollX, overscrollY); + } + + protected double _minVelocity = 1.5; + + private float _DecelerationRatio = 0.002f; + public float DecelerationRatio + { + get + { + return _DecelerationRatio; + } + set + { + if (_DecelerationRatio != value) + { + _DecelerationRatio = value; + OnPropertyChanged(); + } + } + } + + public void UpdateFriction() + { + var friction = FrictionScrolled; + if (friction < 0.1) + { + //silent clamp + friction = 0.1f; + } + + DecelerationRatio = (float)friction / 100f; // 0.2 => 0.002 + } + + public virtual bool StartToFlingFrom(ScrollFlingAnimator animator, float from, float velocity) + { + var contentOffset = from; + + animator.Initialize(contentOffset, velocity, 1f - DecelerationRatio); + + if (PrepareToFlingAfterInitialized(animator)) + { + animator.RunAsync(null).ConfigureAwait(false); + return true; + } - return animator.Speed > 0; - } + return false; + } + + protected virtual async Task FlingFrom(ScrollFlingAnimator animator, float from, float velocity) + { + //todo - add cancellation support + + // Trace.WriteLine($"[FLING] velocity {velocity}"); + + var contentOffset = from;// new float((float)ViewportOffsetX, (float)ViewportOffsetY); + + animator.Initialize(contentOffset, velocity, 1f - DecelerationRatio); - protected async Task FlingAfterInitialized(ScrollFlingAnimator animator) - { + return await FlingAfterInitialized(animator); + } + + protected virtual async Task FlingToAuto(ScrollFlingAnimator animator, float from, float to, float changeSpeedSecs = 0) + { + var velocity = animator.Parameters.VelocityToZero(from, to, changeSpeedSecs); - if (PrepareToFlingAfterInitialized(animator)) - { - await animator.RunAsync(null); + animator.Initialize(from, velocity, 1f - DecelerationRatio); - IsSnapping = false; + if (changeSpeedSecs > 0) + animator.Speed = changeSpeedSecs; + + return await FlingAfterInitialized(animator); + } + + protected virtual async Task FlingTo(ScrollFlingAnimator animator, float from, float to, float timeSeconds) + { + var velocity = animator.Parameters.VelocityTo(from, to, timeSeconds); + + animator.Initialize(from, velocity, 1f - DecelerationRatio); + + animator.Speed = timeSeconds; + + return await FlingAfterInitialized(animator); + } + + protected virtual bool PrepareToFlingAfterInitialized(ScrollFlingAnimator animator) + { + var destination = animator.Parameters.Destination; + bool offsetOk = true; + + var destinationPoint = SKPoint.Empty; + if (animator == _animatorFlingX) + { + destinationPoint = new SKPoint(destination, 0); + offsetOk = OffsetOk(new(destination, 0)); + } + else + if (animator == _animatorFlingY) + { + destinationPoint = new SKPoint(0, destination); + offsetOk = OffsetOk(new(0, destination)); + } + + _changeSpeed = null; + + if (!offsetOk) //detected that scroll will end past the bounds + { + var contentRect = new SKRect(0, 0, ptsContentWidth, ptsContentHeight); + var closestPoint = GetClosestSidePoint(destinationPoint, contentRect, Viewport.Units.Size); + + if (animator == _animatorFlingX) + { + _axis = _axis with { X = closestPoint.X }; + _changeSpeed = animator.Parameters.DurationToValue(closestPoint.X); + animator.Speed = _changeSpeed.Value; + } + else + if (animator == _animatorFlingY) + { + _axis = _axis with { Y = closestPoint.Y }; + _changeSpeed = animator.Parameters.DurationToValue(closestPoint.Y); + animator.Speed = _changeSpeed.Value; + } + } + + return animator.Speed > 0; + } + + protected async Task FlingAfterInitialized(ScrollFlingAnimator animator) + { + + if (PrepareToFlingAfterInitialized(animator)) + { + await animator.RunAsync(null); + + IsSnapping = false; - return true; - } + return true; + } - return false; - } + return false; + } - /* + /* public virtual bool StartToFlingFrom(Vector2 from, Vector2 velocity) { @@ -923,194 +817,194 @@ protected async Task FlingAfterInitialized() */ - /// - /// We might order a scroll before the control was drawn, so it's a kind of startup position - /// saved every time one calls ScrollTo - /// - protected ScrollToPointOrder OrderedScrollTo = ScrollToPointOrder.NotValid; - - /// - /// We might order a scroll before the control was drawn, so it's a kind of startup position - /// saved every time one calls ScrollToIndex - /// - protected ScrollToIndexOrder OrderedScrollToIndex; - - /// - /// In Units - /// - /// - /// - protected void ScrollToOffset(Vector2 targetOffset, float maxTimeSecs) - { - if (maxTimeSecs > 0 && Height > 0) - { - //_animatorFling.Stop(); - //var from = new Vector2((float)ViewportOffsetX, (float)ViewportOffsetY); - //FlingToAuto(from, targetOffset, maxTimeSecs).ConfigureAwait(false); - - StopScrolling(); - ScrollToX(targetOffset.X, true); - ScrollToY(targetOffset.Y, true); - } - else - { - ViewportOffsetX = targetOffset.X; - ViewportOffsetY = targetOffset.Y; - IsSnapping = false; - - this.UpdateVisibleIndex(); - } - } - - - public virtual void MoveToY(float value) - { - if (!ScrollLocked) - { - ViewportOffsetY = value; - } - } - - public virtual void MoveToX(float value) - { - if (!ScrollLocked) - { - ViewportOffsetX = value; - - } - } - - public void ScrollToIndex(int index, bool animate, RelativePositionType option = RelativePositionType.Start) - { - //saving to use upon creating control if this was called before its internal structure was really created - OrderedScrollToIndex = new() - { - Animated = animate, - RelativePosition = option, - Index = index - }; - - ExecuteScrollToIndexOrder(); - } - - public bool ExecuteScrollToOrder() - { - if (OrderedScrollTo.IsValid) - { - ScrollToOffset(new Vector2(OrderedScrollTo.Location.X, - OrderedScrollTo.Location.Y), - OrderedScrollTo.MaxTimeSecs); - OrderedScrollTo = ScrollToPointOrder.NotValid; - return true; - } - - return false; - } - - public bool ExecuteScrollToIndexOrder() - { - if (OrderedScrollToIndex.IsSet) - { - //saving to use upon creating control if this was called before its internal structure was really created - var offset = CalculateScrollOffsetForIndex(OrderedScrollToIndex.Index, - OrderedScrollToIndex.RelativePosition); - - if (PointIsValid(offset)) - { - var time = 0f; - if (OrderedScrollToIndex.Animated) - time = SystemAnimationTimeSecs; - - ScrollTo(offset.X, offset.Y, time); - OrderedScrollToIndex = ScrollToIndexOrder.Default; - return true; - } - } - return false; - } - - public void ScrollTo(float x, float y, float maxSpeedSecs) - { - StopScrolling(); - - OrderedScrollTo = ScrollToPointOrder.ToCoords(x, y, maxSpeedSecs); - - if (!ExecuteScrollToOrder()) - { - this.UpdateVisibleIndex(); - } - } - - public void ScrollToTop(float maxTimeSecs) - { - if (Orientation == ScrollOrientation.Vertical) - { - ScrollTo(InternalViewportOffset.Units.X, 0, maxTimeSecs); - } - else - if (Orientation == ScrollOrientation.Horizontal) - { - ScrollTo(0, InternalViewportOffset.Units.Y, maxTimeSecs); - } - else - { - ScrollTo(0, 0, maxTimeSecs); - } - } - - public void ScrollToBottom(float maxTimeSecs) - { - if (Orientation == ScrollOrientation.Vertical) - { - ScrollTo(InternalViewportOffset.Units.X, _scrollMinY, maxTimeSecs); - } - else - if (Orientation == ScrollOrientation.Horizontal) - { - ScrollTo(_scrollMinX, InternalViewportOffset.Units.Y, maxTimeSecs); - } - else - { - ScrollTo(_scrollMinX, _scrollMinY, maxTimeSecs); - } - } - - private bool _Snapped; - public bool Snapped - { - get - { - return _Snapped; - } - set - { - if (_Snapped != value) - { - _Snapped = value; - OnPropertyChanged(); - } - } - } - - - private bool _IsSnapping; - public bool IsSnapping - { - get - { - return _IsSnapping; - } - set - { - if (_IsSnapping != value) - { - _IsSnapping = value; - OnPropertyChanged(); - } - } - } - - Vector2 _axis; - double? _changeSpeed = null; - private Vector2 _lastVelocity; + /// + /// We might order a scroll before the control was drawn, so it's a kind of startup position + /// saved every time one calls ScrollTo + /// + protected ScrollToPointOrder OrderedScrollTo = ScrollToPointOrder.NotValid; + + /// + /// We might order a scroll before the control was drawn, so it's a kind of startup position + /// saved every time one calls ScrollToIndex + /// + protected ScrollToIndexOrder OrderedScrollToIndex; + + /// + /// In Units + /// + /// + /// + protected void ScrollToOffset(Vector2 targetOffset, float maxTimeSecs) + { + if (maxTimeSecs > 0 && Height > 0) + { + //_animatorFling.Stop(); + //var from = new Vector2((float)ViewportOffsetX, (float)ViewportOffsetY); + //FlingToAuto(from, targetOffset, maxTimeSecs).ConfigureAwait(false); + + StopScrolling(); + ScrollToX(targetOffset.X, true); + ScrollToY(targetOffset.Y, true); + } + else + { + ViewportOffsetX = targetOffset.X; + ViewportOffsetY = targetOffset.Y; + IsSnapping = false; + + this.UpdateVisibleIndex(); + } + } + + + public virtual void MoveToY(float value) + { + if (!ScrollLocked) + { + ViewportOffsetY = value; + } + } + + public virtual void MoveToX(float value) + { + if (!ScrollLocked) + { + ViewportOffsetX = value; + + } + } + + public void ScrollToIndex(int index, bool animate, RelativePositionType option = RelativePositionType.Start) + { + //saving to use upon creating control if this was called before its internal structure was really created + OrderedScrollToIndex = new() + { + Animated = animate, + RelativePosition = option, + Index = index + }; + + ExecuteScrollToIndexOrder(); + } + + public bool ExecuteScrollToOrder() + { + if (OrderedScrollTo.IsValid) + { + ScrollToOffset(new Vector2(OrderedScrollTo.Location.X, + OrderedScrollTo.Location.Y), + OrderedScrollTo.MaxTimeSecs); + OrderedScrollTo = ScrollToPointOrder.NotValid; + return true; + } + + return false; + } + + public bool ExecuteScrollToIndexOrder() + { + if (OrderedScrollToIndex.IsSet) + { + //saving to use upon creating control if this was called before its internal structure was really created + var offset = CalculateScrollOffsetForIndex(OrderedScrollToIndex.Index, + OrderedScrollToIndex.RelativePosition); + + if (PointIsValid(offset)) + { + var time = 0f; + if (OrderedScrollToIndex.Animated) + time = SystemAnimationTimeSecs; + + ScrollTo(offset.X, offset.Y, time); + OrderedScrollToIndex = ScrollToIndexOrder.Default; + return true; + } + } + return false; + } + + public void ScrollTo(float x, float y, float maxSpeedSecs) + { + StopScrolling(); + + OrderedScrollTo = ScrollToPointOrder.ToCoords(x, y, maxSpeedSecs); + + if (!ExecuteScrollToOrder()) + { + this.UpdateVisibleIndex(); + } + } + + public void ScrollToTop(float maxTimeSecs) + { + if (Orientation == ScrollOrientation.Vertical) + { + ScrollTo(InternalViewportOffset.Units.X, 0, maxTimeSecs); + } + else + if (Orientation == ScrollOrientation.Horizontal) + { + ScrollTo(0, InternalViewportOffset.Units.Y, maxTimeSecs); + } + else + { + ScrollTo(0, 0, maxTimeSecs); + } + } + + public void ScrollToBottom(float maxTimeSecs) + { + if (Orientation == ScrollOrientation.Vertical) + { + ScrollTo(InternalViewportOffset.Units.X, _scrollMinY, maxTimeSecs); + } + else + if (Orientation == ScrollOrientation.Horizontal) + { + ScrollTo(_scrollMinX, InternalViewportOffset.Units.Y, maxTimeSecs); + } + else + { + ScrollTo(_scrollMinX, _scrollMinY, maxTimeSecs); + } + } + + private bool _Snapped; + public bool Snapped + { + get + { + return _Snapped; + } + set + { + if (_Snapped != value) + { + _Snapped = value; + OnPropertyChanged(); + } + } + } + + + private bool _IsSnapping; + public bool IsSnapping + { + get + { + return _IsSnapping; + } + set + { + if (_IsSnapping != value) + { + _IsSnapping = value; + OnPropertyChanged(); + } + } + } + + Vector2 _axis; + double? _changeSpeed = null; + private Vector2 _lastVelocity; } diff --git a/src/Engine/Draw/Scroll/SkiaScroll.cs b/src/Engine/Draw/Scroll/SkiaScroll.cs index ff9a5ae1..e3ea1d4d 100644 --- a/src/Engine/Draw/Scroll/SkiaScroll.cs +++ b/src/Engine/Draw/Scroll/SkiaScroll.cs @@ -6,3019 +6,3076 @@ namespace DrawnUi.Maui.Draw { - [ContentProperty("Content")] - public partial class SkiaScroll : SkiaControl, ISkiaGestureListener, IDefinesViewport - { - /// - /// Min velocity in points/sec to flee/swipe when finger is up - /// - public static float ThesholdSwipeOnUp = 40f; - - /// - /// To filter micro-gestures while manually panning - /// - public static float ScrollVelocityThreshold = 20; - - /// - /// Time for the snapping animations as well as the scroll to top etc animations.. - /// - public static float SystemAnimationTimeSecs = 0.2f; - - /// - /// TODO impement this - /// - public enum ScrollingInteractionState - { - None, - Dragging, - Scrolling, - Zooming - } - - public override void OnWillDisposeWithChildren() - { - base.OnWillDisposeWithChildren(); - - Content?.Dispose(); - Header?.Dispose(); - Footer?.Dispose(); - } - - private ScrollingInteractionState _intercationState; - public ScrollingInteractionState InteractionState - { - get - { - return _intercationState; - } - set - { - if (_intercationState != value) - { - _intercationState = value; - OnPropertyChanged(); - } - } - } - - - public virtual void UpdateVisibleIndex() - { - if (LayoutReady && TrackIndexPosition != RelativePositionType.None) - { - CurrentIndexHit = CalculateVisibleIndex(TrackIndexPosition); - CurrentIndex = CurrentIndexHit.Index; - } - } - - #region Scrollers - - - public bool HasContentToScroll - { - get - { - return _hasContentToScroll; - } - - set - { - if (_hasContentToScroll != value) - { - _hasContentToScroll = value; - OnPropertyChanged(); - } - } - } - bool _hasContentToScroll; - - - public static readonly BindableProperty HeaderStickyProperty = BindableProperty.Create( - nameof(HeaderSticky), - typeof(bool), - typeof(SkiaScroll), - false, propertyChanged: NeedInvalidateMeasure); - - /// - /// Should the header stay in place when content is scrolling - /// - public bool HeaderSticky - { - get { return (bool)GetValue(HeaderStickyProperty); } - set { SetValue(HeaderStickyProperty, value); } - } - - - public static readonly BindableProperty ParallaxOverscrollEnabledProperty = BindableProperty.Create( - nameof(ParallaxOverscrollEnabled), - typeof(bool), - typeof(SkiaScroll), - true, propertyChanged: NeedInvalidateMeasure); - - public bool ParallaxOverscrollEnabled - { - get { return (bool)GetValue(ParallaxOverscrollEnabledProperty); } - set { SetValue(ParallaxOverscrollEnabledProperty, value); } - } - - public static readonly BindableProperty HeaderBehindProperty = BindableProperty.Create( - nameof(HeaderBehind), - typeof(bool), - typeof(SkiaScroll), - false, propertyChanged: NeedInvalidateMeasure); - - public bool HeaderBehind - { - get { return (bool)GetValue(HeaderBehindProperty); } - set { SetValue(HeaderBehindProperty, value); } - } - - public static readonly BindableProperty ContentOffsetProperty = BindableProperty.Create( - nameof(ContentOffset), - typeof(double), - typeof(SkiaScroll), - 0.0, propertyChanged: NeedDraw); - - public double ContentOffset - { - get { return (double)GetValue(ContentOffsetProperty); } - set { SetValue(ContentOffsetProperty, value); } - } - - public static readonly BindableProperty HeaderProperty = BindableProperty.Create( - nameof(Header), - typeof(SkiaControl), - typeof(SkiaScroll), - null, propertyChanged: (b, o, n) => - { - if (b is SkiaScroll control) - { - control.SetHeader((SkiaControl)n); - } - }); - - public SkiaControl Header - { - get { return (SkiaControl)GetValue(HeaderProperty); } - set { SetValue(HeaderProperty, value); } - } - - public static readonly BindableProperty HeaderParallaxRatioProperty = BindableProperty.Create( - nameof(HeaderParallaxRatio), - typeof(double), - typeof(SkiaScroll), - 1.0, propertyChanged: NeedDraw); - - public double HeaderParallaxRatio - { - get { return (double)GetValue(HeaderParallaxRatioProperty); } - set { SetValue(HeaderParallaxRatioProperty, value); } - } - - - public static readonly BindableProperty FooterProperty = BindableProperty.Create( - nameof(Footer), - typeof(SkiaControl), - typeof(SkiaScroll), - null, propertyChanged: (b, o, n) => - { - if (b is SkiaScroll control) - { - control.SetFooter((SkiaControl)n); - } - }); - - public SkiaControl Footer - { - get { return (SkiaControl)GetValue(FooterProperty); } - set { SetValue(FooterProperty, value); } - } - - - public static readonly BindableProperty RefreshIndicatorProperty = BindableProperty.Create(nameof(RefreshIndicator), - typeof(IRefreshIndicator), - typeof(SkiaScroll), - null, - propertyChanged: OnNeedSetRefreshIndicator); - - public IRefreshIndicator RefreshIndicator - { - get { return (IRefreshIndicator)GetValue(RefreshIndicatorProperty); } - set { SetValue(RefreshIndicatorProperty, value); } - } - - private static void OnNeedSetRefreshIndicator(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is SkiaScroll control) - { - control.SetRefreshIndicator(newvalue as IRefreshIndicator); - } - } - - protected IRefreshIndicator InternalRefreshIndicator { get; set; } - - private void SetRefreshIndicator(IRefreshIndicator indicator) - { - //delete existing from Views - //and dispose - if (InternalRefreshIndicator is SkiaControl control) - { - control.SetParent(null); - control.Dispose(); - } - - //set props for the new one and and it to views - if (indicator is SkiaControl newControl) - { - InternalRefreshIndicator = indicator; - - if (Orientation == ScrollOrientation.Vertical) - { - newControl.HeightRequest = RefreshDistanceLimit; - } - else - if (Orientation == ScrollOrientation.Horizontal) - { - newControl.WidthRequest = RefreshDistanceLimit; - } - newControl.ZIndex = 1000; - AddSubView(newControl); - } - } - - private static void NeedToScroll(BindableObject bindable, object oldvalue, object newvalue) - { - if ((int)newvalue >= 0 && bindable is SkiaScroll scroll) - { - scroll.ScrollToIndex(index: (int)newvalue, - scroll.OrderedScrollIsAnimated, - scroll.TrackIndexPosition); - scroll.OrderedScroll = -1; - } - } - - public static readonly BindableProperty OrderedScrollProperty = BindableProperty.Create(nameof(OrderedScroll), - typeof(int), - typeof(SkiaScroll), -1, BindingMode.TwoWay, propertyChanged: NeedToScroll); - - public int OrderedScroll - { - get { return (int)GetValue(OrderedScrollProperty); } - set { SetValue(OrderedScrollProperty, value); } - } - - public static readonly BindableProperty OrderedScrollIsAnimatedProperty = BindableProperty.Create(nameof(OrderedScrollIsAnimated), - typeof(bool), - typeof(SkiaScroll), false); - - public bool OrderedScrollIsAnimated - { - get { return (bool)GetValue(OrderedScrollIsAnimatedProperty); } - set { SetValue(OrderedScrollIsAnimatedProperty, value); } - } - - public static readonly BindableProperty RefreshEnabledProperty = BindableProperty.Create(nameof(RefreshEnabled), - typeof(bool), - typeof(SkiaScroll), - false); - public bool RefreshEnabled - { - get { return (bool)GetValue(RefreshEnabledProperty); } - set { SetValue(RefreshEnabledProperty, value); } - } - - public static readonly BindableProperty IsRefreshingProperty = BindableProperty.Create(nameof(IsRefreshing), - typeof(bool), - typeof(SkiaScroll), - false, - propertyChanged: (bindable, old, changed) => - { - if (bindable is SkiaScroll scroll) - { - scroll.SetIsRefreshing((bool)changed); - } - }); - public bool IsRefreshing - { - get { return (bool)GetValue(IsRefreshingProperty); } - set { SetValue(IsRefreshingProperty, value); } - } - - - public static readonly BindableProperty RefreshCommandProperty = BindableProperty.Create(nameof(RefreshCommand), - typeof(ICommand), - typeof(SkiaScroll), - null); - public ICommand RefreshCommand - { - get { return (ICommand)GetValue(RefreshCommandProperty); } - set { SetValue(RefreshCommandProperty, value); } - } - - - public static readonly BindableProperty RefreshDistanceLimitProperty = BindableProperty.Create(nameof(RefreshDistanceLimit), - typeof(float), - typeof(SkiaScroll), - 150f); - /// - /// Applyed to RefreshView - /// - public float RefreshDistanceLimit - { - get { return (float)GetValue(RefreshDistanceLimitProperty); } - set { SetValue(RefreshDistanceLimitProperty, value); } - } - - public static readonly BindableProperty RefreshShowDistanceProperty = BindableProperty.Create( - nameof(RefreshShowDistance), - typeof(float), - typeof(SkiaScroll), - 50f); - - /// - /// Applyed to RefreshView - /// - public float RefreshShowDistance - { - get { return (float)GetValue(RefreshShowDistanceProperty); } - set { SetValue(RefreshShowDistanceProperty, value); } - } - - - public Easing ScrollingEasing = Easing.SpringOut; - - private readonly TimeSpan debounceTime = TimeSpan.FromMilliseconds(10); - private float filterFactor = 0.99f; // (0 to 1) - - protected float _velocitySwipe = 200; //pts - protected float _velocitySwipeRatio = 1.0f; - - protected Vector2 _panningLastDelta; - protected Vector2 _panningCurrentOffsetPts; - private Vector2 _panningStartOffsetPts; - - public async void PlayEdgeGlowAnimation(Color color, double x, double y, bool removePrevious = true) - { - if (removePrevious) - { - UnregisterAllAnimatorsByType(typeof(EdgeGlowAnimator)); - } - var animation = new EdgeGlowAnimator(this) - { - GlowPosition = GlowPosition.Top, - Color = color.ToSKColor(), - X = x, - Y = y, - }; - animation.Start(); - } - - /// - /// Units - /// - public Vector2 OverscrollDistance - { - get - { - return _overscrollDistance; - } - - set - { - if (_overscrollDistance != value) - { - //if (_rubberBandDistanceY == 0 && value != 0) - //{ - // //show effect - // PlayEdgeGlowAnimation(Colors.White, 100, 100); - //} - _overscrollDistance = value; - OnPropertyChanged(); - } - } - } - Vector2 _overscrollDistance; - - - - public bool ScrollLocked - { - get - { - return _scrollLocked; - } - - set - { - if (_scrollLocked != value) - { - _scrollLocked = value; - OnPropertyChanged(); - //Debug.WriteLine($"[SCROLL] ScrollLocked = {value}"); - } - } - } - bool _scrollLocked; - - //private const float PanTimeThreshold = 10; - //private DateTimeOffset lastPanTime = DateTimeOffset.Now; - - protected VelocityTracker VelocityTrackerPan = new(); - - protected VelocityTracker VelocityTrackerScale = new(); - - DateTime lastPanTime; - - - /// - /// There are the bounds the scroll offset can go to.. This is NOT the bounds for the whole content. - /// - protected SKRect ContentOffsetBounds { get; set; } - - - /// - /// Used to clamp while panning while finger is down - /// - /// - /// - /// - protected virtual Vector2 ClampOffsetWithRubberBand(float x, float y) - { - var clampedElastic = RubberBandUtils.ClampOnTrack(new Vector2(x, y), ContentOffsetBounds, (float)RubberEffect); - - if (Orientation == ScrollOrientation.Vertical) - { - var clampedX = Math.Max(ContentOffsetBounds.Left, Math.Min(ContentOffsetBounds.Right, x)); - return clampedElastic with { X = clampedX }; - } - else - if (Orientation == ScrollOrientation.Horizontal) - { - var clampedY = Math.Max(ContentOffsetBounds.Top, Math.Min(ContentOffsetBounds.Bottom, y)); - return clampedElastic with { Y = clampedY }; - } - - - return clampedElastic; - } - - public virtual Vector2 ClampOffset(float x, float y, bool strict = false) - { - if (!Bounces || strict) - { - var clampedX = Math.Max(ContentOffsetBounds.Left, Math.Min(ContentOffsetBounds.Right, x)); - var clampedY = Math.Max(ContentOffsetBounds.Top, Math.Min(ContentOffsetBounds.Bottom, y)); - - return new Vector2(clampedX, clampedY); - } - - return ClampOffsetWithRubberBand(x, y); - } - - public static readonly BindableProperty RespondsToGesturesProperty = BindableProperty.Create( - nameof(RespondsToGestures), - typeof(bool), - typeof(SkiaScroll), - true); - - /// - /// If disabled will not scroll using gestures. Scrolling will still be possible by code. - /// - public bool RespondsToGestures - { - get { return (bool)GetValue(RespondsToGesturesProperty); } - set { SetValue(RespondsToGesturesProperty, value); } - } - - - public static readonly BindableProperty CanScrollUsingHeaderProperty = BindableProperty.Create( - nameof(CanScrollUsingHeader), - typeof(bool), - typeof(SkiaScroll), - true); - - /// - /// If disabled will not scroll using gestures. Scrolling will still be possible by code. - /// - public bool CanScrollUsingHeader - { - get { return (bool)GetValue(CanScrollUsingHeaderProperty); } - set { SetValue(CanScrollUsingHeaderProperty, value); } - } - - protected bool ContentGesturesHit; - - public override bool IsGestureForChild(ISkiaGestureListener listener, float x, float y) - { - if (ContentGesturesHit - && HeaderBehind && listener == Header) - { - return false; //do not pass gestures to header - } - - return base.IsGestureForChild(listener, x, y); - } - - protected bool ChildWasPanning { get; set; } - - protected bool ChildWasTapped { get; set; } - - - protected bool IsContentActive - { - get - { - return Content != null && Content.IsVisible; - } - } - - - protected VelocityAccumulator VelocityAccumulator { get; } = new(); - - int lastNumberOfTouches; - - protected virtual void ResetPan() - { - //Trace.WriteLine("[SCROLL] Pan reset!"); - ChildWasTapped = false; - WasSwiping = false; - IsUserFocused = true; - IsUserPanning = false; - ChildWasPanning = false; - ChildWasTapped = false; - - StopScrolling(); - - VelocityAccumulator.Clear(); - - _panningLastDelta = Vector2.Zero; - _panningStartOffsetPts = new(ViewportOffsetX, ViewportOffsetY); - _panningCurrentOffsetPts = _panningStartOffsetPts; - } - - - private bool lockHeader; - - public override bool UsesRenderingTree => false; - - public override bool IsGestureForChild(SkiaControlWithRect child, SKPoint point) - { - if (lockHeader && child.Control != Header) - { - return false; - } - - var forChild = base.IsGestureForChild(child, point); - if (!HeaderBehind && Header != null) - { - //block gestures for other children if from header got them - if (child.Control == this.Header && forChild) - { - lockHeader = true; - } - } - return forChild; - } - - public override bool IsGestureForChild(ISkiaGestureListener listener, SKPoint point) - { - if (lockHeader && listener != Header) - { - return false; - } - - var forChild = base.IsGestureForChild(listener, point); - if (!HeaderBehind && Header != null) - { - //block gestures for other children if from header got them - if (listener == this.Header && forChild) - { - lockHeader = true; - } - } - return forChild; - } - - /// - /// panning interpolation to avoid trembling finlgers - /// - private const float InterpolationFactor = 0.1f; - - private bool inContact; - - public override ISkiaGestureListener ProcessGestures(SkiaGesturesParameters args, GestureEventProcessingInfo apply) - { - if (args.Type == TouchActionResult.Down) - { - lockHeader = false; - inContact = true; - } - else if (args.Type == TouchActionResult.Up) - { - lockHeader = false; - inContact = false; - } - - var preciseStop = false; - ContentGesturesHit = false; - - var thisOffset = TranslateInputCoords(apply.childOffset); - - if (Content != null && Header != null) - { - var x = args.Event.Location.X + thisOffset.X; - var y = args.Event.Location.Y + thisOffset.Y; - - ContentGesturesHit = Content.HitIsInside(x, y); - } - - if (TouchEffect.LogEnabled) - { - Super.Log($"[SCROLL] {this.Tag} Got {args.Type} touches {args.Event.NumberOfTouches} {VelocityY}.."); - } - - if (args.Type == TouchActionResult.Down && RespondsToGestures) - { - ResetPan(); - } - - bool passedToChildren = false; - ISkiaGestureListener PassToChildren() - { - passedToChildren = true; - return base.ProcessGestures(args, apply); - } - - bool wrongDirection = false; - if (IgnoreWrongDirection && args.Type == TouchActionResult.Panning) - { - var panDirection = DirectionType.Vertical; - if (Math.Abs(args.Event.Distance.Delta.X) > Math.Abs(args.Event.Distance.Delta.Y)) - { - panDirection = DirectionType.Horizontal; - } - if (Orientation == ScrollOrientation.Vertical && panDirection != DirectionType.Vertical) - { - wrongDirection = true; - } - if (Orientation == ScrollOrientation.Horizontal && panDirection != DirectionType.Horizontal) - { - wrongDirection = true; - } - } - - if (!IsUserPanning || wrongDirection || args.Type == TouchActionResult.Up || args.Type == TouchActionResult.Tapped || !RespondsToGestures) - { - var childConsumed = PassToChildren(); - if (childConsumed != null) - { - if (args.Type == TouchActionResult.Panning) - { - ChildWasPanning = true; - } - else if (args.Type == TouchActionResult.Tapped) - { - ChildWasTapped = true; - } - if (args.Type != TouchActionResult.Up) - return childConsumed; - } - else - { - ChildWasPanning = false; - } - } - - ISkiaGestureListener consumed = null; - if (Orientation == ScrollOrientation.Vertical || Orientation == ScrollOrientation.Both) - { - VelocityY = (float)(args.Event.Distance.Velocity.Y / RenderingScale); - } - if (Orientation == ScrollOrientation.Horizontal || Orientation == ScrollOrientation.Both) - { - VelocityX = (float)(args.Event.Distance.Velocity.X / RenderingScale); - } - - var hadNumberOfTouches = lastNumberOfTouches; - lastNumberOfTouches = args.Event.NumberOfTouches; - - if (args.Type == TouchActionResult.Wheel && !ZoomLocked) - { - IsUserFocused = true; - var zoomed = SetZoom(args.Event.Wheel.Scale); - consumed = this; - } - else if (args.Event.NumberOfTouches < 2 && hadNumberOfTouches < 2) - { - switch (args.Type) - { - case TouchActionResult.Tapped: - case TouchActionResult.LongPressing: - if (!passedToChildren) - { - _panningStartOffsetPts = new(InternalViewportOffset.Units.X, InternalViewportOffset.Units.Y); - consumed = PassToChildren(); - } - break; - - case TouchActionResult.Panning when RespondsToGestures: - bool canPan = !ScrollLocked; - if (Orientation == ScrollOrientation.Vertical) - { - canPan &= Math.Abs(VelocityY) > ScrollVelocityThreshold; - } - else if (Orientation == ScrollOrientation.Horizontal) - { - canPan &= Math.Abs(VelocityX) > ScrollVelocityThreshold; - } - else if (Orientation == ScrollOrientation.Both) - { - canPan &= Math.Abs(VelocityX) > ScrollVelocityThreshold || Math.Abs(VelocityY) > ScrollVelocityThreshold; - } - - if (lockHeader && !CanScrollUsingHeader) - { - canPan = false; - } - - if (canPan) - { - bool checkOverscroll = true; - - if (!IsUserFocused) - { - ResetPan(); - checkOverscroll = false; - } - - IsUserPanning = true; - - var movedPtsY = (args.Event.Distance.Delta.Y / RenderingScale) * ChangeDIstancePanned; - var movedPtsX = (args.Event.Distance.Delta.X / RenderingScale) * ChangeDIstancePanned; - - var interpolatedMoveToX = _panningLastDelta.X + (movedPtsX - _panningLastDelta.X) * 0.9f; - var interpolatedMoveToY = _panningLastDelta.Y + (movedPtsY - _panningLastDelta.Y) * 0.9f; - - _panningLastDelta = new Vector2(interpolatedMoveToX, interpolatedMoveToY); - - var moveTo = new Vector2( - (float)Math.Round(_panningCurrentOffsetPts.X + interpolatedMoveToX), - (float)Math.Round(_panningCurrentOffsetPts.Y + interpolatedMoveToY)); - - _panningCurrentOffsetPts = moveTo; - - if (IgnoreWrongDirection && wrongDirection) - { - IsUserPanning = false; - IsUserFocused = false; - return null; - } - - VelocityAccumulator.CaptureVelocity(new(VelocityX, VelocityY)); - - var clamped = ClampOffset(moveTo.X, moveTo.Y); - - if (!Bounces && checkOverscroll) - { - if (!AreEqual(clamped.X, moveTo.X, 1) && !AreEqual(clamped.Y, moveTo.Y, 1)) - { - return null; - } - } - - ViewportOffsetX = clamped.X; - ViewportOffsetY = clamped.Y; - - IsUserPanning = true; - _lastVelocity = new Vector2(VelocityX, VelocityY); - - consumed = this; - } - break; - - case TouchActionResult.Up when RespondsToGestures: - if ((!ChildWasTapped || OverScrolled) && (!ChildWasPanning || IsUserPanning)) - { - if (apply.alreadyConsumed != null) - { - if (CheckNeedToSnap()) - Snap(SystemAnimationTimeSecs); - return null; - } - - bool canSwipe = true; - if (lockHeader && !CanScrollUsingHeader) - { - canSwipe = false; - } - - if (!ScrollLocked && canSwipe) - { - var finalVelocity = VelocityAccumulator.CalculateFinalVelocity(this.MaxVelocity); - - bool fling = false; - bool swipe = false; - - if (!OverScrolled || Orientation == ScrollOrientation.Both) - { - var mainDirection = GetDirectionType(new Vector2(finalVelocity.X, finalVelocity.Y), DirectionType.None, 0.9f); - - if (Orientation != ScrollOrientation.Both && !IsUserPanning) - { - if (IgnoreWrongDirection) - { - if (Orientation == ScrollOrientation.Vertical && mainDirection != DirectionType.Vertical) - { - return null; - } - if (Orientation == ScrollOrientation.Horizontal && mainDirection != DirectionType.Horizontal) - { - return null; - } - } - } - - var swipeThreshold = ThesholdSwipeOnUp * RenderingScale; - if ((Orientation == ScrollOrientation.Both - || (Orientation == ScrollOrientation.Vertical && mainDirection == DirectionType.Vertical) - || (Orientation == ScrollOrientation.Horizontal && mainDirection == DirectionType.Horizontal)) - && (Math.Abs(finalVelocity.X) > swipeThreshold || Math.Abs(finalVelocity.Y) > swipeThreshold)) - { - swipe = true; - } - } - - if (OverScrolled || swipe) - { - IsUserPanning = false; - - bool bounceX = false, bounceY = false; - if (OverScrolled) - { - var contentRect = new SKRect(0, 0, ptsContentWidth, ptsContentHeight); - var closestPoint = GetClosestSidePoint(new SKPoint((float)InternalViewportOffset.Units.X, (float)InternalViewportOffset.Units.Y), contentRect, Viewport.Units.Size); - - var axis = new Vector2(closestPoint.X, closestPoint.Y); - - var velocityY = finalVelocity.Y * ChangeVelocityScrolled; - var velocityX = finalVelocity.X * ChangeVelocityScrolled; - - if (Math.Abs(velocityX) > MaxBounceVelocity) - { - velocityX = Math.Sign(velocityX) * MaxBounceVelocity; - } - - if (Math.Abs(velocityY) > MaxBounceVelocity) - { - velocityY = Math.Sign(velocityY) * MaxBounceVelocity; - } - - if (OverscrollDistance.Y != 0) - { - bounceY = true; - BounceY(InternalViewportOffset.Units.Y, - axis.Y, velocityY); - } - - if (OverscrollDistance.X != 0) - { - bounceX = true; - BounceX(InternalViewportOffset.Units.X, - axis.X, velocityX); - } - - fling = true; - } - - if (Orientation != ScrollOrientation.Neither) - { - if (!Bounces) - { - if (Orientation == ScrollOrientation.Vertical && !bounceY) - { - if ((AreEqual(InternalViewportOffset.Pixels.Y, ContentOffsetBounds.Bottom, 1) && finalVelocity.Y > 0) || - (AreEqual(InternalViewportOffset.Pixels.Y, ContentOffsetBounds.Top, 1) && finalVelocity.Y < 0)) - return null; - } - if (Orientation == ScrollOrientation.Horizontal && !bounceX) - { - if ((AreEqual(InternalViewportOffset.Pixels.X, ContentOffsetBounds.Right, 1) && finalVelocity.X > 0) || - (AreEqual(InternalViewportOffset.Pixels.X, ContentOffsetBounds.Left, 1) && finalVelocity.X < 0)) - return null; - } - } - - var velocityY = finalVelocity.Y * ChangeVelocityScrolled; - var velocityX = finalVelocity.X * ChangeVelocityScrolled; - - 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); - } - } - - if (fling) - { - WasSwiping = true; - consumed = this; - passedToChildren = true; - } - } - - IsUserFocused = false; - IsUserPanning = false; - - if (!fling) - { - if (CheckNeedToSnap()) - Snap(SystemAnimationTimeSecs); - else - { - _destination = SKRect.Empty; - } - } - } - break; - } - break; - } - } - - if (consumed != null || IsUserPanning) - { - return consumed ?? this; - } - - if (!passedToChildren) - return PassToChildren(); - - return null; - } - - - public virtual bool OnFocusChanged(bool focus) - { - return false; - } - - - SpringWithVelocityAnimator _vectorAnimatorBounceX; - - SpringWithVelocityAnimator _vectorAnimatorBounceY; - - /// - /// Fling with deceleration - /// - protected ScrollFlingAnimator _animatorFlingX; - - /// - /// Fling with deceleration - /// - protected ScrollFlingAnimator _animatorFlingY; - - /// - /// Direct scroller for ordered scroll, snap etc - /// - protected RangeAnimator _scrollerX; - - /// - /// Direct scroller for ordered scroll, snap etc - /// - protected RangeAnimator _scrollerY; - - /// - /// Units - /// - protected float _scrollMinX; - - /// - /// Units - /// - protected float _scrollMinY; - - - protected float _scrollMaxX; - protected float _scrollMaxY; - - public void StopScrolling() - { - _scrollerX?.Stop(); - _scrollerY?.Stop(); - - _animatorFlingX?.Stop(); - _animatorFlingY?.Stop(); - - _vectorAnimatorBounceX?.Stop(); - _vectorAnimatorBounceY?.Stop(); - - VelocityTrackerPan.Clear(); - VelocityTrackerScale.Clear(); - - //ViewportOffsetY = InternalViewportOffset.Units.Y; - //ViewportOffsetX = InternalViewportOffset.Units.X; - } - - void UpdateLoadingLock(bool state) - { - SkiaImageManager.Instance.IsLoadingLocked = state; - } - - void UpdateLoadingLock(Vector2 velocity) - { - bool shouldLock; - - switch (Orientation) - { - case ScrollOrientation.Vertical: - shouldLock = Math.Abs(velocity.Y) >= VelocityImageLoaderLock; - break; - case ScrollOrientation.Horizontal: - shouldLock = Math.Abs(velocity.X) >= VelocityImageLoaderLock; - break; - default: - shouldLock = Math.Abs(velocity.Y) >= VelocityImageLoaderLock || Math.Abs(velocity.X) >= VelocityImageLoaderLock; - break; - } - - UpdateLoadingLock(shouldLock); - } - - float _minVelocitySnap = 15f; - - - /// - /// POINTS per sec - /// - private float snapMinimumVelocity = 3f; - - - //public virtual bool ScrollStoppedForSnap() - //{ - // //if (_velocityScrollerY.IsRunning) - // //{ - // // return _velocityScrollerY.mVelocity <= snapMinimumVelocity; - // //} - - // if (_animatorFling.IsRunning) - // { - // return _animatorFling.CurrentVelocity.Y <= snapMinimumVelocity && _animatorFling.CurrentVelocity.X <= snapMinimumVelocity; - // } - - // return !_animatorBounce.IsRunning && !_scrollerX.IsRunning && !_scrollerY.IsRunning; - //} - - //protected bool CanSnap() - //{ - // return (!IsUserFocused - // && SnapToChildren != SnapToChildrenType.Disabled - // && Content is SkiaLayout layout - // && ScrollStoppedForSnap()); - //} - - - - /// - /// ToDo adapt this to same logic as ScrollLooped has ! - /// - /// - //protected virtual void SnapIfNeeded(bool force = false) - //{ - // return; //todo - - // if (force || - // !IsUserFocused - // && SnapToChildren != SnapToChildrenType.Disabled - // && ScrollStoppedForSnap()) - // { - // if (Content is SkiaLayout layout) - // { - // var hit = CurrentIndexHit; - // if (hit?.Index > -1 && layout.Views.Count > hit?.Index) - // { - // float needOffsetX = (float)Math.Truncate(InternalViewportOffset.Pixels.X); - // var initialOffset = needOffsetX; - - // var calcOffset = NotValidPoint(); - // if (SnapToChildren == SnapToChildrenType.Center) - // { - // calcOffset = CalculateScrollOffsetForIndex(hit.Index, RelativePositionType.Center); - // } - - // if (SnapToChildren == SnapToChildrenType.Side) - // { - // if (TrackIndexPosition == RelativePositionType.Start) - // { - // calcOffset = CalculateScrollOffsetForIndex(hit.Index, RelativePositionType.Start); - // } - // else if (TrackIndexPosition == RelativePositionType.End) - // { - // calcOffset = CalculateScrollOffsetForIndex(hit.Index, RelativePositionType.End); - // } - // } - - // if (PointIsValid(calcOffset)) - // { - // if (initialOffset != calcOffset.X) - // { - // System.Diagnostics.Debug.WriteLine($"[SNAP] ------------ {CurrentIndex}"); - // ScrollToX(calcOffset.X, true); - // } - // } - - - // } - // } - // } - //} - - - public static readonly BindableProperty BouncesProperty = BindableProperty.Create(nameof(Bounces), - typeof(bool), - typeof(SkiaScroll), - true); - /// - /// Should the scroll bounce at edges. Set to false if you want this scroll to let the parent SkiaDrawer respond to scroll when the child scroll reached bounds. - /// - public bool Bounces - { - get { return (bool)GetValue(BouncesProperty); } - set { SetValue(BouncesProperty, value); } - } - - public static readonly BindableProperty RubberDampingProperty = BindableProperty.Create( - nameof(RubberDamping), - typeof(double), - typeof(SkiaScroll), - 0.55); - - /// - /// If Bounce is enabled this basically controls how less the scroll will bounce when displaced from limit by finger or inertia. Default is 0.55. - /// - public double RubberDamping - { - get { return (double)GetValue(RubberDampingProperty); } - set { SetValue(RubberDampingProperty, value); } - } - - - public static readonly BindableProperty RubberEffectProperty = BindableProperty.Create( - nameof(RubberEffect), - typeof(double), - typeof(SkiaScroll), - 0.55); - - /// - /// If Bounce is enabled this basically controls how far from the limit can the scroll be elastically offset by finger or inertia. Default is 0.55. - /// - public double RubberEffect - { - get { return (double)GetValue(RubberEffectProperty); } - set { SetValue(RubberEffectProperty, value); } - } - - public float SnapBouncingIfVelocityLessThan - { - get { return (float)GetValue(SnapBouncingIfVelocityLessThanProperty); } - set { SetValue(SnapBouncingIfVelocityLessThanProperty, value); } - } - public static readonly BindableProperty SnapBouncingIfVelocityLessThanProperty = BindableProperty.Create(nameof(SnapBouncingIfVelocityLessThan), - typeof(float), - typeof(SkiaScroll), - 750.0f); - - - public static readonly BindableProperty AutoScrollingSpeedMsProperty = BindableProperty.Create(nameof(AutoScrollingSpeedMs), - typeof(int), - typeof(SkiaScroll), - 600); - - /// - /// For snap and ordered scrolling - /// - public int AutoScrollingSpeedMs - { - get { return (int)GetValue(AutoScrollingSpeedMsProperty); } - set { SetValue(AutoScrollingSpeedMsProperty, value); } - } - - - /// - /// Use this to control how fast the scroll will decelerate. Values 0.1 - 0.9 are the best, default is 0.3. Usually you would set higher friction for ScrollView-like scrolls and much lower for CollectionView-like scrolls (0.1 or 0.2). - /// - public float FrictionScrolled - { - get { return (float)GetValue(FrictionScrolledProperty); } - set { SetValue(FrictionScrolledProperty, value); } - } - - public static readonly BindableProperty FrictionScrolledProperty = BindableProperty.Create(nameof(FrictionScrolled), - typeof(float), - typeof(SkiaScroll), - .3f, - propertyChanged: FrictionValueChanged); - - - public static readonly BindableProperty IgnoreWrongDirectionProperty = BindableProperty.Create( - nameof(IgnoreWrongDirection), - typeof(bool), - typeof(SkiaScroll), - false); - - /// - /// Will ignore gestures of the wrong direction, like if this Orientation is Horizontal will ignore gestures with vertical direction velocity. Default is False. - /// - public bool IgnoreWrongDirection - { - get { return (bool)GetValue(IgnoreWrongDirectionProperty); } - set { SetValue(IgnoreWrongDirectionProperty, value); } - } - - /* - public static readonly BindableProperty IgnoreWrongDirectionLockProperty = BindableProperty.Create( - nameof(IgnoreWrongDirectionLock), + [ContentProperty("Content")] + public partial class SkiaScroll : SkiaControl, ISkiaGestureListener, IDefinesViewport + { + /// + /// Min velocity in points/sec to flee/swipe when finger is up + /// + public static float ThesholdSwipeOnUp = 40f; + + /// + /// To filter micro-gestures while manually panning + /// + public static float ScrollVelocityThreshold = 20; + + /// + /// Time for the snapping animations as well as the scroll to top etc animations.. + /// + public static float SystemAnimationTimeSecs = 0.2f; + + /// + /// TODO impement this + /// + public enum ScrollingInteractionState + { + None, + Dragging, + Scrolling, + Zooming + } + + public override void OnWillDisposeWithChildren() + { + base.OnWillDisposeWithChildren(); + + Content?.Dispose(); + Header?.Dispose(); + Footer?.Dispose(); + } + + private ScrollingInteractionState _intercationState; + public ScrollingInteractionState InteractionState + { + get + { + return _intercationState; + } + set + { + if (_intercationState != value) + { + _intercationState = value; + OnPropertyChanged(); + } + } + } + + + public virtual void UpdateVisibleIndex() + { + if (LayoutReady && TrackIndexPosition != RelativePositionType.None) + { + CurrentIndexHit = CalculateVisibleIndex(TrackIndexPosition); + CurrentIndex = CurrentIndexHit.Index; + } + } + + #region Scrollers + + + public bool HasContentToScroll + { + get + { + return _hasContentToScroll; + } + + set + { + if (_hasContentToScroll != value) + { + _hasContentToScroll = value; + OnPropertyChanged(); + } + } + } + bool _hasContentToScroll; + + + public static readonly BindableProperty HeaderStickyProperty = BindableProperty.Create( + nameof(HeaderSticky), + typeof(bool), + typeof(SkiaScroll), + false, propertyChanged: NeedInvalidateMeasure); + + /// + /// Should the header stay in place when content is scrolling + /// + public bool HeaderSticky + { + get { return (bool)GetValue(HeaderStickyProperty); } + set { SetValue(HeaderStickyProperty, value); } + } + + + public static readonly BindableProperty ParallaxOverscrollEnabledProperty = BindableProperty.Create( + nameof(ParallaxOverscrollEnabled), + typeof(bool), + typeof(SkiaScroll), + true, propertyChanged: NeedInvalidateMeasure); + + public bool ParallaxOverscrollEnabled + { + get { return (bool)GetValue(ParallaxOverscrollEnabledProperty); } + set { SetValue(ParallaxOverscrollEnabledProperty, value); } + } + + public static readonly BindableProperty HeaderBehindProperty = BindableProperty.Create( + nameof(HeaderBehind), + typeof(bool), + typeof(SkiaScroll), + false, propertyChanged: NeedInvalidateMeasure); + + public bool HeaderBehind + { + get { return (bool)GetValue(HeaderBehindProperty); } + set { SetValue(HeaderBehindProperty, value); } + } + + public static readonly BindableProperty ContentOffsetProperty = BindableProperty.Create( + nameof(ContentOffset), + typeof(double), + typeof(SkiaScroll), + 0.0, propertyChanged: NeedDraw); + + public double ContentOffset + { + get { return (double)GetValue(ContentOffsetProperty); } + set { SetValue(ContentOffsetProperty, value); } + } + + public static readonly BindableProperty HeaderProperty = BindableProperty.Create( + nameof(Header), + typeof(SkiaControl), + typeof(SkiaScroll), + null, propertyChanged: (b, o, n) => + { + if (b is SkiaScroll control) + { + control.SetHeader((SkiaControl)n); + } + }); + + public SkiaControl Header + { + get { return (SkiaControl)GetValue(HeaderProperty); } + set { SetValue(HeaderProperty, value); } + } + + public static readonly BindableProperty HeaderParallaxRatioProperty = BindableProperty.Create( + nameof(HeaderParallaxRatio), + typeof(double), + typeof(SkiaScroll), + 1.0, propertyChanged: NeedDraw); + + public double HeaderParallaxRatio + { + get { return (double)GetValue(HeaderParallaxRatioProperty); } + set { SetValue(HeaderParallaxRatioProperty, value); } + } + + + public static readonly BindableProperty FooterProperty = BindableProperty.Create( + nameof(Footer), + typeof(SkiaControl), + typeof(SkiaScroll), + null, propertyChanged: (b, o, n) => + { + if (b is SkiaScroll control) + { + control.SetFooter((SkiaControl)n); + } + }); + + public SkiaControl Footer + { + get { return (SkiaControl)GetValue(FooterProperty); } + set { SetValue(FooterProperty, value); } + } + + + public static readonly BindableProperty RefreshIndicatorProperty = BindableProperty.Create(nameof(RefreshIndicator), + typeof(IRefreshIndicator), + typeof(SkiaScroll), + null, + propertyChanged: OnNeedSetRefreshIndicator); + + public IRefreshIndicator RefreshIndicator + { + get { return (IRefreshIndicator)GetValue(RefreshIndicatorProperty); } + set { SetValue(RefreshIndicatorProperty, value); } + } + + private static void OnNeedSetRefreshIndicator(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaScroll control) + { + control.SetRefreshIndicator(newvalue as IRefreshIndicator); + } + } + + protected IRefreshIndicator InternalRefreshIndicator { get; set; } + + private void SetRefreshIndicator(IRefreshIndicator indicator) + { + //delete existing from Views + //and dispose + if (InternalRefreshIndicator is SkiaControl control) + { + control.SetParent(null); + control.Dispose(); + } + + //set props for the new one and and it to views + if (indicator is SkiaControl newControl) + { + InternalRefreshIndicator = indicator; + + if (Orientation == ScrollOrientation.Vertical) + { + newControl.HeightRequest = RefreshDistanceLimit; + } + else + if (Orientation == ScrollOrientation.Horizontal) + { + newControl.WidthRequest = RefreshDistanceLimit; + } + newControl.ZIndex = 1000; + AddSubView(newControl); + } + } + + private static void NeedToScroll(BindableObject bindable, object oldvalue, object newvalue) + { + if ((int)newvalue >= 0 && bindable is SkiaScroll scroll) + { + scroll.ScrollToIndex(index: (int)newvalue, + scroll.OrderedScrollIsAnimated, + scroll.TrackIndexPosition); + scroll.OrderedScroll = -1; + } + } + + public static readonly BindableProperty OrderedScrollProperty = BindableProperty.Create(nameof(OrderedScroll), + typeof(int), + typeof(SkiaScroll), -1, BindingMode.TwoWay, propertyChanged: NeedToScroll); + + public int OrderedScroll + { + get { return (int)GetValue(OrderedScrollProperty); } + set { SetValue(OrderedScrollProperty, value); } + } + + public static readonly BindableProperty OrderedScrollIsAnimatedProperty = BindableProperty.Create(nameof(OrderedScrollIsAnimated), + typeof(bool), + typeof(SkiaScroll), false); + + public bool OrderedScrollIsAnimated + { + get { return (bool)GetValue(OrderedScrollIsAnimatedProperty); } + set { SetValue(OrderedScrollIsAnimatedProperty, value); } + } + + public static readonly BindableProperty RefreshEnabledProperty = BindableProperty.Create(nameof(RefreshEnabled), typeof(bool), typeof(SkiaScroll), false); + public bool RefreshEnabled + { + get { return (bool)GetValue(RefreshEnabledProperty); } + set { SetValue(RefreshEnabledProperty, value); } + } + + public static readonly BindableProperty IsRefreshingProperty = BindableProperty.Create(nameof(IsRefreshing), + typeof(bool), + typeof(SkiaScroll), + false, + propertyChanged: (bindable, old, changed) => + { + if (bindable is SkiaScroll scroll) + { + scroll.SetIsRefreshing((bool)changed); + } + }); + public bool IsRefreshing + { + get { return (bool)GetValue(IsRefreshingProperty); } + set { SetValue(IsRefreshingProperty, value); } + } + + + public static readonly BindableProperty RefreshCommandProperty = BindableProperty.Create(nameof(RefreshCommand), + typeof(ICommand), + typeof(SkiaScroll), + null); + public ICommand RefreshCommand + { + get { return (ICommand)GetValue(RefreshCommandProperty); } + set { SetValue(RefreshCommandProperty, value); } + } + + + public static readonly BindableProperty RefreshDistanceLimitProperty = BindableProperty.Create(nameof(RefreshDistanceLimit), + typeof(float), + typeof(SkiaScroll), + 150f); + /// + /// Applyed to RefreshView + /// + public float RefreshDistanceLimit + { + get { return (float)GetValue(RefreshDistanceLimitProperty); } + set { SetValue(RefreshDistanceLimitProperty, value); } + } + + public static readonly BindableProperty RefreshShowDistanceProperty = BindableProperty.Create( + nameof(RefreshShowDistance), + typeof(float), + typeof(SkiaScroll), + 50f); + + /// + /// Applyed to RefreshView + /// + public float RefreshShowDistance + { + get { return (float)GetValue(RefreshShowDistanceProperty); } + set { SetValue(RefreshShowDistanceProperty, value); } + } + + + public Easing ScrollingEasing = Easing.SpringOut; + + private readonly TimeSpan debounceTime = TimeSpan.FromMilliseconds(10); + private float filterFactor = 0.99f; // (0 to 1) + + protected float _velocitySwipe = 200; //pts + protected float _velocitySwipeRatio = 1.0f; + + protected Vector2 _panningLastDelta; + protected Vector2 _panningCurrentOffsetPts; + private Vector2 _panningStartOffsetPts; + + public async void PlayEdgeGlowAnimation(Color color, double x, double y, bool removePrevious = true) + { + if (removePrevious) + { + UnregisterAllAnimatorsByType(typeof(EdgeGlowAnimator)); + } + var animation = new EdgeGlowAnimator(this) + { + GlowPosition = GlowPosition.Top, + Color = color.ToSKColor(), + X = x, + Y = y, + }; + animation.Start(); + } + + /// + /// Units + /// + public Vector2 OverscrollDistance + { + get + { + return _overscrollDistance; + } + + set + { + if (_overscrollDistance != value) + { + //if (_rubberBandDistanceY == 0 && value != 0) + //{ + // //show effect + // PlayEdgeGlowAnimation(Colors.White, 100, 100); + //} + _overscrollDistance = value; + OnPropertyChanged(); + } + } + } + Vector2 _overscrollDistance; + + + + public bool ScrollLocked + { + get + { + return _scrollLocked; + } + + set + { + if (_scrollLocked != value) + { + _scrollLocked = value; + OnPropertyChanged(); + //Debug.WriteLine($"[SCROLL] ScrollLocked = {value}"); + } + } + } + bool _scrollLocked; + + //private const float PanTimeThreshold = 10; + //private DateTimeOffset lastPanTime = DateTimeOffset.Now; + + protected VelocityTracker VelocityTrackerPan = new(); + + protected VelocityTracker VelocityTrackerScale = new(); + + DateTime lastPanTime; + + + /// + /// There are the bounds the scroll offset can go to.. This is NOT the bounds for the whole content. + /// + protected SKRect ContentOffsetBounds { get; set; } + + + /// + /// Used to clamp while panning while finger is down + /// + /// + /// + /// + protected virtual Vector2 ClampOffsetWithRubberBand(float x, float y) + { + var clampedElastic = RubberBandUtils.ClampOnTrack(new Vector2(x, y), ContentOffsetBounds, (float)RubberEffect); + + if (Orientation == ScrollOrientation.Vertical) + { + var clampedX = Math.Max(ContentOffsetBounds.Left, Math.Min(ContentOffsetBounds.Right, x)); + return clampedElastic with { X = clampedX }; + } + else + if (Orientation == ScrollOrientation.Horizontal) + { + var clampedY = Math.Max(ContentOffsetBounds.Top, Math.Min(ContentOffsetBounds.Bottom, y)); + return clampedElastic with { Y = clampedY }; + } + + + return clampedElastic; + } + + public virtual Vector2 ClampOffset(float x, float y, bool strict = false) + { + if (!Bounces || strict) + { + var clampedX = Math.Max(ContentOffsetBounds.Left, Math.Min(ContentOffsetBounds.Right, x)); + var clampedY = Math.Max(ContentOffsetBounds.Top, Math.Min(ContentOffsetBounds.Bottom, y)); + + return new Vector2(clampedX, clampedY); + } + + return ClampOffsetWithRubberBand(x, y); + } + + public static readonly BindableProperty RespondsToGesturesProperty = BindableProperty.Create( + nameof(RespondsToGestures), + typeof(bool), + typeof(SkiaScroll), + true); + + /// + /// If disabled will not scroll using gestures. Scrolling will still be possible by code. + /// + public bool RespondsToGestures + { + get { return (bool)GetValue(RespondsToGesturesProperty); } + set { SetValue(RespondsToGesturesProperty, value); } + } + + + public static readonly BindableProperty CanScrollUsingHeaderProperty = BindableProperty.Create( + nameof(CanScrollUsingHeader), + typeof(bool), + typeof(SkiaScroll), + true); + + /// + /// If disabled will not scroll using gestures. Scrolling will still be possible by code. + /// + public bool CanScrollUsingHeader + { + get { return (bool)GetValue(CanScrollUsingHeaderProperty); } + set { SetValue(CanScrollUsingHeaderProperty, value); } + } + + protected bool ContentGesturesHit; + + public override bool IsGestureForChild(ISkiaGestureListener listener, float x, float y) + { + if (ContentGesturesHit + && HeaderBehind && listener == Header) + { + return false; //do not pass gestures to header + } + + return base.IsGestureForChild(listener, x, y); + } + + protected bool ChildWasPanning { get; set; } + + protected bool ChildWasTapped { get; set; } + + + protected bool IsContentActive + { + get + { + return Content != null && Content.IsVisible; + } + } + + + protected VelocityAccumulator VelocityAccumulator { get; } = new(); + + int lastNumberOfTouches; + + protected virtual void ResetPan() + { + //Trace.WriteLine("[SCROLL] Pan reset!"); + ChildWasTapped = false; + WasSwiping = false; + IsUserFocused = true; + IsUserPanning = false; + ChildWasPanning = false; + ChildWasTapped = false; + + StopScrolling(); + + VelocityAccumulator.Clear(); + + _panningLastDelta = Vector2.Zero; + _panningStartOffsetPts = new(ViewportOffsetX, ViewportOffsetY); + _panningCurrentOffsetPts = _panningStartOffsetPts; + } + + + private bool lockHeader; + + public override bool UsesRenderingTree => false; + + public override bool IsGestureForChild(SkiaControlWithRect child, SKPoint point) + { + if (lockHeader && child.Control != Header) + { + return false; + } + + var forChild = base.IsGestureForChild(child, point); + if (!HeaderBehind && Header != null) + { + //block gestures for other children if from header got them + if (child.Control == this.Header && forChild) + { + lockHeader = true; + } + } + return forChild; + } + + public override bool IsGestureForChild(ISkiaGestureListener listener, SKPoint point) + { + if (lockHeader && listener != Header) + { + return false; + } + + var forChild = base.IsGestureForChild(listener, point); + if (!HeaderBehind && Header != null) + { + //block gestures for other children if from header got them + if (listener == this.Header && forChild) + { + lockHeader = true; + } + } + return forChild; + } + + /// + /// panning interpolation to avoid trembling finlgers + /// + private const float InterpolationFactor = 0.1f; + + private bool inContact; + + public override ISkiaGestureListener ProcessGestures(SkiaGesturesParameters args, GestureEventProcessingInfo apply) + { + if (args.Type == TouchActionResult.Down) + { + lockHeader = false; + inContact = true; + } + else + if (args.Type == TouchActionResult.Up) + { + lockHeader = false; + inContact = false; + } + + var preciseStop = false; + ContentGesturesHit = false; + var thisOffset = TranslateInputCoords(apply.childOffset); + if (Content != null && Header != null) + { + var x = args.Event.Location.X + thisOffset.X; + var y = args.Event.Location.Y + thisOffset.Y; + + ContentGesturesHit = Content.HitIsInside(x, y); + } + + if (TouchEffect.LogEnabled) + { + Super.Log($"[SCROLL] {this.Tag} Got {args.Type} touches {args.Event.NumberOfTouches} {VelocityY}.."); + } + + if (args.Type == TouchActionResult.Down && RespondsToGestures) + { + ResetPan(); + } + + bool passedToChildren = false; + ISkiaGestureListener PassToChildren() + { + passedToChildren = true; + return base.ProcessGestures(args, apply); + } + + + bool wrongDirection = false; + + if (args.Type == TouchActionResult.Panning) + { + if (args.Event.Manipulation != null && !ZoomLocked) //todo not only panning? + { + IsUserFocused = true; + var scale = args.Event.Manipulation.Scale + this.ViewportZoom; + Debug.WriteLine($"Scale: {scale}"); + var zoomed = SetZoom(scale); + } + + + if (IgnoreWrongDirection) + { + var panDirection = DirectionType.Vertical; + if (Math.Abs(args.Event.Distance.Delta.X) > Math.Abs(args.Event.Distance.Delta.Y)) + { + panDirection = DirectionType.Horizontal; + } + if (Orientation == ScrollOrientation.Vertical && panDirection != DirectionType.Vertical) + { + wrongDirection = true; + } + if (Orientation == ScrollOrientation.Horizontal && panDirection != DirectionType.Horizontal) + { + wrongDirection = true; + } + } + } + + + if (!IsUserPanning || wrongDirection || args.Type == TouchActionResult.Up || args.Type == TouchActionResult.Tapped || !RespondsToGestures) + { + var childConsumed = PassToChildren(); + if (childConsumed != null) + { + if (args.Type == TouchActionResult.Panning) + { + ChildWasPanning = true; + } + else if (args.Type == TouchActionResult.Tapped) + { + ChildWasTapped = true; + } + if (args.Type != TouchActionResult.Up) + return childConsumed; + } + else + { + ChildWasPanning = false; + } + } + + ISkiaGestureListener consumed = null; + if (Orientation == ScrollOrientation.Vertical || Orientation == ScrollOrientation.Both) + { + VelocityY = (float)(args.Event.Distance.Velocity.Y / RenderingScale); + } + if (Orientation == ScrollOrientation.Horizontal || Orientation == ScrollOrientation.Both) + { + VelocityX = (float)(args.Event.Distance.Velocity.X / RenderingScale); + } + + var hadNumberOfTouches = lastNumberOfTouches; + lastNumberOfTouches = args.Event.NumberOfTouches; + + if (args.Type == TouchActionResult.Wheel && !ZoomLocked) + { + IsUserFocused = true; + Debug.WriteLine($"Wheel: {args.Event.Wheel.Scale}"); + var zoomed = SetZoom(args.Event.Wheel.Scale); + consumed = this; + } + else if (args.Event.NumberOfTouches < 2 && hadNumberOfTouches < 2) + { + switch (args.Type) + { + case TouchActionResult.Tapped: + case TouchActionResult.LongPressing: + if (!passedToChildren) + { + _panningStartOffsetPts = new(InternalViewportOffset.Units.X, InternalViewportOffset.Units.Y); + consumed = PassToChildren(); + } + break; + + case TouchActionResult.Panning when RespondsToGestures: + bool canPan = !ScrollLocked; + if (Orientation == ScrollOrientation.Vertical) + { + canPan &= Math.Abs(VelocityY) > ScrollVelocityThreshold; + } + else if (Orientation == ScrollOrientation.Horizontal) + { + canPan &= Math.Abs(VelocityX) > ScrollVelocityThreshold; + } + else if (Orientation == ScrollOrientation.Both) + { + canPan &= Math.Abs(VelocityX) > ScrollVelocityThreshold || Math.Abs(VelocityY) > ScrollVelocityThreshold; + } + + if (lockHeader && !CanScrollUsingHeader) + { + canPan = false; + } + + if (canPan) + { + bool checkOverscroll = true; + + if (!IsUserFocused) + { + ResetPan(); + checkOverscroll = false; + } + + IsUserPanning = true; + + var movedPtsY = (args.Event.Distance.Delta.Y / RenderingScale) * ChangeDIstancePanned; + var movedPtsX = (args.Event.Distance.Delta.X / RenderingScale) * ChangeDIstancePanned; + + var interpolatedMoveToX = _panningLastDelta.X + (movedPtsX - _panningLastDelta.X) * 0.9f; + var interpolatedMoveToY = _panningLastDelta.Y + (movedPtsY - _panningLastDelta.Y) * 0.9f; + + _panningLastDelta = new Vector2(interpolatedMoveToX, interpolatedMoveToY); + + var moveTo = new Vector2( + (float)Math.Round(_panningCurrentOffsetPts.X + interpolatedMoveToX), + (float)Math.Round(_panningCurrentOffsetPts.Y + interpolatedMoveToY)); + + _panningCurrentOffsetPts = moveTo; + + if (IgnoreWrongDirection && wrongDirection) + { + IsUserPanning = false; + IsUserFocused = false; + return null; + } + + VelocityAccumulator.CaptureVelocity(new(VelocityX, VelocityY)); + + var clamped = ClampOffset(moveTo.X, moveTo.Y); + + if (!Bounces && checkOverscroll) + { + if (!AreEqual(clamped.X, moveTo.X, 1) && !AreEqual(clamped.Y, moveTo.Y, 1)) + { + return null; + } + } + + ViewportOffsetX = clamped.X; + ViewportOffsetY = clamped.Y; + + IsUserPanning = true; + _lastVelocity = new Vector2(VelocityX, VelocityY); + + consumed = this; + } + break; + + case TouchActionResult.Up when RespondsToGestures: + if ((!ChildWasTapped || OverScrolled) && (!ChildWasPanning || IsUserPanning)) + { + if (apply.alreadyConsumed != null) + { + if (CheckNeedToSnap()) + Snap(SystemAnimationTimeSecs); + return null; + } + + bool canSwipe = true; + if (lockHeader && !CanScrollUsingHeader) + { + canSwipe = false; + } + + if (!ScrollLocked && canSwipe) + { + var finalVelocity = VelocityAccumulator.CalculateFinalVelocity(this.MaxVelocity); + + bool fling = false; + bool swipe = false; + + if (!OverScrolled || Orientation == ScrollOrientation.Both) + { + var mainDirection = GetDirectionType(new Vector2(finalVelocity.X, finalVelocity.Y), DirectionType.None, 0.9f); + + if (Orientation != ScrollOrientation.Both && !IsUserPanning) + { + if (IgnoreWrongDirection) + { + if (Orientation == ScrollOrientation.Vertical && mainDirection != DirectionType.Vertical) + { + return null; + } + if (Orientation == ScrollOrientation.Horizontal && mainDirection != DirectionType.Horizontal) + { + return null; + } + } + } + + var swipeThreshold = ThesholdSwipeOnUp * RenderingScale; + if ((Orientation == ScrollOrientation.Both + || (Orientation == ScrollOrientation.Vertical && mainDirection == DirectionType.Vertical) + || (Orientation == ScrollOrientation.Horizontal && mainDirection == DirectionType.Horizontal)) + && (Math.Abs(finalVelocity.X) > swipeThreshold || Math.Abs(finalVelocity.Y) > swipeThreshold)) + { + swipe = true; + } + } + + if (OverScrolled || swipe) + { + IsUserPanning = false; + + bool bounceX = false, bounceY = false; + if (OverScrolled) + { + var contentRect = new SKRect(0, 0, ptsContentWidth, ptsContentHeight); + var closestPoint = GetClosestSidePoint(new SKPoint((float)InternalViewportOffset.Units.X, (float)InternalViewportOffset.Units.Y), contentRect, Viewport.Units.Size); + + var axis = new Vector2(closestPoint.X, closestPoint.Y); + + var velocityY = finalVelocity.Y * ChangeVelocityScrolled; + var velocityX = finalVelocity.X * ChangeVelocityScrolled; + + if (Math.Abs(velocityX) > MaxBounceVelocity) + { + velocityX = Math.Sign(velocityX) * MaxBounceVelocity; + } + + if (Math.Abs(velocityY) > MaxBounceVelocity) + { + velocityY = Math.Sign(velocityY) * MaxBounceVelocity; + } + + if (OverscrollDistance.Y != 0) + { + bounceY = true; + BounceY(InternalViewportOffset.Units.Y, + axis.Y, velocityY); + } + + if (OverscrollDistance.X != 0) + { + bounceX = true; + BounceX(InternalViewportOffset.Units.X, + axis.X, velocityX); + } + + fling = true; + } + + if (Orientation != ScrollOrientation.Neither) + { + if (!Bounces) + { + if (Orientation == ScrollOrientation.Vertical && !bounceY) + { + if ((AreEqual(InternalViewportOffset.Pixels.Y, ContentOffsetBounds.Bottom, 1) && finalVelocity.Y > 0) || + (AreEqual(InternalViewportOffset.Pixels.Y, ContentOffsetBounds.Top, 1) && finalVelocity.Y < 0)) + return null; + } + if (Orientation == ScrollOrientation.Horizontal && !bounceX) + { + if ((AreEqual(InternalViewportOffset.Pixels.X, ContentOffsetBounds.Right, 1) && finalVelocity.X > 0) || + (AreEqual(InternalViewportOffset.Pixels.X, ContentOffsetBounds.Left, 1) && finalVelocity.X < 0)) + return null; + } + } + + var velocityY = finalVelocity.Y * ChangeVelocityScrolled; + var velocityX = finalVelocity.X * ChangeVelocityScrolled; + + 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); + } + } + + if (fling) + { + WasSwiping = true; + consumed = this; + passedToChildren = true; + } + } + + IsUserFocused = false; + IsUserPanning = false; + + if (!fling) + { + if (CheckNeedToSnap()) + Snap(SystemAnimationTimeSecs); + else + { + _destination = SKRect.Empty; + } + } + } + break; + } + break; + } + } + + if (consumed != null || IsUserPanning) + { + return consumed ?? this; + } + + if (!passedToChildren) + return PassToChildren(); + + return null; + } + + + public virtual bool OnFocusChanged(bool focus) + { + return false; + } + + + SpringWithVelocityAnimator _vectorAnimatorBounceX; + + SpringWithVelocityAnimator _vectorAnimatorBounceY; + + /// + /// Fling with deceleration + /// + protected ScrollFlingAnimator _animatorFlingX; + + /// + /// Fling with deceleration + /// + protected ScrollFlingAnimator _animatorFlingY; + + /// + /// Direct scroller for ordered scroll, snap etc + /// + protected RangeAnimator _scrollerX; + + /// + /// Direct scroller for ordered scroll, snap etc + /// + protected RangeAnimator _scrollerY; + + /// + /// Units + /// + protected float _scrollMinX; + + /// + /// Units + /// + protected float _scrollMinY; + + + protected float _scrollMaxX; + protected float _scrollMaxY; + + public void StopScrolling() + { + _scrollerX?.Stop(); + _scrollerY?.Stop(); + + _animatorFlingX?.Stop(); + _animatorFlingY?.Stop(); + + _vectorAnimatorBounceX?.Stop(); + _vectorAnimatorBounceY?.Stop(); + + VelocityTrackerPan.Clear(); + VelocityTrackerScale.Clear(); + + //ViewportOffsetY = InternalViewportOffset.Units.Y; + //ViewportOffsetX = InternalViewportOffset.Units.X; + } + + void UpdateLoadingLock(bool state) + { + SkiaImageManager.Instance.IsLoadingLocked = state; + } + + void UpdateLoadingLock(Vector2 velocity) + { + bool shouldLock; + + switch (Orientation) + { + case ScrollOrientation.Vertical: + shouldLock = Math.Abs(velocity.Y) >= VelocityImageLoaderLock; + break; + case ScrollOrientation.Horizontal: + shouldLock = Math.Abs(velocity.X) >= VelocityImageLoaderLock; + break; + default: + shouldLock = Math.Abs(velocity.Y) >= VelocityImageLoaderLock || Math.Abs(velocity.X) >= VelocityImageLoaderLock; + break; + } + + UpdateLoadingLock(shouldLock); + } + + float _minVelocitySnap = 15f; + + + /// + /// POINTS per sec + /// + private float snapMinimumVelocity = 3f; + + + //public virtual bool ScrollStoppedForSnap() + //{ + // //if (_velocityScrollerY.IsRunning) + // //{ + // // return _velocityScrollerY.mVelocity <= snapMinimumVelocity; + // //} + + // if (_animatorFling.IsRunning) + // { + // return _animatorFling.CurrentVelocity.Y <= snapMinimumVelocity && _animatorFling.CurrentVelocity.X <= snapMinimumVelocity; + // } + + // return !_animatorBounce.IsRunning && !_scrollerX.IsRunning && !_scrollerY.IsRunning; + //} + + //protected bool CanSnap() + //{ + // return (!IsUserFocused + // && SnapToChildren != SnapToChildrenType.Disabled + // && Content is SkiaLayout layout + // && ScrollStoppedForSnap()); + //} + + + + /// + /// ToDo adapt this to same logic as ScrollLooped has ! + /// + /// + //protected virtual void SnapIfNeeded(bool force = false) + //{ + // return; //todo + + // if (force || + // !IsUserFocused + // && SnapToChildren != SnapToChildrenType.Disabled + // && ScrollStoppedForSnap()) + // { + // if (Content is SkiaLayout layout) + // { + // var hit = CurrentIndexHit; + // if (hit?.Index > -1 && layout.Views.Count > hit?.Index) + // { + // float needOffsetX = (float)Math.Truncate(InternalViewportOffset.Pixels.X); + // var initialOffset = needOffsetX; + + // var calcOffset = NotValidPoint(); + // if (SnapToChildren == SnapToChildrenType.Center) + // { + // calcOffset = CalculateScrollOffsetForIndex(hit.Index, RelativePositionType.Center); + // } + + // if (SnapToChildren == SnapToChildrenType.Side) + // { + // if (TrackIndexPosition == RelativePositionType.Start) + // { + // calcOffset = CalculateScrollOffsetForIndex(hit.Index, RelativePositionType.Start); + // } + // else if (TrackIndexPosition == RelativePositionType.End) + // { + // calcOffset = CalculateScrollOffsetForIndex(hit.Index, RelativePositionType.End); + // } + // } + + // if (PointIsValid(calcOffset)) + // { + // if (initialOffset != calcOffset.X) + // { + // System.Diagnostics.Debug.WriteLine($"[SNAP] ------------ {CurrentIndex}"); + // ScrollToX(calcOffset.X, true); + // } + // } + + + // } + // } + // } + //} + + + public static readonly BindableProperty BouncesProperty = BindableProperty.Create(nameof(Bounces), + typeof(bool), + typeof(SkiaScroll), + true); + /// + /// Should the scroll bounce at edges. Set to false if you want this scroll to let the parent SkiaDrawer respond to scroll when the child scroll reached bounds. + /// + public bool Bounces + { + get { return (bool)GetValue(BouncesProperty); } + set { SetValue(BouncesProperty, value); } + } + + public static readonly BindableProperty RubberDampingProperty = BindableProperty.Create( + nameof(RubberDamping), + typeof(double), + typeof(SkiaScroll), + 0.55); + + /// + /// If Bounce is enabled this basically controls how less the scroll will bounce when displaced from limit by finger or inertia. Default is 0.55. + /// + public double RubberDamping + { + get { return (double)GetValue(RubberDampingProperty); } + set { SetValue(RubberDampingProperty, value); } + } + + + public static readonly BindableProperty RubberEffectProperty = BindableProperty.Create( + nameof(RubberEffect), + typeof(double), + typeof(SkiaScroll), + 0.55); + + /// + /// If Bounce is enabled this basically controls how far from the limit can the scroll be elastically offset by finger or inertia. Default is 0.55. + /// + public double RubberEffect + { + get { return (double)GetValue(RubberEffectProperty); } + set { SetValue(RubberEffectProperty, value); } + } + + public float SnapBouncingIfVelocityLessThan + { + get { return (float)GetValue(SnapBouncingIfVelocityLessThanProperty); } + set { SetValue(SnapBouncingIfVelocityLessThanProperty, value); } + } + public static readonly BindableProperty SnapBouncingIfVelocityLessThanProperty = BindableProperty.Create(nameof(SnapBouncingIfVelocityLessThan), + typeof(float), + typeof(SkiaScroll), + 750.0f); + + + public static readonly BindableProperty AutoScrollingSpeedMsProperty = BindableProperty.Create(nameof(AutoScrollingSpeedMs), + typeof(int), + typeof(SkiaScroll), + 600); + + /// + /// For snap and ordered scrolling + /// + public int AutoScrollingSpeedMs + { + get { return (int)GetValue(AutoScrollingSpeedMsProperty); } + set { SetValue(AutoScrollingSpeedMsProperty, value); } + } + + + /// + /// Use this to control how fast the scroll will decelerate. Values 0.1 - 0.9 are the best, default is 0.3. Usually you would set higher friction for ScrollView-like scrolls and much lower for CollectionView-like scrolls (0.1 or 0.2). + /// + public float FrictionScrolled + { + get { return (float)GetValue(FrictionScrolledProperty); } + set { SetValue(FrictionScrolledProperty, value); } + } + + public static readonly BindableProperty FrictionScrolledProperty = BindableProperty.Create(nameof(FrictionScrolled), + typeof(float), + typeof(SkiaScroll), + .3f, + propertyChanged: FrictionValueChanged); + + + public static readonly BindableProperty IgnoreWrongDirectionProperty = BindableProperty.Create( + nameof(IgnoreWrongDirection), + typeof(bool), + typeof(SkiaScroll), + false); + + /// + /// Will ignore gestures of the wrong direction, like if this Orientation is Horizontal will ignore gestures with vertical direction velocity. Default is False. + /// + public bool IgnoreWrongDirection + { + get { return (bool)GetValue(IgnoreWrongDirectionProperty); } + set { SetValue(IgnoreWrongDirectionProperty, value); } + } + + /* + public static readonly BindableProperty IgnoreWrongDirectionLockProperty = BindableProperty.Create( + nameof(IgnoreWrongDirectionLock), + typeof(bool), + typeof(SkiaScroll), + false); + + /// + /// In case if will ignore gestures of the wrong direction, should we lock this direction or multi-directional scrolling (True) is still allowed (False). Default is False. + /// + public bool IgnoreWrongDirectionLock + { + get { return (bool)GetValue(IgnoreWrongDirectionLockProperty); } + set { SetValue(IgnoreWrongDirectionLockProperty, value); } + } + */ + + public static readonly BindableProperty ResetScrollPositionOnContentSizeChangedProperty = BindableProperty.Create( + nameof(ResetScrollPositionOnContentSizeChanged), + typeof(bool), + typeof(SkiaScroll), + false); + + public bool ResetScrollPositionOnContentSizeChanged + { + get { return (bool)GetValue(ResetScrollPositionOnContentSizeChangedProperty); } + set { SetValue(ResetScrollPositionOnContentSizeChangedProperty, value); } + } + + + /// + /// For when the finger is up and swipe is detected + /// + public float ChangeVelocityScrolled + { + get { return (float)GetValue(ChangeVelocityScrolledProperty); } + set { SetValue(ChangeVelocityScrolledProperty, value); } + } + public static readonly BindableProperty ChangeVelocityScrolledProperty = BindableProperty.Create(nameof(ChangeVelocityScrolled), + typeof(float), + typeof(SkiaScroll), + 1.33f); + + public static readonly BindableProperty MaxVelocityProperty = BindableProperty.Create( + nameof(MaxVelocity), + typeof(float), + typeof(SkiaScroll), + 3000f); + + /// + /// Limit user input velocity + /// + public float MaxVelocity + { + get { return (float)GetValue(MaxVelocityProperty); } + set { SetValue(MaxVelocityProperty, value); } + } + + public static readonly BindableProperty MaxBounceVelocityProperty = BindableProperty.Create( + nameof(MaxBounceVelocity), + typeof(float), + typeof(SkiaScroll), + 500f); + + /// + /// Limit bounce velocity + /// + public float MaxBounceVelocity + { + get { return (float)GetValue(MaxBounceVelocityProperty); } + set { SetValue(MaxBounceVelocityProperty, value); } + } + + /// + /// For when the finger is down and panning + /// + public float ChangeDIstancePanned + { + get { return (float)GetValue(ChangeDIstancePannedProperty); } + set { SetValue(ChangeDIstancePannedProperty, value); } + } + + + public static readonly BindableProperty ChangeDIstancePannedProperty = BindableProperty.Create(nameof(ChangeDIstancePanned), + typeof(float), + typeof(SkiaScroll), + 0.975f); + + + + private static void FrictionValueChanged(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaScroll control) + { + control.UpdateFriction(); + } + } + + int _currentIndex = -1; + public int CurrentIndex + { + get + { + return _currentIndex; + } + protected set + { + if (_currentIndex != value) + { + _currentIndex = value; + OnPropertyChanged(); + IndexChanged?.Invoke(this, value); + //Debug.WriteLine($"Scroll {Tag} CurrentIndex {value}"); + } + } + } + + public event EventHandler IndexChanged; + + public ContainsPointResult CurrentIndexHit + { + get + { + return _CurrentIndexHit; + } + set + { + if (value != _CurrentIndexHit) + { + _CurrentIndexHit = value; + OnPropertyChanged(); + } + } + } + private ContainsPointResult _CurrentIndexHit; + + void WatchState() + { + + } + + protected SKPoint DetectIndexChildIndexAt; + + void SetDetectIndexChildPoint(RelativePositionType option = RelativePositionType.Start) + { + //todo this will need to change for multiple columns? + + if (!IsContentActive || Content.MeasuredSize == null || TrackIndexPosition == RelativePositionType.None) + return; + + var point = new SKPoint(); + + + if (this.Orientation == ScrollOrientation.Vertical) + { + var endY = this.Viewport.Pixels.Height; + if (this.Content.MeasuredSize.Pixels.Height < endY) + endY = this.Content.MeasuredSize.Pixels.Height; + + if (option == RelativePositionType.End) + { + point.Y += (endY - TrackIndexPositionOffset); + } + else + if (option == RelativePositionType.Center) + { + point.Y += endY / 2f; + } + + point.X = this.Viewport.Pixels.MidX; + } + else + if (this.Orientation == ScrollOrientation.Horizontal) + { + var endX = this.Viewport.Pixels.Width; + if (this.Content.MeasuredSize.Pixels.Width < endX) + endX = this.Content.MeasuredSize.Pixels.Width; + + if (option == RelativePositionType.End) + { + point.X += endX - TrackIndexPositionOffset; + } + else + if (option == RelativePositionType.Center) + { + point.X += endX / 2f; + } + + point.Y = this.Viewport.Pixels.MidY; + } + + //Debug.WriteLine($"[POINT] V {Viewport.Pixels.Bottom} P {point.Y}"); + + DetectIndexChildIndexAt = point; + } + + /// + /// Calculates CurrentIndex + /// + public virtual ContainsPointResult CalculateVisibleIndex(RelativePositionType option) + { + if (Content is SkiaLayout layout) + { + + var pixelsOffsetX = InternalViewportOffset.Pixels.X;// (float)(ViewportOffsetX * layout.RenderingScale); + var pixelsOffsetY = InternalViewportOffset.Pixels.Y;// (float)(ViewportOffsetY * layout.RenderingScale); + + return GetItemIndex(layout, pixelsOffsetX, pixelsOffsetY, option); + } + else + if (Content is ILayoutInsideViewport inside) + { + + var point = new SKPoint( + DetectIndexChildIndexAt.X + InternalViewportOffset.Pixels.X + DrawingRect.Left, + DetectIndexChildIndexAt.Y + InternalViewportOffset.Pixels.Y + DrawingRect.Top); + + var found = inside.GetVisibleChildIndexAt(point); + + if (found.Index != -1) + { + + + + //todo translate found + var area = found.Area; + area.Offset(-DrawingRect.Left, -DrawingRect.Top); + point.Offset(-DrawingRect.Left, -DrawingRect.Top); + return new ContainsPointResult() + { + Index = found.Index, + Area = area, + Point = point, + Unmodified = new(InternalViewportOffset.Pixels.X, InternalViewportOffset.Pixels.Y) + }; + } + + return found; + } + + return ContainsPointResult.NotFound(); + } + + public virtual ContainsPointResult GetItemIndex(SkiaLayout layout, float pixelsOffsetX, float pixelsOffsetY, RelativePositionType option) + { + if (layout.LatestStackStructure == null) + return ContainsPointResult.NotFound(); + + bool trace = false; + + if (this.Orientation == ScrollOrientation.Vertical) + { + var initialValue = pixelsOffsetY; + + // ----------- proper to infinite start + + if (option == RelativePositionType.Center) + { + pixelsOffsetY -= Viewport.Pixels.Height / 2f; + } + else + if (option == RelativePositionType.End) + { + pixelsOffsetY -= Viewport.Pixels.Height; + } + + if (pixelsOffsetY > 0) + { + //inverted scroll + pixelsOffsetY -= Content.MeasuredSize.Pixels.Height; + + } + else + { + //normal scroll + if (-pixelsOffsetY > Content.MeasuredSize.Pixels.Height) + { + pixelsOffsetY += Content.MeasuredSize.Pixels.Height; + } + } + + // ----------- proper to infinite end + + var point = new SKPoint( + (float)Math.Abs(pixelsOffsetX), + (float)Math.Abs(pixelsOffsetY) + ); + + if (layout.Type == LayoutType.Column || layout.Type == LayoutType.Wrap && layout.Split > 0) //todo grid + { + var stackStructure = layout.LatestStackStructure; + int index = -1; + int row; + int col; + + if (trace) + Trace.WriteLine($"offset: {point.Y}"); + + foreach (var childInfo in stackStructure.GetChildren()) + { + index++; + if (childInfo.Destination.ContainsInclusive(point)) + { + return new ContainsPointResult() + { + Index = index, + Area = childInfo.Destination, + Point = point, + Unmodified = new SKPoint(0, initialValue) + }; + } + } + + } + } + else + if (this.Orientation == ScrollOrientation.Horizontal) + { + var initialValue = pixelsOffsetX; + + // ----------- proper to infinite start + + if (option == RelativePositionType.Center) + { + pixelsOffsetX -= Viewport.Pixels.Width / 2f; + } + else + if (option == RelativePositionType.End) + { + pixelsOffsetX -= Viewport.Pixels.Width; + } + + if (pixelsOffsetX > 0) + { + //inverted scroll + //var bak = pixelsOffsetX; + pixelsOffsetX -= Content.MeasuredSize.Pixels.Width; + //Trace.WriteLine($"[INVERT ] {bak:0.0} --> {pixelsOffsetX:0.0}"); + } + else + { + //normal scroll + if (-pixelsOffsetX > Content.MeasuredSize.Pixels.Width) + { + pixelsOffsetX += Content.MeasuredSize.Pixels.Width; + } + } + + //Trace.WriteLine($"[CALC] for {pixelsOffsetX:0.0}"); + // ----------- proper to infinite end + + + var point = new SKPoint( + (float)Math.Abs(pixelsOffsetX), + (float)Math.Abs(pixelsOffsetY) + ); + + + if (layout.Type == LayoutType.Row || layout.Type == LayoutType.Wrap && layout.Split == 0) //todo grid + { + var stackStructure = layout.StackStructure; + int index = -1; + int row; + int col; + + foreach (var childInfo in stackStructure.GetChildren()) + { + index++; + var childRect = childInfo.Destination.Clone(); + //childRect.Offset(point.X, point.Y); + + if (childRect.ContainsInclusive(point)) + { + return new ContainsPointResult() + { + Index = index, + Area = childRect, + Point = point, + Unmodified = new SKPoint(initialValue, 0) + }; + } + } + + } + } + + return ContainsPointResult.NotFound(); + } + + + protected virtual SKPoint ClampedOrderedScrollOffset(SKPoint scrollTo) + { + var scrollSpaceY = ptsContentHeight - Viewport.Units.Height; + + var offsetViewport = Math.Abs(scrollTo.Y) - Viewport.Units.Height; + + if (scrollSpaceY < 0 || offsetViewport < 0) + { + return NotValidPoint(); + } + + return scrollTo; + } + + /// + /// ToDo this actually work only for Stack and Row + /// + /// + /// + /// + public virtual SKPoint CalculateScrollOffsetForIndex(int index, RelativePositionType option) + { + //Debug.WriteLine($"CalculateScrollOffsetForIndex ? {index}"); + + if (Content is SkiaLayout layout) + { + var childrenCount = layout.ChildrenFactory.GetChildrenCount(); + if ( + ptsContentHeight <= 0 || ptsContentWidth <= 0 || + childrenCount == 0 || index < 0 || index >= childrenCount) + { + return NotValidPoint(); //can throw too + } + + var structure = layout.LatestStackStructure; + if (structure != null && structure.GetCount() > 0)// && layout.StackStructure.Count == childrenCount) + { + float offset = 0; + + //in case index falls out of array bounds due to multiple threads.. + try + { + ControlInStack childInfo = null; + + bool isValid = false; + if (Orientation == ScrollOrientation.Horizontal) + { + if (index < structure.MaxColumns) + { + isValid = true; + childInfo = structure.Get(index, 0); + } + } + else + { + if (index < structure.MaxRows) + { + isValid = true; + childInfo = structure.Get(0, index); + } + } + + if (isValid && childInfo.Measured != null) + { + + if (Orientation == ScrollOrientation.Horizontal) + { + //todo rework + var childOffset = childInfo.Destination.Left / (float)layout.RenderingScale; + + if (option == RelativePositionType.End) + { + offset = childOffset - (this.Viewport.Units.Width - childInfo.Measured.Units.Width); + } + else if (option == RelativePositionType.Center) + { + offset = childOffset - + (this.Viewport.Units.Width - childInfo.Measured.Units.Width) / 2f; + } + else + { + offset = childOffset; + } + + return ClampedOrderedScrollOffset(new SKPoint(-offset, 0)); + + + + } + else + if (Orientation == ScrollOrientation.Vertical) + { + var scrollSpaceY = ptsContentHeight - Viewport.Units.Height; + + if (scrollSpaceY > 0) + { + //todo rework + var childOffset = childInfo.Destination.Top / (float)layout.RenderingScale; + + if (option == RelativePositionType.End) + { + offset = childOffset - (this.Viewport.Units.Height - childInfo.Measured.Units.Height); + } + else if (option == RelativePositionType.Center) + { + + offset = childOffset - + (this.Viewport.Units.Height - childInfo.Measured.Units.Height) / 2f; + } + else + { + offset = childOffset; + } + + //Debug.WriteLine($"CalculateScrollOffsetForIndex OK {index} {offset:0.0}"); + + return new SKPoint(0, -offset); + } + + //return ClampedOrderedScrollOffset(new SKPoint(0, -offset)); + + + + + } + + } + + } + catch (Exception e) + { + Trace.WriteLine(e); + } + } + } + + return NotValidPoint(); + } + + + + + protected virtual bool CheckNeedToSnap() + { + bool ret = !(IsSnapping || Snapped + || IsUserFocused + || OrderedScrollTo.IsValid //already scrolling somewhere + || this.SnapToChildren == SnapToChildrenType.Disabled + || _vectorAnimatorBounceY.IsRunning || _vectorAnimatorBounceX.IsRunning + || _animatorFlingX.IsRunning && (Math.Abs(_animatorFlingX.CurrentVelocity) > _minVelocitySnap + || _animatorFlingY.IsRunning && (Math.Abs(_animatorFlingY.CurrentVelocity) > _minVelocitySnap + || Math.Abs(_animatorFlingY.CurrentVelocity) > _minVelocitySnap) + || Math.Abs(_animatorFlingX.CurrentVelocity) > _minVelocitySnap) + ); + + //Trace.WriteLine($"CheckNeedToSnap {ret}"); + + return ret; + } + + public virtual void Snap(float maxTimeSecs) + { + if (OrderedScrollTo.IsValid || IsSnapping) + { + return; + } + + IsSnapping = true; + + if (Content is SkiaLayout layout) + { + var hit = CurrentIndexHit; + if (hit?.Index > -1 && layout.ChildrenFactory.GetChildrenCount() > hit?.Index) + { + //if (hit.Unmodified == SKPoint.Empty) + //{ + // _isSnapping = false; + // return; + //} + + var needMove = 0f; + if (Orientation == ScrollOrientation.Vertical) + { + //float needOffsetY = (float)Math.Truncate(ViewportOffsetY); + float needOffsetY = (float)Math.Truncate(InternalViewportOffset.Pixels.Y); + var initialOffset = needOffsetY; + if (SnapToChildren == SnapToChildrenType.Center) + { + var center = hit.Area.Height / 2f; + var pointY = hit.Area.Bottom - hit.Point.Y; + needMove = -(pointY - center); + } + else if (SnapToChildren == SnapToChildrenType.Side) + { + + if (TrackIndexPosition == RelativePositionType.Start) + { + needMove = hit.Point.Y - hit.Area.Bottom; + } + else if (TrackIndexPosition == RelativePositionType.End) + { + needMove = -(hit.Area.Bottom - hit.Point.Y); + } + } + + var threshold = RenderingScale * 2; + + needOffsetY = hit.Unmodified.Y + needMove; + if (needMove != 0f && Math.Abs(initialOffset - needOffsetY) > threshold) + { + //Snapped = true; + //ScrollTo(InternalViewportOffset.Units.X, needOffsetY / layout.RenderingScale, maxTimeSecs); + + Snapped = true; + + _animatorFlingX.Stop(); + _animatorFlingY.Stop(); + + ScrollTo(ViewportOffsetX, needOffsetY / layout.RenderingScale, AutoScrollingSpeedMs); + + return; + } + + //Trace.WriteLine($"Snap low threshold"); + } + else if (Orientation == ScrollOrientation.Horizontal) + { + float needOffsetX = (float)Math.Truncate(InternalViewportOffset.Units.X); + var initialOffset = needOffsetX; + if (SnapToChildren == SnapToChildrenType.Center) + { + var center = hit.Area.Width / 2f; + var pointX = hit.Area.Right - hit.Point.X; + needMove = -(pointX - center); + } + else if (SnapToChildren == SnapToChildrenType.Side) + { + + if (TrackIndexPosition == RelativePositionType.Start) + { + needMove = hit.Area.Width - (hit.Area.Right - hit.Point.X); + //needOffsetX += needMove; + } + else if (TrackIndexPosition == RelativePositionType.End) + { + needMove = -(hit.Area.Right - hit.Point.X); + //needOffsetX += needMove; + } + } + + needOffsetX = hit.Unmodified.X + needMove; + if (needMove != 0f && initialOffset != needOffsetX) + { + Snapped = true; + + _animatorFlingX.Stop(); + _animatorFlingY.Stop(); + + ScrollTo(needOffsetX / layout.RenderingScale, ViewportOffsetY, AutoScrollingSpeedMs); + + return; + } + + } + } + + } + + IsSnapping = false; + } + + + + public static readonly BindableProperty SnapToChildrenProperty + = BindableProperty.Create(nameof(SnapToChildren), + typeof(SnapToChildrenType), typeof(SkiaScroll), + SnapToChildrenType.Disabled, propertyChanged: NeedDraw); + /// + /// Whether should snap to children after scrolling stopped + /// + public SnapToChildrenType SnapToChildren + { + get + { + return (SnapToChildrenType)GetValue(SnapToChildrenProperty); + } + set + { + SetValue(SnapToChildrenProperty, value); + } + } + + + public static readonly BindableProperty TrackIndexPositionProperty + = BindableProperty.Create(nameof(TrackIndexPosition), + typeof(RelativePositionType), typeof(SkiaScroll), + RelativePositionType.None, propertyChanged: OnTrackingChanged); + + private static void OnTrackingChanged(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaScroll control) + { + control.SetDetectIndexChildPoint(control.TrackIndexPosition); + NeedDraw(bindable, oldvalue, newvalue); + } + } + + /// + /// The position in viewport you want to track for content layout child index + /// + public RelativePositionType TrackIndexPosition + { + get + { + return (RelativePositionType)GetValue(TrackIndexPositionProperty); + } + set + { + SetValue(TrackIndexPositionProperty, value); + } + } + + + public static readonly BindableProperty TrackIndexPositionOffsetProperty = BindableProperty.Create(nameof(TrackIndexPositionOffset), + typeof(float), + typeof(SkiaScroll), + 8.0f, propertyChanged: OnTrackingChanged); + public float TrackIndexPositionOffset + { + get { return (float)GetValue(TrackIndexPositionOffsetProperty); } + set { SetValue(TrackIndexPositionOffsetProperty, value); } + } + + public static readonly BindableProperty LoadMoreCommandProperty = BindableProperty.Create(nameof(LoadMoreCommand), + typeof(ICommand), + typeof(SkiaScroll), + null); + public ICommand LoadMoreCommand + { + get { return (ICommand)GetValue(LoadMoreCommandProperty); } + set { SetValue(LoadMoreCommandProperty, value); } + } + + public static readonly BindableProperty LoadMoreOffsetProperty = BindableProperty.Create(nameof(LoadMoreOffset), + typeof(float), + typeof(SkiaScroll), + 0.0f, propertyChanged: OnTrackingChanged); + public float LoadMoreOffset + { + get { return (float)GetValue(LoadMoreOffsetProperty); } + set { SetValue(LoadMoreOffsetProperty, value); } + } + + + + #endregion + + + protected SKSize LastContentSizePixels = new SKSize(-1, -1); + protected SKSize LastMeasuredSizePixels = new SKSize(-1, -1); + + + protected override void OnMeasured() + { + base.OnMeasured(); + + if (ContentSize.Pixels != LastContentSizePixels || MeasuredSize.Pixels != LastMeasuredSizePixels) + { + LastContentSizePixels = ContentSize.Pixels; + LastMeasuredSizePixels = MeasuredSize.Pixels; + + InitializeViewport((float)RenderingScale); + + InitializeScroller((float)RenderingScale); + } + } + + private PointF lastVelocity; + private double prevV; + private long c1; + + + protected virtual ISkiaGestureListener PassGestureToChildren(SkiaGesturesParameters args, GestureEventProcessingInfo apply) + { + if (IsContentActive) + { + return Content.OnSkiaGestureEvent(args, apply); + } + + return null; + } + + /// + /// Had no panning just down+up with velocity more than threshold + /// + protected bool WasSwiping { get; set; } + + protected bool IsUserFocused { get; set; } + + protected bool IsUserPanning { get; set; } + + public float VelocityY + { + get + { + return _velocityY; + } + + set + { + if (Math.Abs(value) > MaxVelocity) + { + value = MaxVelocity * Math.Sign(value); + } + if (_velocityY != value) + { + _velocityY = value; + OnPropertyChanged(); + } + } + } + float _velocityY; + + public float VelocityX + { + get + { + return _velocityX; + } + + set + { + if (Math.Abs(value) > MaxVelocity) + { + value = MaxVelocity * Math.Sign(value); + } + if (_velocityX != value) + { + _velocityX = value; + OnPropertyChanged(); + } + } + } + float _velocityX; + + + private DateTime lastInputTime; + + bool SameSign(double a, double b) + { + return Math.Sign(a) == Math.Sign(b); + } + + + + public bool SetZoom(double zoom) + { + if (ZoomLocked) + return false; + + //Debug.WriteLine($"[ZOOM] {zoom:0.000}"); + + if (zoom < ZoomMin) + zoom = ZoomMin; + else + if (zoom > ZoomMax) + zoom = ZoomMax; + + ZoomScaleInternal = zoom; + + ViewportZoom = zoom; + return true; + } + + /* + public bool SetZoom(double zoom) + { + if (ZoomLocked) + return false; + + Debug.WriteLine($"[ZOOM] {zoom:0.000}"); + + if (zoom < ZoomMin) + zoom = ZoomMin; + else if (zoom > ZoomMax) + zoom = ZoomMax; + + // Calculate viewport center in screen coordinates + var viewportCenterScreen = new SKPoint((float)(Width / 2), (float)(Height / 2)); + + // Current content scale + var scale = RenderingScale; // Assuming RenderingScale is your base scale factor + var currentContentScale = (float)(scale * ViewportZoom); + + // Current content offset in pixels + var contentOffsetPixels = new SKPoint( + ViewportOffsetX * currentContentScale, + ViewportOffsetY * currentContentScale); + + // Content coordinates of the center before zooming + var contentCenterBeforeZoom = new SKPoint( + (viewportCenterScreen.X - contentOffsetPixels.X) / currentContentScale, + (viewportCenterScreen.Y - contentOffsetPixels.Y) / currentContentScale); + + // Update the zoom level + ZoomScaleInternal = zoom; + ViewportZoom = zoom; + + // New content scale + var newContentScale = (float)(scale * ViewportZoom); + + // Adjust offsets to keep the content centered + ViewportOffsetX = ((viewportCenterScreen.X - (contentCenterBeforeZoom.X * newContentScale)) / newContentScale); + ViewportOffsetY = ((viewportCenterScreen.Y - (contentCenterBeforeZoom.Y * newContentScale)) / newContentScale); + + return true; + } + */ + + /// + /// We might have difference between pinch scale and manually set zoom. + /// + protected double ZoomScaleInternal { get; set; } + + + protected ScaledSize HeaderSize; + protected ScaledSize FooterSize; + + //private Stopwatch measureWatch = new(); + + public override ScaledSize Measure(float widthConstraint, float heightConstraint, float scale) + { + + if (IsMeasuring || !CanDraw || (widthConstraint < 0 || heightConstraint < 0)) + { + return MeasuredSize; + } + + try + { + + //measureWatch.Restart(); + + IsMeasuring = true; + + var request = CreateMeasureRequest(widthConstraint, heightConstraint, scale); + if (request.IsSame) + { + return MeasuredSize; + } + + var constraints = GetMeasuringConstraints(request); + + if (Content != null && Content.IsVisible) + { + var viewport = GetContentAvailableRect(constraints.Content); + + Viewport = ScaledRect.FromPixels(constraints.Content, request.Scale); + + var zoomedScale = (float)(request.Scale * ViewportZoom); + + heightConstraint = viewport.Height; + + var measuredContent = Content.Measure(viewport.Width, heightConstraint, zoomedScale); + + if (ResetScrollPositionOnContentSizeChanged && (ContentSize.Pixels.Height != measuredContent.Pixels.Height || ContentSize.Pixels.Width != measuredContent.Pixels.Width)) + { + if (ViewportOffsetX != 0 || ViewportOffsetY != 0) + ScrollTo(0, 0, 0); + } + + ContentSize = ScaledSize.FromPixels(measuredContent.Pixels.Width, measuredContent.Pixels.Height, scale); + } + else + { + ContentSize = ScaledSize.Default; + } + + if (Header != null) + HeaderSize = Header.Measure(request.WidthRequest, request.HeightRequest, request.Scale); + else + HeaderSize = ScaledSize.Default; + + if (Footer != null) + FooterSize = Footer.Measure(request.WidthRequest, request.HeightRequest, request.Scale); + else + FooterSize = ScaledSize.Default; + + return SetMeasuredAdaptToContentSize(constraints, scale); + } + finally + { + IsMeasuring = false; + //measureWatch.Stop(); + //if (Tag == "Details") + //{ + // Super.Log($"[Scroll] measured {measureWatch.Elapsed.TotalMilliseconds:0.00}ms"); + //} + + } + + } + + + public ScaledRect Viewport { get; protected set; } = new(); + + protected override ScaledSize SetMeasured(float width, float height, bool widthCut, bool heightCut, float scale) + { + if (Content != null) + { + _lastContentSize = this.Content.MeasuredSize; + } + else + _lastContentSize = ScaledSize.Default; + + return base.SetMeasured(width, height, widthCut, heightCut, scale); + } + + + /// + /// In PIXELS + /// + /// + /// + protected SKRect GetContentAvailableRect(SKRect destination) + { + var childRect = new SKRect(destination.Left, destination.Top, destination.Right, destination.Bottom); + + if (Orientation == ScrollOrientation.Both) + { + childRect.Right = float.PositiveInfinity; + childRect.Bottom = float.PositiveInfinity; + } + else + if (Orientation == ScrollOrientation.Vertical) + { + childRect.Right = destination.Right; + childRect.Bottom = float.PositiveInfinity; + } + if (Orientation == ScrollOrientation.Horizontal) + { + childRect.Right = float.PositiveInfinity; + childRect.Bottom = destination.Bottom; + } + + return childRect; + } + + + + /// + /// This is where the view port is actually is after being scrolled. We used this value to offset viewport on drawing the last frame + /// + protected ScaledPoint InternalViewportOffset { get; set; } = ScaledPoint.FromPixels(0, 0, 1); + + protected ScaledRect ContentViewport { get; set; } = new(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual void AdjustHeaderParallax() + { + if (HeaderParallaxRatio == 1) + { + ParallaxComputedValue = 0; + } + else + { + if (this.Orientation == ScrollOrientation.Vertical) + { + var m = InternalViewportOffset.Units.Y * this.HeaderParallaxRatio; + ParallaxComputedValue = m; + } + else + if (this.Orientation == ScrollOrientation.Horizontal) + { + var m = InternalViewportOffset.Units.X * this.HeaderParallaxRatio; + ParallaxComputedValue = m; + } + } + + } + + + + + /// + /// Input offset parameters in PIXELS. We render the scroll Content using pixal snapping but the prepared content will be scrolled (offset) using subpixels for a smooth look. + /// Creates a valid ViewportRect inside. + /// + /// + /// + /// + /// + /// + protected virtual void PositionViewport(SKRect destination, SKPoint offsetPixels, float viewportScale, float scale) + { + if (!IsContentActive || Content == null) + return; + + if (!IsSnapping) + Snapped = false; + + var isScroling = _animatorFlingY.IsRunning || _animatorFlingX.IsRunning || + _vectorAnimatorBounceY.IsRunning || _vectorAnimatorBounceX.IsRunning + || _scrollerX.IsRunning || _scrollerY.IsRunning || IsUserPanning; + + ContentAvailableSpace = GetContentAvailableRect(destination); + + //we scroll at subpixels but stop only at pixel-snapped + //if (IsScrolling && !isScroling && !IsUserPanning || onceAfterInitializeViewport) + //{ + // var roundY = (float)Math.Round(offsetPixels.Y) - offsetPixels.Y; + // var roundX = (float)Math.Round(offsetPixels.X) - offsetPixels.X; + // offsetPixels.Offset(roundX, roundY); + //} + + InternalViewportOffset = ScaledPoint.FromPixels(offsetPixels.X, offsetPixels.Y, scale); //removed pixel rounding + + var childRect = ContentAvailableSpace; + childRect.Offset(InternalViewportOffset.Pixels.X, InternalViewportOffset.Pixels.Y); + + ContentRectWithOffset = ScaledRect.FromPixels(childRect, scale); + + AdjustHeaderParallax(); + + //content size changed?.. maybe need to set offsets to a valid position then + if (onceAfterInitializeViewport) + { + onceAfterInitializeViewport = false; + var clamped = ClampOffset(InternalViewportOffset.Units.X, InternalViewportOffset.Units.Y, true); + //AdjustHeaderParallax(ScaledPoint.FromUnits(clamped.X, clamped.Y, scale)); + + ViewportOffsetX = clamped.X; + ViewportOffsetY = clamped.Y; + + if (ViewportOffsetX == 0 && ViewportOffsetY == 0) + { + HideRefreshIndicator(); + } + } + + OverscrollDistance = CalculateOverscrollDistance(InternalViewportOffset.Units.X, InternalViewportOffset.Units.Y); + + if (Content is IInsideViewport viewport) + { + SKRect absoluteViewPort = DrawingRect; + //var absoluteViewPort = Viewport.Pixels; + //absoluteViewPort.Offset(this.DrawingRect.Left, DrawingRect.Top); + + if (Header != null) + { + if (this.Orientation == ScrollOrientation.Vertical) + { + absoluteViewPort = new SKRect( + absoluteViewPort.Left, + absoluteViewPort.Top - Header.MeasuredSize.Pixels.Height, + absoluteViewPort.Right, + absoluteViewPort.Bottom - Header.MeasuredSize.Pixels.Height + ); + absoluteViewPort.Offset(0, (float)Math.Round(-ContentOffset * scale)); + } + else + if (this.Orientation == ScrollOrientation.Horizontal) + { + absoluteViewPort = new SKRect(absoluteViewPort.Left - Header.MeasuredSize.Pixels.Width, absoluteViewPort.Top, absoluteViewPort.Right - Header.MeasuredSize.Pixels.Width, absoluteViewPort.Bottom); + absoluteViewPort.Offset((float)Math.Round(-ContentOffset * scale), 0); + } + } + + ContentViewport = ScaledRect.FromPixels(absoluteViewPort, scale); + + viewport.OnViewportWasChanged(ContentViewport); + } + + CheckNeedRefresh(); + + if (LoadMoreCommand != null) + { + + if (_loadMoreTriggeredAt != 0 + && Math.Abs(InternalViewportOffset.Units.Y - _loadMoreTriggeredAt) > (LoadMoreOffset + 100) * scale + && (DateTime.Now - _loadMoreTriggeredTime).TotalSeconds > 3) + //we have scrolled out of the triggered loadMore by 100pts + { + _loadMoreTriggeredAt = 0; //so can track loadMore again + } + + if (HasContentToScroll && _loadMoreTriggeredAt == 0) + { + var threshold = LoadMoreOffset * scale; + + if ((Orientation == ScrollOrientation.Vertical && InternalViewportOffset.Units.Y <= _scrollMinY + threshold) + || (Orientation == ScrollOrientation.Horizontal && InternalViewportOffset.Units.X <= _scrollMinX + threshold)) + { + _loadMoreTriggeredTime = DateTime.Now; + _loadMoreTriggeredAt = InternalViewportOffset.Units.Y; + Debug.WriteLine("LoadMoreCommand"); + LoadMoreCommand?.Execute(this); + } + } + + } + + //POST EVENTS + Scrolled?.Invoke(this, InternalViewportOffset); + + OnScrolled(); + + if (isScroling) + { + IsScrolling = true; + } + else + { + if (IsScrolling) + { + ScrollingEnded?.Invoke(this, InternalViewportOffset); + OnScrollingEnded(); + } + IsScrolling = false; + } + + //Super.Log($"[SCROLL] {InternalViewportOffset.Pixels.Y}"); + //ExecuteDelayedScrollOrders(); //todo move toonscrolled? + } + + private bool _IsScrolling; + public bool IsScrolling + { + get + { + return _IsScrolling; + } + set + { + if (_IsScrolling != value) + { + _IsScrolling = value; + OnPropertyChanged(); + } + } + } + + public override ScaledRect GetOnScreenVisibleArea(float inflateByPixels = 0) + { + if (Virtualisation != VirtualisationType.Disabled) //true by default + { + //passing visible area to be rendered + //when scrolling we will pass changed area to be rendered + //most suitable for large content + var inflated = ContentViewport.Pixels; + inflated.Inflate(inflateByPixels, inflateByPixels); + return ScaledRect.FromPixels(inflated, RenderingScale); + } + else + { + //passing the whole area to be rendered. + //when scrolling we will just translate it + //most suitable for small content + return ContentRectWithOffset; + + //absoluteViewPort = new SKRect(Viewport.Pixels.Left, Viewport.Pixels.Top, + // Viewport.Pixels.Left + ContentSize.Pixels.Width, Viewport.Pixels.Top + ContentSize.Pixels.Height); + } + } + + float _loadMoreTriggeredAt; + + + protected virtual void HideRefreshIndicator() + { + RefreshIndicator?.SetDragRatio(0); + ScrollLocked = false; + wasRefreshing = false; + } /// - /// In case if will ignore gestures of the wrong direction, should we lock this direction or multi-directional scrolling (True) is still allowed (False). Default is False. + /// Notify current scroll offset to some dependent views. /// - public bool IgnoreWrongDirectionLock + public virtual void OnScrolled() { - get { return (bool)GetValue(IgnoreWrongDirectionLockProperty); } - set { SetValue(IgnoreWrongDirectionLockProperty, value); } + //if (RefreshIndicator is { IsVisible: true } && OverScrolled) + //{ + // ApplyScrollPositionToRefreshViewUnsafe(); + //} } - */ - public static readonly BindableProperty ResetScrollPositionOnContentSizeChangedProperty = BindableProperty.Create( - nameof(ResetScrollPositionOnContentSizeChanged), - typeof(bool), - typeof(SkiaScroll), - false); - - public bool ResetScrollPositionOnContentSizeChanged - { - get { return (bool)GetValue(ResetScrollPositionOnContentSizeChangedProperty); } - set { SetValue(ResetScrollPositionOnContentSizeChangedProperty, value); } - } - - - /// - /// For when the finger is up and swipe is detected - /// - public float ChangeVelocityScrolled - { - get { return (float)GetValue(ChangeVelocityScrolledProperty); } - set { SetValue(ChangeVelocityScrolledProperty, value); } - } - public static readonly BindableProperty ChangeVelocityScrolledProperty = BindableProperty.Create(nameof(ChangeVelocityScrolled), - typeof(float), - typeof(SkiaScroll), - 1.33f); - - public static readonly BindableProperty MaxVelocityProperty = BindableProperty.Create( - nameof(MaxVelocity), - typeof(float), - typeof(SkiaScroll), - 3000f); - - /// - /// Limit user input velocity - /// - public float MaxVelocity - { - get { return (float)GetValue(MaxVelocityProperty); } - set { SetValue(MaxVelocityProperty, value); } - } - - public static readonly BindableProperty MaxBounceVelocityProperty = BindableProperty.Create( - nameof(MaxBounceVelocity), - typeof(float), - typeof(SkiaScroll), - 500f); - - /// - /// Limit bounce velocity - /// - public float MaxBounceVelocity - { - get { return (float)GetValue(MaxBounceVelocityProperty); } - set { SetValue(MaxBounceVelocityProperty, value); } - } - - /// - /// For when the finger is down and panning - /// - public float ChangeDIstancePanned - { - get { return (float)GetValue(ChangeDIstancePannedProperty); } - set { SetValue(ChangeDIstancePannedProperty, value); } - } - - - public static readonly BindableProperty ChangeDIstancePannedProperty = BindableProperty.Create(nameof(ChangeDIstancePanned), - typeof(float), - typeof(SkiaScroll), - 0.975f); - - - - private static void FrictionValueChanged(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is SkiaScroll control) - { - control.UpdateFriction(); - } - } - - int _currentIndex = -1; - public int CurrentIndex - { - get - { - return _currentIndex; - } - protected set - { - if (_currentIndex != value) - { - _currentIndex = value; - OnPropertyChanged(); - IndexChanged?.Invoke(this, value); - //Debug.WriteLine($"Scroll {Tag} CurrentIndex {value}"); - } - } - } - - public event EventHandler IndexChanged; - - public ContainsPointResult CurrentIndexHit - { - get - { - return _CurrentIndexHit; - } - set - { - if (value != _CurrentIndexHit) - { - _CurrentIndexHit = value; - OnPropertyChanged(); - } - } - } - private ContainsPointResult _CurrentIndexHit; - - void WatchState() - { - - } - - protected SKPoint DetectIndexChildIndexAt; - - void SetDetectIndexChildPoint(RelativePositionType option = RelativePositionType.Start) - { - //todo this will need to change for multiple columns? - - if (!IsContentActive || Content.MeasuredSize == null || TrackIndexPosition == RelativePositionType.None) - return; - - var point = new SKPoint(); - - - if (this.Orientation == ScrollOrientation.Vertical) - { - var endY = this.Viewport.Pixels.Height; - if (this.Content.MeasuredSize.Pixels.Height < endY) - endY = this.Content.MeasuredSize.Pixels.Height; - - if (option == RelativePositionType.End) - { - point.Y += (endY - TrackIndexPositionOffset); - } - else - if (option == RelativePositionType.Center) - { - point.Y += endY / 2f; - } - - point.X = this.Viewport.Pixels.MidX; - } - else - if (this.Orientation == ScrollOrientation.Horizontal) - { - var endX = this.Viewport.Pixels.Width; - if (this.Content.MeasuredSize.Pixels.Width < endX) - endX = this.Content.MeasuredSize.Pixels.Width; - - if (option == RelativePositionType.End) - { - point.X += endX - TrackIndexPositionOffset; - } - else - if (option == RelativePositionType.Center) - { - point.X += endX / 2f; - } - - point.Y = this.Viewport.Pixels.MidY; - } - - //Debug.WriteLine($"[POINT] V {Viewport.Pixels.Bottom} P {point.Y}"); - - DetectIndexChildIndexAt = point; - } - - /// - /// Calculates CurrentIndex - /// - public virtual ContainsPointResult CalculateVisibleIndex(RelativePositionType option) - { - if (Content is SkiaLayout layout) - { - - var pixelsOffsetX = InternalViewportOffset.Pixels.X;// (float)(ViewportOffsetX * layout.RenderingScale); - var pixelsOffsetY = InternalViewportOffset.Pixels.Y;// (float)(ViewportOffsetY * layout.RenderingScale); - - return GetItemIndex(layout, pixelsOffsetX, pixelsOffsetY, option); - } - else - if (Content is ILayoutInsideViewport inside) - { - - var point = new SKPoint( - DetectIndexChildIndexAt.X + InternalViewportOffset.Pixels.X + DrawingRect.Left, - DetectIndexChildIndexAt.Y + InternalViewportOffset.Pixels.Y + DrawingRect.Top); - - var found = inside.GetVisibleChildIndexAt(point); - - if (found.Index != -1) - { - - - - //todo translate found - var area = found.Area; - area.Offset(-DrawingRect.Left, -DrawingRect.Top); - point.Offset(-DrawingRect.Left, -DrawingRect.Top); - return new ContainsPointResult() - { - Index = found.Index, - Area = area, - Point = point, - Unmodified = new(InternalViewportOffset.Pixels.X, InternalViewportOffset.Pixels.Y) - }; - } - - return found; - } - - return ContainsPointResult.NotFound(); - } - - public virtual ContainsPointResult GetItemIndex(SkiaLayout layout, float pixelsOffsetX, float pixelsOffsetY, RelativePositionType option) - { - if (layout.LatestStackStructure == null) - return ContainsPointResult.NotFound(); - - bool trace = false; - - if (this.Orientation == ScrollOrientation.Vertical) - { - var initialValue = pixelsOffsetY; - - // ----------- proper to infinite start - - if (option == RelativePositionType.Center) - { - pixelsOffsetY -= Viewport.Pixels.Height / 2f; - } - else - if (option == RelativePositionType.End) - { - pixelsOffsetY -= Viewport.Pixels.Height; - } - - if (pixelsOffsetY > 0) - { - //inverted scroll - pixelsOffsetY -= Content.MeasuredSize.Pixels.Height; - - } - else - { - //normal scroll - if (-pixelsOffsetY > Content.MeasuredSize.Pixels.Height) - { - pixelsOffsetY += Content.MeasuredSize.Pixels.Height; - } - } - - // ----------- proper to infinite end - - var point = new SKPoint( - (float)Math.Abs(pixelsOffsetX), - (float)Math.Abs(pixelsOffsetY) - ); - - if (layout.Type == LayoutType.Column || layout.Type == LayoutType.Wrap && layout.Split > 0) //todo grid - { - var stackStructure = layout.LatestStackStructure; - int index = -1; - int row; - int col; - - if (trace) - Trace.WriteLine($"offset: {point.Y}"); - - foreach (var childInfo in stackStructure.GetChildren()) - { - index++; - if (childInfo.Destination.ContainsInclusive(point)) - { - return new ContainsPointResult() - { - Index = index, - Area = childInfo.Destination, - Point = point, - Unmodified = new SKPoint(0, initialValue) - }; - } - } - - } - } - else - if (this.Orientation == ScrollOrientation.Horizontal) - { - var initialValue = pixelsOffsetX; - - // ----------- proper to infinite start - - if (option == RelativePositionType.Center) - { - pixelsOffsetX -= Viewport.Pixels.Width / 2f; - } - else - if (option == RelativePositionType.End) - { - pixelsOffsetX -= Viewport.Pixels.Width; - } - - if (pixelsOffsetX > 0) - { - //inverted scroll - //var bak = pixelsOffsetX; - pixelsOffsetX -= Content.MeasuredSize.Pixels.Width; - //Trace.WriteLine($"[INVERT ] {bak:0.0} --> {pixelsOffsetX:0.0}"); - } - else - { - //normal scroll - if (-pixelsOffsetX > Content.MeasuredSize.Pixels.Width) - { - pixelsOffsetX += Content.MeasuredSize.Pixels.Width; - } - } - - //Trace.WriteLine($"[CALC] for {pixelsOffsetX:0.0}"); - // ----------- proper to infinite end - - - var point = new SKPoint( - (float)Math.Abs(pixelsOffsetX), - (float)Math.Abs(pixelsOffsetY) - ); - - - if (layout.Type == LayoutType.Row || layout.Type == LayoutType.Wrap && layout.Split == 0) //todo grid - { - var stackStructure = layout.StackStructure; - int index = -1; - int row; - int col; - - foreach (var childInfo in stackStructure.GetChildren()) - { - index++; - var childRect = childInfo.Destination.Clone(); - //childRect.Offset(point.X, point.Y); - - if (childRect.ContainsInclusive(point)) - { - return new ContainsPointResult() - { - Index = index, - Area = childRect, - Point = point, - Unmodified = new SKPoint(initialValue, 0) - }; - } - } - - } - } - - return ContainsPointResult.NotFound(); - } - - - protected virtual SKPoint ClampedOrderedScrollOffset(SKPoint scrollTo) - { - var scrollSpaceY = ptsContentHeight - Viewport.Units.Height; - - var offsetViewport = Math.Abs(scrollTo.Y) - Viewport.Units.Height; - - if (scrollSpaceY < 0 || offsetViewport < 0) - { - return NotValidPoint(); - } - - return scrollTo; - } - - /// - /// ToDo this actually work only for Stack and Row - /// - /// - /// - /// - public virtual SKPoint CalculateScrollOffsetForIndex(int index, RelativePositionType option) - { - //Debug.WriteLine($"CalculateScrollOffsetForIndex ? {index}"); - - if (Content is SkiaLayout layout) - { - var childrenCount = layout.ChildrenFactory.GetChildrenCount(); - if ( - ptsContentHeight <= 0 || ptsContentWidth <= 0 || - childrenCount == 0 || index < 0 || index >= childrenCount) - { - return NotValidPoint(); //can throw too - } - - var structure = layout.LatestStackStructure; - if (structure != null && structure.GetCount() > 0)// && layout.StackStructure.Count == childrenCount) - { - float offset = 0; - - //in case index falls out of array bounds due to multiple threads.. - try - { - ControlInStack childInfo = null; - - bool isValid = false; - if (Orientation == ScrollOrientation.Horizontal) - { - if (index < structure.MaxColumns) - { - isValid = true; - childInfo = structure.Get(index, 0); - } - } - else - { - if (index < structure.MaxRows) - { - isValid = true; - childInfo = structure.Get(0, index); - } - } - - if (isValid && childInfo.Measured != null) - { - - if (Orientation == ScrollOrientation.Horizontal) - { - //todo rework - var childOffset = childInfo.Destination.Left / (float)layout.RenderingScale; - - if (option == RelativePositionType.End) - { - offset = childOffset - (this.Viewport.Units.Width - childInfo.Measured.Units.Width); - } - else if (option == RelativePositionType.Center) - { - offset = childOffset - - (this.Viewport.Units.Width - childInfo.Measured.Units.Width) / 2f; - } - else - { - offset = childOffset; - } - - return ClampedOrderedScrollOffset(new SKPoint(-offset, 0)); - - - - } - else - if (Orientation == ScrollOrientation.Vertical) - { - var scrollSpaceY = ptsContentHeight - Viewport.Units.Height; - - if (scrollSpaceY > 0) - { - //todo rework - var childOffset = childInfo.Destination.Top / (float)layout.RenderingScale; - - if (option == RelativePositionType.End) - { - offset = childOffset - (this.Viewport.Units.Height - childInfo.Measured.Units.Height); - } - else if (option == RelativePositionType.Center) - { - - offset = childOffset - - (this.Viewport.Units.Height - childInfo.Measured.Units.Height) / 2f; - } - else - { - offset = childOffset; - } - - //Debug.WriteLine($"CalculateScrollOffsetForIndex OK {index} {offset:0.0}"); - - return new SKPoint(0, -offset); - } - - //return ClampedOrderedScrollOffset(new SKPoint(0, -offset)); - - - - - } - - } - - } - catch (Exception e) - { - Trace.WriteLine(e); - } - } - } - - return NotValidPoint(); - } - - - - - protected virtual bool CheckNeedToSnap() - { - bool ret = !(IsSnapping || Snapped - || IsUserFocused - || OrderedScrollTo.IsValid //already scrolling somewhere - || this.SnapToChildren == SnapToChildrenType.Disabled - || _vectorAnimatorBounceY.IsRunning || _vectorAnimatorBounceX.IsRunning - || _animatorFlingX.IsRunning && (Math.Abs(_animatorFlingX.CurrentVelocity) > _minVelocitySnap - || _animatorFlingY.IsRunning && (Math.Abs(_animatorFlingY.CurrentVelocity) > _minVelocitySnap - || Math.Abs(_animatorFlingY.CurrentVelocity) > _minVelocitySnap) - || Math.Abs(_animatorFlingX.CurrentVelocity) > _minVelocitySnap) - ); - - //Trace.WriteLine($"CheckNeedToSnap {ret}"); - - return ret; - } - - public virtual void Snap(float maxTimeSecs) - { - if (OrderedScrollTo.IsValid || IsSnapping) - { - return; - } - - IsSnapping = true; - - if (Content is SkiaLayout layout) - { - var hit = CurrentIndexHit; - if (hit?.Index > -1 && layout.ChildrenFactory.GetChildrenCount() > hit?.Index) - { - //if (hit.Unmodified == SKPoint.Empty) - //{ - // _isSnapping = false; - // return; - //} - - var needMove = 0f; - if (Orientation == ScrollOrientation.Vertical) - { - //float needOffsetY = (float)Math.Truncate(ViewportOffsetY); - float needOffsetY = (float)Math.Truncate(InternalViewportOffset.Pixels.Y); - var initialOffset = needOffsetY; - if (SnapToChildren == SnapToChildrenType.Center) - { - var center = hit.Area.Height / 2f; - var pointY = hit.Area.Bottom - hit.Point.Y; - needMove = -(pointY - center); - } - else if (SnapToChildren == SnapToChildrenType.Side) - { - - if (TrackIndexPosition == RelativePositionType.Start) - { - needMove = hit.Point.Y - hit.Area.Bottom; - } - else if (TrackIndexPosition == RelativePositionType.End) - { - needMove = -(hit.Area.Bottom - hit.Point.Y); - } - } - - var threshold = RenderingScale * 2; - - needOffsetY = hit.Unmodified.Y + needMove; - if (needMove != 0f && Math.Abs(initialOffset - needOffsetY) > threshold) - { - //Snapped = true; - //ScrollTo(InternalViewportOffset.Units.X, needOffsetY / layout.RenderingScale, maxTimeSecs); - - Snapped = true; - - _animatorFlingX.Stop(); - _animatorFlingY.Stop(); - - ScrollTo(ViewportOffsetX, needOffsetY / layout.RenderingScale, AutoScrollingSpeedMs); - - return; - } - - //Trace.WriteLine($"Snap low threshold"); - } - else if (Orientation == ScrollOrientation.Horizontal) - { - float needOffsetX = (float)Math.Truncate(InternalViewportOffset.Units.X); - var initialOffset = needOffsetX; - if (SnapToChildren == SnapToChildrenType.Center) - { - var center = hit.Area.Width / 2f; - var pointX = hit.Area.Right - hit.Point.X; - needMove = -(pointX - center); - } - else if (SnapToChildren == SnapToChildrenType.Side) - { - - if (TrackIndexPosition == RelativePositionType.Start) - { - needMove = hit.Area.Width - (hit.Area.Right - hit.Point.X); - //needOffsetX += needMove; - } - else if (TrackIndexPosition == RelativePositionType.End) - { - needMove = -(hit.Area.Right - hit.Point.X); - //needOffsetX += needMove; - } - } - - needOffsetX = hit.Unmodified.X + needMove; - if (needMove != 0f && initialOffset != needOffsetX) - { - Snapped = true; - - _animatorFlingX.Stop(); - _animatorFlingY.Stop(); - - ScrollTo(needOffsetX / layout.RenderingScale, ViewportOffsetY, AutoScrollingSpeedMs); - - return; - } - - } - } - - } - - IsSnapping = false; - } - - - - public static readonly BindableProperty SnapToChildrenProperty - = BindableProperty.Create(nameof(SnapToChildren), - typeof(SnapToChildrenType), typeof(SkiaScroll), - SnapToChildrenType.Disabled, propertyChanged: NeedDraw); - /// - /// Whether should snap to children after scrolling stopped - /// - public SnapToChildrenType SnapToChildren - { - get - { - return (SnapToChildrenType)GetValue(SnapToChildrenProperty); - } - set - { - SetValue(SnapToChildrenProperty, value); - } - } - - - public static readonly BindableProperty TrackIndexPositionProperty - = BindableProperty.Create(nameof(TrackIndexPosition), - typeof(RelativePositionType), typeof(SkiaScroll), - RelativePositionType.None, propertyChanged: OnTrackingChanged); - - private static void OnTrackingChanged(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is SkiaScroll control) - { - control.SetDetectIndexChildPoint(control.TrackIndexPosition); - NeedDraw(bindable, oldvalue, newvalue); - } - } - - /// - /// The position in viewport you want to track for content layout child index - /// - public RelativePositionType TrackIndexPosition - { - get - { - return (RelativePositionType)GetValue(TrackIndexPositionProperty); - } - set - { - SetValue(TrackIndexPositionProperty, value); - } - } - - - public static readonly BindableProperty TrackIndexPositionOffsetProperty = BindableProperty.Create(nameof(TrackIndexPositionOffset), - typeof(float), - typeof(SkiaScroll), - 8.0f, propertyChanged: OnTrackingChanged); - public float TrackIndexPositionOffset - { - get { return (float)GetValue(TrackIndexPositionOffsetProperty); } - set { SetValue(TrackIndexPositionOffsetProperty, value); } - } - - public static readonly BindableProperty LoadMoreCommandProperty = BindableProperty.Create(nameof(LoadMoreCommand), - typeof(ICommand), - typeof(SkiaScroll), - null); - public ICommand LoadMoreCommand - { - get { return (ICommand)GetValue(LoadMoreCommandProperty); } - set { SetValue(LoadMoreCommandProperty, value); } - } - - public static readonly BindableProperty LoadMoreOffsetProperty = BindableProperty.Create(nameof(LoadMoreOffset), - typeof(float), - typeof(SkiaScroll), - 0.0f, propertyChanged: OnTrackingChanged); - public float LoadMoreOffset - { - get { return (float)GetValue(LoadMoreOffsetProperty); } - set { SetValue(LoadMoreOffsetProperty, value); } - } - - - - #endregion - - - protected SKSize LastContentSizePixels = new SKSize(-1, -1); - protected SKSize LastMeasuredSizePixels = new SKSize(-1, -1); - - - protected override void OnMeasured() - { - base.OnMeasured(); - - if (ContentSize.Pixels != LastContentSizePixels || MeasuredSize.Pixels != LastMeasuredSizePixels) - { - LastContentSizePixels = ContentSize.Pixels; - LastMeasuredSizePixels = MeasuredSize.Pixels; - - InitializeViewport((float)RenderingScale); - - InitializeScroller((float)RenderingScale); - } - } - - private PointF lastVelocity; - private double prevV; - private long c1; - - - protected virtual ISkiaGestureListener PassGestureToChildren(SkiaGesturesParameters args, GestureEventProcessingInfo apply) - { - if (IsContentActive) - { - return Content.OnSkiaGestureEvent(args, apply); - } - - return null; - } - - /// - /// Had no panning just down+up with velocity more than threshold - /// - protected bool WasSwiping { get; set; } - - protected bool IsUserFocused { get; set; } - - protected bool IsUserPanning { get; set; } - - public float VelocityY - { - get - { - return _velocityY; - } - - set - { - if (Math.Abs(value) > MaxVelocity) - { - value = MaxVelocity * Math.Sign(value); - } - if (_velocityY != value) - { - _velocityY = value; - OnPropertyChanged(); - } - } - } - float _velocityY; + public virtual void OnScrollingEnded() + { + + } + + public event EventHandler ScrollingEnded; + + public event EventHandler Scrolled; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual void ApplyScrollPositionToRefreshViewUnsafe() + { + var ratio = 0.0f; + if (Orientation == ScrollOrientation.Vertical) + { + ratio = (OverscrollDistance.Y - RefreshShowDistance) / (RefreshDistanceLimit - RefreshShowDistance); + if (ratio >= 0) + RefreshIndicator.SetDragRatio(ratio); + } + else + if (Orientation == ScrollOrientation.Horizontal) + { + ratio = (OverscrollDistance.X - RefreshShowDistance) / (RefreshDistanceLimit - RefreshShowDistance); + if (ratio >= 0) + RefreshIndicator.SetDragRatio(ratio); + } + + + if (IsUserPanning) + { + if (RefreshCommand != null && ratio >= 1 && !wasRefreshing && !ScrollLocked) + { + SetIsRefreshing(true); + RefreshCommand.Execute(this); + } + } + else + { + HideRefreshIndicator(); + } + } + + public virtual void CheckNeedRefresh() + { + if (IsRefreshing) + { + return; + } + + if (RefreshEnabled && RefreshIndicator != null) + { + if (OverScrolled) + { + ApplyScrollPositionToRefreshViewUnsafe(); + } + else + if (RefreshIndicator.IsVisible) + { + HideRefreshIndicator(); + } + } + } + + bool wasRefreshing; + + public void SetIsRefreshing(bool state) + { + //lock scrolling at top + if (state) + { + wasRefreshing = true; + IsRefreshing = true; + ScrollLocked = true; + } + else + { + if (ViewportOffsetX == 0 && ViewportOffsetY == 0) + { + HideRefreshIndicator(); + } + else + { + ScrollToTop(SystemAnimationTimeSecs); + } + ScrollLocked = false; + } + + } + + //public void CheckSnap(bool force = false) + //{ + // if (this.SnapToChildren != SnapToChildrenType.Disabled && !_isSnapping) + // Task.Run(() => + // { + // if (!_isSnapping) + // SnapIfNeeded(force); + // }).ConfigureAwait(false); + //} + + protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float scale, object arguments) + { + if (destination.Width == 0 || destination.Height == 0) + return; + + base.Paint(ctx, destination, scale, arguments); + + DrawViews(ctx, ContentRectWithOffset.Pixels, _zoomedScale); + + //if (Virtualisation) //true by default + //{ + // DrawViews(ctx, ContentRectWithOffset.Pixels, _zoomedScale); + //} + //else + //{ + // DrawViews(ctx, ContentAvailableSpace, _zoomedScale); + //} + } + + protected override void Draw(SkiaDrawingContext context, SKRect destination, + float scale) + { + + + isDrawing = true; + if (IsContentActive) + { + //content size changed, we need to initialize scroller again at least + if (_lastContentSize != this.Content.MeasuredSize) + { + NeedMeasure = true; + } + } + + Arrange(destination, SizeRequest.Width, SizeRequest.Height, scale); + + _zoomedScale = (float)(scale * ViewportZoom); + + if (!CheckIsGhost()) + { + 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; + + //reposition viewport (scroll) + if (needReposition) + { + _updatedViewportForPixX = posX; + _updatedViewportForPixY = posY; + _destination = destination; + + PositionViewport(DrawingRect, new(posX, posY), _zoomedScale, (float)scale); + + InvalidateCache(); + } + + DrawWithClipAndTransforms(context, DrawingRect, DrawingRect, true, + true, (ctx) => + { + PaintWithEffects(ctx, DrawingRect, scale, CreatePaintArguments()); + }); + + //Paint(context, DrawingRect, scale, CreatePaintArguments()); + } + + FinalizeDrawingWithRenderObject(context, scale); + + OnDrawn(context, DrawingRect, _zoomedScale, scale); + + isDrawing = false; + } + + public double ParallaxComputedValue + { + get => _parallaxComputedValue; + set + { + if (value.Equals(_parallaxComputedValue)) return; + _parallaxComputedValue = value; + OnPropertyChanged(); + } + } + + protected override int DrawViews(SkiaDrawingContext context, SKRect destination, float scale, + bool debug = false) + { + if (destination.Width <= 0 || destination.Height <= 0) + { + return 0; + } + + int Render(SkiaDrawingContext ctx) + { + var drawViews = new List(5) { Content }; + var offsetFooter = 0f; + var translateContent = 0.0; + + if (Header != null) + { + bool drawHeaderBefore = false; + + if (this.Orientation == ScrollOrientation.Vertical) + { + translateContent = Header.MeasuredSize.Units.Height; + + if (!ParallaxOverscrollEnabled) + { + if (OverscrollDistance.Y <= 0) + { + Header.AddTranslationY = ParallaxComputedValue; + } + else + { + Header.AddTranslationY = 0; + } + } + else + { + Header.AddTranslationY = ParallaxComputedValue; + } + + // Adjust the header hitbox for parallax + var headerTop = destination.Top - Header.UseTranslationY; + var headerBottom = headerTop + Header.MeasuredSize.Pixels.Height; + var hitboxHeader = new SKRect(destination.Left, (float)headerTop, destination.Right, (float)headerBottom); + + if (!HeaderBehind && !HeaderSticky) + { + //draw only if onscreen + if (hitboxHeader.IntersectsWith(this.Viewport.Pixels)) + drawHeaderBefore = true; + } + else + { + //will not draw header as one of the views, but as overlay, like refreshview below + translateContent += ContentOffset; + } + + if (Content != null) + Content.AddTranslationY = translateContent; + + offsetFooter += Header.MeasuredSize.Units.Height + (float)ContentOffset; + + if (drawHeaderBefore) + { + drawViews.Add(Header); + } + else + if (HeaderBehind) + { + if (hitboxHeader.IntersectsWith(this.Viewport.Pixels)) + Header.Render(context, DrawingRect, scale); + } + + } + else + if (this.Orientation == ScrollOrientation.Horizontal) + { + translateContent = Header.MeasuredSize.Units.Width; + + if (!ParallaxOverscrollEnabled) + { + if (OverscrollDistance.X <= 0) + { + Header.AddTranslationX = ParallaxComputedValue; + } + else + { + Header.AddTranslationX = 0; + } + } + else + { + Header.AddTranslationX = ParallaxComputedValue; + } + + // Adjust the header hitbox for parallax in horizontal orientation + var headerLeft = destination.Left + Header.UseTranslationX; + var headerRight = headerLeft + Header.MeasuredSize.Pixels.Width; + var hitboxHeader = new SKRect((float)headerLeft, destination.Top, (float)headerRight, destination.Bottom); + + if (!HeaderBehind && !HeaderSticky) + { + // Draw only if onscreen + if (hitboxHeader.IntersectsWith(this.Viewport.Pixels)) + drawHeaderBefore = true; + } + else + { + // Will not draw header as one of the views, but as overlay + translateContent += ContentOffset; + } + + + if (Content != null) + Content.AddTranslationY = translateContent; + + offsetFooter += Header.MeasuredSize.Units.Width + (float)ContentOffset; ; + + if (drawHeaderBefore) + { + drawViews.Add(Header); + } + else + if (HeaderBehind) + { + if (hitboxHeader.IntersectsWith(this.Viewport.Pixels)) + Header.Render(context, DrawingRect, scale); + } + + } + + } + + if (Footer != null) + { + if (this.Orientation == ScrollOrientation.Vertical) + { + if (IsContentActive) + { + offsetFooter += Content.DrawingRect.Height; + } + Footer.AddTranslationY = offsetFooter / scale; + + //draw only if onscreen + var hitbox = new SKRect(destination.Left, destination.Top + offsetFooter, destination.Right, destination.Top + offsetFooter + Footer.MeasuredSize.Pixels.Height); + if (hitbox.IntersectsWith(this.Viewport.Pixels)) + drawViews.Add(Footer); + } + else + if (this.Orientation == ScrollOrientation.Horizontal) + { + if (IsContentActive) + { + offsetFooter += Content.DrawingRect.Width; + } + Footer.AddTranslationX = offsetFooter / scale; + + //draw only if onscreen + var hitbox = new SKRect(destination.Left + offsetFooter, destination.Top, destination.Left + offsetFooter + Footer.MeasuredSize.Pixels.Width, destination.Bottom); + if (hitbox.IntersectsWith(this.Viewport.Pixels)) + drawViews.Add(Footer); + } + + + } + + return RenderViewsList(drawViews, ctx, destination, scale); + } + + var drawn = Render(context); + + if (Header != null && HeaderSticky && !HeaderBehind) + { + Header.Render(context, DrawingRect, scale); + drawn++; + } + + if (RefreshEnabled && RefreshIndicator != null && OverScrolled) + { + if (InternalRefreshIndicator is SkiaControl refreshIndicator) + { + if (refreshIndicator.CanDraw) + { + refreshIndicator.Render(context, DrawingRect, scale); + drawn++; + } + } + } + + return drawn; + } + + + float _updatedViewportForPixX; + float _updatedViewportForPixY; + //float _lastPosViewportScale; + + public SKRect ContentAvailableSpace { get; protected set; } + + /// + /// The viewport for content + /// + public ScaledRect ContentRectWithOffset { get; protected set; } + + public SkiaScroll() : base() + { + Init(); + } + + protected void Init() + { + UpdateFriction(); + SetRefreshIndicator(RefreshIndicator); + } + + public override void SetChildren(IEnumerable views) + { + //do not use subviews as we are using Content property for this control + + return; + } + + public override void ApplyBindingContext() + { + base.ApplyBindingContext(); + + Content?.SetInheritedBindingContext(BindingContext); + } + + /// + /// Use Content property for direct access + /// + /// + protected virtual void SetContent(SkiaControl view) + { + var oldContent = Views.Except(new[] { Footer, Header }).FirstOrDefault(x => x is not IRefreshIndicator); + if (view != oldContent) + { + if (oldContent != null) + { + RemoveSubView(oldContent); + } + if (view != null) + { + AddSubView(view); + } + } + } - public float VelocityX - { - get - { - return _velocityX; - } + public void SetHeader(SkiaControl view) + { + var oldContent = Views.Except(new[] { Footer, Content }).FirstOrDefault(x => x is not IRefreshIndicator); + if (view != oldContent) + { + if (oldContent != null) + { + RemoveSubView(oldContent); + } + if (view != null) + { + view.ZIndex = 1; + AddSubView(view); + } + } + } - set - { - if (Math.Abs(value) > MaxVelocity) - { - value = MaxVelocity * Math.Sign(value); - } - if (_velocityX != value) - { - _velocityX = value; - OnPropertyChanged(); - } - } - } - float _velocityX; + public void SetFooter(SkiaControl view) + { + var oldContent = Views.Except(new[] { Header, Content }).FirstOrDefault(x => x is not IRefreshIndicator); + if (view != oldContent) + { + if (oldContent != null) + { + RemoveSubView(oldContent); + } + if (view != null) + { + AddSubView(view); + } + } + } + #region PROPERTIES - private DateTime lastInputTime; + public static readonly BindableProperty ScrollingSpeedMsProperty = BindableProperty.Create(nameof(ScrollingSpeedMs), + typeof(int), + typeof(SkiaScroll), + 400); - bool SameSign(double a, double b) - { - return Math.Sign(a) == Math.Sign(b); - } + /// + /// Used by range scroller (ScrollToX, ScrollToY) + /// + public int ScrollingSpeedMs + { + get { return (int)GetValue(ScrollingSpeedMsProperty); } + set { SetValue(ScrollingSpeedMsProperty, value); } + } + public static readonly BindableProperty ZoomLockedProperty = BindableProperty.Create(nameof(ZoomLocked), + typeof(bool), + typeof(SkiaScroll), + true); + public bool ZoomLocked + { + get { return (bool)GetValue(ZoomLockedProperty); } + set { SetValue(ZoomLockedProperty, value); } + } - public bool SetZoom(double zoom) - { - if (ZoomLocked) - return false; + public static readonly BindableProperty ZoomMinProperty = BindableProperty.Create(nameof(ZoomMin), + typeof(double), + typeof(SkiaScroll), + 0.1); + public double ZoomMin + { + get { return (double)GetValue(ZoomMinProperty); } + set { SetValue(ZoomMinProperty, value); } + } - //Debug.WriteLine($"[ZOOM] {zoom:0.000}"); + public static readonly BindableProperty ZoomMaxProperty = BindableProperty.Create(nameof(ZoomMax), + typeof(double), + typeof(SkiaScroll), + 10.0); + public double ZoomMax + { + get { return (double)GetValue(ZoomMaxProperty); } + set { SetValue(ZoomMaxProperty, value); } + } - if (zoom < ZoomMin) - zoom = ZoomMin; - else - if (zoom > ZoomMax) - zoom = ZoomMax; - - ZoomScaleInternal = zoom; + public static readonly BindableProperty ViewportZoomProperty = BindableProperty.Create(nameof(ViewportZoom), + typeof(double), typeof(SkiaScroll), + 1.0, + propertyChanged: NeedDraw); + public double ViewportZoom + { + get { return (double)GetValue(ViewportZoomProperty); } + set { SetValue(ViewportZoomProperty, value); } + } - ViewportZoom = zoom; - return true; - } + public static readonly BindableProperty VelocityImageLoaderLockProperty = BindableProperty.Create( + nameof(VelocityImageLoaderLock), + typeof(double), + typeof(SkiaScroll), + 2500.0); - /// - /// We might have difference between pinch scale and manually set zoom. - /// - protected double ZoomScaleInternal { get; set; } - - - protected ScaledSize HeaderSize; - protected ScaledSize FooterSize; - - //private Stopwatch measureWatch = new(); - - public override ScaledSize Measure(float widthConstraint, float heightConstraint, float scale) - { - - if (IsMeasuring || !CanDraw || (widthConstraint < 0 || heightConstraint < 0)) - { - return MeasuredSize; - } - - try - { - - //measureWatch.Restart(); - - IsMeasuring = true; - - var request = CreateMeasureRequest(widthConstraint, heightConstraint, scale); - if (request.IsSame) - { - return MeasuredSize; - } - - var constraints = GetMeasuringConstraints(request); - - if (Content != null && Content.IsVisible) - { - var viewport = GetContentAvailableRect(constraints.Content); - - Viewport = ScaledRect.FromPixels(constraints.Content, request.Scale); - - var zoomedScale = (float)(request.Scale * ViewportZoom); - - heightConstraint = viewport.Height; - - var measuredContent = Content.Measure(viewport.Width, heightConstraint, zoomedScale); - - if (ResetScrollPositionOnContentSizeChanged && (ContentSize.Pixels.Height != measuredContent.Pixels.Height || ContentSize.Pixels.Width != measuredContent.Pixels.Width)) - { - if (ViewportOffsetX != 0 || ViewportOffsetY != 0) - ScrollTo(0, 0, 0); - } - - ContentSize = ScaledSize.FromPixels(measuredContent.Pixels.Width, measuredContent.Pixels.Height, scale); - } - else - { - ContentSize = ScaledSize.Default; - } - - if (Header != null) - HeaderSize = Header.Measure(request.WidthRequest, request.HeightRequest, request.Scale); - else - HeaderSize = ScaledSize.Default; + /// + /// Range at which the image loader will stop or resume loading images while scrolling + /// + public double VelocityImageLoaderLock + { + get { return (double)GetValue(VelocityImageLoaderLockProperty); } + set { SetValue(VelocityImageLoaderLockProperty, value); } + } - if (Footer != null) - FooterSize = Footer.Measure(request.WidthRequest, request.HeightRequest, request.Scale); - else - FooterSize = ScaledSize.Default; - return SetMeasuredAdaptToContentSize(constraints, scale); - } - finally - { - IsMeasuring = false; - //measureWatch.Stop(); - //if (Tag == "Details") - //{ - // Super.Log($"[Scroll] measured {measureWatch.Elapsed.TotalMilliseconds:0.00}ms"); - //} - - } - - } - - - public ScaledRect Viewport { get; protected set; } = new(); - - protected override ScaledSize SetMeasured(float width, float height, bool widthCut, bool heightCut, float scale) - { - if (Content != null) - { - _lastContentSize = this.Content.MeasuredSize; - } - else - _lastContentSize = ScaledSize.Default; - - return base.SetMeasured(width, height, widthCut, heightCut, scale); - } - - - /// - /// In PIXELS - /// - /// - /// - protected SKRect GetContentAvailableRect(SKRect destination) - { - var childRect = new SKRect(destination.Left, destination.Top, destination.Right, destination.Bottom); - - if (Orientation == ScrollOrientation.Both) - { - childRect.Right = float.PositiveInfinity; - childRect.Bottom = float.PositiveInfinity; - } - else - if (Orientation == ScrollOrientation.Vertical) - { - childRect.Right = destination.Right; - childRect.Bottom = float.PositiveInfinity; - } - if (Orientation == ScrollOrientation.Horizontal) - { - childRect.Right = float.PositiveInfinity; - childRect.Bottom = destination.Bottom; - } - - return childRect; - } - - - - /// - /// This is where the view port is actually is after being scrolled. We used this value to offset viewport on drawing the last frame - /// - protected ScaledPoint InternalViewportOffset { get; set; } = ScaledPoint.FromPixels(0, 0, 1); - - protected ScaledRect ContentViewport { get; set; } = new(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected virtual void AdjustHeaderParallax() - { - if (HeaderParallaxRatio == 1) - { - ParallaxComputedValue = 0; - } - else - { - if (this.Orientation == ScrollOrientation.Vertical) - { - var m = InternalViewportOffset.Units.Y * this.HeaderParallaxRatio; - ParallaxComputedValue = m; - } - else - if (this.Orientation == ScrollOrientation.Horizontal) - { - var m = InternalViewportOffset.Units.X * this.HeaderParallaxRatio; - ParallaxComputedValue = m; - } - } - - } - - - - - /// - /// Input offset parameters in PIXELS. We render the scroll Content using pixal snapping but the prepared content will be scrolled (offset) using subpixels for a smooth look. - /// Creates a valid ViewportRect inside. - /// - /// - /// - /// - /// - /// - protected virtual void PositionViewport(SKRect destination, SKPoint offsetPixels, float viewportScale, float scale) - { - if (!IsContentActive || Content == null) - return; - - if (!IsSnapping) - Snapped = false; - - var isScroling = _animatorFlingY.IsRunning || _animatorFlingX.IsRunning || - _vectorAnimatorBounceY.IsRunning || _vectorAnimatorBounceX.IsRunning - || _scrollerX.IsRunning || _scrollerY.IsRunning || IsUserPanning; - - ContentAvailableSpace = GetContentAvailableRect(destination); - - //we scroll at subpixels but stop only at pixel-snapped - //if (IsScrolling && !isScroling && !IsUserPanning || onceAfterInitializeViewport) - //{ - // var roundY = (float)Math.Round(offsetPixels.Y) - offsetPixels.Y; - // var roundX = (float)Math.Round(offsetPixels.X) - offsetPixels.X; - // offsetPixels.Offset(roundX, roundY); - //} - - InternalViewportOffset = ScaledPoint.FromPixels(offsetPixels.X, offsetPixels.Y, scale); //removed pixel rounding - - var childRect = ContentAvailableSpace; - childRect.Offset(InternalViewportOffset.Pixels.X, InternalViewportOffset.Pixels.Y); - - ContentRectWithOffset = ScaledRect.FromPixels(childRect, scale); - - AdjustHeaderParallax(); - - //content size changed?.. maybe need to set offsets to a valid position then - if (onceAfterInitializeViewport) - { - onceAfterInitializeViewport = false; - var clamped = ClampOffset(InternalViewportOffset.Units.X, InternalViewportOffset.Units.Y, true); - //AdjustHeaderParallax(ScaledPoint.FromUnits(clamped.X, clamped.Y, scale)); - - ViewportOffsetX = clamped.X; - ViewportOffsetY = clamped.Y; - - if (ViewportOffsetX == 0 && ViewportOffsetY == 0) - { - HideRefreshIndicator(); - } - } - - OverscrollDistance = CalculateOverscrollDistance(InternalViewportOffset.Units.X, InternalViewportOffset.Units.Y); - - if (Content is IInsideViewport viewport) - { - SKRect absoluteViewPort = DrawingRect; - //var absoluteViewPort = Viewport.Pixels; - //absoluteViewPort.Offset(this.DrawingRect.Left, DrawingRect.Top); - - if (Header != null) - { - if (this.Orientation == ScrollOrientation.Vertical) - { - absoluteViewPort = new SKRect( - absoluteViewPort.Left, - absoluteViewPort.Top - Header.MeasuredSize.Pixels.Height, - absoluteViewPort.Right, - absoluteViewPort.Bottom - Header.MeasuredSize.Pixels.Height - ); - absoluteViewPort.Offset(0, (float)Math.Round(-ContentOffset * scale)); - } - else - if (this.Orientation == ScrollOrientation.Horizontal) - { - absoluteViewPort = new SKRect(absoluteViewPort.Left - Header.MeasuredSize.Pixels.Width, absoluteViewPort.Top, absoluteViewPort.Right - Header.MeasuredSize.Pixels.Width, absoluteViewPort.Bottom); - absoluteViewPort.Offset((float)Math.Round(-ContentOffset * scale), 0); - } - } - - ContentViewport = ScaledRect.FromPixels(absoluteViewPort, scale); - - viewport.OnViewportWasChanged(ContentViewport); - } - - CheckNeedRefresh(); - - if (LoadMoreCommand != null) - { - - if (_loadMoreTriggeredAt != 0 - && Math.Abs(InternalViewportOffset.Units.Y - _loadMoreTriggeredAt) > (LoadMoreOffset + 100) * scale - && (DateTime.Now - _loadMoreTriggeredTime).TotalSeconds > 3) - //we have scrolled out of the triggered loadMore by 100pts - { - _loadMoreTriggeredAt = 0; //so can track loadMore again - } - - if (HasContentToScroll && _loadMoreTriggeredAt == 0) - { - var threshold = LoadMoreOffset * scale; - - if ((Orientation == ScrollOrientation.Vertical && InternalViewportOffset.Units.Y <= _scrollMinY + threshold) - || (Orientation == ScrollOrientation.Horizontal && InternalViewportOffset.Units.X <= _scrollMinX + threshold)) - { - _loadMoreTriggeredTime = DateTime.Now; - _loadMoreTriggeredAt = InternalViewportOffset.Units.Y; - Debug.WriteLine("LoadMoreCommand"); - LoadMoreCommand?.Execute(this); - } - } - - } - - //POST EVENTS - Scrolled?.Invoke(this, InternalViewportOffset); - - OnScrolled(); - - if (isScroling) - { - IsScrolling = true; - } - else - { - if (IsScrolling) - { - ScrollingEnded?.Invoke(this, InternalViewportOffset); - OnScrollingEnded(); - } - IsScrolling = false; - } - - //Super.Log($"[SCROLL] {InternalViewportOffset.Pixels.Y}"); - //ExecuteDelayedScrollOrders(); //todo move toonscrolled? - } - - private bool _IsScrolling; - public bool IsScrolling - { - get - { - return _IsScrolling; - } - set - { - if (_IsScrolling != value) - { - _IsScrolling = value; - OnPropertyChanged(); - } - } - } - - public override ScaledRect GetOnScreenVisibleArea(float inflateByPixels = 0) - { - if (Virtualisation != VirtualisationType.Disabled) //true by default - { - //passing visible area to be rendered - //when scrolling we will pass changed area to be rendered - //most suitable for large content - var inflated = ContentViewport.Pixels; - inflated.Inflate(inflateByPixels, inflateByPixels); - return ScaledRect.FromPixels(inflated, RenderingScale); - } - else - { - //passing the whole area to be rendered. - //when scrolling we will just translate it - //most suitable for small content - return ContentRectWithOffset; - - //absoluteViewPort = new SKRect(Viewport.Pixels.Left, Viewport.Pixels.Top, - // Viewport.Pixels.Left + ContentSize.Pixels.Width, Viewport.Pixels.Top + ContentSize.Pixels.Height); - } - } - - float _loadMoreTriggeredAt; - - - protected virtual void HideRefreshIndicator() - { - RefreshIndicator?.SetDragRatio(0); - ScrollLocked = false; - wasRefreshing = false; - } - - /// - /// Notify current scroll offset to some dependent views. - /// - public virtual void OnScrolled() - { - //if (RefreshIndicator is { IsVisible: true } && OverScrolled) - //{ - // ApplyScrollPositionToRefreshViewUnsafe(); - //} - } - - public virtual void OnScrollingEnded() - { - - } - - public event EventHandler ScrollingEnded; - - public event EventHandler Scrolled; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected virtual void ApplyScrollPositionToRefreshViewUnsafe() - { - var ratio = 0.0f; - if (Orientation == ScrollOrientation.Vertical) - { - ratio = (OverscrollDistance.Y - RefreshShowDistance) / (RefreshDistanceLimit - RefreshShowDistance); - if (ratio >= 0) - RefreshIndicator.SetDragRatio(ratio); - } - else - if (Orientation == ScrollOrientation.Horizontal) - { - ratio = (OverscrollDistance.X - RefreshShowDistance) / (RefreshDistanceLimit - RefreshShowDistance); - if (ratio >= 0) - RefreshIndicator.SetDragRatio(ratio); - } - - - if (IsUserPanning) - { - if (RefreshCommand != null && ratio >= 1 && !wasRefreshing && !ScrollLocked) - { - SetIsRefreshing(true); - RefreshCommand.Execute(this); - } - } - else - { - HideRefreshIndicator(); - } - } - - public virtual void CheckNeedRefresh() - { - if (IsRefreshing) - { - return; - } - - if (RefreshEnabled && RefreshIndicator != null) - { - if (OverScrolled) - { - ApplyScrollPositionToRefreshViewUnsafe(); - } - else - if (RefreshIndicator.IsVisible) - { - HideRefreshIndicator(); - } - } - } - - bool wasRefreshing; - - public void SetIsRefreshing(bool state) - { - //lock scrolling at top - if (state) - { - wasRefreshing = true; - IsRefreshing = true; - ScrollLocked = true; - } - else - { - if (ViewportOffsetX == 0 && ViewportOffsetY == 0) - { - HideRefreshIndicator(); - } - else - { - ScrollToTop(SystemAnimationTimeSecs); - } - ScrollLocked = false; - } - - } - - //public void CheckSnap(bool force = false) - //{ - // if (this.SnapToChildren != SnapToChildrenType.Disabled && !_isSnapping) - // Task.Run(() => - // { - // if (!_isSnapping) - // SnapIfNeeded(force); - // }).ConfigureAwait(false); - //} - - protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float scale, object arguments) - { - if (destination.Width == 0 || destination.Height == 0) - return; - - base.Paint(ctx, destination, scale, arguments); - - DrawViews(ctx, ContentRectWithOffset.Pixels, _zoomedScale); - - //if (Virtualisation) //true by default - //{ - // DrawViews(ctx, ContentRectWithOffset.Pixels, _zoomedScale); - //} - //else - //{ - // DrawViews(ctx, ContentAvailableSpace, _zoomedScale); - //} - } - - protected override void Draw(SkiaDrawingContext context, SKRect destination, - float scale) - { - - - isDrawing = true; - if (IsContentActive) - { - //content size changed, we need to initialize scroller again at least - if (_lastContentSize != this.Content.MeasuredSize) - { - NeedMeasure = true; - } - } - - Arrange(destination, SizeRequest.Width, SizeRequest.Height, scale); - - _zoomedScale = (float)(scale * ViewportZoom); - - if (!CheckIsGhost()) - { - 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; - - //reposition viewport (scroll) - if (needReposition) - { - _updatedViewportForPixX = posX; - _updatedViewportForPixY = posY; - _destination = destination; - - PositionViewport(DrawingRect, new(posX, posY), _zoomedScale, (float)scale); - - InvalidateCache(); - } - - DrawWithClipAndTransforms(context, DrawingRect, DrawingRect, true, - true, (ctx) => - { - PaintWithEffects(ctx, DrawingRect, scale, CreatePaintArguments()); - }); - - //Paint(context, DrawingRect, scale, CreatePaintArguments()); - } - - FinalizeDrawingWithRenderObject(context, scale); - - OnDrawn(context, DrawingRect, _zoomedScale, scale); - - isDrawing = false; - } - - public double ParallaxComputedValue - { - get => _parallaxComputedValue; - set - { - if (value.Equals(_parallaxComputedValue)) return; - _parallaxComputedValue = value; - OnPropertyChanged(); - } - } - - protected override int DrawViews(SkiaDrawingContext context, SKRect destination, float scale, - bool debug = false) - { - if (destination.Width <= 0 || destination.Height <= 0) - { - return 0; - } - - int Render(SkiaDrawingContext ctx) - { - var drawViews = new List(5) { Content }; - var offsetFooter = 0f; - var translateContent = 0.0; - - if (Header != null) - { - bool drawHeaderBefore = false; - - if (this.Orientation == ScrollOrientation.Vertical) - { - translateContent = Header.MeasuredSize.Units.Height; - - if (!ParallaxOverscrollEnabled) - { - if (OverscrollDistance.Y <= 0) - { - Header.AddTranslationY = ParallaxComputedValue; - } - else - { - Header.AddTranslationY = 0; - } - } - else - { - Header.AddTranslationY = ParallaxComputedValue; - } - - // Adjust the header hitbox for parallax - var headerTop = destination.Top - Header.UseTranslationY; - var headerBottom = headerTop + Header.MeasuredSize.Pixels.Height; - var hitboxHeader = new SKRect(destination.Left, (float)headerTop, destination.Right, (float)headerBottom); - - if (!HeaderBehind && !HeaderSticky) - { - //draw only if onscreen - if (hitboxHeader.IntersectsWith(this.Viewport.Pixels)) - drawHeaderBefore = true; - } - else - { - //will not draw header as one of the views, but as overlay, like refreshview below - translateContent += ContentOffset; - } - - if (Content != null) - Content.AddTranslationY = translateContent; - - offsetFooter += Header.MeasuredSize.Units.Height + (float)ContentOffset; - - if (drawHeaderBefore) - { - drawViews.Add(Header); - } - else - if (HeaderBehind) - { - if (hitboxHeader.IntersectsWith(this.Viewport.Pixels)) - Header.Render(context, DrawingRect, scale); - } - - } - else - if (this.Orientation == ScrollOrientation.Horizontal) - { - translateContent = Header.MeasuredSize.Units.Width; - - if (!ParallaxOverscrollEnabled) - { - if (OverscrollDistance.X <= 0) - { - Header.AddTranslationX = ParallaxComputedValue; - } - else - { - Header.AddTranslationX = 0; - } - } - else - { - Header.AddTranslationX = ParallaxComputedValue; - } - - // Adjust the header hitbox for parallax in horizontal orientation - var headerLeft = destination.Left + Header.UseTranslationX; - var headerRight = headerLeft + Header.MeasuredSize.Pixels.Width; - var hitboxHeader = new SKRect((float)headerLeft, destination.Top, (float)headerRight, destination.Bottom); - - if (!HeaderBehind && !HeaderSticky) - { - // Draw only if onscreen - if (hitboxHeader.IntersectsWith(this.Viewport.Pixels)) - drawHeaderBefore = true; - } - else - { - // Will not draw header as one of the views, but as overlay - translateContent += ContentOffset; - } - - - if (Content != null) - Content.AddTranslationY = translateContent; - - offsetFooter += Header.MeasuredSize.Units.Width + (float)ContentOffset; ; - - if (drawHeaderBefore) - { - drawViews.Add(Header); - } - else - if (HeaderBehind) - { - if (hitboxHeader.IntersectsWith(this.Viewport.Pixels)) - Header.Render(context, DrawingRect, scale); - } - - } - - } - - if (Footer != null) - { - if (this.Orientation == ScrollOrientation.Vertical) - { - if (IsContentActive) - { - offsetFooter += Content.DrawingRect.Height; - } - Footer.AddTranslationY = offsetFooter / scale; - - //draw only if onscreen - var hitbox = new SKRect(destination.Left, destination.Top + offsetFooter, destination.Right, destination.Top + offsetFooter + Footer.MeasuredSize.Pixels.Height); - if (hitbox.IntersectsWith(this.Viewport.Pixels)) - drawViews.Add(Footer); - } - else - if (this.Orientation == ScrollOrientation.Horizontal) - { - if (IsContentActive) - { - offsetFooter += Content.DrawingRect.Width; - } - Footer.AddTranslationX = offsetFooter / scale; - - //draw only if onscreen - var hitbox = new SKRect(destination.Left + offsetFooter, destination.Top, destination.Left + offsetFooter + Footer.MeasuredSize.Pixels.Width, destination.Bottom); - if (hitbox.IntersectsWith(this.Viewport.Pixels)) - drawViews.Add(Footer); - } - - - } - - return RenderViewsList(drawViews, ctx, destination, scale); - } - - var drawn = Render(context); - - if (Header != null && HeaderSticky && !HeaderBehind) - { - Header.Render(context, DrawingRect, scale); - drawn++; - } - - if (RefreshEnabled && RefreshIndicator != null && OverScrolled) - { - if (InternalRefreshIndicator is SkiaControl refreshIndicator) - { - if (refreshIndicator.CanDraw) - { - refreshIndicator.Render(context, DrawingRect, scale); - drawn++; - } - } - } - - return drawn; - } - - - float _updatedViewportForPixX; - float _updatedViewportForPixY; - //float _lastPosViewportScale; - - public SKRect ContentAvailableSpace { get; protected set; } - - /// - /// The viewport for content - /// - public ScaledRect ContentRectWithOffset { get; protected set; } - - public SkiaScroll() : base() - { - Init(); - } - - protected void Init() - { - UpdateFriction(); - SetRefreshIndicator(RefreshIndicator); - } - - public override void SetChildren(IEnumerable views) - { - //do not use subviews as we are using Content property for this control - - return; - } - - public override void ApplyBindingContext() - { - base.ApplyBindingContext(); - - if (this.Content != null && Content.BindingContext == null) //todo remove this last condition! - { - Content.BindingContext = BindingContext; - } - } - - /// - /// Use Content property for direct access - /// - /// - protected virtual void SetContent(SkiaControl view) - { - var oldContent = Views.Except(new[] { Footer, Header }).FirstOrDefault(x => x is not IRefreshIndicator); - if (view != oldContent) - { - if (oldContent != null) - { - RemoveSubView(oldContent); - } - if (view != null) - { - AddSubView(view); - } - } - } - - public void SetHeader(SkiaControl view) - { - var oldContent = Views.Except(new[] { Footer, Content }).FirstOrDefault(x => x is not IRefreshIndicator); - if (view != oldContent) - { - if (oldContent != null) - { - RemoveSubView(oldContent); - } - if (view != null) - { - view.ZIndex = 1; - AddSubView(view); - } - } - } - - public void SetFooter(SkiaControl view) - { - var oldContent = Views.Except(new[] { Header, Content }).FirstOrDefault(x => x is not IRefreshIndicator); - if (view != oldContent) - { - if (oldContent != null) - { - RemoveSubView(oldContent); - } - if (view != null) - { - AddSubView(view); - } - } - } - - #region PROPERTIES - - public static readonly BindableProperty ScrollingSpeedMsProperty = BindableProperty.Create(nameof(ScrollingSpeedMs), - typeof(int), - typeof(SkiaScroll), - 400); - - /// - /// Used by range scroller (ScrollToX, ScrollToY) - /// - public int ScrollingSpeedMs - { - get { return (int)GetValue(ScrollingSpeedMsProperty); } - set { SetValue(ScrollingSpeedMsProperty, value); } - } - - public static readonly BindableProperty ZoomLockedProperty = BindableProperty.Create(nameof(ZoomLocked), - typeof(bool), - typeof(SkiaScroll), - true); - public bool ZoomLocked - { - get { return (bool)GetValue(ZoomLockedProperty); } - set { SetValue(ZoomLockedProperty, value); } - } - - - public static readonly BindableProperty ZoomMinProperty = BindableProperty.Create(nameof(ZoomMin), - typeof(double), - typeof(SkiaScroll), - 0.1); - public double ZoomMin - { - get { return (double)GetValue(ZoomMinProperty); } - set { SetValue(ZoomMinProperty, value); } - } - - public static readonly BindableProperty ZoomMaxProperty = BindableProperty.Create(nameof(ZoomMax), - typeof(double), - typeof(SkiaScroll), - 10.0); - public double ZoomMax - { - get { return (double)GetValue(ZoomMaxProperty); } - set { SetValue(ZoomMaxProperty, value); } - } - - public static readonly BindableProperty ViewportZoomProperty = BindableProperty.Create(nameof(ViewportZoom), - typeof(double), typeof(SkiaScroll), - 1.0, - propertyChanged: NeedDraw); - public double ViewportZoom - { - get { return (double)GetValue(ViewportZoomProperty); } - set { SetValue(ViewportZoomProperty, value); } - } - - public static readonly BindableProperty VelocityImageLoaderLockProperty = BindableProperty.Create( - nameof(VelocityImageLoaderLock), - typeof(double), - typeof(SkiaScroll), - 2500.0); - - /// - /// Range at which the image loader will stop or resume loading images while scrolling - /// - public double VelocityImageLoaderLock - { - get { return (double)GetValue(VelocityImageLoaderLockProperty); } - set { SetValue(VelocityImageLoaderLockProperty, value); } - } - - - /* + /* public static readonly BindableProperty ViewportOffsetYProperty = BindableProperty.Create(nameof(ViewportOffsetY), typeof(double), typeof(SkiaScroll), 0.0, @@ -3056,211 +3113,211 @@ public double ViewportOffsetX } } */ - public static readonly BindableProperty ContentProperty = BindableProperty.Create( - nameof(Content), - typeof(SkiaControl), typeof(SkiaScroll), - null, - propertyChanged: OnReplaceContent); - - private static void OnReplaceContent(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is SkiaScroll control) - { - control.SetContent(newvalue as SkiaControl); - } - } - public SkiaControl Content - { - get { return (SkiaControl)GetValue(ContentProperty); } - set { SetValue(ContentProperty, value); } - } - - public static readonly BindableProperty OrientationProperty = BindableProperty.Create(nameof(Orientation), typeof(ScrollOrientation), typeof(SkiaScroll), - ScrollOrientation.Vertical, - propertyChanged: NeedDraw); - /// - /// Gets or sets the scrolling direction of the ScrollView. This is a bindable property. - /// - public ScrollOrientation Orientation - { - get { return (ScrollOrientation)GetValue(OrientationProperty); } - set { SetValue(OrientationProperty, value); } - } - - - public static readonly BindableProperty ScrollTypeProperty = BindableProperty.Create(nameof(ViewportScrollType), typeof(ViewportScrollType), typeof(SkiaScroll), - ViewportScrollType.Scrollable, - propertyChanged: NeedDraw); - /// - /// Gets or sets the scrolling direction of the ScrollView. This is a bindable property. - /// - public ViewportScrollType ScrollType - { - get { return (ViewportScrollType)GetValue(ScrollTypeProperty); } - set { SetValue(ScrollTypeProperty, value); } - } - - public static readonly BindableProperty VirtualisationProperty = BindableProperty.Create( - nameof(Virtualisation), - typeof(VirtualisationType), - typeof(SkiaScroll), - VirtualisationType.Enabled, - propertyChanged: NeedInvalidateMeasure); - - /// - /// Default is true, children get the visible viewport area for rendering and can virtualize. - /// If set to false children get the full content area for rendering and draw all at once. - /// - public VirtualisationType Virtualisation - { - get { return (VirtualisationType)GetValue(VirtualisationProperty); } - set { SetValue(VirtualisationProperty, value); } - } - - - //todo ZOOM - - - #endregion - - #region ScrollViewKeyboardAwareBehavior - - public static readonly BindableProperty AdaptToKeyboardForProperty = BindableProperty.Create( - nameof(AdaptToKeyboardFor), - typeof(SkiaControl), - typeof(SkiaScroll), - null, propertyChanged: OnNeedAdaptToKeyboard); - - public SkiaControl AdaptToKeyboardFor - { - get { return (SkiaControl)GetValue(AdaptToKeyboardForProperty); } - set { SetValue(AdaptToKeyboardForProperty, value); } - } - - public static readonly BindableProperty AdaptToKeyboardSizeProperty = BindableProperty.Create( - nameof(AdaptToKeyboardSize), - typeof(double), - typeof(SkiaScroll), - 0.0, propertyChanged: OnNeedAdaptToKeyboard); - - public double AdaptToKeyboardSize - { - get { return (double)GetValue(AdaptToKeyboardSizeProperty); } - set { SetValue(AdaptToKeyboardSizeProperty, value); } - } - - private static void OnNeedAdaptToKeyboard(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is SkiaScroll control) - { - control.AdaptToKeyboard(); - } - } - - double AddPadding = 0; - private double _scrollTo; - - public void CalculateNeededScrollForKeyboard() - { - _scrollTo = -1; - - try - { - if (AdaptToKeyboardFor == null || AdaptToKeyboardSize == 0 || !this.LayoutReady) - return; - - var myPos = AdaptToKeyboardFor.GetPositionOnCanvasInPoints(); - var scrollPos = this.GetPositionOnCanvasInPoints(); - - var scrollRect = new SKRect(0, scrollPos.Y, 10, (float)this.Height + scrollPos.Y); - var parentHeight = Superview.Height; - var screenRect = new SKRect(0, 0, 10, (float)(parentHeight - AdaptToKeyboardSize)); - var viewportRect = scrollRect.IntersectWith(screenRect); - var elementRect = new SKRect(0, myPos.Y, 10, (float)AdaptToKeyboardFor.Height + myPos.Y); + public static readonly BindableProperty ContentProperty = BindableProperty.Create( + nameof(Content), + typeof(SkiaControl), typeof(SkiaScroll), + null, + propertyChanged: OnReplaceContent); + + private static void OnReplaceContent(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaScroll control) + { + control.SetContent(newvalue as SkiaControl); + } + } + public SkiaControl Content + { + get { return (SkiaControl)GetValue(ContentProperty); } + set { SetValue(ContentProperty, value); } + } + + public static readonly BindableProperty OrientationProperty = BindableProperty.Create(nameof(Orientation), typeof(ScrollOrientation), typeof(SkiaScroll), + ScrollOrientation.Vertical, + propertyChanged: NeedDraw); + /// + /// Gets or sets the scrolling direction of the ScrollView. This is a bindable property. + /// + public ScrollOrientation Orientation + { + get { return (ScrollOrientation)GetValue(OrientationProperty); } + set { SetValue(OrientationProperty, value); } + } + + + public static readonly BindableProperty ScrollTypeProperty = BindableProperty.Create(nameof(ViewportScrollType), typeof(ViewportScrollType), typeof(SkiaScroll), + ViewportScrollType.Scrollable, + propertyChanged: NeedDraw); + /// + /// Gets or sets the scrolling direction of the ScrollView. This is a bindable property. + /// + public ViewportScrollType ScrollType + { + get { return (ViewportScrollType)GetValue(ScrollTypeProperty); } + set { SetValue(ScrollTypeProperty, value); } + } + + public static readonly BindableProperty VirtualisationProperty = BindableProperty.Create( + nameof(Virtualisation), + typeof(VirtualisationType), + typeof(SkiaScroll), + VirtualisationType.Enabled, + propertyChanged: NeedInvalidateMeasure); + + /// + /// Default is true, children get the visible viewport area for rendering and can virtualize. + /// If set to false children get the full content area for rendering and draw all at once. + /// + public VirtualisationType Virtualisation + { + get { return (VirtualisationType)GetValue(VirtualisationProperty); } + set { SetValue(VirtualisationProperty, value); } + } + + + //todo ZOOM + + + #endregion + + #region ScrollViewKeyboardAwareBehavior + + public static readonly BindableProperty AdaptToKeyboardForProperty = BindableProperty.Create( + nameof(AdaptToKeyboardFor), + typeof(SkiaControl), + typeof(SkiaScroll), + null, propertyChanged: OnNeedAdaptToKeyboard); + + public SkiaControl AdaptToKeyboardFor + { + get { return (SkiaControl)GetValue(AdaptToKeyboardForProperty); } + set { SetValue(AdaptToKeyboardForProperty, value); } + } + + public static readonly BindableProperty AdaptToKeyboardSizeProperty = BindableProperty.Create( + nameof(AdaptToKeyboardSize), + typeof(double), + typeof(SkiaScroll), + 0.0, propertyChanged: OnNeedAdaptToKeyboard); + + public double AdaptToKeyboardSize + { + get { return (double)GetValue(AdaptToKeyboardSizeProperty); } + set { SetValue(AdaptToKeyboardSizeProperty, value); } + } + + private static void OnNeedAdaptToKeyboard(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is SkiaScroll control) + { + control.AdaptToKeyboard(); + } + } + + double AddPadding = 0; + private double _scrollTo; + + public void CalculateNeededScrollForKeyboard() + { + _scrollTo = -1; + + try + { + if (AdaptToKeyboardFor == null || AdaptToKeyboardSize == 0 || !this.LayoutReady) + return; - var needScrollMore = elementRect.Bottom - viewportRect.Bottom + AddPadding; + var myPos = AdaptToKeyboardFor.GetPositionOnCanvasInPoints(); + var scrollPos = this.GetPositionOnCanvasInPoints(); - if (needScrollMore > 0) - _scrollTo = this.ViewportOffsetY - needScrollMore; + var scrollRect = new SKRect(0, scrollPos.Y, 10, (float)this.Height + scrollPos.Y); + var parentHeight = Superview.Height; + var screenRect = new SKRect(0, 0, 10, (float)(parentHeight - AdaptToKeyboardSize)); + var viewportRect = scrollRect.IntersectWith(screenRect); + var elementRect = new SKRect(0, myPos.Y, 10, (float)AdaptToKeyboardFor.Height + myPos.Y); - } - catch (Exception e) - { - Trace.WriteLine(e); - } + var needScrollMore = elementRect.Bottom - viewportRect.Bottom + AddPadding; - } + if (needScrollMore > 0) + _scrollTo = this.ViewportOffsetY - needScrollMore; - public virtual void AdaptToKeyboard() - { - Tasks.StartDelayed(TimeSpan.FromMilliseconds(150), () => - { - CalculateNeededScrollForKeyboard(); + } + catch (Exception e) + { + Trace.WriteLine(e); + } + + } - //scroll to show on screen - if (LayoutReady && _scrollTo < 0) - { - //Debug.WriteLine($"[SCROLLING] to {_scrollTo} actual offset {this.OffsetY}, last {lastOffsetY}"); - ViewportOffsetY = (float)_scrollTo; - } - }); - } + public virtual void AdaptToKeyboard() + { + Tasks.StartDelayed(TimeSpan.FromMilliseconds(150), () => + { + CalculateNeededScrollForKeyboard(); + + //scroll to show on screen + if (LayoutReady && _scrollTo < 0) + { + //Debug.WriteLine($"[SCROLLING] to {_scrollTo} actual offset {this.OffsetY}, last {lastOffsetY}"); + ViewportOffsetY = (float)_scrollTo; + } + }); + } - #endregion + #endregion - protected override void OnPropertyChanged([CallerMemberName] string propertyName = "") - { - base.OnPropertyChanged(propertyName); + protected override void OnPropertyChanged([CallerMemberName] string propertyName = "") + { + base.OnPropertyChanged(propertyName); - if (propertyName == nameof(ViewportZoom) - || propertyName == nameof(Orientation)) - { - Invalidate(); - } - } + if (propertyName == nameof(ViewportZoom) + || propertyName == nameof(Orientation)) + { + Invalidate(); + } + } - public override void InvalidateViewport() - { - //owns viewport - Repaint(); - } + public override void InvalidateViewport() + { + //owns viewport + Repaint(); + } - #region RENDERiNG + #region RENDERiNG - public override bool WillClipBounds => true; + public override bool WillClipBounds => true; - bool isDrawing; - private SKRect _destination; - private ScaledSize _lastContentSize; - private float _velocityKY; - private float _velocityKX; - private float _zoomedScale = 1; - private double _LastPanDistanceY; - private double _LastPanDistanceX; - private DateTime _loadMoreTriggeredTime; - private double _parallaxComputedValue; - private float _offsetMoved; - private long _offsetMovedTime; + bool isDrawing; + private SKRect _destination; + private ScaledSize _lastContentSize; + private float _velocityKY; + private float _velocityKX; + private float _zoomedScale = 1; + private double _LastPanDistanceY; + private double _LastPanDistanceX; + private DateTime _loadMoreTriggeredTime; + private double _parallaxComputedValue; + private float _offsetMoved; + private long _offsetMovedTime; - protected virtual void OnDrawn(SkiaDrawingContext context, SKRect destination, - float zoomedScale, - double scale = 1.0) - { + protected virtual void OnDrawn(SkiaDrawingContext context, SKRect destination, + float zoomedScale, + double scale = 1.0) + { - } + } - //public Action Measured { get; set; } + //public Action Measured { get; set; } - #endregion + #endregion - } + } } diff --git a/src/Engine/Draw/SkiaBackdrop.cs b/src/Engine/Draw/SkiaBackdrop.cs index f8040f88..f31c62d2 100644 --- a/src/Engine/Draw/SkiaBackdrop.cs +++ b/src/Engine/Draw/SkiaBackdrop.cs @@ -134,6 +134,14 @@ public override void OnDisposing() } } + public override bool CanUseCacheDoubleBuffering + { + get + { + return false; // we cannot make surface snapshots in background yet with current renderers + } + } + protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float scale, object arguments) { if (IsDisposed || IsDisposing) @@ -186,6 +194,7 @@ protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float } else { + SKImage snapshot; if (UseContext) { @@ -208,6 +217,7 @@ protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float Snapshot = snapshot; kill?.Dispose(); } + } } diff --git a/src/Engine/Draw/Text/UsedGlyph.cs b/src/Engine/Draw/Text/UsedGlyph.cs index 7a707da0..cfb40d13 100644 --- a/src/Engine/Draw/Text/UsedGlyph.cs +++ b/src/Engine/Draw/Text/UsedGlyph.cs @@ -10,6 +10,8 @@ public struct UsedGlyph public bool IsAvailable { get; set; } + + public bool IsNumber() { // This will only work correctly if Text is a single character. diff --git a/src/Engine/DrawnUi.Maui.csproj b/src/Engine/DrawnUi.Maui.csproj index 7ed105dd..d7c25ce1 100644 --- a/src/Engine/DrawnUi.Maui.csproj +++ b/src/Engine/DrawnUi.Maui.csproj @@ -4,8 +4,9 @@ $(TargetFrameworks);net8.0-windows10.0.19041.0 true true - enable - + enable + true + 14.2 14.0 21.0 @@ -72,7 +73,11 @@ - + + + + + True diff --git a/src/Engine/Features/Animations/Animators/PingPongAnimator.cs b/src/Engine/Features/Animations/Animators/PingPongAnimator.cs index 24c8e7c8..58c69edd 100644 --- a/src/Engine/Features/Animations/Animators/PingPongAnimator.cs +++ b/src/Engine/Features/Animations/Animators/PingPongAnimator.cs @@ -9,11 +9,15 @@ public PingPongAnimator(SkiaControl player) : base(player) protected override bool FinishedRunning() { + + if (Repeat < 0) //forever { + CycleFInished?.Invoke(); + (mMaxValue, mMinValue) = (mMinValue, mMaxValue); Distance = mMaxValue - mMinValue; - + mValue = mMinValue; mLastFrameTime = 0; mStartFrameTime = 0; @@ -21,16 +25,19 @@ protected override bool FinishedRunning() } else if (Repeat > 0) { + CycleFInished?.Invoke(); + Repeat--; (mMaxValue, mMinValue) = (mMinValue, mMaxValue); Distance = mMaxValue - mMinValue; - + mValue = mMinValue; mLastFrameTime = 0; mStartFrameTime = 0; return false; } + return base.FinishedRunning(); } } \ No newline at end of file diff --git a/src/Engine/Features/Animations/Animators/RangeVectorAnimator.cs b/src/Engine/Features/Animations/Animators/RangeVectorAnimator.cs index 6bbe3ffb..262cb57a 100644 --- a/src/Engine/Features/Animations/Animators/RangeVectorAnimator.cs +++ b/src/Engine/Features/Animations/Animators/RangeVectorAnimator.cs @@ -47,7 +47,7 @@ protected override bool UpdateValue(long deltaT, long deltaFromStart) { var secs = deltaFromStart / 1_000_000_000.0f; - if (secs > Parameters.DurationSecs) + if (secs >= Parameters.DurationSecs) { Vector = _end; //Parameters.ValueAt(Parameters.DurationSecs); return true; diff --git a/src/Engine/Features/Animations/Animators/SkiaValueAnimator.cs b/src/Engine/Features/Animations/Animators/SkiaValueAnimator.cs index eb894605..b635eb66 100644 --- a/src/Engine/Features/Animations/Animators/SkiaValueAnimator.cs +++ b/src/Engine/Features/Animations/Animators/SkiaValueAnimator.cs @@ -72,8 +72,20 @@ public void Seek(float msTime) } + /// + /// Animator self finished a cycle, might still repeat + /// + public Action CycleFInished { get; set; } + + /// + /// Animator self finished running without being stopped manually + /// + public Action Finished { get; set; } + protected virtual bool FinishedRunning() { + CycleFInished?.Invoke(); + bool finished = true; if (Repeat < 0) //forever { @@ -93,6 +105,7 @@ protected virtual bool FinishedRunning() else { Stop(); + Finished?.Invoke(); } return finished; } diff --git a/src/Engine/Features/Effects/IStateEffect.cs b/src/Engine/Features/Effects/IStateEffect.cs index 1d4ce0e4..384b6e12 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. By SkiaControl.OnBeforeDrawing method. + /// Will be invoked before actually painting but after gestures processing and other internal calculations. By SkiaControl.OnBeforeDrawing method. Beware if you call Update() inside will never stop updating. /// void UpdateState(); } \ No newline at end of file diff --git a/src/Engine/Features/Effects/SkiaDoubleAttachedTexturesEffect.cs b/src/Engine/Features/Effects/SkiaDoubleAttachedTexturesEffect.cs new file mode 100644 index 00000000..443c93e1 --- /dev/null +++ b/src/Engine/Features/Effects/SkiaDoubleAttachedTexturesEffect.cs @@ -0,0 +1,6 @@ +namespace DrawnUi.Maui.Draw; + +public class SkiaDoubleAttachedTexturesEffect : SkiaShaderEffect +{ + +} \ No newline at end of file diff --git a/src/Engine/Features/Effects/SkiaShaderEffect.cs b/src/Engine/Features/Effects/SkiaShaderEffect.cs index 11e675f6..d2d31fb0 100644 --- a/src/Engine/Features/Effects/SkiaShaderEffect.cs +++ b/src/Engine/Features/Effects/SkiaShaderEffect.cs @@ -2,19 +2,9 @@ namespace DrawnUi.Maui.Draw; - -public class StateEffect : SkiaEffect, IStateEffect -{ - public virtual void UpdateState() - { - - } -} - - public class SkiaShaderEffect : SkiaEffect, IPostRendererEffect { - protected SKPaint _paintWithShader; + protected SKPaint PaintWithShader; public static readonly BindableProperty UseContextProperty = BindableProperty.Create(nameof(UseContext), typeof(bool), @@ -47,7 +37,7 @@ public bool AutoCreateInputTexture } /// - /// Create snapshot from the current parent drawing state to use as input texture for the shader + /// Create snapshot from the current parent control drawing state to use as input texture for the shader /// /// /// @@ -72,11 +62,26 @@ protected virtual SKImage CreateSnapshot(SkiaDrawingContext ctx, SKRect destinat } + protected virtual SKImage GetPrimaryTextureImage(SkiaDrawingContext ctx, SKRect destination) + { + if (Parent?.RenderObject?.Image == null && AutoCreateInputTexture) + { + return CreateSnapshot(ctx, destination); + } + + return Parent?.RenderObject?.Image; + } + + /// + /// EffectPostRenderer + /// + /// + /// public virtual void Render(SkiaDrawingContext ctx, SKRect destination) { - if (_paintWithShader == null) + if (PaintWithShader == null) { - _paintWithShader = new SKPaint() + PaintWithShader = new SKPaint() { //todo check how if this affect anything after upcoming skiasharp3 fix //FilterQuality = SKFilterQuality.High, @@ -84,11 +89,11 @@ public virtual void Render(SkiaDrawingContext ctx, SKRect destination) }; } - SKImage source = Parent.RenderObject.Image; + SKImage source = GetPrimaryTextureImage(ctx, destination); - _paintWithShader.Shader = CreateShader(ctx, destination, source); + PaintWithShader.Shader = CreateShader(ctx, destination, source); - ctx.Canvas.DrawRect(destination, _paintWithShader); + ctx.Canvas.DrawRect(destination, PaintWithShader); } protected SKShader PrimaryTexture; @@ -124,7 +129,7 @@ public virtual SKShader CreateShader(SkiaDrawingContext ctx, SKRect destination, if (NeedApply) { - if (source == null) + if (source == null && AutoCreateInputTexture) { source = CreateSnapshot(ctx, destination); } @@ -178,14 +183,14 @@ protected virtual SKRuntimeEffectUniforms CreateUniforms(SKRect destination) return uniforms; } - protected virtual SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingContext ctx, SKRect destination, SKShader texture1) + protected virtual SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingContext ctx, SKRect destination, SKShader primaryTexture) { - if (texture1 != null) + if (primaryTexture != null) { //.ToShader(SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); return new SKRuntimeEffectChildren(CompiledShader) { - { "iImage1", texture1 }, + { "iImage1", primaryTexture }, //{ "iImage2", _texture2 } }; } @@ -200,7 +205,7 @@ protected virtual SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingCont protected virtual void CompileShader() { string shaderCode = SkSl.LoadFromResources(ShaderSource); - CompiledShader = SkSl.Compile(shaderCode); + CompiledShader = SkSl.Compile(shaderCode, ShaderSource); } protected virtual void ApplyShaderSource() @@ -216,7 +221,7 @@ protected virtual void ApplyShaderSource() typeof(SkiaShaderEffect), string.Empty, propertyChanged: NeedChangeSource); - private static void NeedChangeSource(BindableObject bindable, object oldvalue, object newvalue) + protected static void NeedChangeSource(BindableObject bindable, object oldvalue, object newvalue) { if (bindable is SkiaShaderEffect control) { @@ -257,7 +262,7 @@ protected override void OnDisposing() #if SKIA3 TexturesUniforms?.Dispose(); #endif - _paintWithShader?.Dispose(); + PaintWithShader?.Dispose(); PrimaryTexture?.Dispose(); diff --git a/src/Engine/Features/Effects/StateEffect.cs b/src/Engine/Features/Effects/StateEffect.cs new file mode 100644 index 00000000..06171050 --- /dev/null +++ b/src/Engine/Features/Effects/StateEffect.cs @@ -0,0 +1,9 @@ +namespace DrawnUi.Maui.Draw; + +public class StateEffect : SkiaEffect, IStateEffect +{ + public virtual void UpdateState() + { + + } +} \ No newline at end of file diff --git a/src/Engine/Features/Shaders/SKSL.cs b/src/Engine/Features/Shaders/SKSL.cs index 116f84ef..a53802b8 100644 --- a/src/Engine/Features/Shaders/SKSL.cs +++ b/src/Engine/Features/Shaders/SKSL.cs @@ -22,7 +22,14 @@ public static string LoadFromResources(string fileName) return json; } - public static SKRuntimeEffect Compile(string shaderCode) + /// + /// Will compile your SKSL shader code into SKRuntimeEffect. + /// The filename parameter is used for debugging purposes only + /// + /// + /// + /// + public static SKRuntimeEffect Compile(string shaderCode, string filename = null) { string errors; #if SKIA3 @@ -34,11 +41,13 @@ public static SKRuntimeEffect Compile(string shaderCode) { ThrowCompilationError(shaderCode, errors); } - Debug.WriteLine($"[SKSL] Compiled shader code!"); + + Debug.WriteLine($"[SKSL] Compiled shader {filename}!"); + return effect; } - static void ThrowCompilationError(string shaderCode, string errors) + static void ThrowCompilationError(string shaderCode, string errors, string filename = null) { // Regular expression to find the line number in the error message var regex = new Regex(@"error: (\d+):"); @@ -63,6 +72,11 @@ static void ThrowCompilationError(string shaderCode, string errors) } if (!string.IsNullOrEmpty(error)) { + if (!string.IsNullOrEmpty(filename)) + { + throw new ApplicationException($"Shader compilation of '{filename}' failed:{Environment.NewLine}{errors}{Environment.NewLine}" + error); + } + throw new ApplicationException($"Shader compilation failed:{Environment.NewLine}{errors}{Environment.NewLine}" + error); } } diff --git a/src/Engine/Internals/Enums/PointedDirectionType.cs b/src/Engine/Internals/Enums/PointedDirectionType.cs new file mode 100644 index 00000000..018b8dd0 --- /dev/null +++ b/src/Engine/Internals/Enums/PointedDirectionType.cs @@ -0,0 +1,19 @@ +namespace DrawnUi.Maui.Draw; + +public enum PointedDirectionType +{ + LeftToRight, + + RightToLeft, + + TopToBottom, + + BottomToTop +} + +public enum LinearDirectionType +{ + Forward, + + Backward, +} \ No newline at end of file diff --git a/src/Engine/Internals/Extensions/DependencyExtensions.cs b/src/Engine/Internals/Extensions/DependencyExtensions.cs index 58d2bc23..740b7c59 100644 --- a/src/Engine/Internals/Extensions/DependencyExtensions.cs +++ b/src/Engine/Internals/Extensions/DependencyExtensions.cs @@ -9,12 +9,17 @@ 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; using System.Security.Authentication; +#if !SKIA3 +using SkiaSharp.Views.Maui.Controls.Compatibility; +#endif + + namespace DrawnUi.Maui.Draw; public class UiSettings diff --git a/src/Engine/Internals/Helpers/Looper.cs b/src/Engine/Internals/Helpers/Looper.cs index 8658ff58..30a0a69d 100644 --- a/src/Engine/Internals/Helpers/Looper.cs +++ b/src/Engine/Internals/Helpers/Looper.cs @@ -51,7 +51,7 @@ public void StartOnMainThread(int targetFps, bool useLegacy = false) return; _loopStarting = true; - if (MainThread.IsMainThread) //Choreographer is available + if (MainThread.IsMainThread) { if (!_loopStarted) { diff --git a/src/Engine/Internals/Interfaces/ISkiaGestureListener.cs b/src/Engine/Internals/Interfaces/ISkiaGestureListener.cs index 58c5157a..fe8f028e 100644 --- a/src/Engine/Internals/Interfaces/ISkiaGestureListener.cs +++ b/src/Engine/Internals/Interfaces/ISkiaGestureListener.cs @@ -2,37 +2,38 @@ public interface ISkiaGestureListener { - /// - /// Called when a gesture is detected. - /// - /// - /// - /// - /// - /// WHO CONSUMED if gesture consumed and blocked to be passed, NULL if gesture not locked and could be passed below. - /// If you pass this to subview you must set your own offset parameters, do not pass what you received its for this level use. - public ISkiaGestureListener OnSkiaGestureEvent(SkiaGesturesParameters args, GestureEventProcessingInfo apply); + /// + /// Called when a gesture is detected. + /// + /// + /// + /// + /// + /// WHO CONSUMED if gesture consumed and blocked to be passed, NULL if gesture not locked and could be passed below. + /// If you pass this to subview you must set your own offset parameters, do not pass what you received its for this level use. + public ISkiaGestureListener OnSkiaGestureEvent(SkiaGesturesParameters args, GestureEventProcessingInfo apply); - public bool InputTransparent { get; } + public bool InputTransparent { get; } - bool CanDraw { get; } + bool CanDraw { get; } - string Tag { get; } + string Tag { get; } - Guid Uid { get; } + Guid Uid { get; } - int ZIndex { get; } + int ZIndex { get; } - DateTime GestureListenerRegistrationTime { get; set; } + DateTime GestureListenerRegistrationTime { get; set; } - /// - /// This will be called only for views registered at Superview.FocusedChild. The view will return it's overriden value to indicate if it accepts focus or grabs it. - /// - /// - /// - public bool OnFocusChanged(bool focus); + /// + /// This will be called only for views registered at Superview.FocusedChild. + /// The view must return true of false to indicate if it accepts focus. + /// + /// + /// + public bool OnFocusChanged(bool focus); - public bool HitIsInside(float x, float y); + public bool HitIsInside(float x, float y); } diff --git a/src/Engine/Super.cs b/src/Engine/Super.cs index c32bd0b8..2fbe7342 100644 --- a/src/Engine/Super.cs +++ b/src/Engine/Super.cs @@ -37,6 +37,8 @@ static Super() //}); } + public static bool Multithreaded = false; + #if (!ONPLATFORM) protected static void SetupFrameLooper() @@ -322,26 +324,26 @@ public static void ResizeWindow(Window window, int width, int height, bool isFix window.Y = y; #else - + var platformWindow = window.Handler?.PlatformView as UIKit.UIWindow; - MainThread.BeginInvokeOnMainThread(()=> + MainThread.BeginInvokeOnMainThread(() => { - var frame = new CoreGraphics.CGRect(x, y,platformWindow.Frame.Width,platformWindow.Frame.Height); - + var frame = new CoreGraphics.CGRect(x, y, platformWindow.Frame.Width, platformWindow.Frame.Height); + platformWindow.Frame = frame; - + var windowScene = UIKit.UIApplication.SharedApplication.ConnectedScenes.ToArray().First() as UIKit.UIWindowScene; - + platformWindow.WindowScene.KeyWindow.Frame = frame; - + windowScene.RequestGeometryUpdate( new UIKit.UIWindowSceneGeometryPreferencesMac(frame), error => { - var stopp=1; + var stopp = 1; }); }); - + #endif #if WINDOWS @@ -364,25 +366,25 @@ public static void ResizeWindow(Window window, int width, int height, bool isFix //windowScene.SizeRestrictions.AllowsFullScreen = false; //windowScene.SizeRestrictions.MinimumSize = new(width, height); //windowScene.SizeRestrictions.MaximumSize = new(width, height); - + var scale = windowScene.Screen.Scale; - + // Tasks.StartDelayed(TimeSpan.FromSeconds(3),()=> // { - //todo move to view appeared etc - // MainThread.BeginInvokeOnMainThread(()=> - // { - // windowScene.RequestGeometryUpdate( - // new UIKit.UIWindowSceneGeometryPreferencesMac(frame), - // error => - // { - // var stopp=1; - // }); - // - //}); - - // }); - + //todo move to view appeared etc + // MainThread.BeginInvokeOnMainThread(()=> + // { + // windowScene.RequestGeometryUpdate( + // new UIKit.UIWindowSceneGeometryPreferencesMac(frame), + // error => + // { + // var stopp=1; + // }); + // + //}); + + // }); + } } diff --git a/src/Engine/Views/Canvas.cs b/src/Engine/Views/Canvas.cs index 3b4f6412..497d3949 100644 --- a/src/Engine/Views/Canvas.cs +++ b/src/Engine/Views/Canvas.cs @@ -479,6 +479,8 @@ protected virtual void ProcessGestures(SkiaGesturesParameters args) /// public virtual void OnGestureEvent(TouchActionType type, TouchActionEventArgs args1, TouchActionResult touchAction) { + //Debug.WriteLine($"[Canvas] {type}"); + var args = SkiaGesturesParameters.Create(touchAction, args1); if (args.Type == TouchActionResult.Panning) diff --git a/src/Engine/Views/DrawnView.cs b/src/Engine/Views/DrawnView.cs index 314df18d..540b4e6e 100644 --- a/src/Engine/Views/DrawnView.cs +++ b/src/Engine/Views/DrawnView.cs @@ -2,2548 +2,2671 @@ using DrawnUi.Maui.Infrastructure.Extensions; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; using System.Runtime.CompilerServices; using System.Text; 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) - return; - -#if ONPLATFORM - UpdatePlatform(); -#endif - } - - public bool IsUsingHardwareAcceleration - { - get - { - if (!Super.CanUseHardwareAcceleration) - return false; -#if SKIA3 - return HardwareAcceleration != HardwareAccelerationMode.Disabled; -#else - return HardwareAcceleration != HardwareAccelerationMode.Disabled - && !(DeviceInfo.Current.DeviceType == DeviceType.Virtual && DeviceInfo.Platform == DevicePlatform.iOS) //simulator - && DeviceInfo.Platform != DevicePlatform.WinUI //no Angle yet - && DeviceInfo.Platform != DevicePlatform.MacCatalyst; //bug, create GRContext returns null -#endif - } - } - - public bool NeedRedraw { get; set; } - - public bool IsDirty - { - get - { - return _isDirty; - } - - set - { - if (_isDirty != value) - { - if (!value && UpdateMode == UpdateMode.Constant) - { - value = true; - } - _isDirty = value; - OnPropertyChanged(); - } - } - } - - bool _isDirty; - - - public Queue ToBeDisposed { get; } = new(); - - public virtual bool IsVisibleInViewTree() - { - return IsVisible; //this is used by animators, do not make this heavier! - } - - public void TakeScreenShot(Action callback) - { - CallbackScreenshot = callback; - Update(); - } - - public virtual void InvalidateByChild(SkiaControl child) - { - Invalidate(); - } - - public virtual void UpdateByChild(SkiaControl child) - { - Update(); - } - - public virtual ScaledRect GetOnScreenVisibleArea(float inflateByPixels = 0) - { - var bounds = new SKRect(0 - inflateByPixels, 0 - inflateByPixels, (int)(Width * RenderingScale + inflateByPixels), (int)(Height * RenderingScale + inflateByPixels)); - - return ScaledRect.FromPixels(bounds, (float)RenderingScale); - } - - protected override void OnHandlerChanged() - { - base.OnHandlerChanged(); - -#if ANDROID - OnHandlerChangedInternal(); -#endif - - if (Handler == null) - { - DestroySkiaView(); - -#if ONPLATFORM - DisposePlatform(); -#endif - } - else - { - CreateSkiaView(); - -#if ONPLATFORM - - SetupRenderingLoop(); - -#endif - } - - HandlerWasSet?.Invoke(this, Handler != null); - //InvalidateChildren(); //need clear gfx cache - } - - public event EventHandler HandlerWasSet; - - protected virtual void TakeScreenShotInternal(SKSurface surface) - { - if (surface != null) - { - //Console.WriteLine($"[DrawnView] TakeScreenShotInternal ------------------------- START"); - surface.Canvas.Flush(); - CallbackScreenshot?.Invoke(surface.Snapshot()); - //Console.WriteLine($"[DrawnView] TakeScreenShotInternal ------------------------- END"); - } - CallbackScreenshot = null; - } - - /// - /// Postpone the action to be executed before the next frame being drawn. Exception-safe. - /// - /// - public void PostponeExecutionBeforeDraw(Action action) - { - ExecuteBeforeDraw.Enqueue(action); - } - - - /// - ///Postpone the action to be executed after the current frame is drawn. Exception-safe. - /// - /// - public void PostponeExecutionAfterDraw(Action action) - { - ExecuteAfterDraw.Enqueue(action); - } - - public Queue ExecuteBeforeDraw { get; } = new(256); - public Queue ExecuteAfterDraw { get; } = new(256); - - protected Action CallbackScreenshot; - - protected Dictionary RenderingTrees = new(128); - - public void RegisterRenderingChain(VisualTreeChain chain) - { - // Generate NodeIndices dictionary - RenderingTrees.TryAdd(chain.Child, chain); - - //initial state - for (int i = 0; i < chain.Nodes.Count; i++) - { - UpdateRenderingChains(chain.Nodes[i]); - } - } - - public void UnregisterRenderingChain(SkiaControl child) - { - RenderingTrees.Remove(child, out VisualTreeChain whatever); - } - - public VisualTreeChain GetRenderingChain(SkiaControl child) - { - RenderingTrees.TryGetValue(child, out VisualTreeChain chain); - return chain; - } - - private static bool debugV; - - static void DebugV(bool value) - { - if (debugV != value) - { - Debug.WriteLine($"V Changed {value}"); - debugV = value; - } - } - - public void UpdateRenderingChains(SkiaControl node) - { - // Check each chain - foreach (VisualTreeChain chain in RenderingTrees.Values) - { - // If the chain includes the node, update the chain's transform - if (chain.NodeIndices.TryGetValue(node, out int index)) - { - // If the node is the root of the chain, reset the chain's transform - if (index == 0) - { - chain.Transform = new VisualTransform(); - } - - chain.Transform.IsVisible = chain.Nodes.All(x => x.IsVisible) && node.CanDraw; - - //Debug.WriteLine($"U {node.GetType().Name} {node.IsVisible} => {chain.Transform.IsVisible}"); - - var translation = new SKPoint((float)node.UseTranslationX, (float)node.UseTranslationY); - chain.Transform.Translation += translation; - chain.Transform.Opacity *= (float)node.Opacity; - chain.Transform.Rotation += (float)node.Rotation; - chain.Transform.Scale = new SKPoint((float)(chain.Transform.Scale.X * node.ScaleX), (float)(chain.Transform.Scale.Y * node.ScaleY)); - - //var log = $"{node.GetType().Name} {translation} | "; - //Debug.WriteLine(log); - //chain.Transform.Logs += log; - chain.Transform.RenderedNodes++; - } - } - } - - IReadOnlyList IVisualTreeElement.GetVisualChildren() - { - return Views.Cast() - .ToList(); - } - - public void RegisterGestureListener(ISkiaGestureListener gestureListener) - { - if (gestureListener == null) - return; - - gestureListener.GestureListenerRegistrationTime = DateTime.UtcNow; - GestureListeners.Add(gestureListener); - } - - public void UnregisterGestureListener(ISkiaGestureListener gestureListener) - { - if (gestureListener == null) - return; - - GestureListeners.Remove(gestureListener); - } - - protected object LockIterateListeners = new(); - - - /// - /// Children we should check for touch hits - /// - public SortedGestureListeners GestureListeners { get; } = new(); - - //public SortedSet GestureListeners { get; } = new(new DescendingZIndexGestureListenerComparer()); - - - public SKRect DrawingRect - { - get - { - return new SKRect(0, 0, (float)(Width * RenderingScale), (float)(Height * RenderingScale)); - } - } - - public void AddAnimator(ISkiaAnimator animator) - { - PostponeExecutionBeforeDraw(() => - { - lock (LockAnimatingControls) - { - animator.IsDeactivated = false; - AnimatingControls.TryAdd(animator.Uid, animator); - } - }); - - Update(); - } - - public void RemoveAnimator(Guid uid) - { - lock (LockAnimatingControls) - { - if (AnimatingControls.TryGetValue(uid, out var animator)) - { - animator.IsDeactivated = true; - } - } - } - - /// - /// Called by a control that whats to be constantly animated or doesn't anymore, - /// so we know whether we should refresh canvas non-stop - /// - /// - /// - public bool RegisterAnimator(ISkiaAnimator animator) - { - AddAnimator(animator); - - return true; - } - - public void UnregisterAnimator(Guid uid) - { - RemoveAnimator(uid); - } - - public virtual IEnumerable UnregisterAllAnimatorsByType(Type type) - { - lock (LockAnimatingControls) - { - var ret = AnimatingControls.Where(x => x.Value.GetType() == type).Select(s => s.Value).ToArray(); - foreach (var animator in ret) - { - try - { - animator.IsDeactivated = true; - } - catch (Exception e) - { - Super.Log(e); - } - } - return ret; - } - } - - public virtual IEnumerable UnregisterAllAnimatorsByParent(SkiaControl parent) - { - lock (LockAnimatingControls) - { - var ret = AnimatingControls.Values.Where(x => x.Parent == parent).ToArray(); - foreach (var animator in ret) - { - try - { - animator.IsDeactivated = true; - } - catch (Exception e) - { - Super.Log(e); - } - } - return ret; - } - } - - public virtual IEnumerable SetViewTreeVisibilityByParent(SkiaControl parent, bool state) - { - lock (LockAnimatingControls) - { - var ret = AnimatingControls.Values.Where(x => x.Parent == parent).ToArray(); - foreach (var animator in ret) - { - try - { - animator.IsHiddenInViewTree = !state; - } - catch (Exception e) - { - Super.Log(e); - } - } - return ret; - } - } - - - public virtual IEnumerable SetPauseStateOfAllAnimatorsByParent(SkiaControl parent, bool state) - { - lock (LockAnimatingControls) - { - var ret = AnimatingControls.Values.Where(x => x.Parent == parent).ToArray(); - foreach (var animator in ret) - { - try - { - if (state) - animator.Pause(); - else - animator.Resume(); - } - catch (Exception e) - { - Super.Log(e); - } - } - return ret; - } - } - - public int ExecutePostAnimators(SkiaDrawingContext context, double scale) - { - var executed = 0; - - try - { - if (PostAnimators.Count == 0 || IsDisposing || IsDisposed) - return executed; - - foreach (var skiaAnimation in PostAnimators) - { - if (skiaAnimation.IsRunning && !skiaAnimation.IsPaused) - { - executed++; - var finished = skiaAnimation.TickFrame(context.FrameTimeNanos); - if (skiaAnimation is ICanRenderOnCanvas renderer) - { - var renderedrawn = renderer.Render(this, context, scale); - } - - if (finished) - { - skiaAnimation.Stop(); - } - } - } - } - catch (Exception e) - { - Super.Log(e); - } - - return executed; - } - - protected object LockAnimatingControls = new(); - - /// - /// Executed after the rendering - /// - public List PostAnimators { get; } = new(128); - - List _listRemoveAnimators = new(512); - - /// - /// Tracking controls that what to be animated right now so we constantly refresh - /// canvas until there is none left - /// - public Dictionary AnimatingControls { get; } = new(512); - - protected int ExecuteAnimators(long nanos) - { - - var executed = 0; - - //lock (LockAnimatingControls) - { - try - { - - if (AnimatingControls.Count == 0) - return executed; - - _listRemoveAnimators.Clear(); - - foreach (var key in AnimatingControls.Keys) - { - var skiaAnimation = AnimatingControls[key]; - - if (skiaAnimation.IsDeactivated - || skiaAnimation.Parent != null && skiaAnimation.Parent.IsDisposed) - { - _listRemoveAnimators.Add(key); - continue; - } - - bool canPlay = !skiaAnimation.IsHiddenInViewTree; //!(skiaAnimation.Parent != null && !skiaAnimation.Parent.IsVisibleInViewTree()); - - if (canPlay) - { - if (skiaAnimation.IsPaused) - skiaAnimation.Resume(); //continue anim from current time instead of the old one - - skiaAnimation.TickFrame(nanos); - executed++; - } - else - { - if (!skiaAnimation.IsPaused) - skiaAnimation.Pause(); - } - } - - foreach (var key in _listRemoveAnimators) - { - AnimatingControls.Remove(key); - } - - } - catch (Exception e) - { - Super.Log(e); - } - - return executed; - } - - } - - public virtual void OnCanvasViewChanged() - { - Update(); - } - - public ISkiaDrawable CanvasView - { - get => _canvasView; - protected set - { - if (Equals(value, _canvasView)) return; - _canvasView = value; - renderedFrames = 0; - OnPropertyChanged(); - OnPropertyChanged(nameof(CanvasFps)); - OnPropertyChanged(nameof(CanDraw)); - OnCanvasViewChanged(); - } - } - - private bool _initialized; - private void Init() - { - if (!_initialized) - { - _initialized = true; - - HorizontalOptions = LayoutOptions.Start; - VerticalOptions = LayoutOptions.Start; - Padding = new Thickness(0); - - SizeChanged += ViewSizeChanged; - - //bug this creates garbage on aandroid on every frame - // DeviceDisplay.Current.MainDisplayInfoChanged += OnMainDisplayInfoChanged; - } - } - - private void OnMainDisplayInfoChanged(object sender, DisplayInfoChangedEventArgs e) - { - switch (e.DisplayInfo.Rotation) - { - case Microsoft.Maui.Devices.DisplayRotation.Rotation90: - DeviceRotation = 90; - break; - case Microsoft.Maui.Devices.DisplayRotation.Rotation180: - DeviceRotation = 180; - break; - case Microsoft.Maui.Devices.DisplayRotation.Rotation270: - DeviceRotation = 270; - break; - case Microsoft.Maui.Devices.DisplayRotation.Rotation0: - default: - DeviceRotation = 0; - break; - } - - if (Parent != null) - RenderingScale = (float)e.DisplayInfo.Density; - } - - public void SetDeviceOrientation(int rotation) - { - DeviceRotation = rotation; - } - - public event EventHandler DeviceRotationChanged; - - public static readonly BindableProperty DisplayRotationProperty = BindableProperty.Create( - nameof(DeviceRotation), - typeof(int), - typeof(DrawnView), - 0, - propertyChanged: UpdateRotation); - - private static void UpdateRotation(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is DrawnView control) - { - control.DeviceRotationChanged?.Invoke(control, (int)newvalue); ; - } - } - - public int DeviceRotation - { - get { return (int)GetValue(DisplayRotationProperty); } - set { SetValue(DisplayRotationProperty, value); } - } - - //{ - // HorizontalOptions = LayoutOptions.Fill, - // VerticalOptions = LayoutOptions.Fill - //}; - - public bool HasHandler { get; set; } - - public virtual void DisconnectedHandler() - { - HasHandler = false; - Super.NeedGlobalRefresh -= OnNeedUpdate; - } - - public long NeedGlobalRefreshCount { get; set; } - - private void OnNeedUpdate(object sender, EventArgs e) - { - NeedCheckParentVisibility = true; - NeedGlobalRefreshCount++; - Update(); - } - - /// - /// Invoked when IsHiddenInViewTree changes - /// - /// - public virtual void OnCanRenderChanged(bool state) - { - if (state) - { - NeedMeasure = true; - Update(); - } - } - - public virtual void ConnectedHandler() - { - HasHandler = true; - Super.NeedGlobalRefresh -= OnNeedUpdate; - Super.NeedGlobalRefresh += OnNeedUpdate; - } - - - protected void FixDensity() - { - if (RenderingScale <= 0.0) - { - RenderingScale = (float)GetDensity(); - - if (RenderingScale <= 0.0) - { - RenderingScale = (float)(CanvasView.CanvasSize.Width / this.Width); - } - OnDensityChanged(); - } - } - - /// - /// Set this to true if you do not want the canvas to be redrawn as transparent and showing content below the canvas (splash?..) when UpdateLocked is True - /// - public bool StopDrawingWhenUpdateIsLocked { get; set; } - - - public DateTime TimeDrawingStarted { get; protected set; } - public DateTime TimeDrawingComplete { get; protected set; } - - volatile bool _isWaiting = false; - - public virtual void InvalidateViewport() - { - Update(); - } - - public virtual void Repaint() - { - Update(); - } - - - public bool OrderedDraw { get; protected set; } - - - double _lastUpdateTimeNanos; - - public void ResetUpdate() - { - NeedCheckParentVisibility = true; - OrderedDraw = false; - InvalidatedCanvas = 0; - } - - /// - /// A very important tracking prop to avoid saturating main thread with too many updates - /// - protected long InvalidatedCanvas { get; set; } - - public bool IsRendering { get; protected set; } - - protected Grid Delayed { get; set; } - - public static double GetDensity() - { - return Super.Screen.Density; - } - - protected void SwapToDelayed() - { - if (Delayed != null) - { - var normal = Delayed.Children[0] as SkiaView; - var accel = Delayed.Children[1] as SkiaViewAccelerated; - var kill = CanvasView; - kill.OnDraw = null; - accel.OnDraw = OnDrawSurface; - CanvasView = accel; - Delayed = null; - - normal?.Disconnect(); - } - } - - /// - /// Will safely destroy existing if any - /// - protected void CreateSkiaView() - { - DestroySkiaView(); - -#if ONPLATFORM - PlatformHardwareAccelerationChanged(); -#endif - - if (IsUsingHardwareAcceleration) - { - if (HardwareAcceleration == HardwareAccelerationMode.Prerender) - { - //create normal view, then after it has rendered 3 frames swap to the accelerated view, to avoid the blank screen when accelerated view is initializing (slow) - var pre = new SkiaView(this); - pre.OnDraw = OnDrawSurface; - CanvasView = pre; - var accel = new SkiaViewAccelerated(this); - - var content = new Grid() - { - - }; - content.Children.Add(pre); - content.Children.Add(accel); - Content = content; - Delayed = content; - return; - } - else - if (HardwareAcceleration == HardwareAccelerationMode.Enabled) - { - var view = new SkiaViewAccelerated(this); - view.OnDraw = OnDrawSurface; - CanvasView = view; - } - } - else - { - var view = new SkiaView(this); - view.OnDraw = OnDrawSurface; - CanvasView = view; - } - - Content = CanvasView as View; - } - - protected void DestroySkiaView() - { - var kill = CanvasView; - if (kill != null) - CanvasView = null; - if (kill != null) - { - kill.OnDraw = null; - kill.Dispose(); - } - } - - public bool IsDisposing { get; set; } - - public void Dispose() - { - IsDisposing = true; - - if (IsDisposed) - return; - - try - { - this.SizeChanged -= OnFormsSizeChanged; - - OnDisposing(); - - IsDisposed = true; - - Parent = null; - - PaintSystem?.Dispose(); - - DestroySkiaView(); - - DisposeDisposables(); - - GestureListeners.Clear(); - - ClearChildren(); - - MainThread.BeginInvokeOnMainThread(() => - { - try - { - this.Handler?.DisconnectHandler(); - - Content = null; - } - catch (Exception e) - { - Super.Log(e); - } - }); - - } - catch (Exception e) - { - Super.Log(e); - } - - } - - /// - /// Makes the control dirty, in need to be remeasured and rendered but this doesn't call Update, it's up yo you - /// - public virtual void Invalidate() - { - NeedMeasure = true; - } - - public void InvalidateParents() - { - Invalidate(); - } - - public virtual void OnSuperviewShouldRenderChanged(bool state) - { - foreach (var view in Views.ToList()) - { - view.OnSuperviewShouldRenderChanged(state); - } - } - - /// - /// We need to invalidate children maui changed our storyboard size - /// - public void InvalidateChildren() - { - - foreach (var view in Views) - { - view.InvalidateWithChildren(); - } - - NeedMeasure = true; - - Update(); - } - - - public void PrintDebug(string indent = "") - { - Console.WriteLine($"{indent}└─ {GetType().Name} {Width:0.0},{Height:0.0} pts"); - foreach (var view in Views) - { - //Console.Write($"{indent} ├─ "); - view.PrintDebug(indent); - } - } - - - public bool NeedAutoSize - { - get - { - return NeedAutoHeight || NeedAutoWidth; - } - } - - public bool NeedAutoHeight - { - get - { - return VerticalOptions.Alignment != LayoutAlignment.Fill && HeightRequest < 0; - } - } - public bool NeedAutoWidth - { - get - { - return HorizontalOptions.Alignment != LayoutAlignment.Fill && WidthRequest < 0; - } - } - - public virtual bool ShouldInvalidateByChildren - { - get - { - return NeedAutoSize; - } - } - - public static readonly BindableProperty UpdateLockedProperty = BindableProperty.Create( - nameof(UpdateLocked), - typeof(bool), - typeof(DrawnView), - false); - - public bool UpdateLocked - { - get { return (bool)GetValue(UpdateLockedProperty); } - set { SetValue(UpdateLockedProperty, value); } - } - - - - - protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) - { - base.OnPropertyChanged(propertyName); - - - if (propertyName == nameof(HeightRequest) - || propertyName == nameof(WidthRequest) - || propertyName == nameof(Padding) - || propertyName == nameof(Margin) - || propertyName == nameof(HorizontalOptions) - || propertyName == nameof(VerticalOptions) - ) - { - Invalidate(); - } - else - if (propertyName == nameof(IsVisible)) - { - Super.NeedGlobalUpdate(); - } - } - - public Guid Uid { get; } = Guid.NewGuid(); - - public static (double X1, double Y1, double X2, double Y2) LinearGradientAngleToPoints(double direction) - { - //adapt to css style - direction -= 90; - - //allow negative angles - if (direction < 0) - direction = 360 + direction; - - if (direction > 360) - direction = 360; - - (double x, double y) pointOfAngle(double a) - { - return (x: Math.Cos(a), y: Math.Sin(a)); - }; - - double degreesToRadians(double d) - { - return ((d * Math.PI) / 180); - } - - var eps = Math.Pow(2, -52); - var angle = (direction % 360); - var startPoint = pointOfAngle(degreesToRadians(180 - angle)); - var endPoint = pointOfAngle(degreesToRadians(360 - angle)); - - if (startPoint.x <= 0 || Math.Abs(startPoint.x) <= eps) - startPoint.x = 0; - - if (startPoint.y <= 0 || Math.Abs(startPoint.y) <= eps) - startPoint.y = 0; - - if (endPoint.x <= 0 || Math.Abs(endPoint.x) <= eps) - endPoint.x = 0; - - if (endPoint.y <= 0 || Math.Abs(endPoint.y) <= eps) - endPoint.y = 0; - - return (startPoint.x, startPoint.y, endPoint.x, endPoint.y); - } - - - public virtual void OnDensityChanged() - { - Update(); //todo for children!!!!! - } - - - #region DISPOSE BY XAMARIN FORMS - - //private bool _isRendererSet; - //protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) - //{ - // base.OnPropertyChanged(propertyName); - // if (propertyName == "Renderer") - // { - // _isRendererSet = !_isRendererSet; - // if (!_isRendererSet) - // { - // Dispose(); - // Debug.WriteLine("SkiaBaseView disposed!"); - // } - // else - // { - // IsDisposed = false; - // } - // } - //} - - #endregion - - private bool _IsGhost; - public bool IsGhost - { - get { return _IsGhost; } - set - { - if (_IsGhost != value) - { - _IsGhost = value; - OnPropertyChanged(); - } - } - } - - private void ViewSizeChanged(object sender, EventArgs e) - { - //xamarin forms changed our size if used inside xamarin layout - OnSizeChanged(); - } - - public DrawnView() - { - Init(); - } - - public Action Clipping { get; set; } - - public virtual SKPath CreateClip(object arguments, bool usePosition, SKPath path = null) - { - path ??= new SKPath(); - - if (usePosition) - { - path.AddRect(DrawingRect); - } - else - { - path.AddRect(new(0, 0, DrawingRect.Width, DrawingRect.Height)); - } - return path; - } - - public string Tag { get; set; } - - public bool IsRootView(float width, float height, SKRect destination) - { - if (this.Parent != null) - { - return true; //Xamarin/MAUI - } - - return this.Parent == null && destination.Width == width && destination.Height == height; - } - - //------------------------------------------------------------- - /// - /// destination in PIXELS, requests in UNITS. resulting Destination prop will be filed in PIXELS. - /// - /// PIXELS - /// UNITS - /// UNITS - /// - public SKRect CalculateLayout(SKRect destination, double widthRequest, - double heightRequest, double scale = 1.0) - //------------------------------------------------------------- - { - var scaledOffsetMargin = 0; - - var rectWidth = destination.Width - scaledOffsetMargin * 2; - var wants = Math.Round(widthRequest * scale); - if (wants > 0 && wants < rectWidth) - rectWidth = (int)wants; - - var rectHeight = destination.Height - scaledOffsetMargin * 2; - wants = Math.Round(heightRequest * scale); - if (wants > 0 && wants < rectHeight) - rectHeight = (int)wants; - - var availableWidth = destination.Width - scaledOffsetMargin * 2; - var availableHeight = destination.Height - scaledOffsetMargin * 2; - - var layoutHorizontal = new LayoutOptions(HorizontalOptions.Alignment, HorizontalOptions.Expands); - var layoutVertical = new LayoutOptions(VerticalOptions.Alignment, VerticalOptions.Expands); - - - //todo sensor rotation - - //initial fill - var left = destination.Left + scaledOffsetMargin; - var top = destination.Top + scaledOffsetMargin; - var right = left + rectWidth; - var bottom = top + rectHeight; - - - //layoutHorizontal - if (layoutHorizontal.Alignment == LayoutAlignment.Center) - { - //center - left += (availableWidth - rectWidth) / 2.0f; - right = left + rectWidth; - - if (left < destination.Left) - { - left = (float)(destination.Left); - right = left + rectWidth; - } - - if (right > destination.Right) - { - right = (float)(destination.Right); - } - } - else - if (layoutHorizontal.Alignment == LayoutAlignment.End) - { - //end - right = destination.Right; - left = right - rectWidth; - if (left < destination.Left) - { - left = (float)(destination.Left); - } - } - else - { - - - //start or fill - right = left + rectWidth; - if (right > destination.Right) - { - right = (float)(destination.Right); - } - } - - //VerticalOptions - if (layoutVertical.Alignment == LayoutAlignment.Center) - { - //center - top += availableHeight / 2.0f - rectHeight / 2.0f; - bottom = top + rectHeight; - if (top < destination.Top) - { - top = (float)(destination.Top); - bottom = top + rectHeight; - } - else - if (bottom > destination.Bottom) - { - bottom = (float)(destination.Bottom); - top = bottom - rectHeight; - } - } - else - if (layoutVertical.Alignment == LayoutAlignment.End && double.IsFinite(destination.Bottom)) - { - //end - bottom = destination.Bottom; - top = bottom - rectHeight; - if (top < destination.Top) - { - top = (float)(destination.Top); - } - - } - else - { - //start or fill - bottom = top + rectHeight; - if (bottom > destination.Bottom) - { - bottom = (float)(destination.Bottom); - } - - } - - return new SKRect((float)left, (float)top, (float)right, (float)bottom); - } - - - //------------------------------------------------------------- - /// - /// destination in PIXELS, requests in UNITS. resulting Destination prop will be filed in PIXELS. - /// - /// PIXELS - /// UNITS - /// UNITS - /// - public virtual void Arrange(SKRect destination, double widthRequest, double heightRequest, double scale = 1.0) - //------------------------------------------------------------- - { + [ContentProperty("Children")] + public partial class DrawnView : ContentView, IDrawnBase, IAnimatorsManager, IVisualTreeElement + { + public class DiagnosticData + { + public int LayersSaved { get; set; } + } - Destination = CalculateLayout(destination, widthRequest, heightRequest, scale); - - if (Destination.Width < 0 || Destination.Height < 0) - { - var stop = true; - } - } - - public ScaledSize MeasuredSize { get; set; } = new(); - - - - public virtual ScaledSize Measure(float widthConstraintPts, float heightConstraintPts) - { - if (!IsVisible) - { - return SetMeasured(0, 0, (float)RenderingScale); - } - - if (widthConstraintPts < 0 || heightConstraintPts < 0) - { - //not setting NeedMeasure=false; - return ScaledSize.Default; - } - - var widthPixels = (float)((WidthRequest)); - var heightPixels = (float)((HeightRequest)); - - if (WidthRequest > 0 && widthPixels < widthConstraintPts) - widthConstraintPts = widthPixels; - - if (HeightRequest > 0 && heightPixels < heightConstraintPts) - heightConstraintPts = heightPixels; - - return SetMeasured(widthConstraintPts, heightConstraintPts, (float)RenderingScale); - } - - protected virtual bool NeedMeasure { get; set; } = true; - - protected ScaledSize SetMeasured(float width, float height, float scale) - { - NeedMeasure = false; - - if (!double.IsNaN(height)) - { - //Height = height / scale; - } - else - { - height = -1; - //Height = height; - } - - if (!double.IsNaN(width)) - { - //Width = width / scale; - } - else - { - width = -1; - //Width = width; - } - - MeasuredSize = ScaledSize.FromUnits(width, height, scale); - - //SetValueCore(RenderingScaleProperty, scale, SetValueFlags.None); - - OnMeasured(); - - return MeasuredSize; - } - - protected virtual void OnMeasured() - { - Measured?.Invoke(this, MeasuredSize); - } - - public event EventHandler Measured; - - private void OnFormsSizeChanged(object sender, EventArgs e) - { - Update(); - } - - public bool IsDisposed { get; protected set; } - - - - SkiaDrawingContext CreateContext(SKSurface surface) - { - return new SkiaDrawingContext() - { - Superview = this, - FrameTimeNanos = CanvasView.FrameTime, - Surface = surface, - Canvas = surface.Canvas, - Width = surface.Canvas.DeviceClipBounds.Width, - Height = surface.Canvas.DeviceClipBounds.Height - }; - } - - /// - /// Can use this to manage double buffering to detect if we are in the drawing thread or in background. - /// - public int DrawingThreadId { get; protected set; } - - protected bool WasRendered { get; set; } - - public event EventHandler WasDrawn; - public event EventHandler WillDraw; - public event EventHandler WillFirstTimeDraw; - - private bool OnDrawSurface(SKSurface surface, SKRect rect) - { - //lock (LockDraw) - { - - if (!OnStartRendering(surface.Canvas)) - return UpdateMode == UpdateMode.Constant; - - try - { - if (NeedMeasure) - { - FixDensity(); - } - - var args = CreateContext(surface); - - this.DrawingThreadId = Thread.CurrentThread.ManagedThreadId; - - if (!WasRendered) - { - WillFirstTimeDraw?.Invoke(this, args); - } - - WillDraw?.Invoke(this, null); - - Draw(args, rect, (float)RenderingScale); - } - finally - { - OnFinalizeRendering(); - - WasRendered = true; - } - - return IsDirty; - } - - } - - - - protected virtual bool OnStartRendering(SKCanvas canvas) - { - var monitor = InvalidatedCanvas; - monitor--; - if (monitor < 0) - { - monitor = 0; - } - InvalidatedCanvas = monitor; - OrderedDraw = false; - - if (!CanDraw || canvas == null) - return false; - - TimeDrawingStarted = DateTime.Now; - IsRendering = true; - - IsDirty = false; //any control can set to true after that - - return true; - } - - protected virtual void OnFinalizeRendering() - { - - TimeDrawingComplete = DateTime.Now; - - IsRendering = false; - - if (UpdateMode == UpdateMode.Constant) - IsDirty = true; - - if (IsDirty) - { - Update(); - } - - //track and cap queued updates - var monitor = InvalidatedCanvas; - monitor--; - if (monitor < 0) - { - monitor = 0; - } - InvalidatedCanvas = monitor; - - WasDrawn?.Invoke(this, null); - } + public DiagnosticData Diagnostics = new(); + public virtual void Update() + { + if (!Super.EnableRendering) + return; - - public virtual void OnDisposing() - { #if ONPLATFORM - DisposePlatform(); + UpdatePlatform(); #endif + } + + public bool IsUsingHardwareAcceleration + { + get + { + if (!Super.CanUseHardwareAcceleration) + return false; +#if SKIA3 + return HardwareAcceleration != HardwareAccelerationMode.Disabled; +#else + return HardwareAcceleration != HardwareAccelerationMode.Disabled + && !(DeviceInfo.Current.DeviceType == DeviceType.Virtual && DeviceInfo.Platform == DevicePlatform.iOS) //simulator + && DeviceInfo.Platform != DevicePlatform.WinUI //no Angle yet + && DeviceInfo.Platform != DevicePlatform.MacCatalyst; //bug, create GRContext returns null +#endif + } + } + + public bool NeedRedraw { get; set; } + + public bool IsDirty + { + get + { + return _isDirty; + } + + set + { + if (_isDirty != value) + { + if (!value && UpdateMode == UpdateMode.Constant) + { + value = true; + } + _isDirty = value; + OnPropertyChanged(); + } + } + } + + bool _isDirty; + + + public Queue ToBeDisposed { get; } = new(); + + public virtual bool IsVisibleInViewTree() + { + return IsVisible; //this is used by animators, do not make this heavier! + } + + public void TakeScreenShot(Action callback) + { + CallbackScreenshot = callback; + Update(); + } + + public virtual void InvalidateByChild(SkiaControl child) + { + Invalidate(); + } + + public virtual void UpdateByChild(SkiaControl child) + { + Update(); + } + + public virtual ScaledRect GetOnScreenVisibleArea(float inflateByPixels = 0) + { + var bounds = new SKRect(0 - inflateByPixels, 0 - inflateByPixels, (int)(Width * RenderingScale + inflateByPixels), (int)(Height * RenderingScale + inflateByPixels)); + + return ScaledRect.FromPixels(bounds, (float)RenderingScale); + } + + protected override void OnHandlerChanged() + { + base.OnHandlerChanged(); - DeviceDisplay.Current.MainDisplayInfoChanged -= OnMainDisplayInfoChanged; - } - - - - //public virtual void Render(SkiaDrawingContext context, SKRect destination, float scale, - // ) - //{ - // AvailableDestination = destination; - // Draw(context, destination, scale); - //} - public SKRect AvailableDestination { get; set; } - - - /// - /// will be reset to null by InvalidateViewsList() - /// - private IReadOnlyList _orderedChildren; - - /// - /// For non templated simple subviews - /// - /// - public IReadOnlyList GetOrderedSubviews() - { - if (_orderedChildren == null) - { - _orderedChildren = Views.OrderBy(x => x.ZIndex).ToList(); - } - return _orderedChildren; - } - - /// - /// To make GetOrderedSubviews() regenerate next result instead of using cached - /// - public void InvalidateViewsList() - { - _orderedChildren = null; - } - - #region FPS - - private double _fpsAverage; - private int _fpsCount; - protected double _fps; - - /// - /// Frame started rendering nanoseconds - /// - public long FrameTime { get; protected set; } - - /// - /// Actual FPS - /// - public double CanvasFps - { - get - { - if (CanvasView == null) - return 0.0; - return CanvasView.FPS; - } - } - - /// - /// Average FPS - /// - public double FPS { get; protected set; } - - #endregion - - protected object LockDraw = new(); - - long renderedFrames; - - public virtual void DisposeDisposables() - { - try - { - while (ToBeDisposed.TryDequeue(out var disposable)) - { - disposable?.Dispose(); - } - } - catch (Exception e) - { - Super.Log("****************************************************"); - Super.Log(e); - Super.Log("****************************************************"); - throw e; - } - } - - public void PostponeInvalidation(SkiaControl key, Action action) - { - lock (_lockInvalidations) - { - GetBackInvalidations()[action] = key; - Repaint(); - } - } - - private object _lockInvalidations = new(); - private bool _invalidationsA; - protected readonly Dictionary InvalidationActionsA = new(); - protected readonly Dictionary InvalidationActionsB = new(); - - protected Dictionary GetFrontInvalidations() - { - lock (_lockInvalidations) - { - return _invalidationsA ? InvalidationActionsA : InvalidationActionsB; - } - } - - protected Dictionary GetBackInvalidations() - { - lock (_lockInvalidations) - { - return !_invalidationsA ? InvalidationActionsA : InvalidationActionsB; - } - } - - - protected void SwapInvalidations() - { - lock (_lockInvalidations) - { - _invalidationsA = !_invalidationsA; - } - } - - /// - /// For debugging purposes check if dont have concurrent threads - /// - public int DrawingThreads { get; protected set; } - - protected Dictionary DirtyChildren = new(); - - public void SetChildAsDirty(SkiaControl child) - { - if (dirtyChilrenProcessing) - return; - - DirtyChildren[child.Uid] = child; - } - - private volatile bool dirtyChilrenProcessing; - - protected void CommitInvalidations() - { - SwapInvalidations(); - var invalidations = GetFrontInvalidations(); - foreach (var invalidation in invalidations) - { - invalidation.Key.Invoke(); - } - invalidations.Clear(); - } +#if ANDROID + OnHandlerChangedInternal(); +#endif - protected virtual void Draw(SkiaDrawingContext context, SKRect destination, float scale) - { - ++renderedFrames; - - //Debug.WriteLine($"[DRAW] {Tag}"); - - DisposeDisposables(); - - if (IsDisposed || UpdateLocked) - { - return; - } - - - { - - - if (NeedGlobalRefreshCount != _globalRefresh) - { - _globalRefresh = NeedGlobalRefreshCount; - foreach (var item in Children) - { - item.InvalidateMeasureInternal(); - } - } - - try - { - DrawingThreads++; - - FrameTime = CanvasView.FrameTime; - - FPS = CanvasFps; - - CommitInvalidations(); - - while (ExecuteBeforeDraw.TryDequeue(out Action action)) - { - try - { - action?.Invoke(); - } - catch (Exception e) - { - Super.Log(e); - } - } - - var executed = ExecuteAnimators(context.FrameTimeNanos); - - if (this.Width > 0 && this.Height > 0) - { - // ABSOLUTE like inside grid - var children = GetOrderedSubviews(); - - foreach (var child in children) - { - child.OptionalOnBeforeDrawing(); //could set IsVisible or whatever inside - if (child.CanDraw) //still visible - { - var rectForChild = new SKRect( - destination.Left + (float)Math.Round((Padding.Left) * scale), - destination.Top + (float)Math.Round((Padding.Top) * scale), - destination.Right - (float)Math.Round((Padding.Right) * scale), - destination.Bottom - (float)Math.Round((Padding.Bottom) * scale)); - child.Render(context, rectForChild, (float)scale); - } - } - - dirtyChilrenProcessing = true; - foreach (var child in DirtyChildren.Values) - { - if (child != null && !child.IsDisposing) - { - child.InvalidatedParent = false; - child?.InvalidateParent(); - } - } - DirtyChildren.Clear(); - dirtyChilrenProcessing = false; - - //notify registered tree final nodes of rendering tree state - foreach (var tree in RenderingTrees) - { - //DebugV(tree.Value.Transform.IsVisible); - //todo what was this case? disabled as bugging - //if (tree.Value.Nodes.Count != tree.Value.Transform.RenderedNodes) - //{ - //tree.Value.Transform.IsVisible = false; - //} - tree.Key.SetVisualTransform(tree.Value.Transform); - } - - var postExecuted = ExecutePostAnimators(context, scale); - - //Kick to redraw if need animate - if (executed + postExecuted > 0) - { - IsDirty = true; - } - - if (CallbackScreenshot != null) - { - TakeScreenShotInternal(context.Superview.CanvasView.Surface); - } - } - - while (ExecuteAfterDraw.TryDequeue(out Action action)) - { - try - { - action?.Invoke(); - } - catch (Exception e) - { - Super.Log($"[DrawnView] Handled ExecuteAfterDraw: {e}"); - } - } - - - if (renderedFrames is >= 3 and < 5 && HardwareAcceleration == HardwareAccelerationMode.Prerender) - { - //looks like we have finally loaded - SwapToDelayed(); - } - - } - catch (Exception e) - { - Super.Log(e); //most probably data was modified while drawing - - NeedMeasure = true; - IsDirty = true; - } - finally - { - DrawingThreads--; - } - } + if (Handler == null) + { + DestroySkiaView(); +#if ONPLATFORM + DisposePlatform(); +#endif + } + else + { + CreateSkiaView(); - } +#if ONPLATFORM + SetupRenderingLoop(); +#endif + } + + HandlerWasSet?.Invoke(this, Handler != null); + //InvalidateChildren(); //need clear gfx cache + } + + public event EventHandler HandlerWasSet; + + protected virtual void TakeScreenShotInternal(SKSurface surface) + { + if (surface != null) + { + //Console.WriteLine($"[DrawnView] TakeScreenShotInternal ------------------------- START"); + surface.Canvas.Flush(); + CallbackScreenshot?.Invoke(surface.Snapshot()); + //Console.WriteLine($"[DrawnView] TakeScreenShotInternal ------------------------- END"); + } + CallbackScreenshot = null; + } + + /// + /// Postpone the action to be executed before the next frame being drawn. Exception-safe. + /// + /// + public void PostponeExecutionBeforeDraw(Action action) + { + ExecuteBeforeDraw.Enqueue(action); + } + + + /// + ///Postpone the action to be executed after the current frame is drawn. Exception-safe. + /// + /// + public void PostponeExecutionAfterDraw(Action action) + { + ExecuteAfterDraw.Enqueue(action); + } + + public Queue ExecuteBeforeDraw { get; } = new(256); + public Queue ExecuteAfterDraw { get; } = new(256); + + protected Action CallbackScreenshot; + + protected Dictionary RenderingTrees = new(128); + + public void RegisterRenderingChain(VisualTreeChain chain) + { + // Generate NodeIndices dictionary + RenderingTrees.TryAdd(chain.Child, chain); + + //initial state + for (int i = 0; i < chain.Nodes.Count; i++) + { + UpdateRenderingChains(chain.Nodes[i]); + } + } + + public void UnregisterRenderingChain(SkiaControl child) + { + RenderingTrees.Remove(child, out VisualTreeChain whatever); + } + + public VisualTreeChain GetRenderingChain(SkiaControl child) + { + RenderingTrees.TryGetValue(child, out VisualTreeChain chain); + return chain; + } + + private static bool debugV; + + static void DebugV(bool value) + { + if (debugV != value) + { + Debug.WriteLine($"V Changed {value}"); + debugV = value; + } + } + + public void UpdateRenderingChains(SkiaControl node) + { + // Check each chain + foreach (VisualTreeChain chain in RenderingTrees.Values) + { + // If the chain includes the node, update the chain's transform + if (chain.NodeIndices.TryGetValue(node, out int index)) + { + // If the node is the root of the chain, reset the chain's transform + if (index == 0) + { + chain.Transform = new VisualTransform(); + } + + chain.Transform.IsVisible = chain.Nodes.All(x => x.IsVisible) && node.CanDraw; + + //Debug.WriteLine($"U {node.GetType().Name} {node.IsVisible} => {chain.Transform.IsVisible}"); + + var translation = new SKPoint((float)node.UseTranslationX, (float)node.UseTranslationY); + chain.Transform.Translation += translation; + chain.Transform.Opacity *= (float)node.Opacity; + chain.Transform.Rotation += (float)node.Rotation; + chain.Transform.Scale = new SKPoint((float)(chain.Transform.Scale.X * node.ScaleX), (float)(chain.Transform.Scale.Y * node.ScaleY)); + + //var log = $"{node.GetType().Name} {translation} | "; + //Debug.WriteLine(log); + //chain.Transform.Logs += log; + chain.Transform.RenderedNodes++; + } + } + } + + IReadOnlyList IVisualTreeElement.GetVisualChildren() + { + return Views.Cast() + .ToList(); + } + + public void RegisterGestureListener(ISkiaGestureListener gestureListener) + { + if (gestureListener == null) + return; + + gestureListener.GestureListenerRegistrationTime = DateTime.UtcNow; + GestureListeners.Add(gestureListener); + } + + public void UnregisterGestureListener(ISkiaGestureListener gestureListener) + { + if (gestureListener == null) + return; + + GestureListeners.Remove(gestureListener); + } + + protected object LockIterateListeners = new(); + + + /// + /// Children we should check for touch hits + /// + public SortedGestureListeners GestureListeners { get; } = new(); + + //public SortedSet GestureListeners { get; } = new(new DescendingZIndexGestureListenerComparer()); + + + public SKRect DrawingRect + { + get + { + return new SKRect(0, 0, (float)(Width * RenderingScale), (float)(Height * RenderingScale)); + } + } + + public void AddAnimator(ISkiaAnimator animator) + { + PostponeExecutionBeforeDraw(() => + { + lock (LockAnimatingControls) + { + animator.IsDeactivated = false; + AnimatingControls.TryAdd(animator.Uid, animator); + } + }); + + Update(); + } + + public void RemoveAnimator(Guid uid) + { + lock (LockAnimatingControls) + { + if (AnimatingControls.TryGetValue(uid, out var animator)) + { + animator.IsDeactivated = true; + } + } + } + + /// + /// Called by a control that whats to be constantly animated or doesn't anymore, + /// so we know whether we should refresh canvas non-stop + /// + /// + /// + public bool RegisterAnimator(ISkiaAnimator animator) + { + AddAnimator(animator); + + return true; + } + + public void UnregisterAnimator(Guid uid) + { + RemoveAnimator(uid); + } + + public virtual IEnumerable UnregisterAllAnimatorsByType(Type type) + { + lock (LockAnimatingControls) + { + var ret = AnimatingControls.Where(x => x.Value.GetType() == type).Select(s => s.Value).ToArray(); + foreach (var animator in ret) + { + try + { + animator.IsDeactivated = true; + } + catch (Exception e) + { + Super.Log(e); + } + } + return ret; + } + } + + public virtual IEnumerable UnregisterAllAnimatorsByParent(SkiaControl parent) + { + lock (LockAnimatingControls) + { + var ret = AnimatingControls.Values.Where(x => x.Parent == parent).ToArray(); + foreach (var animator in ret) + { + try + { + animator.IsDeactivated = true; + } + catch (Exception e) + { + Super.Log(e); + } + } + return ret; + } + } + + public virtual IEnumerable SetViewTreeVisibilityByParent(SkiaControl parent, bool state) + { + lock (LockAnimatingControls) + { + var ret = AnimatingControls.Values.Where(x => x.Parent == parent).ToArray(); + foreach (var animator in ret) + { + try + { + animator.IsHiddenInViewTree = !state; + } + catch (Exception e) + { + Super.Log(e); + } + } + return ret; + } + } + + + public virtual IEnumerable SetPauseStateOfAllAnimatorsByParent(SkiaControl parent, bool state) + { + lock (LockAnimatingControls) + { + var ret = AnimatingControls.Values.Where(x => x.Parent == parent).ToArray(); + foreach (var animator in ret) + { + try + { + if (state) + animator.Pause(); + else + animator.Resume(); + } + catch (Exception e) + { + Super.Log(e); + } + } + return ret; + } + } + + public int ExecutePostAnimators(SkiaDrawingContext context, double scale) + { + var executed = 0; + + try + { + if (PostAnimators.Count == 0 || IsDisposing || IsDisposed) + return executed; + + foreach (var skiaAnimation in PostAnimators) + { + if (skiaAnimation.IsRunning && !skiaAnimation.IsPaused) + { + executed++; + var finished = skiaAnimation.TickFrame(context.FrameTimeNanos); + if (skiaAnimation is ICanRenderOnCanvas renderer) + { + var renderedrawn = renderer.Render(this, context, scale); + } + + if (finished) + { + skiaAnimation.Stop(); + } + } + } + } + catch (Exception e) + { + Super.Log(e); + } + + return executed; + } + + protected object LockAnimatingControls = new(); + + /// + /// Executed after the rendering + /// + public List PostAnimators { get; } = new(128); + + List _listRemoveAnimators = new(512); + + /// + /// Tracking controls that what to be animated right now so we constantly refresh + /// canvas until there is none left + /// + public Dictionary AnimatingControls { get; } = new(512); + + protected int ExecuteAnimators(long nanos) + { + + var executed = 0; + + //lock (LockAnimatingControls) + { + try + { + + if (AnimatingControls.Count == 0) + return executed; + + _listRemoveAnimators.Clear(); + + foreach (var key in AnimatingControls.Keys) + { + var skiaAnimation = AnimatingControls[key]; + + if (skiaAnimation.IsDeactivated + || skiaAnimation.Parent != null && skiaAnimation.Parent.IsDisposed) + { + _listRemoveAnimators.Add(key); + continue; + } + + bool canPlay = !skiaAnimation.IsHiddenInViewTree; //!(skiaAnimation.Parent != null && !skiaAnimation.Parent.IsVisibleInViewTree()); + + if (canPlay) + { + if (skiaAnimation.IsPaused) + skiaAnimation.Resume(); //continue anim from current time instead of the old one + + skiaAnimation.TickFrame(nanos); + executed++; + } + else + { + if (!skiaAnimation.IsPaused) + skiaAnimation.Pause(); + } + } + + foreach (var key in _listRemoveAnimators) + { + AnimatingControls.Remove(key); + } + + } + catch (Exception e) + { + Super.Log(e); + } + + return executed; + } + + } + + public virtual void OnCanvasViewChanged() + { + Update(); + } + + public ISkiaDrawable CanvasView + { + get => _canvasView; + protected set + { + if (Equals(value, _canvasView)) return; + _canvasView = value; + renderedFrames = 0; + OnPropertyChanged(); + OnPropertyChanged(nameof(CanvasFps)); + OnPropertyChanged(nameof(CanDraw)); + OnCanvasViewChanged(); + } + } + + private bool _initialized; + private void Init() + { + if (!_initialized) + { + _initialized = true; + + HorizontalOptions = LayoutOptions.Start; + VerticalOptions = LayoutOptions.Start; + Padding = new Thickness(0); + + SizeChanged += ViewSizeChanged; + + //bug this creates garbage on aandroid on every frame + // DeviceDisplay.Current.MainDisplayInfoChanged += OnMainDisplayInfoChanged; + } + } + + private void OnMainDisplayInfoChanged(object sender, DisplayInfoChangedEventArgs e) + { + switch (e.DisplayInfo.Rotation) + { + case Microsoft.Maui.Devices.DisplayRotation.Rotation90: + DeviceRotation = 90; + break; + case Microsoft.Maui.Devices.DisplayRotation.Rotation180: + DeviceRotation = 180; + break; + case Microsoft.Maui.Devices.DisplayRotation.Rotation270: + DeviceRotation = 270; + break; + case Microsoft.Maui.Devices.DisplayRotation.Rotation0: + default: + DeviceRotation = 0; + break; + } + + if (Parent != null) + RenderingScale = (float)e.DisplayInfo.Density; + } + + public void SetDeviceOrientation(int rotation) + { + DeviceRotation = rotation; + } + + public event EventHandler DeviceRotationChanged; + + public static readonly BindableProperty DisplayRotationProperty = BindableProperty.Create( + nameof(DeviceRotation), + typeof(int), + typeof(DrawnView), + 0, + propertyChanged: UpdateRotation); + + private static void UpdateRotation(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is DrawnView control) + { + control.DeviceRotationChanged?.Invoke(control, (int)newvalue); ; + } + } + + public int DeviceRotation + { + get { return (int)GetValue(DisplayRotationProperty); } + set { SetValue(DisplayRotationProperty, value); } + } + + //{ + // HorizontalOptions = LayoutOptions.Fill, + // VerticalOptions = LayoutOptions.Fill + //}; + + public bool HasHandler { get; set; } + + public virtual void DisconnectedHandler() + { + HasHandler = false; + Super.NeedGlobalRefresh -= OnNeedUpdate; + } + + public long NeedGlobalRefreshCount { get; set; } + + private void OnNeedUpdate(object sender, EventArgs e) + { + NeedCheckParentVisibility = true; + NeedGlobalRefreshCount++; + Update(); + } + + /// + /// Invoked when IsHiddenInViewTree changes + /// + /// + public virtual void OnCanRenderChanged(bool state) + { + if (state) + { + NeedMeasure = true; + Update(); + } + } + + public virtual void ConnectedHandler() + { + HasHandler = true; + Super.NeedGlobalRefresh -= OnNeedUpdate; + Super.NeedGlobalRefresh += OnNeedUpdate; + } + + + protected void FixDensity() + { + if (RenderingScale <= 0.0) + { + RenderingScale = (float)GetDensity(); + + if (RenderingScale <= 0.0) + { + RenderingScale = (float)(CanvasView.CanvasSize.Width / this.Width); + } + OnDensityChanged(); + } + } + + /// + /// Set this to true if you do not want the canvas to be redrawn as transparent and showing content below the canvas (splash?..) when UpdateLocked is True + /// + public bool StopDrawingWhenUpdateIsLocked { get; set; } + + + public DateTime TimeDrawingStarted { get; protected set; } + public DateTime TimeDrawingComplete { get; protected set; } + + volatile bool _isWaiting = false; + + public virtual void InvalidateViewport() + { + Update(); + } + + public virtual void Repaint() + { + Update(); + } + + + public bool OrderedDraw { get; protected set; } + + + double _lastUpdateTimeNanos; + + public void ResetUpdate() + { + NeedCheckParentVisibility = true; + OrderedDraw = false; + InvalidatedCanvas = 0; + } + + /// + /// A very important tracking prop to avoid saturating main thread with too many updates + /// + protected long InvalidatedCanvas { get; set; } + + public bool IsRendering { get; protected set; } + + protected Grid Delayed { get; set; } + + public static double GetDensity() + { + return Super.Screen.Density; + } + + protected void SwapToDelayed() + { + if (Delayed != null) + { + var normal = Delayed.Children[0] as SkiaView; + var accel = Delayed.Children[1] as SkiaViewAccelerated; + var kill = CanvasView; + kill.OnDraw = null; + accel.OnDraw = OnDrawSurface; + CanvasView = accel; + Delayed = null; + + normal?.Disconnect(); + } + } + + /// + /// Will safely destroy existing if any + /// + protected void CreateSkiaView() + { + DestroySkiaView(); +#if ONPLATFORM + PlatformHardwareAccelerationChanged(); +#endif - public static readonly BindableProperty TintColorProperty = BindableProperty.Create(nameof(TintColor), typeof(Color), typeof(DrawnView), - Colors.Transparent, - propertyChanged: RedrawCanvas); - public Color TintColor - { - get { return (Color)GetValue(TintColorProperty); } - set { SetValue(TintColorProperty, value); } - } + if (IsUsingHardwareAcceleration) + { + if (HardwareAcceleration == HardwareAccelerationMode.Prerender) + { + //create normal view, then after it has rendered 3 frames swap to the accelerated view, to avoid the blank screen when accelerated view is initializing (slow) + var pre = new SkiaView(this); + pre.OnDraw = OnDrawSurface; + CanvasView = pre; + var accel = new SkiaViewAccelerated(this); + + var content = new Grid() + { + + }; + content.Children.Add(pre); + content.Children.Add(accel); + Content = content; + Delayed = content; + return; + } + else + if (HardwareAcceleration == HardwareAccelerationMode.Enabled) + { + var view = new SkiaViewAccelerated(this); + view.OnDraw = OnDrawSurface; + CanvasView = view; + } + } + else + { + var view = new SkiaView(this); + view.OnDraw = OnDrawSurface; + CanvasView = view; + } + + Content = CanvasView as View; + } + + protected void DestroySkiaView() + { + var kill = CanvasView; + if (kill != null) + CanvasView = null; + if (kill != null) + { + kill.OnDraw = null; + kill.Dispose(); + } + } + + public bool IsDisposing { get; set; } + + public void Dispose() + { + IsDisposing = true; + + if (IsDisposed) + return; + + try + { + this.SizeChanged -= OnFormsSizeChanged; + + OnDisposing(); + + IsDisposed = true; + + Parent = null; + + PaintSystem?.Dispose(); + + DestroySkiaView(); + + DisposeDisposables(); + + GestureListeners.Clear(); + + ClearChildren(); + + MainThread.BeginInvokeOnMainThread(() => + { + try + { + this.Handler?.DisconnectHandler(); + + Content = null; + } + catch (Exception e) + { + Super.Log(e); + } + }); + + } + catch (Exception e) + { + Super.Log(e); + } + + } + + /// + /// Makes the control dirty, in need to be remeasured and rendered but this doesn't call Update, it's up yo you + /// + public virtual void Invalidate() + { + NeedMeasure = true; + } + + public void InvalidateParents() + { + Invalidate(); + } + + public virtual void OnSuperviewShouldRenderChanged(bool state) + { + foreach (var view in Views.ToList()) + { + view.OnSuperviewShouldRenderChanged(state); + } + } + + /// + /// We need to invalidate children maui changed our storyboard size + /// + public void InvalidateChildren() + { + + foreach (var view in Views) + { + view.InvalidateWithChildren(); + } + + NeedMeasure = true; + + Update(); + } + + + public void PrintDebug(string indent = "") + { + Console.WriteLine($"{indent}└─ {GetType().Name} {Width:0.0},{Height:0.0} pts"); + foreach (var view in Views) + { + //Console.Write($"{indent} ├─ "); + view.PrintDebug(indent); + } + } + + + public bool NeedAutoSize + { + get + { + return NeedAutoHeight || NeedAutoWidth; + } + } + + public bool NeedAutoHeight + { + get + { + return VerticalOptions.Alignment != LayoutAlignment.Fill && HeightRequest < 0; + } + } + public bool NeedAutoWidth + { + get + { + return HorizontalOptions.Alignment != LayoutAlignment.Fill && WidthRequest < 0; + } + } + + public virtual bool ShouldInvalidateByChildren + { + get + { + return NeedAutoSize; + } + } + + public static readonly BindableProperty UpdateLockedProperty = BindableProperty.Create( + nameof(UpdateLocked), + typeof(bool), + typeof(DrawnView), + false); + + public bool UpdateLocked + { + get { return (bool)GetValue(UpdateLockedProperty); } + set { SetValue(UpdateLockedProperty, value); } + } + + + + + protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + base.OnPropertyChanged(propertyName); + + + if (propertyName == nameof(HeightRequest) + || propertyName == nameof(WidthRequest) + || propertyName == nameof(Padding) + || propertyName == nameof(Margin) + || propertyName == nameof(HorizontalOptions) + || propertyName == nameof(VerticalOptions) + ) + { + Invalidate(); + } + else + if (propertyName == nameof(IsVisible)) + { + Super.NeedGlobalUpdate(); + } + } + + public Guid Uid { get; } = Guid.NewGuid(); + + public static (double X1, double Y1, double X2, double Y2) LinearGradientAngleToPoints(double direction) + { + //adapt to css style + direction -= 90; + + //allow negative angles + if (direction < 0) + direction = 360 + direction; + + if (direction > 360) + direction = 360; + + (double x, double y) pointOfAngle(double a) + { + return (x: Math.Cos(a), y: Math.Sin(a)); + }; + + double degreesToRadians(double d) + { + return ((d * Math.PI) / 180); + } + + var eps = Math.Pow(2, -52); + var angle = (direction % 360); + var startPoint = pointOfAngle(degreesToRadians(180 - angle)); + var endPoint = pointOfAngle(degreesToRadians(360 - angle)); + + if (startPoint.x <= 0 || Math.Abs(startPoint.x) <= eps) + startPoint.x = 0; + + if (startPoint.y <= 0 || Math.Abs(startPoint.y) <= eps) + startPoint.y = 0; + + if (endPoint.x <= 0 || Math.Abs(endPoint.x) <= eps) + endPoint.x = 0; + + if (endPoint.y <= 0 || Math.Abs(endPoint.y) <= eps) + endPoint.y = 0; + + return (startPoint.x, startPoint.y, endPoint.x, endPoint.y); + } + + + public virtual void OnDensityChanged() + { + Update(); //todo for children!!!!! + } + + + #region DISPOSE BY XAMARIN FORMS + + //private bool _isRendererSet; + //protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) + //{ + // base.OnPropertyChanged(propertyName); + // if (propertyName == "Renderer") + // { + // _isRendererSet = !_isRendererSet; + // if (!_isRendererSet) + // { + // Dispose(); + // Debug.WriteLine("SkiaBaseView disposed!"); + // } + // else + // { + // IsDisposed = false; + // } + // } + //} + + #endregion + + private bool _IsGhost; + public bool IsGhost + { + get { return _IsGhost; } + set + { + if (_IsGhost != value) + { + _IsGhost = value; + OnPropertyChanged(); + } + } + } + + private void ViewSizeChanged(object sender, EventArgs e) + { + //xamarin forms changed our size if used inside xamarin layout + OnSizeChanged(); + } + + public DrawnView() + { + Init(); + } + + public Action Clipping { get; set; } + + public virtual SKPath CreateClip(object arguments, bool usePosition, SKPath path = null) + { + path ??= new SKPath(); + + if (usePosition) + { + path.AddRect(DrawingRect); + } + else + { + path.AddRect(new(0, 0, DrawingRect.Width, DrawingRect.Height)); + } + return path; + } + + public string Tag { get; set; } + + public bool IsRootView(float width, float height, SKRect destination) + { + if (this.Parent != null) + { + return true; //Xamarin/MAUI + } + + return this.Parent == null && destination.Width == width && destination.Height == height; + } + + //------------------------------------------------------------- + /// + /// destination in PIXELS, requests in UNITS. resulting Destination prop will be filed in PIXELS. + /// + /// PIXELS + /// UNITS + /// UNITS + /// + public SKRect CalculateLayout(SKRect destination, double widthRequest, + double heightRequest, double scale = 1.0) + //------------------------------------------------------------- + { + var scaledOffsetMargin = 0; + + var rectWidth = destination.Width - scaledOffsetMargin * 2; + var wants = Math.Round(widthRequest * scale); + if (wants > 0 && wants < rectWidth) + rectWidth = (int)wants; + + var rectHeight = destination.Height - scaledOffsetMargin * 2; + wants = Math.Round(heightRequest * scale); + if (wants > 0 && wants < rectHeight) + rectHeight = (int)wants; + + var availableWidth = destination.Width - scaledOffsetMargin * 2; + var availableHeight = destination.Height - scaledOffsetMargin * 2; + + var layoutHorizontal = new LayoutOptions(HorizontalOptions.Alignment, HorizontalOptions.Expands); + var layoutVertical = new LayoutOptions(VerticalOptions.Alignment, VerticalOptions.Expands); + + + //todo sensor rotation + + //initial fill + var left = destination.Left + scaledOffsetMargin; + var top = destination.Top + scaledOffsetMargin; + var right = left + rectWidth; + var bottom = top + rectHeight; + + + //layoutHorizontal + if (layoutHorizontal.Alignment == LayoutAlignment.Center) + { + //center + left += (availableWidth - rectWidth) / 2.0f; + right = left + rectWidth; + + if (left < destination.Left) + { + left = (float)(destination.Left); + right = left + rectWidth; + } + + if (right > destination.Right) + { + right = (float)(destination.Right); + } + } + else + if (layoutHorizontal.Alignment == LayoutAlignment.End) + { + //end + right = destination.Right; + left = right - rectWidth; + if (left < destination.Left) + { + left = (float)(destination.Left); + } + } + else + { + + + //start or fill + right = left + rectWidth; + if (right > destination.Right) + { + right = (float)(destination.Right); + } + } + + //VerticalOptions + if (layoutVertical.Alignment == LayoutAlignment.Center) + { + //center + top += availableHeight / 2.0f - rectHeight / 2.0f; + bottom = top + rectHeight; + if (top < destination.Top) + { + top = (float)(destination.Top); + bottom = top + rectHeight; + } + else + if (bottom > destination.Bottom) + { + bottom = (float)(destination.Bottom); + top = bottom - rectHeight; + } + } + else + if (layoutVertical.Alignment == LayoutAlignment.End && double.IsFinite(destination.Bottom)) + { + //end + bottom = destination.Bottom; + top = bottom - rectHeight; + if (top < destination.Top) + { + top = (float)(destination.Top); + } + + } + else + { + //start or fill + bottom = top + rectHeight; + if (bottom > destination.Bottom) + { + bottom = (float)(destination.Bottom); + } + + } + + return new SKRect((float)left, (float)top, (float)right, (float)bottom); + } + + + //------------------------------------------------------------- + /// + /// destination in PIXELS, requests in UNITS. resulting Destination prop will be filed in PIXELS. + /// + /// PIXELS + /// UNITS + /// UNITS + /// + public virtual void Arrange(SKRect destination, double widthRequest, double heightRequest, double scale = 1.0) + //------------------------------------------------------------- + { + + + Destination = CalculateLayout(destination, widthRequest, heightRequest, scale); + + if (Destination.Width < 0 || Destination.Height < 0) + { + var stop = true; + } + } + + public ScaledSize MeasuredSize { get; set; } = new(); + + + + public virtual ScaledSize Measure(float widthConstraintPts, float heightConstraintPts) + { + if (!IsVisible) + { + return SetMeasured(0, 0, (float)RenderingScale); + } + + if (widthConstraintPts < 0 || heightConstraintPts < 0) + { + //not setting NeedMeasure=false; + return ScaledSize.Default; + } + + var widthPixels = (float)((WidthRequest)); + var heightPixels = (float)((HeightRequest)); + + if (WidthRequest > 0 && widthPixels < widthConstraintPts) + widthConstraintPts = widthPixels; + + if (HeightRequest > 0 && heightPixels < heightConstraintPts) + heightConstraintPts = heightPixels; + + return SetMeasured(widthConstraintPts, heightConstraintPts, (float)RenderingScale); + } + + protected virtual bool NeedMeasure { get; set; } = true; + + protected ScaledSize SetMeasured(float width, float height, float scale) + { + NeedMeasure = false; + + if (!double.IsNaN(height)) + { + //Height = height / scale; + } + else + { + height = -1; + //Height = height; + } + + if (!double.IsNaN(width)) + { + //Width = width / scale; + } + else + { + width = -1; + //Width = width; + } + + MeasuredSize = ScaledSize.FromUnits(width, height, scale); - public static readonly BindableProperty ClearColorProperty = BindableProperty.Create(nameof(ClearColor), typeof(Color), typeof(DrawnView), - Colors.Transparent, - propertyChanged: RedrawCanvas); - public Color ClearColor - { - get { return (Color)GetValue(ClearColorProperty); } - set { SetValue(ClearColorProperty, value); } - } + //SetValueCore(RenderingScaleProperty, scale, SetValueFlags.None); - public static readonly BindableProperty RenderingScaleProperty = BindableProperty.Create(nameof(RenderingScale), typeof(float), typeof(DrawnView), - -1.0f, - propertyChanged: RedrawCanvas); - public float RenderingScale - { - get - { - var value = (float)GetValue(RenderingScaleProperty); - if (value < 0.1) - { - return (float)GetDensity(); - } - return value; - } - set - { - SetValue(RenderingScaleProperty, value); - } - } + OnMeasured(); + return MeasuredSize; + } - private static void OnHardwareModeChanged(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is DrawnView control && control.Handler != null) - { - control.CreateSkiaView(); - } - } + protected virtual void OnMeasured() + { + Measured?.Invoke(this, MeasuredSize); + } - public static readonly BindableProperty HardwareAccelerationProperty = BindableProperty.Create(nameof(HardwareAcceleration), - typeof(HardwareAccelerationMode), - typeof(DrawnView), - HardwareAccelerationMode.Disabled, - propertyChanged: OnHardwareModeChanged); + public event EventHandler Measured; - public HardwareAccelerationMode HardwareAcceleration - { - get { return (HardwareAccelerationMode)GetValue(HardwareAccelerationProperty); } - set { SetValue(HardwareAccelerationProperty, value); } - } + private void OnFormsSizeChanged(object sender, EventArgs e) + { + Update(); + } - public static readonly BindableProperty CanRenderOffScreenProperty = BindableProperty.Create(nameof(CanRenderOffScreen), - typeof(bool), - typeof(DrawnView), - false); - /// - /// If this is check you view will be refreshed even offScreen or hidden - /// - public bool CanRenderOffScreen - { - get { return (bool)GetValue(CanRenderOffScreenProperty); } - set { SetValue(CanRenderOffScreenProperty, value); } - } + public bool IsDisposed { get; protected set; } - /// - /// Indicates that it is allowed to be rendered by engine, internal use - /// - /// - public bool CanDraw - { - get - { - var canRenderOffScreen = !IsHiddenInViewTree || CanRenderOffScreen; - return CanvasView != null && !IsDisposed && IsVisible && Handler != null && canRenderOffScreen; - } - } - /// - /// Indicates that view is either hidden or offscreen. - /// This disables rendering if you don't set CanRenderOffScreen to true - /// - public bool IsHiddenInViewTree - { - get - { - return _stopRendering; - } - protected set - { - if (value != _stopRendering) - { - _stopRendering = value; - OnCanRenderChanged(!value); - } - } - } - bool _stopRendering; + SkiaDrawingContext CreateContext(SKSurface surface) + { + return new SkiaDrawingContext() + { + Superview = this, + FrameTimeNanos = CanvasView.FrameTime, + Surface = surface, + Canvas = surface.Canvas, + Width = surface.Canvas.DeviceClipBounds.Width, + Height = surface.Canvas.DeviceClipBounds.Height + }; + } - public bool NeedCheckParentVisibility - { - get - { - return _needCheckParentVisibility; - } - - set - { - if (_needCheckParentVisibility != value) - { - _needCheckParentVisibility = value; - OnPropertyChanged(); - OnSuperviewShouldRenderChanged(value); //maybe use just event handler??? - } - } - } - bool _needCheckParentVisibility; - private long _globalRefresh; + /// + /// Can use this to manage double buffering to detect if we are in the drawing thread or in background. + /// + public int DrawingThreadId { get; protected set; } + protected bool WasRendered { get; set; } + public event EventHandler WasDrawn; + public event EventHandler WillDraw; + public event EventHandler WillFirstTimeDraw; - public static MemoryStream StreamFromString(string value) - { - return new MemoryStream(Encoding.UTF8.GetBytes(value ?? "")); - } + private bool OnDrawSurface(SKSurface surface, SKRect rect) + { + lock (LockDraw) + { + if (!OnStartRendering(surface.Canvas)) + return UpdateMode == UpdateMode.Constant; - protected SKPaint PaintSystem { get; set; } + try + { + if (NeedMeasure) + { + FixDensity(); + } - public SKRect Destination { get; protected set; } + var args = CreateContext(surface); - public void PaintTintBackground(SKCanvas canvas) - { - if (TintColor != null && TintColor != Colors.Transparent) - { - if (PaintSystem == null) - { - PaintSystem = new SKPaint(); - } - PaintSystem.Style = SKPaintStyle.StrokeAndFill; - PaintSystem.Color = TintColor.ToSKColor(); - canvas.DrawRect(Destination, PaintSystem); - } - } + this.DrawingThreadId = Thread.CurrentThread.ManagedThreadId; - public void PaintClearBackground(SKCanvas canvas) - { - if (ClearColor != Colors.Transparent) - { - if (PaintSystem == null) - { - PaintSystem = new SKPaint(); - } - PaintSystem.Style = SKPaintStyle.StrokeAndFill; - PaintSystem.Color = ClearColor.ToSKColor(); - canvas.DrawRect(Destination, PaintSystem); - } - } + if (!WasRendered) + { + WillFirstTimeDraw?.Invoke(this, args); + } - //static int countRedraws = 0; - protected static void RedrawCanvas(BindableObject bindable, object oldvalue, object newvalue) - { + WillDraw?.Invoke(this, null); - var control = bindable as DrawnView; - { - if (control != null && !control.IsDisposed) - { - control.Update(); - } - } - } + Draw(args, rect, (float)RenderingScale); + } + finally + { + OnFinalizeRendering(); + WasRendered = true; + } - protected override void OnBindingContextChanged() - { - base.OnBindingContextChanged(); + return IsDirty; + } - foreach (var view in this.Views) - { - view.BindingContext = BindingContext; - } - } + } - #region GRADIENTS - public SKShader CreateGradientAsShader(SKRect destination, SkiaGradient gradient) - { - if (gradient != null && gradient.Type != GradientType.None) - { - var colors = new List(); - foreach (var color in gradient.Colors) - { - var usingColor = color; - if (gradient.Light < 1.0) - { - usingColor = usingColor.MakeDarker(100 - gradient.Light * 100); - } - else if (gradient.Light > 1.0) - { - usingColor = usingColor.MakeLighter(gradient.Light * 100 - 100); - } - - var newAlpha = usingColor.Alpha * gradient.Opacity; - usingColor = usingColor.WithAlpha(newAlpha); - colors.Add(usingColor.ToSKColor()); - } - - float[] colorPositions = null; - if (gradient.ColorPositions?.Count == colors.Count) - { - colorPositions = gradient.ColorPositions.Select(x => (float)x).ToArray(); - } - - switch (gradient.Type) - { - case GradientType.Sweep: - - return SKShader.CreateSweepGradient( - new SKPoint(destination.Left + destination.Width / 2.0f, - destination.Top + destination.Height / 2.0f), - colors.ToArray(), - colorPositions, - gradient.TileMode, (float)Value1, (float)(Value1 + Value2)); - - case GradientType.Circular: - return SKShader.CreateRadialGradient( - new SKPoint(destination.Left + destination.Width / 2.0f, - destination.Top + destination.Height / 2.0f), - Math.Max(destination.Width, destination.Height) / 2.0f, - colors.ToArray(), - colorPositions, - gradient.TileMode); - - case GradientType.Linear: - default: - return SKShader.CreateLinearGradient( - new SKPoint(destination.Left + destination.Width * gradient.StartXRatio, - destination.Top + destination.Height * gradient.StartYRatio), - new SKPoint(destination.Left + destination.Width * gradient.EndXRatio, - destination.Top + destination.Height * gradient.EndYRatio), - colors.ToArray(), - colorPositions, - gradient.TileMode); - break; - } - - } - - return null; - } + protected virtual bool OnStartRendering(SKCanvas canvas) + { + var monitor = InvalidatedCanvas; + monitor--; + if (monitor < 0) + { + monitor = 0; + } + InvalidatedCanvas = monitor; + OrderedDraw = false; + if (!CanDraw || canvas == null) + return false; - #endregion + TimeDrawingStarted = DateTime.Now; + IsRendering = true; - #region SUBVIEWS + IsDirty = false; //any control can set to true after that - public List Views { get; } = new(); + return true; + } - public virtual void ClearChildren() - { - foreach (var child in Views.ToList()) - { - RemoveSubView(child); - child.Dispose(); - } - - Views.Clear(); - InvalidateViewsList(); - } + protected virtual void OnFinalizeRendering() + { - public virtual void SetChildren(IEnumerable views) - { - ClearChildren(); - foreach (var child in views) - { - AddOrRemoveView(child, true); - } - } + TimeDrawingComplete = DateTime.Now; - public void AddSubView(SkiaControl control) - { - if (control == null) - return; - control.SetParent(this); - OnChildAdded(control); - //if (Debugger.IsAttached) - ReportHotreloadChildAdded(control); - } + IsRendering = false; - public virtual void ReportHotreloadChildAdded(SkiaControl child) - { - if (child == null) - return; + if (UpdateMode == UpdateMode.Constant) + IsDirty = true; - var index = Views.FindIndex(child); - VisualDiagnostics.OnChildAdded(this, child, index); - } + if (IsDirty) + { + Update(); + } - public void RemoveSubView(SkiaControl control) - { - if (control == null) - return; + //track and cap queued updates + var monitor = InvalidatedCanvas; + monitor--; + if (monitor < 0) + { + monitor = 0; + } + InvalidatedCanvas = monitor; - //if (Debugger.IsAttached) - ReportHotreloadChildRemoved(control); + WasDrawn?.Invoke(this, null); + } - control.SetParent(null); - OnChildRemoved(control); - } - public virtual void ReportHotreloadChildRemoved(SkiaControl control) - { - if (control == null) - return; - var index = Views.FindIndex(control); - VisualDiagnostics.OnChildRemoved(this, control, index); - } + public virtual void OnDisposing() + { + if (_visibilityParent != null) + { + _visibilityParent.PropertyChanged -= OnParentVisibilityCheck; + } - protected virtual void OnChildAdded(SkiaControl child) - { - InvalidateViewsList(); - } +#if ONPLATFORM + DisposePlatform(); +#endif - protected virtual void OnChildRemoved(SkiaControl child) - { - InvalidateViewsList(); - } + DeviceDisplay.Current.MainDisplayInfoChanged -= OnMainDisplayInfoChanged; + } + + + + //public virtual void Render(SkiaDrawingContext context, SKRect destination, float scale, + // ) + //{ + // AvailableDestination = destination; + // Draw(context, destination, scale); + //} + public SKRect AvailableDestination { get; set; } + + + /// + /// will be reset to null by InvalidateViewsList() + /// + private IReadOnlyList _orderedChildren; + + /// + /// For non templated simple subviews + /// + /// + public IReadOnlyList GetOrderedSubviews() + { + if (_orderedChildren == null) + { + _orderedChildren = Views.OrderBy(x => x.ZIndex).ToList(); + } + return _orderedChildren; + } + + /// + /// To make GetOrderedSubviews() regenerate next result instead of using cached + /// + public void InvalidateViewsList() + { + _orderedChildren = null; + } + + #region FPS + + private double _fpsAverage; + private int _fpsCount; + protected double _fps; + + /// + /// Frame started rendering nanoseconds + /// + public long FrameTime { get; protected set; } + + /// + /// Actual FPS + /// + public double CanvasFps + { + get + { + if (CanvasView == null) + return 0.0; + return CanvasView.FPS; + } + } + + /// + /// Average FPS + /// + public double FPS { get; protected set; } + + #endregion + + protected object LockDraw = new(); + + long renderedFrames; + + public virtual void DisposeDisposables() + { + try + { + while (ToBeDisposed.TryDequeue(out var disposable)) + { + disposable?.Dispose(); + } + } + catch (Exception e) + { + Super.Log("****************************************************"); + Super.Log(e); + Super.Log("****************************************************"); + throw e; + } + } + + public void PostponeInvalidation(SkiaControl key, Action action) + { + lock (_lockInvalidations) + { + GetBackInvalidations()[action] = key; + Repaint(); + } + } + + private object _lockInvalidations = new(); + private bool _invalidationsA; + protected readonly Dictionary InvalidationActionsA = new(); + protected readonly Dictionary InvalidationActionsB = new(); + + protected Dictionary GetFrontInvalidations() + { + lock (_lockInvalidations) + { + return _invalidationsA ? InvalidationActionsA : InvalidationActionsB; + } + } + + protected Dictionary GetBackInvalidations() + { + lock (_lockInvalidations) + { + return !_invalidationsA ? InvalidationActionsA : InvalidationActionsB; + } + } + + + protected void SwapInvalidations() + { + lock (_lockInvalidations) + { + _invalidationsA = !_invalidationsA; + } + } + + /// + /// For debugging purposes check if dont have concurrent threads + /// + public int DrawingThreads { get; protected set; } + + protected Dictionary DirtyChildren = new(); + + public void SetChildAsDirty(SkiaControl child) + { + if (dirtyChilrenProcessing) + return; + + DirtyChildren[child.Uid] = child; + } + + private volatile bool dirtyChilrenProcessing; + + protected void CommitInvalidations() + { + SwapInvalidations(); + var invalidations = GetFrontInvalidations(); + foreach (var invalidation in invalidations) + { + invalidation.Key.Invoke(); + } + invalidations.Clear(); + } + + #region BACKGROUND RENDERING + + + protected object LockStartOffscreenQueue = new(); + private bool _processingOffscrenRendering = false; + + /// + /// Make sure offscreen rendering queue is running + /// + public void KickOffscreenCacheRendering() + { + lock (LockStartOffscreenQueue) + { + if (!_processingOffscrenRendering) + { + _processingOffscrenRendering = true; + Task.Run(async () => //100% background thread + { + await ProcessOffscreenCacheRenderingAsync(); + + }).ConfigureAwait(false); + } + } + } + + public void PushToOffscreenRendering(SkiaControl control) + { + _offscreenCacheRenderingQueue.Enqueue(new OffscreenCommand(control)); + KickOffscreenCacheRendering(); + } + + public record OffscreenCommand(SkiaControl Control); + + protected SemaphoreSlim semaphoreOffscreenProcess = new(1); + + private readonly Queue _offscreenCacheRenderingQueue = new(); + + public async Task ProcessOffscreenCacheRenderingAsync() + { + + await semaphoreOffscreenProcess.WaitAsync(); + + try + { + if (_offscreenCacheRenderingQueue.Count == 0) + return; + + _processingOffscrenRendering = true; + + var command = _offscreenCacheRenderingQueue.Dequeue(); + while (!command.Control.IsDisposed && !command.Control.IsDisposing) + { + try + { + var action = command.Control.GetOffscreenRenderingAction(); + action?.Invoke(); + + if (_offscreenCacheRenderingQueue.Count > 0) + command = _offscreenCacheRenderingQueue.Dequeue(); + else + break; + } + catch (Exception e) + { + Super.Log(e); + } + } + + //if (NeedUpdate || RenderObjectNeedsUpdate) //someone changed us while rendering inner content + //{ + // Update(); //kick + //} + //Update(); //kick + + + + } + finally + { + _processingOffscrenRendering = false; + semaphoreOffscreenProcess.Release(); + } + + } + + + #endregion + + protected virtual void Draw(SkiaDrawingContext context, SKRect destination, float scale) + { + ++renderedFrames; + + //Debug.WriteLine($"[DRAW] {Tag}"); + + DisposeDisposables(); + + if (IsDisposed || UpdateLocked) + { + return; + } + + + { + + + if (NeedGlobalRefreshCount != _globalRefresh) + { + _globalRefresh = NeedGlobalRefreshCount; + foreach (var item in Children) + { + item.InvalidateMeasureInternal(); + } + } + + try + { + DrawingThreads++; + + FrameTime = CanvasView.FrameTime; + + FPS = CanvasFps; + + CommitInvalidations(); + + while (ExecuteBeforeDraw.TryDequeue(out Action action)) + { + try + { + action?.Invoke(); + } + catch (Exception e) + { + Super.Log(e); + } + } + + var executed = ExecuteAnimators(context.FrameTimeNanos); + + if (this.Width > 0 && this.Height > 0) + { + // ABSOLUTE like inside grid + var children = GetOrderedSubviews(); + + foreach (var child in children) + { + child.OptionalOnBeforeDrawing(); //could set IsVisible or whatever inside + if (child.CanDraw) //still visible + { + var rectForChild = new SKRect( + destination.Left + (float)Math.Round((Padding.Left) * scale), + destination.Top + (float)Math.Round((Padding.Top) * scale), + destination.Right - (float)Math.Round((Padding.Right) * scale), + destination.Bottom - (float)Math.Round((Padding.Bottom) * scale)); + child.Render(context, rectForChild, (float)scale); + } + } + + dirtyChilrenProcessing = true; + foreach (var child in DirtyChildren.Values) + { + if (child != null && !child.IsDisposing) + { + child.InvalidatedParent = false; + child?.InvalidateParent(); + } + } + DirtyChildren.Clear(); + dirtyChilrenProcessing = false; + + //notify registered tree final nodes of rendering tree state + foreach (var tree in RenderingTrees) + { + //DebugV(tree.Value.Transform.IsVisible); + //todo what was this case? disabled as bugging + //if (tree.Value.Nodes.Count != tree.Value.Transform.RenderedNodes) + //{ + //tree.Value.Transform.IsVisible = false; + //} + tree.Key.SetVisualTransform(tree.Value.Transform); + } + + var postExecuted = ExecutePostAnimators(context, scale); + + //Kick to redraw if need animate + if (executed + postExecuted > 0) + { + IsDirty = true; + } + + if (CallbackScreenshot != null) + { + TakeScreenShotInternal(context.Superview.CanvasView.Surface); + } + } + + while (ExecuteAfterDraw.TryDequeue(out Action action)) + { + try + { + action?.Invoke(); + } + catch (Exception e) + { + Super.Log($"[DrawnView] Handled ExecuteAfterDraw: {e}"); + } + } + + + if (renderedFrames is >= 3 and < 5 && HardwareAcceleration == HardwareAccelerationMode.Prerender) + { + //looks like we have finally loaded + SwapToDelayed(); + } + + } + catch (Exception e) + { + Super.Log(e); //most probably data was modified while drawing + + NeedMeasure = true; + IsDirty = true; + } + finally + { + DrawingThreads--; + } + } + + + } + + + + + public static readonly BindableProperty TintColorProperty = BindableProperty.Create(nameof(TintColor), typeof(Color), typeof(DrawnView), + Colors.Transparent, + propertyChanged: RedrawCanvas); + public Color TintColor + { + get { return (Color)GetValue(TintColorProperty); } + set { SetValue(TintColorProperty, value); } + } + + public static readonly BindableProperty ClearColorProperty = BindableProperty.Create(nameof(ClearColor), typeof(Color), typeof(DrawnView), + Colors.Transparent, + propertyChanged: RedrawCanvas); + public Color ClearColor + { + get { return (Color)GetValue(ClearColorProperty); } + set { SetValue(ClearColorProperty, value); } + } + + public static readonly BindableProperty RenderingScaleProperty = BindableProperty.Create(nameof(RenderingScale), typeof(float), typeof(DrawnView), + -1.0f, + propertyChanged: RedrawCanvas); + public float RenderingScale + { + get + { + var value = (float)GetValue(RenderingScaleProperty); + if (value < 0.1) + { + return (float)GetDensity(); + } + return value; + } + set + { + SetValue(RenderingScaleProperty, value); + } + } + + + private static void OnHardwareModeChanged(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is DrawnView control && control.Handler != null) + { + control.CreateSkiaView(); + } + } + + public static readonly BindableProperty HardwareAccelerationProperty = BindableProperty.Create(nameof(HardwareAcceleration), + typeof(HardwareAccelerationMode), + typeof(DrawnView), + HardwareAccelerationMode.Disabled, + propertyChanged: OnHardwareModeChanged); + + public HardwareAccelerationMode HardwareAcceleration + { + get { return (HardwareAccelerationMode)GetValue(HardwareAccelerationProperty); } + set { SetValue(HardwareAccelerationProperty, value); } + } + + public static readonly BindableProperty CanRenderOffScreenProperty = BindableProperty.Create(nameof(CanRenderOffScreen), + typeof(bool), + typeof(DrawnView), + false); + /// + /// If this is check you view will be refreshed even offScreen or hidden + /// + public bool CanRenderOffScreen + { + get { return (bool)GetValue(CanRenderOffScreenProperty); } + set { SetValue(CanRenderOffScreenProperty, value); } + } + + + /// + /// Indicates that it is allowed to be rendered by engine, internal use + /// + /// + public bool CanDraw + { + get + { + var canRenderOffScreen = !IsHiddenInViewTree || CanRenderOffScreen; + return CanvasView != null && !IsDisposed && IsVisible && Handler != null && canRenderOffScreen; + } + } + + /// + /// Indicates that view is either hidden or offscreen. + /// This disables rendering if you don't set CanRenderOffScreen to true + /// + public bool IsHiddenInViewTree + { + get + { + return _stopRendering; + } + protected set + { + if (value != _stopRendering) + { + _stopRendering = value; + OnCanRenderChanged(!value); + } + } + } + bool _stopRendering; + + public bool NeedCheckParentVisibility + { + get + { + return _needCheckParentVisibility; + } + + set + { + if (_needCheckParentVisibility != value) + { + _needCheckParentVisibility = value; + OnPropertyChanged(); + OnSuperviewShouldRenderChanged(value); //maybe use just event handler??? + } + } + } + bool _needCheckParentVisibility; + private long _globalRefresh; + + + + public static MemoryStream StreamFromString(string value) + { + return new MemoryStream(Encoding.UTF8.GetBytes(value ?? "")); + } + + + protected SKPaint PaintSystem { get; set; } + + public SKRect Destination { get; protected set; } + + public void PaintTintBackground(SKCanvas canvas) + { + if (TintColor != null && TintColor != Colors.Transparent) + { + if (PaintSystem == null) + { + PaintSystem = new SKPaint(); + } + PaintSystem.Style = SKPaintStyle.StrokeAndFill; + PaintSystem.Color = TintColor.ToSKColor(); + canvas.DrawRect(Destination, PaintSystem); + } + } + + public void PaintClearBackground(SKCanvas canvas) + { + if (ClearColor != Colors.Transparent) + { + if (PaintSystem == null) + { + PaintSystem = new SKPaint(); + } + PaintSystem.Style = SKPaintStyle.StrokeAndFill; + PaintSystem.Color = ClearColor.ToSKColor(); + canvas.DrawRect(Destination, PaintSystem); + } + } + + //static int countRedraws = 0; + protected static void RedrawCanvas(BindableObject bindable, object oldvalue, object newvalue) + { + + var control = bindable as DrawnView; + { + if (control != null && !control.IsDisposed) + { + control.Update(); + } + } + } + + + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + + foreach (var view in this.Views) + { + view.SetInheritedBindingContext(BindingContext); + } + } + + #region GRADIENTS + + public SKShader CreateGradientAsShader(SKRect destination, SkiaGradient gradient) + { + if (gradient != null && gradient.Type != GradientType.None) + { + var colors = new List(); + foreach (var color in gradient.Colors) + { + var usingColor = color; + if (gradient.Light < 1.0) + { + usingColor = usingColor.MakeDarker(100 - gradient.Light * 100); + } + else if (gradient.Light > 1.0) + { + usingColor = usingColor.MakeLighter(gradient.Light * 100 - 100); + } + + var newAlpha = usingColor.Alpha * gradient.Opacity; + usingColor = usingColor.WithAlpha(newAlpha); + colors.Add(usingColor.ToSKColor()); + } + + float[] colorPositions = null; + if (gradient.ColorPositions?.Count == colors.Count) + { + colorPositions = gradient.ColorPositions.Select(x => (float)x).ToArray(); + } + + switch (gradient.Type) + { + case GradientType.Sweep: + + return SKShader.CreateSweepGradient( + new SKPoint(destination.Left + destination.Width / 2.0f, + destination.Top + destination.Height / 2.0f), + colors.ToArray(), + colorPositions, + gradient.TileMode, (float)Value1, (float)(Value1 + Value2)); + + case GradientType.Circular: + return SKShader.CreateRadialGradient( + new SKPoint(destination.Left + destination.Width / 2.0f, + destination.Top + destination.Height / 2.0f), + Math.Max(destination.Width, destination.Height) / 2.0f, + colors.ToArray(), + colorPositions, + gradient.TileMode); + + case GradientType.Linear: + default: + return SKShader.CreateLinearGradient( + new SKPoint(destination.Left + destination.Width * gradient.StartXRatio, + destination.Top + destination.Height * gradient.StartYRatio), + new SKPoint(destination.Left + destination.Width * gradient.EndXRatio, + destination.Top + destination.Height * gradient.EndYRatio), + colors.ToArray(), + colorPositions, + gradient.TileMode); + break; + } + + } + + return null; + } + + + + #endregion + + #region SUBVIEWS + + public List Views { get; } = new(); + + public virtual void ClearChildren() + { + foreach (var child in Views.ToList()) + { + RemoveSubView(child); + child.Dispose(); + } + + Views.Clear(); + InvalidateViewsList(); + } + + public virtual void SetChildren(IEnumerable views) + { + ClearChildren(); + foreach (var child in views) + { + AddOrRemoveView(child, true); + } + } + + public void AddSubView(SkiaControl control) + { + if (control == null) + return; + control.SetParent(this); + OnChildAdded(control); + //if (Debugger.IsAttached) + ReportHotreloadChildAdded(control); + } + + public virtual void ReportHotreloadChildAdded(SkiaControl child) + { + if (child == null) + return; + + var index = Views.FindIndex(child); + VisualDiagnostics.OnChildAdded(this, child, index); + } + + public void RemoveSubView(SkiaControl control) + { + if (control == null) + return; + + //if (Debugger.IsAttached) + ReportHotreloadChildRemoved(control); + + control.SetParent(null); + OnChildRemoved(control); + } + + public virtual void ReportHotreloadChildRemoved(SkiaControl control) + { + if (control == null) + return; + + var index = Views.FindIndex(control); + VisualDiagnostics.OnChildRemoved(this, control, index); + } + + protected virtual void OnChildAdded(SkiaControl child) + { + InvalidateViewsList(); + } + + protected virtual void OnChildRemoved(SkiaControl child) + { + InvalidateViewsList(); + } - #endregion + #endregion - #region Children + #region Children #pragma warning disable NU1605, CS0108 - public static readonly BindableProperty ChildrenProperty = BindableProperty.Create( - nameof(Children), - typeof(IList), - typeof(DrawnView), - defaultValueCreator: (instance) => - { - var created = new ObservableCollection(); - ChildrenPropertyChanged(instance, null, created); - return created; - }, - validateValue: (bo, v) => v is IList, - propertyChanged: ChildrenPropertyChanged); + public static readonly BindableProperty ChildrenProperty = BindableProperty.Create( + nameof(Children), + typeof(IList), + typeof(DrawnView), + defaultValueCreator: (instance) => + { + var created = new ObservableCollection(); + ChildrenPropertyChanged(instance, null, created); + return created; + }, + validateValue: (bo, v) => v is IList, + propertyChanged: ChildrenPropertyChanged); - public IList Children - { - get => (IList)GetValue(ChildrenProperty); - set => SetValue(ChildrenProperty, value); - } + public IList Children + { + get => (IList)GetValue(ChildrenProperty); + set => SetValue(ChildrenProperty, value); + } #pragma warning restore NU1605, CS0108 - protected void AddOrRemoveView(SkiaControl subView, bool add) - { - if (subView != null) - { - if (add) - { - AddSubView(subView); - subView.BindingContext = this.BindingContext; - } - else - { - RemoveSubView(subView); - subView.BindingContext = null; - //subView.Dispose(); ????? - } - - } - } - - private static void ChildrenPropertyChanged(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is DrawnView skiaControl) - { - var enumerableChildren = (IEnumerable)newvalue; - - if (oldvalue != null) - { - var oldViews = (IEnumerable)oldvalue; - - if (oldvalue is INotifyCollectionChanged oldCollection) - { - oldCollection.CollectionChanged -= skiaControl.OnChildrenCollectionChanged; - } - - foreach (var subView in oldViews) - { - skiaControl.AddOrRemoveView(subView, false); - } - } - - //foreach (var subView in enumerableChildren) - //{ - // skiaControl.SetChildren(enumerableChildren); - //} - skiaControl.SetChildren(enumerableChildren); - - if (newvalue is INotifyCollectionChanged newCollection) - { - newCollection.CollectionChanged += skiaControl.OnChildrenCollectionChanged; - } - - skiaControl.Update(); - - } - - } - - private void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - foreach (SkiaControl newChildren in e.NewItems) - { - newChildren.SetParent(this); - } - - break; - - case NotifyCollectionChangedAction.Reset: - case NotifyCollectionChangedAction.Remove: - foreach (SkiaControl oldChildren in e.OldItems ?? new SkiaControl[0]) - { - oldChildren.SetParent(null); - } - - break; - } - - Update(); - } - - #endregion - - public static readonly BindableProperty UpdateModeProperty = BindableProperty.Create( - nameof(UpdateMode), - typeof(UpdateMode), - typeof(DrawnView), - UpdateMode.Dynamic, - propertyChanged: ChangeUpdateMode); - - private static void ChangeUpdateMode(BindableObject bindable, object oldvalue, object newvalue) - { - if (bindable is DrawnView control) - { - control.Update(); - } - } - - public UpdateMode UpdateMode - { - get { return (UpdateMode)GetValue(UpdateModeProperty); } - set { SetValue(UpdateModeProperty, value); } - } - - public static readonly BindableProperty ClipEffectsProperty = BindableProperty.Create( - nameof(ClipEffects), - typeof(bool), - typeof(DrawnView), - true); - - public bool ClipEffects - { - get { return (bool)GetValue(ClipEffectsProperty); } - set { SetValue(ClipEffectsProperty, value); } - } - - public static readonly BindableProperty Value1Property = BindableProperty.Create( - nameof(Value1), - typeof(double), - typeof(DrawnView), - 0.0, - propertyChanged: RedrawCanvas); - - public double Value1 - { - get { return (double)GetValue(Value1Property); } - set { SetValue(Value1Property, value); } - } - - public static readonly BindableProperty Value2Property = BindableProperty.Create( - nameof(Value2), - typeof(double), - typeof(DrawnView), - 0.0, - propertyChanged: RedrawCanvas); - - public double Value2 - { - get { return (double)GetValue(Value2Property); } - set { SetValue(Value2Property, value); } - } - - public static readonly BindableProperty Value3Property = BindableProperty.Create( - nameof(Value3), - typeof(double), - typeof(DrawnView), - 0.0, - propertyChanged: RedrawCanvas); - - public double Value3 - { - get { return (double)GetValue(Value3Property); } - set { SetValue(Value3Property, value); } - } - - public static readonly BindableProperty Value4Property = BindableProperty.Create( - nameof(Value4), - typeof(double), - typeof(DrawnView), - 0.0, - propertyChanged: RedrawCanvas); - - public double Value4 - { - get { return (double)GetValue(Value4Property); } - set { SetValue(Value4Property, value); } - } - - - private bool _FocusLocked; - public bool FocusLocked - { - get - { - return _FocusLocked; - } - set - { - if (_FocusLocked != value) - { - _FocusLocked = value; - OnPropertyChanged(); - } - } - } - - public event EventHandler FocusedItemChanged; - - public class FocusedItemChangedArgs : EventArgs - { - public FocusedItemChangedArgs(SkiaControl item, bool isFocused) - { - Item = item; - IsFocused = isFocused; - } - - public bool IsFocused { get; set; } - - public SkiaControl Item { get; set; } - } - - /// - /// Internal call by control, after reporting will affect FocusedChild but will not get FocusedItemChanged as it was its own call - /// - /// - public void ReportFocus(ISkiaGestureListener value, ISkiaGestureListener setter = null) - { - if (_focusedChild != value && !FocusLocked) - { - if (_focusedChild != null) - { - Debug.WriteLine($"[UNFOCUSED] {_focusedChild}"); - if (_focusedChild != setter || setter == null) - _focusedChild.OnFocusChanged(false); - - FocusedItemChanged?.Invoke(this, new(_focusedChild as SkiaControl, false)); - } - - - - if (value != null) - { - if (value != setter || setter == null) - { - var accept = value.OnFocusChanged(true); - if (!accept) - { - value = null; - } - } - - if (value != null) - { - FocusedItemChanged?.Invoke(this, new(value as SkiaControl, true)); - } - } - - _focusedChild = value; - Debug.WriteLine($"[FOCUSED] {_focusedChild}"); - - if (_focusedChild == null) - { - Debug.WriteLine($"[FOCUSED] {_focusedChild}"); - - //with delay maybe some other control will focus itsself in that time - ResetFocusWithDelay(150); - } - - - OnPropertyChanged(nameof(FocusedChild)); - } - } - - private static RestartingTimer _timerResetFocus; - - public void ResetFocusWithDelay(int ms) - { - if (_timerResetFocus == null) - { - _timerResetFocus = new(TimeSpan.FromMilliseconds(ms), (arg) => - { - if (FocusedChild == null) - { - - MainThread.BeginInvokeOnMainThread(() => - { - try - { - this.Focus(); - } - catch (Exception e) - { - Super.Log(e); - } + protected void AddOrRemoveView(SkiaControl subView, bool add) + { + if (subView != null) + { + if (add) + { + AddSubView(subView); + subView.BindingContext = this.BindingContext; + } + else + { + RemoveSubView(subView); + subView.BindingContext = null; + //subView.Dispose(); ????? + } + + } + } + + private static void ChildrenPropertyChanged(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is DrawnView skiaControl) + { + var enumerableChildren = (IEnumerable)newvalue; + + if (oldvalue != null) + { + var oldViews = (IEnumerable)oldvalue; + + if (oldvalue is INotifyCollectionChanged oldCollection) + { + oldCollection.CollectionChanged -= skiaControl.OnChildrenCollectionChanged; + } + + foreach (var subView in oldViews) + { + skiaControl.AddOrRemoveView(subView, false); + } + } + + //foreach (var subView in enumerableChildren) + //{ + // skiaControl.SetChildren(enumerableChildren); + //} + skiaControl.SetChildren(enumerableChildren); + + if (newvalue is INotifyCollectionChanged newCollection) + { + newCollection.CollectionChanged += skiaControl.OnChildrenCollectionChanged; + } + + skiaControl.Update(); + + } + + } + + private void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (SkiaControl newChildren in e.NewItems) + { + newChildren.SetParent(this); + } + + break; + + case NotifyCollectionChangedAction.Reset: + case NotifyCollectionChangedAction.Remove: + foreach (SkiaControl oldChildren in e.OldItems ?? new SkiaControl[0]) + { + oldChildren.SetParent(null); + } + + break; + } + + Update(); + } + + #endregion + + public static readonly BindableProperty UpdateModeProperty = BindableProperty.Create( + nameof(UpdateMode), + typeof(UpdateMode), + typeof(DrawnView), + UpdateMode.Dynamic, + propertyChanged: ChangeUpdateMode); + + private static void ChangeUpdateMode(BindableObject bindable, object oldvalue, object newvalue) + { + if (bindable is DrawnView control) + { + control.Update(); + } + } + + public UpdateMode UpdateMode + { + get { return (UpdateMode)GetValue(UpdateModeProperty); } + set { SetValue(UpdateModeProperty, value); } + } + + public static readonly BindableProperty ClipEffectsProperty = BindableProperty.Create( + nameof(ClipEffects), + typeof(bool), + typeof(DrawnView), + true); + + public bool ClipEffects + { + get { return (bool)GetValue(ClipEffectsProperty); } + set { SetValue(ClipEffectsProperty, value); } + } + + public static readonly BindableProperty Value1Property = BindableProperty.Create( + nameof(Value1), + typeof(double), + typeof(DrawnView), + 0.0, + propertyChanged: RedrawCanvas); + + public double Value1 + { + get { return (double)GetValue(Value1Property); } + set { SetValue(Value1Property, value); } + } + + public static readonly BindableProperty Value2Property = BindableProperty.Create( + nameof(Value2), + typeof(double), + typeof(DrawnView), + 0.0, + propertyChanged: RedrawCanvas); + + public double Value2 + { + get { return (double)GetValue(Value2Property); } + set { SetValue(Value2Property, value); } + } + + public static readonly BindableProperty Value3Property = BindableProperty.Create( + nameof(Value3), + typeof(double), + typeof(DrawnView), + 0.0, + propertyChanged: RedrawCanvas); + + public double Value3 + { + get { return (double)GetValue(Value3Property); } + set { SetValue(Value3Property, value); } + } + + public static readonly BindableProperty Value4Property = BindableProperty.Create( + nameof(Value4), + typeof(double), + typeof(DrawnView), + 0.0, + propertyChanged: RedrawCanvas); + + public double Value4 + { + get { return (double)GetValue(Value4Property); } + set { SetValue(Value4Property, value); } + } + + + private bool _FocusLocked; + public bool FocusLocked + { + get + { + return _FocusLocked; + } + set + { + if (_FocusLocked != value) + { + _FocusLocked = value; + OnPropertyChanged(); + } + } + } + + public event EventHandler FocusedItemChanged; + + public class FocusedItemChangedArgs : EventArgs + { + public FocusedItemChangedArgs(SkiaControl item, bool isFocused) + { + Item = item; + IsFocused = isFocused; + } + + public bool IsFocused { get; set; } + + public SkiaControl Item { get; set; } + } + + /// + /// Internal call by control, after reporting will affect FocusedChild but will not get FocusedItemChanged as it was its own call + /// + /// + public void ReportFocus(ISkiaGestureListener value, ISkiaGestureListener setter = null) + { + if (_focusedChild != value && !FocusLocked) + { + if (_focusedChild != null) + { + Debug.WriteLine($"[UNFOCUSED] {_focusedChild}"); + if (_focusedChild != setter || setter == null) + _focusedChild.OnFocusChanged(false); + + FocusedItemChanged?.Invoke(this, new(_focusedChild as SkiaControl, false)); + } + + + + if (value != null) + { + if (value != setter || setter == null) + { + var accept = value.OnFocusChanged(true); + if (!accept) + { + value = null; + } + } + + if (value != null) + { + FocusedItemChanged?.Invoke(this, new(value as SkiaControl, true)); + } + } + + _focusedChild = value; + Debug.WriteLine($"[FOCUSED] {_focusedChild}"); + + if (_focusedChild == null) + { + Debug.WriteLine($"[FOCUSED] {_focusedChild}"); + + //with delay maybe some other control will focus itsself in that time + ResetFocusWithDelay(150); + } + + + OnPropertyChanged(nameof(FocusedChild)); + } + } + + private static RestartingTimer _timerResetFocus; + + public void ResetFocusWithDelay(int ms) + { + if (_timerResetFocus == null) + { + _timerResetFocus = new(TimeSpan.FromMilliseconds(ms), (arg) => + { + if (FocusedChild == null) + { + + MainThread.BeginInvokeOnMainThread(() => + { + try + { + this.Focus(); + } + catch (Exception e) + { + Super.Log(e); + } #if ANDROID ResetFocus(); #else - TouchEffect.CloseKeyboard(); + TouchEffect.CloseKeyboard(); #endif - }); - - - } - }); - _timerResetFocus.Start(null); - } - else - { - _timerResetFocus.Restart(null); - } - } - - public ISkiaGestureListener FocusedChild - { - get - { - return _focusedChild; - } - set - { - ReportFocus(value); - } - } - ISkiaGestureListener _focusedChild; - private ISkiaDrawable _canvasView; - private bool _wasBusy; - - /// - /// - /// - [Obsolete("Used by Update() when Super.UseLegacyLoop is True")] - protected void InvalidateCanvas() - { - IsDirty = true; - - if (CanvasView == null) - { - OrderedDraw = false; - return; - } - - //sanity check - var widthPixels = (int)CanvasView.CanvasSize.Width; - var heightPixels = (int)CanvasView.CanvasSize.Height; - if (widthPixels > 0 && heightPixels > 0) - { + }); + + + } + }); + _timerResetFocus.Start(null); + } + else + { + _timerResetFocus.Restart(null); + } + } + + public ISkiaGestureListener FocusedChild + { + get + { + return _focusedChild; + } + set + { + ReportFocus(value); + } + } + ISkiaGestureListener _focusedChild; + private ISkiaDrawable _canvasView; + private bool _wasBusy; + + /// + /// + /// + [Obsolete("Used by Update() when Super.UseLegacyLoop is True")] + protected void InvalidateCanvas() + { + IsDirty = true; + + if (CanvasView == null) + { + OrderedDraw = false; + return; + } + + //sanity check + var widthPixels = (int)CanvasView.CanvasSize.Width; + var heightPixels = (int)CanvasView.CanvasSize.Height; + if (widthPixels > 0 && heightPixels > 0) + { #if ANDROID || WINDOWS - if (NeedCheckParentVisibility) - CheckElementVisibility(this); - Continue(); + if (NeedCheckParentVisibility) + CheckElementVisibility(this); + Continue(); #else - MainThread.BeginInvokeOnMainThread(() => - { - try - { - if (NeedCheckParentVisibility) - CheckElementVisibility(this); - Continue(); - } - catch (Exception e) - { - Console.WriteLine(e); - } - - }); + MainThread.BeginInvokeOnMainThread(() => + { + try + { + if (NeedCheckParentVisibility) + CheckElementVisibility(this); + Continue(); + } + catch (Exception e) + { + Console.WriteLine(e); + } + + }); #endif - void Continue() - { - if (CanvasView != null) - { - if (CanDraw && !CanvasView.IsDrawing && !_isWaiting) //passed checks // - { - _wasBusy = false; - _isWaiting = true; - InvalidatedCanvas++; - MainThread.BeginInvokeOnMainThread(async () => - { - try - { + void Continue() + { + if (CanvasView != null) + { + 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; - _lastUpdateTimeNanos = nowNanos; + //cap fps around 120fps + var nowNanos = Super.GetCurrentTimeNanos(); + var elapsedMicros = (nowNanos - _lastUpdateTimeNanos) / 1_000.0; + _lastUpdateTimeNanos = nowNanos; - var needWait = - Super.CapMicroSecs + var needWait = + Super.CapMicroSecs #if IOS || MACCATALYST * 2 // apple is double buffered #endif - - elapsedMicros; - if (needWait < 1) - needWait = 1; - - var ms = (int)(needWait / 1000); - if (ms < 1) - ms = 1; - await Task.Delay(ms); + - elapsedMicros; + if (needWait < 1) + needWait = 1; + + var ms = (int)(needWait / 1000); + if (ms < 1) + ms = 1; + await Task.Delay(ms); #else - await Task.Delay(1); + 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) - { - Super.Log(e); - } - finally - { - _isWaiting = false; - if (_wasBusy) - { - Update(); - } - } - - }); - return; - } - else - { - _wasBusy = true; - } - } - OrderedDraw = false; - } - } - else - { - OrderedDraw = false; - } - } - - - public bool GetIsVisibleWithParent(VisualElement element) - { - if (element != null) - { - if (!element.IsVisible) - return false; - - if (element.Parent is VisualElement visualParent) - { - return GetIsVisibleWithParent(visualParent); - } - } - - return true; - } + CanvasView?.Update(); //very rarely could throw on windows here if maui destroys view when navigating, so we secured with try-catch + } + catch (Exception e) + { + Super.Log(e); + } + finally + { + _isWaiting = false; + if (_wasBusy) + { + Update(); + } + } + + }); + return; + } + else + { + _wasBusy = true; + } + } + OrderedDraw = false; + } + } + else + { + OrderedDraw = false; + } + } + + protected override void OnParentSet() + { + base.OnParentSet(); + + NeedCheckParentVisibility = true; + } + + private VisualElement _visibilityParent; + + private void OnParentVisibilityCheck(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(IsVisible)) + { + _visibilityParent.PropertyChanged -= OnParentVisibilityCheck; + NeedCheckParentVisibility = true; + } + } + + + public bool GetIsVisibleWithParent(VisualElement element) + { + if (element != null) + { + if (!element.IsVisible) + { + if (element is not DrawnView) + { + if (_visibilityParent != null) + _visibilityParent.PropertyChanged -= OnParentVisibilityCheck; + + _visibilityParent = element; + _visibilityParent.PropertyChanged += OnParentVisibilityCheck; + element.PropertyChanged += OnParentVisibilityCheck; + } + return false; + } + + if (element.Parent is VisualElement visualParent) + { + return GetIsVisibleWithParent(visualParent); + } + } + + return true; + } #if !ONPLATFORM @@ -2558,13 +2681,13 @@ protected virtual void OnSizeChanged() #endif - public virtual void ClipSmart(SKCanvas canvas, SKPath path, SKClipOperation operation = SKClipOperation.Intersect) - { - canvas.ClipPath(path, operation, false); - } + public virtual void ClipSmart(SKCanvas canvas, SKPath path, SKClipOperation operation = SKClipOperation.Intersect) + { + canvas.ClipPath(path, operation, false); + } - } + } } \ No newline at end of file diff --git a/src/Engine/skia3.props b/src/Engine/skia3.props index 3eb1699d..dfc9ed19 100644 --- a/src/Engine/skia3.props +++ b/src/Engine/skia3.props @@ -4,8 +4,8 @@ - - + + diff --git a/src/samples/Sandbox/Game/SpaceShooter.xaml b/src/samples/Sandbox/Game/SpaceShooter.xaml index 0cd1ffa8..bb822459 100644 --- a/src/samples/Sandbox/Game/SpaceShooter.xaml +++ b/src/samples/Sandbox/Game/SpaceShooter.xaml @@ -11,6 +11,7 @@ HorizontalOptions="Fill" IsClippedToBounds="True" Tag="Game" + VerticalOptions="Fill"> @@ -193,6 +194,7 @@ VerticalOptions="End" WidthRequest="40" ZIndex="6" + x:DataType="game:SpaceShooter" Value="{Binding Health}" /> @@ -210,6 +212,7 @@ + diff --git a/src/samples/Sandbox/MainPage.xaml.cs b/src/samples/Sandbox/MainPage.xaml.cs index 7a711d26..3bb09b98 100644 --- a/src/samples/Sandbox/MainPage.xaml.cs +++ b/src/samples/Sandbox/MainPage.xaml.cs @@ -1,8 +1,4 @@ -using AppoMobi.Maui.Gestures; -using AppoMobi.Specials; -using DrawnUi.Maui.Infrastructure; -using DrawnUi.Maui.Views; -using Sandbox; +using Sandbox; using Sandbox.Views; namespace MauiNet8; diff --git a/src/samples/Sandbox/MainPageDev.xaml b/src/samples/Sandbox/MainPageDev.xaml index 01732e61..9c5d71ed 100644 --- a/src/samples/Sandbox/MainPageDev.xaml +++ b/src/samples/Sandbox/MainPageDev.xaml @@ -5,6 +5,7 @@ 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:system="clr-namespace:System;assembly=System.Runtime" xmlns:views="clr-namespace:Sandbox.Views" x:Name="ThisPage" BackgroundColor="Black"> @@ -30,86 +31,98 @@ HorizontalOptions="Fill" VerticalOptions="Fill"> - - - - - + --> - - - - - + + + + Images/8.jpg + Images/monkey1.jpg + Images/hugrobot2.jpg + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + HorizontalTextAlignment="Center" + Text="{Binding Source={x:Reference MainCarousel}, Path=TransitionProgress, StringFormat='{0}'}" + TextColor="White" + VerticalTextAlignment="Center" />--> + + + diff --git a/src/samples/Sandbox/MainPageDev.xaml.cs b/src/samples/Sandbox/MainPageDev.xaml.cs index 4d161380..94367af4 100644 --- a/src/samples/Sandbox/MainPageDev.xaml.cs +++ b/src/samples/Sandbox/MainPageDev.xaml.cs @@ -13,24 +13,7 @@ namespace MauiNet8; -public class BuggedLayout : SkiaLayout -{ - protected override void Draw(SkiaDrawingContext context, SKRect destination, float scale) - { - base.Draw(context, destination, scale); - //if (UsingCacheType == SkiaCacheType.ImageDoubleBuffered && RenderObject != null) - //{ - // Debug.WriteLine($"[D] {Superview.CanvasView.CanvasSize.Width} -> {RenderObject.Bounds.Width} at {DrawingRect.Width}"); - - // if (DrawingRect.Width != RenderObject.Bounds.Width) - // { - // InvalidateMeasure(); - // } - //} - - } -} public partial class MainPageDev : BasePage { @@ -40,11 +23,17 @@ public MainPageDev() { try { - InitializeComponent(); - Test(); + Items.AddRange(new[] + { + "8.jpg","monkey1.jpg","hugrobot2.jpg" + }); + + InitializeComponent(); _shaders = Files.ListAssets(path); + + } catch (Exception e) { @@ -52,6 +41,26 @@ public MainPageDev() } } + public ObservableRangeCollection Items { get; } = new(); + + private int _SelectedIndex; + public int SelectedIndex + { + get + { + return _SelectedIndex; + } + set + { + if (_SelectedIndex != value) + { + _SelectedIndex = value; + OnPropertyChanged(); + } + } + } + + void Test() { // string shaderCode = SkSl.LoadFromResources($"{MauiProgram.ShadersFolder}/apple.sksl"); diff --git a/src/samples/Sandbox/MainPageDev2.xaml b/src/samples/Sandbox/MainPageDev2.xaml new file mode 100644 index 00000000..39cbc107 --- /dev/null +++ b/src/samples/Sandbox/MainPageDev2.xaml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/samples/Sandbox/MainPageDev2.xaml.cs b/src/samples/Sandbox/MainPageDev2.xaml.cs new file mode 100644 index 00000000..36ba3503 --- /dev/null +++ b/src/samples/Sandbox/MainPageDev2.xaml.cs @@ -0,0 +1,128 @@ + +using AppoMobi.Specials; +using DrawnUi.Maui.Infrastructure; +using DrawnUi.Maui.Internals; +using Sandbox; +using Sandbox.Resources.Strings; +using Sandbox.Views; +using Sandbox.Views.Xaml2Pdf; +using System.Diagnostics; +using System.Text; + + + +namespace MauiNet8; + + + +public partial class MainPageDev2 : BasePage +{ + private readonly List _shaders; + + public MainPageDev2() + { + try + { + + Items.AddRange(new[] + { + "8.jpg","monkey1.jpg","hugrobot2.jpg" + }); + + InitializeComponent(); + + _shaders = Files.ListAssets(path); + + + } + catch (Exception e) + { + Super.DisplayException(this, e); + } + } + + public ObservableRangeCollection Items { get; } = new(); + + void Test() + { + // 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/Resources/Raw/Images/8.jpg b/src/samples/Sandbox/Resources/Raw/Images/8.jpg index 860d3fdf..741f134f 100644 Binary files a/src/samples/Sandbox/Resources/Raw/Images/8.jpg and b/src/samples/Sandbox/Resources/Raw/Images/8.jpg differ diff --git a/src/samples/Sandbox/Resources/Raw/Images/_8.jpg b/src/samples/Sandbox/Resources/Raw/Images/_8.jpg new file mode 100644 index 00000000..860d3fdf Binary files /dev/null and b/src/samples/Sandbox/Resources/Raw/Images/_8.jpg differ diff --git a/src/samples/Sandbox/Resources/Raw/Images/hugrobot2.jpg b/src/samples/Sandbox/Resources/Raw/Images/hugrobot2.jpg new file mode 100644 index 00000000..e967ba87 Binary files /dev/null and b/src/samples/Sandbox/Resources/Raw/Images/hugrobot2.jpg differ diff --git a/src/samples/Sandbox/Resources/Raw/Images/monkey1.jpg b/src/samples/Sandbox/Resources/Raw/Images/monkey1.jpg new file mode 100644 index 00000000..886639d7 Binary files /dev/null and b/src/samples/Sandbox/Resources/Raw/Images/monkey1.jpg differ diff --git a/src/samples/Sandbox/Sandbox.csproj b/src/samples/Sandbox/Sandbox.csproj index 71e69c0e..98f59ffd 100644 --- a/src/samples/Sandbox/Sandbox.csproj +++ b/src/samples/Sandbox/Sandbox.csproj @@ -112,6 +112,10 @@ + + + + @@ -189,19 +193,14 @@ + True True ResStrings.resx - - MainPageDrawnSpans.xaml - - - - MainPageGif.xaml - + PublicResXFileCodeGenerator @@ -210,6 +209,9 @@ + + + @@ -220,7 +222,14 @@ + + + + + + + @@ -283,6 +292,8 @@ + + diff --git a/src/samples/Sandbox/ViewModels/MainPageViewModel.cs b/src/samples/Sandbox/ViewModels/MainPageViewModel.cs index 52e11f46..0f192fa3 100644 --- a/src/samples/Sandbox/ViewModels/MainPageViewModel.cs +++ b/src/samples/Sandbox/ViewModels/MainPageViewModel.cs @@ -1,6 +1,4 @@ using AppoMobi.Specials; -using DrawnUi.Maui; -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Windows.Input; @@ -22,7 +20,7 @@ public Color SelectedColor { _selectedColor = value; OnPropertyChanged(); - Debug.WriteLine($"Tint {value}"); + //Debug.WriteLine($"Tint {value}"); } } } diff --git a/src/samples/Sandbox/Views/Controls/ShaderDoubleTexturesEffect.cs b/src/samples/Sandbox/Views/Controls/ShaderDoubleTexturesEffect.cs deleted file mode 100644 index 9913cd66..00000000 --- a/src/samples/Sandbox/Views/Controls/ShaderDoubleTexturesEffect.cs +++ /dev/null @@ -1,183 +0,0 @@ -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 664ea1dd..c3e6a866 100644 --- a/src/samples/Sandbox/Views/Controls/ShaderTransition.cs +++ b/src/samples/Sandbox/Views/Controls/ShaderTransition.cs @@ -1,4 +1,3 @@ -using AppoMobi.Maui.Gestures; using DrawnUi.Maui.Infrastructure; namespace Sandbox.Views.Controls; @@ -48,7 +47,7 @@ protected override SKRuntimeEffectUniforms CreateUniforms(SKRect destination) return uniforms; } - protected override SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingContext ctx, SKRect destination, SKShader texture1) + protected override SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingContext ctx, SKRect destination, SKShader primaryTexture) { if (ControlTo == null || ControlTo.RenderObject == null) { @@ -59,14 +58,14 @@ protected override SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingCon var snapshot2 = ControlTo.RenderObject.Image; - if (texture1 != null && snapshot2 != null) + if (primaryTexture != null && snapshot2 != null) { //var texture1 = snapshot.ToShader(SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); var texture2 = snapshot2.ToShader(SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); return new SKRuntimeEffectChildren(CompiledShader) { - { "iImage1", texture1 }, + { "iImage1", primaryTexture }, { "iImage2", texture2 } }; } @@ -142,56 +141,6 @@ public virtual ISkiaGestureListener ProcessGestures( } } - - -public class TestShaderEffect : SkiaShaderEffect -{ - - public static readonly BindableProperty ProgressProperty = BindableProperty.Create(nameof(Progress), - typeof(double), - typeof(TestShaderEffect), - 0.0); - public double Progress - { - get { return (double)GetValue(ProgressProperty); } - set { SetValue(ProgressProperty, value); } - } - -} - -public class AnimatedShaderTransition : ShaderTransition -{ - public AnimatedShaderTransition() - { - // "transitions/fade.sksl"; - // "transitions/doorway.sksl"; - //ShaderFilename = "transitions/cube.sksl"; - //ShaderFilename = "transitions/crosswarp.sksl"; - ShaderFilename = "transitions/new.sksl"; - - } - - private PingPongAnimator _animator; - - protected override void OnLayoutReady() - { - base.OnLayoutReady(); - - if (_animator == null) - { - _animator = new(this); - - _animator.Start((v) => - { - this.Progress = v; - Update(); - }, 0, 1, 3500); - } - - } - -} - public class ShaderTransition : SkiaControl { protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float scale, object arguments) @@ -237,13 +186,34 @@ protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float private SKRuntimeEffectChildren _passTextures; public double Progress { get; set; } - public string ShaderFilename { get; set; } + + public string ShaderFilename + { + get => _shaderFilename; + set + { + if (value == _shaderFilename) + return; + + _shaderFilename = value; + OnPropertyChanged(); + Recompile(); + } + } + + void CreateShader() { - string shaderCode = SkSl.LoadFromResources($"{MauiProgram.ShadersFolder}/{ShaderFilename}"); + string shaderCode = SkSl.LoadFromResources($"{ShaderFilename}"); _compiledShader = SkSl.Compile(shaderCode); } + public void Recompile() + { + _needRecompile = true; + UpdateTextures(); + } + public static readonly BindableProperty ControlFromProperty = BindableProperty.Create( nameof(ControlFrom), typeof(SkiaControl), typeof(ShaderTransition), @@ -286,7 +256,7 @@ void ApplyControlFrom(SkiaControl control) if (_controlFrom != null) { _controlFrom.CreatedCache += OnCacheCreatedFrom; - _controlFrom.DelegateDrawCache += DrawContentImageFrom; + //_controlFrom.DelegateDrawCache += DrawContentImageFrom; } } @@ -307,10 +277,13 @@ void ApplyControlTo(SkiaControl control) if (_controlTo != null) { _controlTo.CreatedCache += OnCacheCreatedTo; - _controlTo.DelegateDrawCache += DrawContentImageTo; + //_controlTo.DelegateDrawCache += DrawContentImageTo; } } + private bool _needRecompile; + private string _shaderFilename; + public override void OnDisposing() { base.OnDisposing(); @@ -320,14 +293,14 @@ public override void OnDisposing() _compiledShader?.Dispose(); _passTextures?.Dispose(); - } + void DetachFrom() { if (_controlFrom != null) { _controlFrom.CreatedCache -= OnCacheCreatedFrom; - _controlFrom.DelegateDrawCache -= DrawContentImageFrom; + //_controlFrom.DelegateDrawCache -= DrawContentImageFrom; _controlFrom = null; } } @@ -337,21 +310,23 @@ void DetachTo() if (_controlTo != null) { _controlTo.CreatedCache -= OnCacheCreatedTo; - _controlTo.DelegateDrawCache -= DrawContentImageTo; + //_controlTo.DelegateDrawCache -= DrawContentImageTo; _controlTo = null; } } - private void DrawContentImageFrom(CachedObject arg1, SkiaDrawingContext arg2, SKRect arg3) - { } - private void DrawContentImageTo(CachedObject arg1, SkiaDrawingContext arg2, SKRect arg3) - { + //private void DrawContentImageTo(CachedObject arg1, SkiaDrawingContext arg2, SKRect arg3) + //{ - } + //} private void OnCacheCreatedTo(object sender, CachedObject e) { UpdateTextures(); } + + //private void DrawContentImageFrom(CachedObject arg1, SkiaDrawingContext arg2, SKRect arg3) + //{ } + private void OnCacheCreatedFrom(object sender, CachedObject e) { UpdateTextures(); @@ -366,6 +341,13 @@ void UpdateTextures() || _controlTo.RenderObject == null || _controlTo.RenderObject.Image == null) return; + if (_needRecompile) + { + _needRecompile = false; + _compiledShader?.Dispose(); + _compiledShader = null; + } + if (_compiledShader == null) { CreateShader(); diff --git a/src/samples/Sandbox/Views/Controls/Shaders/AnimatedCarousel.cs b/src/samples/Sandbox/Views/Controls/Shaders/AnimatedCarousel.cs new file mode 100644 index 00000000..3dc05e35 --- /dev/null +++ b/src/samples/Sandbox/Views/Controls/Shaders/AnimatedCarousel.cs @@ -0,0 +1,392 @@ +using AppoMobi.Maui.Gestures; +using DrawnUi.Maui.Infrastructure; +using DrawnUi.Maui.Internals; + +namespace Sandbox.Views.Controls; + +public enum PlayType +{ + Default, + Next, + Random +} + +public record Transition(string Name, string Source, int Speed); + +/// +/// Subclassed CarouselWithTransitions to add automation to transitions, +/// totally specific to this demo case +/// +public class AnimatedCarousel : CarouselWithTransitions +{ + + public AnimatedCarousel() + { + //_shaders = Files.ListAssets(path); //not using this, will provide a limited list below: + + //some adapted shaders from https://github.com/gl-transitions/gl-transitions + _transitions = new List + { + //pointless + //new("Bookflip", "bookflip.sksl", 1500), + //new("Bounce", "bounce.sksl", 1500), + + new("Bow Tie Horizontal", "bowtiehorizontal.sksl", 750), + new("Bow Tie Vertical", "bowtievertical.sksl", 750), + + //bugs + //new("Butterfly Waves Crawler", "butterflywavescrawler.sksl", 250), + + new("Circlecrop", "circlecrop.sksl", 1500), + + new("Circleopen", "circleopen.sksl", 750), + + new("Colorphase", "colorphase.sksl", 750), + + new("Cross-hatch", "crosshatch.sksl", 1000), + + new("Cross-warp", "crosswarp.sksl", 750), + + new("Cross-zoom", "crosszoom.sksl", 750), + + //bugs + new("Cube", "cube.sksl", 750), + + new("Doorway", "doorway.sksl", 750), + + new("Dreamy", "dreamy.sksl", 750), + + new("Dreamy Zoom", "dreamyzoom.sksl", 750), + + new("Edge", "edgetransition.sksl", 750), + + new("Fade", "fade.sksl", 500), + + new("Fade Color", "fadecolor.sksl", 750), + + new("Fade Grayscale", "fadegrayscale.sksl", 750), + + new("Film Burn", "filmburn.sksl", 1250), + + new("Fly Eye", "flyeye.sksl", 1000), + + new("Heart", "heart.sksl", 750), + + new("Kaleidoscope", "kaleidoscope.sksl", 1000), + + new("Morph", "morph.sksl", 500), + + new("Mosaic", "mosaic.sksl", 750), + + new("Page Curl", "pagecurl.sksl", 1000), + + new("Pixelize", "pixelize.sksl", 1000), + + new("Rolls", "rolls.sksl", 750), + + new("Scale In", "scalein.sksl", 750), + + new("Swirl", "swirl.sksl", 1250), + + new("Tangent Motion Blur", "tangentmotionblur.sksl", 1000), + + new("Tv Static", "tvstatic.sksl", 750), + + new("Waterdrop", "waterdrop.sksl", 750), + + new("Wind", "wind.sksl", 750), + + new("Window Blinds", "windowblinds.sksl", 750), + + new("Window Slice", "windowslice.sksl", 1000), + + new("Wipe Down", "wipedown.sksl", 750), + + new("Wipe Left", "wipeleft.sksl", 750), + + new("Wipe Right", "wiperight.sksl", 750), + + new("Wipe Up", "wipeup.sksl", 750) + }; + + SetTransition(_transitions.First(x => x.Name == "Page Curl")); + } + + protected override void OnChildrenInitialized() + { + base.OnChildrenInitialized(); + + SetupAnimator(); + } + + private RangeAnimator _animator; + private LinearDirectionType animatingTo; + + void SetupAnimator() + { + if (!ChildrenInitialized) + return; + + if (_animator != null) + { + _animator.Stop(); + _animator.Dispose(); + _animator = null; + } + + if (_animator == null && AnimatorSpeedMs > 0) + { + _animator = new(this) + { + CycleFInished = () => + { + //every + if (PlayingType == PlayType.Random) + { + SetTransition(GettRandomShader()); + } + else + if (PlayingType == PlayType.Next) + { + SetTransition(GetNextShader()); + } + + //ping-pong-looping SelectedIndex + var index = SelectedIndex; + if (animatingTo == LinearDirectionType.Forward) + { + index++; + if (index > MaxIndex) + { + animatingTo = LinearDirectionType.Backward; + index -= 2; + } + } + else + { + index--; + if (index < 0) + { + animatingTo = LinearDirectionType.Forward; + index = 1; + } + } + SelectedIndex = index; + + SetupAnimator(); + } + }; + + _animator.Start((v) => + { + + }, 0, 1, (uint)AnimatorSpeedMs); + } + + } + + public void SetTransition(Transition transition) + { + ShaderFile = transition.Source; + LinearSpeedMs = transition.Speed; + } + + public static readonly BindableProperty AnimatorSpeedMsProperty = BindableProperty.Create(nameof(AnimatorSpeedMsProperty), + typeof(double), + typeof(AnimatedCarousel), + 0.0, + propertyChanged: (b, o, n) => + { + if (b is AnimatedCarousel control) + { + control.SetupAnimator(); + } + }); + + /// + /// If you set this higher than 0 will have an animator running ping-pong through slides. + /// + public double AnimatorSpeedMs + { + get { return (double)GetValue(AnimatorSpeedMsProperty); } + set { SetValue(AnimatorSpeedMsProperty, value); } + } + + + /// + /// overrided to track selected image filename + /// + /// + protected override void OnSelectedIndexChanged(int index) + { + base.OnSelectedIndexChanged(index); + + if (ItemsSource is IList strings) + { + if (index >= 0) + { + SelectedString = strings[index]; + } + else + { + SelectedString = string.Empty; + } + } + } + + private string _selectedString; + /// + /// to track selected image filename + /// + public string SelectedString + { + get + { + return _selectedString; + } + set + { + if (_selectedString != value) + { + _selectedString = value; + OnPropertyChanged(); + } + } + } + + + #region Select transition + + private readonly List _transitions; + //private readonly List _shaders; + + protected PlayType PlayingType { get; set; } + + private string path = @"Shaders\transitions"; + public string FullShaderPath + { + get + { + return $"{path}\\{ShaderFile}"; + } + set + { + + } + } + + private string _ShaderFile; + public string ShaderFile + { + get + { + return _ShaderFile; + } + set + { + if (_ShaderFile != value) + { + _ShaderFile = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(FullShaderPath)); + ShaderFilename = FullShaderPath; + } + } + } + + public override ISkiaGestureListener OnSkiaGestureEvent(SkiaGesturesParameters args, GestureEventProcessingInfo apply) + { + + if (args.Type == TouchActionResult.Tapped) + { + MainThread.BeginInvokeOnMainThread(SelectFIle); + return this; + } + + return base.OnSkiaGestureEvent(args, apply); + } + + static List options; + + async void SelectFIle() + { + if (_transitions.Count > 1) + { + if (options == null) + { + options = _transitions.Select(x => new SelectableAction + { + Action = async () => + { + PlayingType = PlayType.Default; + SetTransition(x); + }, + Title = x.Name + }).ToList(); + options.Insert(0, new SelectableAction() + { + Action = async () => + { + PlayingType = PlayType.Random; + SetTransition(GettRandomShader()); + }, + Title = "Loop All Random" + }); + options.Insert(0, new SelectableAction() + { + Action = async () => + { + PlayingType = PlayType.Next; + SetTransition(GetNextShader()); + }, + Title = "Loop All" + }); + } + + var selected = await PresentSelection(options, "Select Shader") as SelectableAction; + selected?.Action(); + } + } + + private int _loopIndex; + + Transition GetNextShader() + { + _loopIndex++; + if (_loopIndex > _transitions.Count - 1) + { + _loopIndex = 0; + } + return _transitions[_loopIndex]; + } + + Transition GettRandomShader() + { + var index = Random.Next(_transitions.Count - 1); + return _transitions[index]; + } + + 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; + } + + #endregion +} \ No newline at end of file diff --git a/src/samples/Sandbox/Views/Controls/Shaders/AnimatedShaderTransition.cs b/src/samples/Sandbox/Views/Controls/Shaders/AnimatedShaderTransition.cs new file mode 100644 index 00000000..f9ddcf1b --- /dev/null +++ b/src/samples/Sandbox/Views/Controls/Shaders/AnimatedShaderTransition.cs @@ -0,0 +1,223 @@ +using AppoMobi.Maui.Gestures; +using DrawnUi.Maui.Infrastructure; +using DrawnUi.Maui.Internals; + +namespace Sandbox.Views.Controls; + +/// +/// Custom control to ping-pong loop transition and change to new random file after every animation cycle +/// +public class AnimatedShaderTransition : ShaderTransition, ISkiaGestureListener +{ + public AnimatedShaderTransition() + { + _shaders = Files.ListAssets(path); + + ShaderFile = "fade.sksl"; //default + } + + private readonly List _shaders; + + protected PlayType PlayingType { get; set; } + + private string path = @"Shaders\transitions"; + public string FullShaderPath + { + get + { + return $"{path}\\{ShaderFile}"; + } + set + { + + } + } + + private string _ShaderFile; + public string ShaderFile + { + get + { + return _ShaderFile; + } + set + { + if (_ShaderFile != value) + { + _ShaderFile = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(FullShaderPath)); + ShaderFilename = FullShaderPath; + } + } + } + private PingPongAnimator _animator; + + protected override void OnLayoutReady() + { + base.OnLayoutReady(); + + SetupAnimator(); + } + + void SetupAnimator() + { + if (!IsLayoutReady) + return; + + if (_animator != null) + { + _animator.Stop(); + _animator.Dispose(); + _animator = null; + } + + if (_animator == null) + { + _animator = new(this) + { + CycleFInished = () => + { + if (PlayingType == PlayType.Random) + { + ShaderFile = GettRandomShader(); + } + else + if (PlayingType == PlayType.Next) + { + ShaderFile = GetNextShader(); + } + } + }; + + _animator.Start((v) => + { + this.Progress = v; + Update(); + }, 0, 1, (uint)DurationMs); + } + + } + + + public static readonly BindableProperty DurationMsProperty = BindableProperty.Create(nameof(DurationMsProperty), + typeof(double), + typeof(ShaderAnimatedTransitionEffect), + 3500.0, + propertyChanged: (b, o, n) => + { + if (b is AnimatedShaderTransition control) + { + control.SetupAnimator(); + } + }); + + + + public double DurationMs + { + get { return (double)GetValue(DurationMsProperty); } + set { SetValue(DurationMsProperty, value); } + } + + + public override ISkiaGestureListener OnSkiaGestureEvent(SkiaGesturesParameters args, GestureEventProcessingInfo apply) + { + + if (args.Type == TouchActionResult.Tapped) + { + MainThread.BeginInvokeOnMainThread(SelectFIle); + return this; + } + + return base.OnSkiaGestureEvent(args, apply); + } + + public bool OnFocusChanged(bool focus) + { + return false; + } + + static List options; + + async void SelectFIle() + { + if (_shaders.Count > 1) + { + if (options == null) + { + options = _shaders.Select(name => new SelectableAction + { + Action = async () => + { + PlayingType = PlayType.Default; + ShaderFile = name; + }, + Title = name + }).ToList(); + options.Insert(0, new SelectableAction() + { + Action = async () => + { + PlayingType = PlayType.Random; + ShaderFile = GettRandomShader(); + }, + Title = "Loop All Random" + }); + options.Insert(0, new SelectableAction() + { + Action = async () => + { + PlayingType = PlayType.Next; + ShaderFile = GetNextShader(); + }, + Title = "Loop All" + }); + } + + var selected = await PresentSelection(options, "Select Shader") as SelectableAction; + selected?.Action(); + } + } + + private int _loopIndex; + + string GetNextShader() + { + _loopIndex++; + if (_loopIndex > _shaders.Count - 1) + { + _loopIndex = 0; + } + return _shaders[_loopIndex]; + } + + string GettRandomShader() + { + var index = Random.Next(_shaders.Count - 1); + return _shaders[index]; + } + + 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; + } + +} \ No newline at end of file diff --git a/src/samples/Sandbox/Views/Controls/Shaders/CarouselWithTransitions.cs b/src/samples/Sandbox/Views/Controls/Shaders/CarouselWithTransitions.cs new file mode 100644 index 00000000..cadd2fed --- /dev/null +++ b/src/samples/Sandbox/Views/Controls/Shaders/CarouselWithTransitions.cs @@ -0,0 +1,119 @@ +using System.Diagnostics; +using System.Numerics; + +namespace Sandbox.Views.Controls; + +/// +/// Sublclassed SkiaCarousel showing a shader effect for transitions +/// +public class CarouselWithTransitions : SkiaCarousel +{ + public CarouselWithTransitions() + { + Effect = new() + { + ShaderSource = ShaderFilename + }; + } + + private string _ShaderFilename = @"Shaders\transitions\fade.sksl"; + public string ShaderFilename + { + get + { + return _ShaderFilename; + } + set + { + if (_ShaderFilename != value) + { + _ShaderFilename = value; + + if (Effect != null) + Effect.ShaderSource = value; + + OnPropertyChanged(); + } + } + } + + private ShaderTransitionEffect Effect { get; } + + public override void Render(SkiaDrawingContext context, SKRect destination, float scale) + { + if (!this.VisualEffects.Contains(Effect)) + { + VisualEffects.Add(Effect); //all the magic will be done with this effect + } + + base.Render(context, destination, scale); + } + + protected virtual void OnFromToChanged() + { + + } + + protected override void OnScrollProgressChanged() + { + if (ScrollProgress >= 0 && ScrollProgress <= 1) //ignore bouncing + { + var currentIndex = 0; + if (ScrollProgress > 0) + currentIndex = (int)Math.Floor((MaxIndex) * this.ScrollProgress); + + var progress = this.TransitionProgress; + + if (IndexFrom != currentIndex) + { + if (currentIndex < MaxIndex) + { + IndexTo = currentIndex + 1; + IndexFrom = currentIndex; + + if (IndexToLast != IndexTo || IndexFromLast != IndexFrom) + { + IndexToLast = IndexTo; + IndexFromLast = IndexFrom; + + var viewFrom = ChildrenFactory.GetChildAt(IndexFrom); + var viewTo = ChildrenFactory.GetChildAt(IndexTo); + + if (viewFrom == null || viewTo == null) + { + throw new ApplicationException("Unexpected null"); + } + + Effect.ControlFrom = viewFrom; + Effect.ControlTo = viewTo; + + //Debug.WriteLine($"Set new sources {IndexFrom} ({viewFrom.BindingContext}) <=> {IndexTo} ({viewTo.BindingContext}) at progress {progress:0.00}, scroll {ScrollProgress:0.00}"); + } + + } + else + { + progress = 1.0; + } + + OnFromToChanged(); + } + + Effect.Progress = progress; + + Effect.Update(); + } + + } + + //to skip default slides animation via translation, not calling base + protected override void AnimateVisibleChild(SkiaControl view, Vector2 position) + { + } + + private int IndexFrom = -1; + private int IndexTo = -1; + private int IndexFromLast = -1; + private int IndexToLast = -1; + +} \ No newline at end of file diff --git a/src/samples/Sandbox/Views/Controls/MultiRippleWithTouchEffect.cs b/src/samples/Sandbox/Views/Controls/Shaders/MultiRippleWithTouchEffect.cs similarity index 100% rename from src/samples/Sandbox/Views/Controls/MultiRippleWithTouchEffect.cs rename to src/samples/Sandbox/Views/Controls/Shaders/MultiRippleWithTouchEffect.cs diff --git a/src/samples/Sandbox/Views/Controls/Shaders/ShaderAnimatedTransitionEffect.cs b/src/samples/Sandbox/Views/Controls/Shaders/ShaderAnimatedTransitionEffect.cs new file mode 100644 index 00000000..0422567e --- /dev/null +++ b/src/samples/Sandbox/Views/Controls/Shaders/ShaderAnimatedTransitionEffect.cs @@ -0,0 +1,45 @@ +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 ShaderAnimatedTransitionEffect : ShaderTransitionEffect +{ + + public static readonly BindableProperty DurationMsProperty = BindableProperty.Create(nameof(DurationMsProperty), + typeof(double), + typeof(ShaderAnimatedTransitionEffect), + 350.0, + propertyChanged: NeedChangeSource); + public double DurationMs + { + get { return (double)GetValue(DurationMsProperty); } + set { SetValue(DurationMsProperty, value); } + } + + bool _initialized; + + public override 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, (uint)DurationMs); + } + } + + base.Update(); + } + + private PingPongAnimator _animator; +} \ No newline at end of file diff --git a/src/samples/Sandbox/Views/Controls/Shaders/ShaderDoubleTexturesEffect.cs b/src/samples/Sandbox/Views/Controls/Shaders/ShaderDoubleTexturesEffect.cs new file mode 100644 index 00000000..4936ee8c --- /dev/null +++ b/src/samples/Sandbox/Views/Controls/Shaders/ShaderDoubleTexturesEffect.cs @@ -0,0 +1,472 @@ +using AppoMobi.Specials; +using SKBitmap = SkiaSharp.SKBitmap; + +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); + } + + protected override void OnDisposing() + { + base.OnDisposing(); + + SecondaryTexture?.Dispose(); + LoadedSecondaryBitmap?.Dispose(); + + LoadedPrimaryBitmap?.Dispose(); + ResizedPrimaryImage?.Dispose(); + + DetachFrom(); + DetachTo(); + } + + protected SKShaderTileMode TilingSecondaryTexture = SKShaderTileMode.Mirror; + + protected override SKRuntimeEffectChildren CreateTexturesUniforms(SkiaDrawingContext ctx, SKRect destination, SKShader primaryTexture) + { + var secondaryTexture = GetSecondaryTexture(); + + if (primaryTexture != null && secondaryTexture != null) + { + return new SKRuntimeEffectChildren(CompiledShader) + { + { "iImage1", primaryTexture }, //main + { "iImage2", secondaryTexture } //secondary + }; + } + else + { + return new SKRuntimeEffectChildren(CompiledShader) + { + }; + } + } + + //normally primary texture comes from the parent control + //but here we add props to have it from file or + //from another control + #region PrimaryTexture + + + protected override SKImage GetPrimaryTextureImage(SkiaDrawingContext ctx, SKRect destination) + { + if (ControlFrom != null) + { + return _controlFrom.RenderObject?.Image; + } + + if (PrimarySource != null) + { + if (!_primarySourceBitmapResized && ParentReady() && LoadedPrimaryBitmap != null) + { + ResizePrimaryLoadedBitmap(); + } + return ResizedPrimaryImage; + } + + return base.GetPrimaryTextureImage(ctx, destination); + } + + #region FromControl + + SkiaControl _controlFrom; + + void ApplyControlFrom(SkiaControl control) + { + if (_controlFrom == control) + return; + + DetachFrom(); + _controlFrom = control; + } + + void DetachFrom() + { + if (_controlFrom != null) + { + _controlFrom = null; + } + } + + private static void ApplyControlFromProperty(BindableObject bindable, object oldvalue, object newvalue) + { + if (oldvalue != newvalue && bindable is ShaderDoubleTexturesEffect control) + { + control.ApplyControlFrom(newvalue as SkiaControl); + } + } + + public static readonly BindableProperty ControlFromProperty = BindableProperty.Create( + nameof(ControlFrom), + typeof(SkiaControl), typeof(ShaderDoubleTexturesEffect), + null, + propertyChanged: ApplyControlFromProperty); + + public SkiaControl ControlFrom + { + get { return (SkiaControl)GetValue(ControlFromProperty); } + set { SetValue(ControlFromProperty, value); } + } + + + + #endregion + + #region FromFile + + public static readonly BindableProperty PrimarySourceProperty = BindableProperty.Create( + nameof(PrimarySource), + typeof(string), + typeof(ShaderDoubleTexturesEffect), + defaultValue: null, + propertyChanged: ApplyPrimarySourceProperty); + + public string PrimarySource + { + get { return (string)GetValue(PrimarySourceProperty); } + set { SetValue(PrimarySourceProperty, value); } + } + + private static void ApplyPrimarySourceProperty(BindableObject bindable, object oldvalue, object newvalue) + { + if (oldvalue != newvalue && bindable is ShaderDoubleTexturesEffect control) + { + control.ApplyPrimarySource((string)newvalue); + } + } + + void ApplyPrimarySource(string source) + { + Task.Run(async () => + { + await LoadPrimarySource(source); + if (ParentReady() && LoadedPrimaryBitmap != null) + { + ResizePrimaryLoadedBitmap(); + } + }); + } + + public void ResizePrimaryLoadedBitmap() + { + SKImage image = null; + + var kill = ResizedPrimaryImage; + + if (LoadedPrimaryBitmap != null) + { + _primarySourceBitmapResized = true; + + 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)) + { + var rect = new SKRect(0, 0, (int)outRect.Width, (int)outRect.Height); + //resize source to apply higher quality and have it antialised + using var bmp = LoadedPrimaryBitmap.Resize(new SKSizeI((int)rect.Width, (int)rect.Height), SKFilterQuality.High); + + canvas.DrawBitmap(bmp, rect); + canvas.Flush(); + } + + ResizedPrimaryImage = SKImage.FromBitmap(resizedBitmap); + + if (kill != null) + { + Tasks.StartDelayed(TimeSpan.FromSeconds(2.5), () => + { + kill.Dispose(); + }); + } + } + + } + + /// + /// Loaded from file + /// + protected SKImage ResizedPrimaryImage { get; set; } + protected SKBitmap LoadedPrimaryBitmap { get; set; } + private readonly SemaphoreSlim _semaphoreLoadPrimaryFile = new(1, 1); + private bool _primarySourceBitmapResized; + + /// + /// Loading from local files only + /// + /// + /// + public async Task LoadPrimarySource(string fileName) + { + if (string.IsNullOrEmpty(fileName)) + return; + + await _semaphoreLoadPrimaryFile.WaitAsync(); + + try + { + var kill = LoadedPrimaryBitmap; + + + if (fileName.SafeContainsInLower("file://")) + { + var fullFilename = fileName.Replace("file://", "", StringComparison.InvariantCultureIgnoreCase); + using var stream = new FileStream(fullFilename, System.IO.FileMode.Open); + LoadedPrimaryBitmap = SKBitmap.Decode(stream); + } + else + { + using var stream = await FileSystem.OpenAppPackageFileAsync(fileName); + LoadedPrimaryBitmap = SKBitmap.Decode(stream); + } + + _primarySourceBitmapResized = false; + } + catch (Exception e) + { + Console.WriteLine($"LoadSource failed to load {fileName}"); + Console.WriteLine(e); + return; + } + finally + { + _semaphoreLoadPrimaryFile.Release(); + } + } + + #endregion + + + #endregion + + #region SecondaryTexture + + protected virtual SKShader GetSecondaryTexture() + { + if (!_secondarySourceSet && ParentReady()) + { + if (ControlTo != null) + { + ImportCacheTo(); + } + else + { + ResizeSecondaryLoadedBitmapAndCompileTexture(); + } + } + + return SecondaryTexture; + } + + /// + /// Flag set my CompileSecondaryTexture + /// + private bool _secondarySourceSet; + + /// + /// Will be normally set by CompileSecondaryTexture + /// + protected SKShader SecondaryTexture; + + public void CompileSecondaryTexture(SKImage image) + { + var dispose = SecondaryTexture; + + if (image != null) + { + SecondaryTexture = image.ToShader(TilingSecondaryTexture, TilingSecondaryTexture); + } + + if (dispose != SecondaryTexture) + dispose?.Dispose(); + + _secondarySourceSet = true; + + Update(); + } + + #region FromFile + + protected SKBitmap LoadedSecondaryBitmap; + private readonly SemaphoreSlim _semaphoreLoadSecondaryFile = new(1, 1); + + + 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 LoadSecondarySource(source); + if (ParentReady() && LoadedSecondaryBitmap != null) + { + ResizeSecondaryLoadedBitmapAndCompileTexture(); + } + + }); + } + + public void ResizeSecondaryLoadedBitmapAndCompileTexture() + { + SKImage image = null; + + if (LoadedSecondaryBitmap != 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)) + { + var rect = new SKRect(0, 0, (int)outRect.Width, (int)outRect.Height); + using var bmp = LoadedSecondaryBitmap.Resize(new SKSizeI((int)rect.Width, (int)rect.Height), SKFilterQuality.High); + + canvas.DrawBitmap(bmp, rect); + canvas.Flush(); + } + + image = SKImage.FromBitmap(resizedBitmap); + } + + CompileSecondaryTexture(image); + } + + + /// + /// Loading from local files only + /// + /// + /// + public async Task LoadSecondarySource(string fileName) + { + if (string.IsNullOrEmpty(fileName)) + return; + + await _semaphoreLoadSecondaryFile.WaitAsync(); + + try + { + if (fileName.SafeContainsInLower("file://")) + { + var fullFilename = fileName.Replace("file://", "", StringComparison.InvariantCultureIgnoreCase); + using var stream = new FileStream(fullFilename, System.IO.FileMode.Open); + LoadedSecondaryBitmap = SKBitmap.Decode(stream); + } + else + { + using var stream = await FileSystem.OpenAppPackageFileAsync(fileName); + LoadedSecondaryBitmap = SKBitmap.Decode(stream); + } + + _secondarySourceSet = false; + return; + } + catch (Exception e) + { + Console.WriteLine($"LoadSource failed to load animation {fileName}"); + Console.WriteLine(e); + return; + } + finally + { + _semaphoreLoadSecondaryFile.Release(); + } + } + + #endregion + + #region FromControl + + SkiaControl _controlTo; + + protected void ImportCacheTo() + { + if (_controlTo?.RenderObject?.Image == null || !ParentReady()) + return; + + CompileSecondaryTexture(_controlTo.RenderObject.Image); + } + + private void OnCacheCreatedTo(object sender, CachedObject e) + { + ImportCacheTo(); + } + + void ApplyControlTo(SkiaControl control) + { + if (_controlTo == control) + return; + + DetachTo(); + _controlTo = control; + if (_controlTo != null) + { + _controlTo.CreatedCache += OnCacheCreatedTo; + ImportCacheTo(); + } + } + + void DetachTo() + { + if (_controlTo != null) + { + _controlTo.CreatedCache -= OnCacheCreatedTo; + _controlTo = null; + } + } + + private static void ApplyControlToProperty(BindableObject bindable, object oldvalue, object newvalue) + { + if (oldvalue != newvalue && bindable is ShaderDoubleTexturesEffect control) + { + control.ApplyControlTo(newvalue as SkiaControl); + } + } + + public static readonly BindableProperty ControlToProperty = BindableProperty.Create( + nameof(ControlTo), + typeof(SkiaControl), typeof(ShaderDoubleTexturesEffect), + null, + propertyChanged: ApplyControlToProperty); + + public SkiaControl ControlTo + { + get { return (SkiaControl)GetValue(ControlToProperty); } + set { SetValue(ControlToProperty, value); } + } + + + + #endregion + + #endregion + + + + +} \ No newline at end of file diff --git a/src/samples/Sandbox/Views/Controls/ShaderTransitionEffect.cs b/src/samples/Sandbox/Views/Controls/Shaders/ShaderTransitionEffect.cs similarity index 69% rename from src/samples/Sandbox/Views/Controls/ShaderTransitionEffect.cs rename to src/samples/Sandbox/Views/Controls/Shaders/ShaderTransitionEffect.cs index 17499239..acf2c4b4 100644 --- a/src/samples/Sandbox/Views/Controls/ShaderTransitionEffect.cs +++ b/src/samples/Sandbox/Views/Controls/Shaders/ShaderTransitionEffect.cs @@ -2,9 +2,6 @@ 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 { @@ -15,29 +12,17 @@ protected virtual void OnTransitionEnded() public event EventHandler TransitionEnded; - bool _initialized; + private PointF _mouse; #region IStateEffect - public void UpdateState() + /// + /// Will be invoked before actually painting but after gestures processing and other internal calculations. By SkiaControl.OnBeforeDrawing method. Beware if you call Update() inside will never stop updating. + /// + public virtual 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) @@ -57,33 +42,12 @@ public virtual ISkiaGestureListener ProcessGestures( { _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) + void ApplyTargetControl(SkiaControl control) { if (_controlSource == control) return; @@ -99,7 +63,7 @@ void ApplyReflectionSourceControl(SkiaControl control) public static readonly BindableProperty TargetProperty = BindableProperty.Create( nameof(Target), typeof(SkiaControl), - typeof(ShaderTransitionEffect), + typeof(ShaderAnimatedTransitionEffect), defaultValue: null, propertyChanged: ApplyTargetProperty); @@ -115,7 +79,7 @@ private static void ApplyTargetProperty(BindableObject bindable, object oldvalue { if (oldvalue != newvalue && bindable is ShaderTransitionEffect control) { - control.ApplyReflectionSourceControl((SkiaControl)newvalue); + control.ApplyTargetControl((SkiaControl)newvalue); } } @@ -180,9 +144,9 @@ protected override void OnDisposing() #endregion - #region PROGRESS ANIMATOR + #region PROGRESS + - private PingPongAnimator _animator; public double Progress { get; set; } @@ -200,4 +164,7 @@ protected override SKRuntimeEffectUniforms CreateUniforms(SKRect destination) #endregion + + + } \ No newline at end of file diff --git a/src/samples/Sandbox/Views/MainPageBackdrop.xaml b/src/samples/Sandbox/Views/MainPageBackdrop.xaml index 6ec4756f..13829b48 100644 --- a/src/samples/Sandbox/Views/MainPageBackdrop.xaml +++ b/src/samples/Sandbox/Views/MainPageBackdrop.xaml @@ -161,13 +161,10 @@ - - - diff --git a/src/samples/Sandbox/Views/MainPageCarousels.xaml.cs b/src/samples/Sandbox/Views/MainPageCarousels.xaml.cs index 4157a3c4..b7a7d3a2 100644 --- a/src/samples/Sandbox/Views/MainPageCarousels.xaml.cs +++ b/src/samples/Sandbox/Views/MainPageCarousels.xaml.cs @@ -1,6 +1,6 @@ namespace Sandbox.Views { - public partial class MainPageCarousels + public partial class MainPageCarousels { public MainPageCarousels() @@ -20,7 +20,7 @@ public MainPageCarousels() } } - private void SkiaButton_Tapped(object sender, AppoMobi.Maui.Gestures.TouchActionEventArgs e) + private void SkiaButton_Tapped(object sender, SkiaGesturesParameters skiaGesturesParameters) { //MainCarousel.ChildrenFactory.PrintDebugVisible(); } diff --git a/src/samples/Sandbox/Views/MainPageDrawers.xaml b/src/samples/Sandbox/Views/MainPageDrawers.xaml index 8a7dfec0..4ef673e3 100644 --- a/src/samples/Sandbox/Views/MainPageDrawers.xaml +++ b/src/samples/Sandbox/Views/MainPageDrawers.xaml @@ -193,13 +193,7 @@ TranslationY="50" VerticalOptions="Start" /> - + + + diff --git a/src/samples/Sandbox/Views/MainPageScroll.xaml b/src/samples/Sandbox/Views/MainPageScroll.xaml index 448cebe3..242f40b6 100644 --- a/src/samples/Sandbox/Views/MainPageScroll.xaml +++ b/src/samples/Sandbox/Views/MainPageScroll.xaml @@ -29,19 +29,23 @@ BackgroundColor="Black" HorizontalOptions="Fill" Orientation="Both" - VerticalOptions="Fill"> + VerticalOptions="Fill" + ZoomLocked="False" + ZoomMax="3" + ZoomMin="1"> diff --git a/src/samples/Sandbox/Views/MainPageShaderRipples.xaml b/src/samples/Sandbox/Views/MainPageShaderRipples.xaml index d2541369..6a45f038 100644 --- a/src/samples/Sandbox/Views/MainPageShaderRipples.xaml +++ b/src/samples/Sandbox/Views/MainPageShaderRipples.xaml @@ -12,99 +12,99 @@ x:Name="ThisPage" x:DataType="demo:MainPageViewModel"> - + + + - + + - --> + + + + + + - - - - - - - - - - + Tag="Content" + Type="Column"> + + HeightRequest="350" + HorizontalOptions="Fill"> - - + + - - - - - - + LoadSourceOnFirstDraw="False" + Source="Images/8.jpg" + UseCache="Image" /> + + + + - + - + - + - - - - + + - + + + - +