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