diff --git a/README.md b/README.md
index b473276..81d758e 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,15 @@
# DrawnUI for .NET MAUI
![License](https://img.shields.io/github/license/taublast/DrawnUi.Maui.svg)
+![NuGet Version](https://img.shields.io/nuget/v/AppoMobi.Maui.DrawnUi.svg)
![NuGet Downloads](https://img.shields.io/nuget/dt/AppoMobi.Maui.DrawnUi.svg)
Rendering engine to draw your UI on a Skia canvas, with gestures and animations, designed to draw pixel-perfect custom controls instead of using native ones, powered by [SkiaSharp](https://github.com/mono/SkiaSharp)😍.
Create and render your custom controls on a hardware-accelerated Skia canvas, with effects and animations.
+**IMPORTANT UPDATE**
+Reverted SkiaSharp from 2.88.9 to 2.88.9-preview.2.2
+until [scaling issue](https://github.com/taublast/DrawnUi.Maui/issues/130) is solved for Windows
+
Supports **iOS**, **MacCatalyst**, **Android**, **Windows**.
* To use inside a usual MAUI app, consume drawn controls here and there inside `Canvas` views.
@@ -98,18 +103,16 @@ V3 preview: subclassed `SkiaShaderEffect`, implementing `ISkiaGestureProcessor`,
## What's New
-* Nuget 1.2.9.1 stable for SkiaSharp 2.88.9
-* Pdf helper added
-* Sandbox adapted, added example for multi-page pdf
-* SkiaImageManager fix for non-loading images
-* SkiaImage speed up loading from cached images
-* ContentLayout changed to skialayout fixing padding and margin problem
-* SkiaLayout invalidation and some others fixes
-* SkiaSlider wrong initial position fix
-* Gestures nuget updated
-* Added FluentExtensions
-* Another SkiaScroll fix for RefreshView
-* Fix crash when disposing from different threads
+### Nuget 1.2.9.2
+for SkiaSharp 2.88.9-preview.2.2
+
+* Reverted SkiaSharp to 2.88.9-preview.2.2 back from 2.88.9
+until [scaling issue](https://github.com/taublast/DrawnUi.Maui/issues/130) is solved for Windows
+* Antialiasing [issue](https://github.com/taublast/DrawnUi.Maui/issues/122) solved for SkiaShape
+* SkiaImageManager cancelling loads fix
+* Fixed native crash when using Super.ReuseBitmaps
+* SkiaSvg made more GC-friendly
+* Added `WithParent` to FluentExtensions
* Some more
## Development Notes
diff --git a/dev/DrawnUi.Maui-dev.sln b/dev/DrawnUi.Maui-dev.sln
index dc47e8f..32dbb91 100644
--- a/dev/DrawnUi.Maui-dev.sln
+++ b/dev/DrawnUi.Maui-dev.sln
@@ -10,7 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
movenugets.bat = movenugets.bat
nuget_uploadnugets.bat = nuget_uploadnugets.bat
..\README.md = ..\README.md
- ..\src\ToDo.txt = ..\src\ToDo.txt
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DrawnUi.Maui", "..\src\Engine\DrawnUi.Maui.csproj", "{D76B6239-94A0-482C-A0FF-A764017B7393}"
diff --git a/dev/github_uploadnugets.bat b/dev/github_uploadnugets.bat
index 8a9e95e..c7b82b8 100644
--- a/dev/github_uploadnugets.bat
+++ b/dev/github_uploadnugets.bat
@@ -13,8 +13,8 @@ REM Define the source directory for the packages
set "source_dir=E:\Nugets"
REM Define the list of file masks for the packages
-set "mask[1]=DrawnUi.Maui*.1.2.9.1*.nupkg"
-set "mask[2]=AppoMobi.Maui.DrawnUi.1.2.9.1*.*nupkg"
+set "mask[1]=DrawnUi.Maui*.1.2.9.2*.nupkg"
+set "mask[2]=AppoMobi.Maui.DrawnUi.1.2.9.2*.*nupkg"
set "mask_count=2"
REM Loop through each file mask
diff --git a/dev/nuget_uploadnugets.bat b/dev/nuget_uploadnugets.bat
index 4dbf611..43c915a 100644
--- a/dev/nuget_uploadnugets.bat
+++ b/dev/nuget_uploadnugets.bat
@@ -13,8 +13,8 @@ REM Define the source directory for the packages
set "source_dir=E:\Nugets"
REM Define the list of file masks for the packages
-set "mask[1]=DrawnUi.Maui*.1.2.9.1*.nupkg"
-set "mask[2]=AppoMobi.Maui.DrawnUi.1.2.9.1*.*nupkg"
+set "mask[1]=DrawnUi.Maui*.1.2.9.2*.nupkg"
+set "mask[2]=AppoMobi.Maui.DrawnUi.1.2.9.2*.*nupkg"
set "mask_count=2"
REM Loop through each file mask
diff --git a/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj b/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj
index 4eabe18..7430a7e 100644
--- a/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj
+++ b/src/Addons/DrawnUi.Maui.Camera/DrawnUi.Maui.Camera.csproj
@@ -44,7 +44,7 @@
-
+
\ No newline at end of file
diff --git a/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj b/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj
index 7493554..8ce580e 100644
--- a/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj
+++ b/src/Addons/DrawnUi.Maui.Game/DrawnUi.Maui.Game.csproj
@@ -42,7 +42,7 @@
-
+
diff --git a/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj b/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj
index 55df7af..06ce899 100644
--- a/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj
+++ b/src/Addons/DrawnUi.Maui.MapsUi/src/DrawnUi.Maui.MapsUi.csproj
@@ -54,7 +54,7 @@
-
+
diff --git a/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj b/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj
index 68f9e05..272db96 100644
--- a/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj
+++ b/src/Addons/DrawnUi.Maui.Rive/DrawnUi.Maui.Rive.csproj
@@ -47,7 +47,7 @@
-
+
diff --git a/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj b/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj
index 67abbb5..2697355 100644
--- a/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj
+++ b/src/Addons/DrawnUi.MauiGraphics/DrawnUi.MauiGraphics.csproj
@@ -41,7 +41,7 @@
-
+
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 4c3a490..3d6f4fd 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -5,7 +5,7 @@
Using SkiaSharp 2.xx. Checkout the DrawnUi Sandbox project for usage example.
- 1.2.9.1
+ 1.2.9.2
diff --git a/src/Engine/Draw/Base/SkiaControl.Shared.cs b/src/Engine/Draw/Base/SkiaControl.Shared.cs
index 230ca60..bec80af 100644
--- a/src/Engine/Draw/Base/SkiaControl.Shared.cs
+++ b/src/Engine/Draw/Base/SkiaControl.Shared.cs
@@ -35,6 +35,8 @@ public SkiaControl()
Init();
}
+ protected static SKBlendMode DefaultBlendMode = SKBlendMode.SrcOver;
+
public virtual bool IsVisibleInViewTree()
{
var isVisible = IsVisible && !IsDisposed;
@@ -3436,6 +3438,9 @@ void PostProcessMeasuredChild(ScaledSize measured, SkiaControl child, bool ignor
///
public virtual void SetInheritedBindingContext(object context)
{
+ if (IsDisposing)
+ return;
+
BindingContext = context;
}
@@ -3444,7 +3449,10 @@ public virtual void SetInheritedBindingContext(object context)
///
public virtual void ApplyBindingContext()
{
- foreach (var content in this.Views)
+ if (IsDisposing)
+ return;
+
+ foreach (var content in this.Views.ToList())
{
content.SetInheritedBindingContext(BindingContext);
}
@@ -3461,6 +3469,9 @@ public virtual void ApplyBindingContext()
///
protected override void OnBindingContextChanged()
{
+ if (IsDisposing)
+ return;
+
BindingContextWasSet = true;
try
diff --git a/src/Engine/Draw/Images/LoadedImageSource.cs b/src/Engine/Draw/Images/LoadedImageSource.cs
index c8028f2..7e6c240 100644
--- a/src/Engine/Draw/Images/LoadedImageSource.cs
+++ b/src/Engine/Draw/Images/LoadedImageSource.cs
@@ -14,18 +14,28 @@ public LoadedImageSource Clone()
// Clone the SKBitmap
var bitmapClone = new SKBitmap(Bitmap.Width, Bitmap.Height, Bitmap.ColorType, Bitmap.AlphaType);
Bitmap.CopyTo(bitmapClone);
- return new LoadedImageSource(bitmapClone);
+ return new LoadedImageSource(bitmapClone)
+ {
+ ProtectBitmapFromDispose = this.ProtectBitmapFromDispose,
+ ProtectFromDispose = this.ProtectFromDispose
+ };
}
else if (Image != null)
{
// Clone the SKImage
var imageClone = SKImage.FromBitmap(SKBitmap.FromImage(Image));
- return new LoadedImageSource(imageClone);
+ return new LoadedImageSource(imageClone)
+ {
+ ProtectFromDispose = this.ProtectFromDispose
+ };
}
else
{
// If there's no image or bitmap, return a new empty instance
- return new LoadedImageSource();
+ return new LoadedImageSource()
+ {
+ ProtectFromDispose = this.ProtectFromDispose
+ };
}
}
@@ -36,14 +46,23 @@ public LoadedImageSource Clone()
///
public bool ProtectFromDispose { get; set; }
+ ///
+ /// Should be set to true for loaded with SkiaImageManager.ReuseBitmaps
+ ///
+ public bool ProtectBitmapFromDispose { get; set; }
+
public void Dispose()
{
if (!IsDisposed && !ProtectFromDispose)
{
IsDisposed = true;
- Bitmap?.Dispose();
+ if (!ProtectBitmapFromDispose)
+ {
+ Bitmap?.Dispose();
+ }
Bitmap = null;
+
Image?.Dispose();
Image = null;
}
diff --git a/src/Engine/Draw/Images/SkiaImage.cs b/src/Engine/Draw/Images/SkiaImage.cs
index 4732c2b..6a68cb0 100644
--- a/src/Engine/Draw/Images/SkiaImage.cs
+++ b/src/Engine/Draw/Images/SkiaImage.cs
@@ -33,7 +33,7 @@ protected virtual LoadedImageSource SetImage(LoadedImageSource loaded)
if (kill != null)
{
if (SkiaImageManager.ReuseBitmaps)
- kill.Bitmap = null; //do not dispose shared cached image
+ kill.ProtectBitmapFromDispose = true; //do not dispose shared cached image
DisposeObject(kill);
}
@@ -322,7 +322,10 @@ public void SetFromBase64(string input)
var bitmap = SKBitmap.Decode(pixelArray);
- ImageBitmap = new LoadedImageSource(bitmap);
+ ImageBitmap = new LoadedImageSource(bitmap)
+ {
+ ProtectBitmapFromDispose = SkiaImageManager.ReuseBitmaps
+ };
//SetImage(new InstancedBitmap(bitmap));
}
@@ -360,7 +363,8 @@ public LoadedImageSource SetBitmapInternal(SKBitmap bitmap, bool protectFromDisp
{
return SetImage(new LoadedImageSource(bitmap)
{
- ProtectFromDispose = protectFromDispose
+ ProtectFromDispose = protectFromDispose,
+ ProtectBitmapFromDispose = SkiaImageManager.ReuseBitmaps
});
}
@@ -368,7 +372,8 @@ public LoadedImageSource SetImageInternal(SKImage image, bool protectFromDispose
{
return SetImage(new LoadedImageSource(image)
{
- ProtectFromDispose = protectFromDispose
+ ProtectFromDispose = protectFromDispose,
+ ProtectBitmapFromDispose = SkiaImageManager.ReuseBitmaps
});
}
@@ -451,7 +456,10 @@ public virtual void SetImageSource(ImageSource source)
var cachedBitmap = SkiaImageManager.Instance.GetFromCache(uri);
if (cachedBitmap != null)
{
- ImageBitmap = new LoadedImageSource(cachedBitmap);
+ ImageBitmap = new LoadedImageSource(cachedBitmap)
+ {
+ ProtectBitmapFromDispose = SkiaImageManager.ReuseBitmaps
+ };
OnSuccess?.Invoke(this, new ContentLoadedEventArgs(uri));
return;
}
@@ -518,7 +526,10 @@ async Task LoadAction()
if (bitmap != null)
{
CancelLoading?.Cancel();
- ImageBitmap = new LoadedImageSource(bitmap); //at the end will use SetImage(new InstancedBitmap(bitmap));
+ ImageBitmap = new LoadedImageSource(bitmap)
+ {
+ ProtectBitmapFromDispose = SkiaImageManager.ReuseBitmaps
+ }; //at the end will use SetImage(new InstancedBitmap(bitmap));
TraceLog($"[SkiaImage] Loaded {source}");
OnSuccess?.Invoke(this, new ContentLoadedEventArgs(url));
OnSourceSuccess();
@@ -1036,6 +1047,7 @@ protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float
ImagePaint = new()
{
IsAntialias = true,
+ IsDither = IsDistorted,
FilterQuality = SKFilterQuality.High
};
}
diff --git a/src/Engine/Draw/Layout/SkiaLayout.Grid.Structure.cs b/src/Engine/Draw/Layout/SkiaLayout.Grid.Structure.cs
index f13d1fd..df99eae 100644
--- a/src/Engine/Draw/Layout/SkiaLayout.Grid.Structure.cs
+++ b/src/Engine/Draw/Layout/SkiaLayout.Grid.Structure.cs
@@ -7,13 +7,18 @@ namespace DrawnUi.Maui.Draw;
public static class FluentExtensions
{
-
public static T With(this T view, Action action) where T : SkiaControl
{
action?.Invoke(view);
return view;
}
+ public static T WithParent(this T view, IDrawnBase parent) where T : SkiaControl
+ {
+ parent.AddSubView(view);
+ return view;
+ }
+
public static T CenterX(this T view) where T : SkiaControl
{
view.HorizontalOptions = LayoutOptions.Center;
diff --git a/src/Engine/Draw/SkiaShape.cs b/src/Engine/Draw/SkiaShape.cs
index 7185dec..c07edbf 100644
--- a/src/Engine/Draw/SkiaShape.cs
+++ b/src/Engine/Draw/SkiaShape.cs
@@ -533,17 +533,10 @@ protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float
RenderingPaint ??= new SKPaint()
{
- //IsAntialias = true,
+ IsAntialias = true
};
- //if (IsDistorted)
- //{
- // RenderingPaint.FilterQuality = SKFilterQuality.Medium;
- //}
- //else
- //{
- // RenderingPaint.FilterQuality = SKFilterQuality.None;
- //}
+ RenderingPaint.IsDither = IsDistorted;
if (BackgroundColor != null)
{
diff --git a/src/Engine/Draw/Svg/SkiaSvg.cs b/src/Engine/Draw/Svg/SkiaSvg.cs
index 045fa3e..448c495 100644
--- a/src/Engine/Draw/Svg/SkiaSvg.cs
+++ b/src/Engine/Draw/Svg/SkiaSvg.cs
@@ -1,8 +1,8 @@
-using DrawnUi.Maui.Features.Images;
-using Svg.Skia;
-using System.Reflection;
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
+using DrawnUi.Maui.Features.Images;
+using Svg.Skia;
namespace DrawnUi.Maui.Draw
{
@@ -472,6 +472,8 @@ protected string LoadedString
}
}
+ protected SKPaint RenderingPaint { get; set; }
+
public override void OnDisposing()
{
LoadedString = null;
@@ -479,6 +481,9 @@ public override void OnDisposing()
Svg?.Dispose();
Svg = null;
+ RenderingPaint?.Dispose();
+ RenderingPaint = null;
+
base.OnDisposing();
}
@@ -824,29 +829,29 @@ public static SKRect CalculateDisplayRect(SKRect dest, float bmpWidth, float bmp
switch (horizontal)
{
case DrawImageAlignment.Center:
- x = (dest.Width - bmpWidth) / 2.0f;
- break;
+ x = (dest.Width - bmpWidth) / 2.0f;
+ break;
case DrawImageAlignment.Start:
- break;
+ break;
case DrawImageAlignment.End:
- x = dest.Width - bmpWidth;
- break;
+ x = dest.Width - bmpWidth;
+ break;
}
switch (vertical)
{
case DrawImageAlignment.Center:
- y = (dest.Height - bmpHeight) / 2.0f;
- break;
+ y = (dest.Height - bmpHeight) / 2.0f;
+ break;
case DrawImageAlignment.Start:
- break;
+ break;
case DrawImageAlignment.End:
- y = dest.Height - bmpHeight;
- break;
+ y = dest.Height - bmpHeight;
+ break;
}
x += dest.Left;
@@ -1085,82 +1090,81 @@ protected override void Paint(SkiaDrawingContext ctx, SKRect destination, float
if (Svg != null)// !string.IsNullOrEmpty(LoadedString))
{
+ RenderingPaint ??= new SKPaint()
+ {
+ IsAntialias = true
+ };
+ RenderingPaint.IsDither = IsDistorted;
+ RenderingPaint.BlendMode = DefaultBlendMode;
+
SKMatrix matrix = CreateSvgMatrix(area, scale);
SKPath clipPath = null;
if (TintColor != Colors.Transparent && !UseGradient)
{
+ RenderingPaint.Shader = null;//todo dispose
- using (var paint = new SKPaint())
- {
- //drop shadow
- AddShadow(paint, scale);
- paint.IsAntialias = true;
- paint.ColorFilter = SKColorFilter.CreateBlendMode(TintColor.ToSKColor(), SKBlendMode.SrcIn);
-
- ctx.Canvas.DrawPicture(Svg.Picture, ref matrix, paint);
- }
+ AddShadow(RenderingPaint, scale);
+ RenderingPaint.ColorFilter = SKColorFilter.CreateBlendMode(TintColor.ToSKColor(), SKBlendMode.SrcIn);
+ ctx.Canvas.DrawPicture(Svg.Picture, ref matrix, RenderingPaint);
}
else
if (UseGradient) //todo use standart gradient property instead
{
+ RenderingPaint.ColorFilter = null; //todo dispose
+
ctx.Canvas.DrawPicture(Svg.Picture, ref matrix);
- //draw gradient rectangle above
- using (var paint = new SKPaint())
- {
- //drop shadow
- AddShadow(paint, scale);
- paint.IsAntialias = true;
- paint.Shader = SKShader.CreateLinearGradient(
- new SKPoint(Destination.Width * StartXRatio, Destination.Height * StartYRatio),
- new SKPoint(Destination.Width * EndXRatio, Destination.Height * EndYRatio),
- new SKColor[]
- {
+ //will draw gradient rectangle above
+
+ //drop shadow
+ AddShadow(RenderingPaint, scale);
+ RenderingPaint.Shader = SKShader.CreateLinearGradient(
+ new SKPoint(Destination.Width * StartXRatio, Destination.Height * StartYRatio),
+ new SKPoint(Destination.Width * EndXRatio, Destination.Height * EndYRatio),
+ new SKColor[]
+ {
StartColor.ToSKColor(),
EndColor.ToSKColor()
- },
- null,
- SKShaderTileMode.Clamp);
+ },
+ null,
+ SKShaderTileMode.Clamp);
- paint.BlendMode = GradientBlendMode;
-
- ctx.Canvas.DrawRect(Destination, paint);
- }
+ RenderingPaint.BlendMode = GradientBlendMode;
+ ctx.Canvas.DrawRect(Destination, RenderingPaint);
}
else
{
- using (var paint = new SKPaint())
- {
- paint.IsAntialias = true;
- //drop shadow
- AddShadow(paint, scale);
+ RenderingPaint.Shader = null;//todo dispose
+ RenderingPaint.ColorFilter = null; //todo dispose
+
+ //drop shadow
+ AddShadow(RenderingPaint, scale);
- // paint.ColorFilter = SKColorFilter.CreateBlendMode(BackgroundColors.ToSKColor(), SKBlendMode.SrcIn);
+ // paint.ColorFilter = SKColorFilter.CreateBlendMode(BackgroundColors.ToSKColor(), SKBlendMode.SrcIn);
- if (Clipping != null)
+ if (Clipping != null)
+ {
+ if (clipPath == null)
{
- if (clipPath == null)
- {
- clipPath = new SKPath();
- Clipping.Invoke(clipPath, Destination);
- }
+ clipPath = new SKPath();
+ Clipping.Invoke(clipPath, Destination);
+ }
- var saved = ctx.Canvas.Save();
- ClipSmart(ctx.Canvas, clipPath);
+ var saved = ctx.Canvas.Save();
+ ClipSmart(ctx.Canvas, clipPath);
- ctx.Canvas.DrawPicture(Svg.Picture, ref matrix, paint);
+ ctx.Canvas.DrawPicture(Svg.Picture, ref matrix, RenderingPaint);
- ctx.Canvas.RestoreToCount(saved);
+ ctx.Canvas.RestoreToCount(saved);
- clipPath.Dispose();
- }
- else
- {
- ctx.Canvas.DrawPicture(Svg.Picture, ref matrix, paint);
- }
+ clipPath.Dispose();
+ }
+ else
+ {
+ ctx.Canvas.DrawPicture(Svg.Picture, ref matrix, RenderingPaint);
}
}
}
diff --git a/src/Engine/Features/Images/SkiaImageManager.cs b/src/Engine/Features/Images/SkiaImageManager.cs
index 80b3e4c..d4596b4 100644
--- a/src/Engine/Features/Images/SkiaImageManager.cs
+++ b/src/Engine/Features/Images/SkiaImageManager.cs
@@ -569,7 +569,7 @@ private async void ProcessQueue()
{
try
{
- if (IsLoadingLocked || semaphoreLoad.CurrentCount < 1)
+ if (IsLoadingLocked)
{
TraceLog($"ImageLoadManager: Loading Locked!");
await Task.Delay(50);
diff --git a/src/Engine/Views/DrawnView.cs b/src/Engine/Views/DrawnView.cs
index ccc2d4f..f3b5cf7 100644
--- a/src/Engine/Views/DrawnView.cs
+++ b/src/Engine/Views/DrawnView.cs
@@ -1638,10 +1638,16 @@ public async Task ProcessOffscreenCacheRenderingAsync()
_processingOffscrenRendering = true;
var command = _offscreenCacheRenderingQueue.Dequeue();
- while (!command.Control.IsDisposed && !command.Control.IsDisposing)
+ while (command != null)
{
try
{
+ if (command.Control.IsDisposed || command.Control.IsDisposing)
+ {
+ _offscreenCacheRenderingQueue.Clear();
+ break;
+ }
+
var action = command.Control.GetOffscreenRenderingAction();
action?.Invoke();
@@ -2619,6 +2625,4 @@ public virtual void ClipSmart(SKCanvas canvas, SKPath path, SKClipOperation oper
}
-
-
}
diff --git a/src/Engine/skia2.props b/src/Engine/skia2.props
index 9d98f31..ba701db 100644
--- a/src/Engine/skia2.props
+++ b/src/Engine/skia2.props
@@ -1,9 +1,10 @@
-
-
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/src/samples/Sandbox/Chat/WidgetChatMessage.cs b/src/samples/Sandbox/Chat/WidgetChatMessage.cs
new file mode 100644
index 0000000..9f1f40b
--- /dev/null
+++ b/src/samples/Sandbox/Chat/WidgetChatMessage.cs
@@ -0,0 +1,528 @@
+using System.ComponentModel;
+using DrawnUi.Maui.Extensions;
+using Sandbox;
+
+namespace AppoMobi.Forms.Controls
+{
+
+ public class WidgetChatMessage : WidgetListCell
+ {
+ //public override void Draw(SKCanvas canvas, SKImageInfo info, SKRect destination, double scale = 1.0, SkiaAnchor anchor = SkiaAnchor.TopLeft)
+ //{
+ // if (IsRootView(ParentControl, info, destination))
+ // canvas.Clear(ClearColors.ToSKColor());
+
+ // if (IsSelected)
+ // {
+ // Opacity = 0.7;
+ // }
+ // else
+ // {
+ // Opacity = 1.0;
+ // }
+
+ // base.Draw(canvas, info, destination, scale);
+ //}
+
+
+ public SkiaShape FrameNewDate { get; set; }
+
+ public SkiaLabel LabelFirstDate { get; set; }
+
+ public SkiaLayout MainHorizontalStack { get; set; }
+
+ public SkiaLayout MessageStack { get; set; }
+
+ //public SkiaLayout StackBubble { get; set; }
+
+ public SkiaImage Banner { get; set; }
+
+ public SkiaLabel LabelMessage { get; set; }
+
+ public SkiaLabel LabelTime { get; set; }
+
+ public SkiaSvg IconAttachment { get; set; }
+
+ public SkiaSvg BubbleArrowIncoming { get; set; }
+
+ public SkiaSvg BubbleArrowOutcoming { get; set; }
+
+ public SkiaSvg IconWasSent { get; set; }
+
+ public SkiaSvg IconWasDelivered { get; set; }
+
+ private ChatMessage _oldContext;
+
+ public override void OnDisposing()
+ {
+ if (_oldContext != null)
+ _oldContext.PropertyChanged -= OnContextPropertyChanged;
+
+ Banner.OnError -= OnImageError;
+
+ DetachGestures();
+
+ base.OnDisposing();
+ }
+
+ private void OnImageError(object sender, EventArgs e)
+ {
+ //todo AvatarDefault.IsVisible = true;
+ Update();
+ }
+
+ private void OnContextPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+
+ if (e.PropertyName == "Id")//Message sent
+ {
+ SetContentFull(BindingContext as ChatMessage);
+ return;
+ }
+
+ if (e.PropertyName == "Read"
+ || e.PropertyName == "Sent"
+ || e.PropertyName == "Delivered")
+ {
+ UpdateStatus(BindingContext as ChatMessage);
+ Update();
+ }
+ else
+ if (e.PropertyName == "Notify")
+ {
+ var item = BindingContext as ChatMessage;
+ if (item != null)
+ {
+ IsNew = item.Notify;
+ }
+ else
+ {
+ IsNew = false;
+ }
+ UpdateContainer(BindingContext as ChatMessage);
+ Update();
+ }
+ }
+
+ private void OnFirstTimeMeasured(object sender, ScaledSize scaledSize)
+ {
+ var label = sender as SkiaLabel;
+
+ FrameNewDate.WidthRequest = label.MeasuredSize.Units.Width + label.Margin.Left + label.Margin.Right;
+ }
+
+ //this is more smooth when invoked here comparing to actions inside OnBindingContextChanged
+ void OnTextSizeChanged(object sender, ScaledSize scaledSize)
+ {
+ var label = sender as SkiaLabel;
+ var requestContainerWidth = label.MeasuredSize.Units.Width + label.Margin.Left + label.Margin.Right + 8;
+
+ if (Template == ChatMetaType.File)
+ {
+ requestContainerWidth += 16 + 8 + 4;
+ }
+
+ if (MainFrame.Destination.Width != requestContainerWidth)
+ {
+ MainFrame.WidthRequest = requestContainerWidth;
+ }
+
+ var addHeight = 0;
+ if (ShowDate)
+ {
+ addHeight = 24 + 16;
+ }
+
+ //set forms view dynamic height...
+ if (Template == ChatMetaType.Image || Template == ChatMetaType.Video || Template == ChatMetaType.Article)
+ {
+ HeightRequest = addHeight + label.MeasuredSize.Units.Height + label.Margin.Top + label.Margin.Bottom + Banner.HeightRequest + 10;
+ }
+ else
+ {
+ HeightRequest = addHeight + label.MeasuredSize.Units.Height + label.Margin.Top + label.Margin.Bottom + 10;
+ }
+ }
+
+ public WidgetChatMessage()
+ {
+
+ MainHorizontalStack = new SkiaLayout()
+ {
+ Tag = "MainHorizontalStack",
+ Type = LayoutType.Row,
+ Padding = new Thickness(8, 0, 8, 0),
+ Spacing = 0,
+ VerticalOptions = LayoutOptions.Fill,
+ HorizontalOptions = LayoutOptions.End
+ }.WithParent(this);
+
+ FrameNewDate = new SkiaShape()
+ {
+ Tag = "FrameNewDate",
+ Type = ShapeType.Rectangle,
+ CornerRadius = 8,
+ HeightRequest = 24,
+ WidthRequest = 100,
+ HorizontalOptions = LayoutOptions.Center,
+ Margin = new Thickness(8, 8, 8, 8),
+ }.WithParent(this);
+
+ LabelFirstDate = new SkiaLabel()
+ {
+ TranslationY = -4,
+ Margin = new Thickness(10, 0, 10, 0),
+ LineBreakMode = LineBreakMode.NoWrap,
+ MaxLines = 1,
+ FontAttributes = FontAttributes.Bold,
+ Text = $"Time",
+ FontSize = 10,
+ HorizontalOptions = LayoutOptions.Center,
+ VerticalOptions = LayoutOptions.Center,
+ TextColor = App.Current.Resources.Get("ColorPrimary")
+ }.With((c) =>
+ {
+ c.Measured += OnFirstTimeMeasured;
+ }).WithParent(FrameNewDate);
+
+
+
+ BubbleArrowIncoming = new SkiaSvg()
+ {
+ Tag = "arrowL",
+ TranslationY = 5,
+ TintColor = Colors.White,
+ SvgString = App.Current.Resources.Get("SvgChatFromLeft"),
+ VerticalOptions = LayoutOptions.Start,
+ HorizontalOptions = LayoutOptions.Start,
+ HeightRequest = 8,
+ WidthRequest = 8
+ }.WithParent(MainHorizontalStack);
+
+ #region BUBBLE
+
+ MainFrame = new SkiaShape()
+ {
+ Tag = "MainFrame",
+ Margin = new Thickness(0, 0, 0, 8),
+ CornerRadius = 8,
+ BackgroundColor = App.Current.Resources.Get("ColorPaperSecondary"),
+ // StrokeColor = StaticResources.ColorPaperSecondary,
+ HorizontalOptions = LayoutOptions.Fill,
+ VerticalOptions = LayoutOptions.Fill,
+ }.WithParent(MainHorizontalStack);
+
+ MessageStack = new SkiaLayout()
+ {
+ Tag = "MessageStack",
+ IsClippedToBounds = true,
+ Type = LayoutType.Row,
+ Spacing = 4,
+ VerticalOptions = LayoutOptions.Fill,
+ HorizontalOptions = LayoutOptions.End
+ }.WithParent(MainFrame);
+
+ Banner = new SkiaImage()
+ {
+ IsVisible = false,
+ Tag = "Banner",
+ Background = App.Current.Resources.Get("ColorPrimaryLight"),
+ //Margin = new Thickness(0, 0, 0, 0),
+ HorizontalOptions = LayoutOptions.Fill,
+ VerticalOptions = LayoutOptions.Start,
+ HeightRequest = 200,
+ }.WithParent(MessageStack).With((c) =>
+ {
+ c.OnError += OnImageError;
+ });
+
+ MessageStack.BreakLine();
+
+ IconAttachment = new SkiaSvg()
+ {
+ Tag = "Attachment",
+ Margin = new Thickness(8, 0, 0, 0),
+ SvgString = App.Current.Resources.Get("SvgAttachment"),
+ VerticalOptions = LayoutOptions.Center,
+ HorizontalOptions = LayoutOptions.Start,
+ TintColor = App.Current.Resources.Get("ColorAccentLight"),
+ HeightRequest = 16,
+ WidthRequest = 16
+ }.WithParent(MessageStack);
+
+ LabelMessage = new SkiaLabel()
+ {
+ Tag = "Message",
+ //TintColor = Colors.FloralWhite,
+ LineBreakMode = LineBreakMode.WordWrap,
+ MaxLines = -1,
+ Text = $"Message",
+ FontSize = 12,
+ HorizontalOptions = LayoutOptions.Fill,
+ Margin = new Thickness(8, 8, 8, 8),
+ VerticalOptions = LayoutOptions.Center,
+ TextColor = App.Current.Resources.Get("ColorText"),
+ }.WithParent(MessageStack).With((c) =>
+ {
+ c.Measured += OnTextSizeChanged;
+ });
+
+ LabelTime = new SkiaLabel()
+ {
+ Tag = "LabelTime",
+ LineBreakMode = LineBreakMode.NoWrap,
+ MaxLines = 1,
+ Text = $"Time",
+ FontSize = 9,
+ HorizontalOptions = LayoutOptions.End,
+ Margin = new Thickness(0, 0, 2, 2),
+ VerticalOptions = LayoutOptions.End,
+ //TextColor = StaticResources.ColorPrimaryLight,
+ }.WithParent(MainFrame);
+
+ IconWasSent = new SkiaSvg()
+ {
+ Tag = "WasDelivered",
+ Margin = new Thickness(0, 0, 7, 3),
+ TintColor = App.Current.Resources.Get("ColorPrimaryLight"),
+ SvgString = App.Current.Resources.Get("SvgCheck"),
+ VerticalOptions = LayoutOptions.End,
+ HorizontalOptions = LayoutOptions.End,
+ HeightRequest = 11,
+ WidthRequest = 11
+ }.WithParent(MainFrame);
+
+ IconWasDelivered = new SkiaSvg()
+ {
+ Tag = "WasSeen",
+ //TranslationY = 5,
+ Margin = new Thickness(0, 0, 3, 3),
+ TintColor = App.Current.Resources.Get("ColorPrimaryLight"),
+ SvgString = App.Current.Resources.Get("SvgCheck"),
+ VerticalOptions = LayoutOptions.End,
+ HorizontalOptions = LayoutOptions.End,
+ HeightRequest = 11,
+ WidthRequest = 11
+ }.WithParent(MainFrame);
+
+ #endregion
+
+ BubbleArrowOutcoming = new SkiaSvg()
+ {
+ Tag = "arrowR",
+ TranslationY = 5,
+ SvgString = App.Current.Resources.Get("SvgChatFromRight"),
+ VerticalOptions = LayoutOptions.Start,
+ HorizontalOptions = LayoutOptions.Start,
+ HeightRequest = 8,
+ WidthRequest = 8
+ }.WithParent(MainHorizontalStack);
+
+ // GESTURES
+ AttachGestures();
+
+ }
+
+
+
+ public bool ShowDate { get; set; }
+
+ void UpdateContainer(ChatMessage item)
+ {
+ if (item == null)
+ return;
+
+ IconWasSent.IsVisible = false;
+ IconWasDelivered.IsVisible = false;
+
+ if (ShowDate)
+ {
+ FrameNewDate.IsVisible = true;
+ MainHorizontalStack.Padding = new Thickness(0, 24 + 16, 0, 0);
+ }
+ else
+ {
+ FrameNewDate.IsVisible = false;
+ MainHorizontalStack.Padding = new Thickness(0, 0, 0, 0);
+ }
+
+ if (item.Outgoing)
+ {
+ LabelTime.Margin = new Thickness(0, 0, 10, 2);
+
+ LabelTime.TextColor = App.Current.Resources.Get("ColorPrimary");
+ MainFrame.Background = App.Current.Resources.Get("ColorPaperSecondary");
+
+ MainHorizontalStack.HorizontalOptions = LayoutOptions.End;
+ BubbleArrowIncoming.IsVisible = false;
+ //BubbleArrowOutcoming.IsVisible = true;
+ if (item.IsFirst)
+ {
+ BubbleArrowOutcoming.IsVisible = true;
+ MainFrame.Margin = new Thickness(0, 0, 0, 8);
+ }
+ else
+ {
+ BubbleArrowOutcoming.IsVisible = false;
+ MainFrame.Margin = new Thickness(0, 0, 8, 8);
+ }
+
+ }
+ else
+ {
+ LabelTime.Margin = new Thickness(0, 0, 2, 2);
+
+ LabelTime.TextColor = App.Current.Resources.Get("ColorPrimaryLight");
+ MainFrame.Background = Colors.White;
+
+ MainHorizontalStack.HorizontalOptions = LayoutOptions.Start;
+ BubbleArrowOutcoming.IsVisible = false;
+ if (item.IsFirst)
+ {
+ BubbleArrowIncoming.IsVisible = true;
+ MainFrame.Margin = new Thickness(0, 0, 0, 8);
+ }
+ else
+ {
+ BubbleArrowIncoming.IsVisible = false;
+ MainFrame.Margin = new Thickness(8, 0, 0, 8);
+ }
+ }
+
+ if (Template == ChatMetaType.File)
+ {
+ IconAttachment.IsVisible = true;
+ LabelMessage.TextColor = App.Current.Resources.Get("ColorAccentLight");
+ }
+ else
+ {
+ IconAttachment.IsVisible = false;
+ LabelMessage.TextColor = App.Current.Resources.Get("ColorText");
+ }
+
+ if (Template == ChatMetaType.Image || Template == ChatMetaType.Video)
+ {
+ Banner.HeightRequest = 200;
+ Banner.IsVisible = true;
+ }
+ else
+ if (Template == ChatMetaType.Article)
+ {
+ Banner.HeightRequest = 80;
+ Banner.IsVisible = true;
+ }
+ else
+ {
+ Banner.IsVisible = false;
+ }
+
+ }
+
+ void UpdateStatus(ChatMessage item)
+ {
+ if (item == null)
+ return;
+
+ if (item.Outgoing)
+ {
+ if (item.Read)
+ {
+ IconWasSent.TintColor = Colors.DeepSkyBlue;
+ IconWasDelivered.TintColor = Colors.DeepSkyBlue;
+ IconWasSent.IsVisible = true;
+ IconWasDelivered.IsVisible = true;
+ }
+ else
+ {
+ IconWasSent.TintColor = App.Current.Resources.Get("ColorPrimary");
+ IconWasDelivered.TintColor = App.Current.Resources.Get("ColorPrimary");
+ IconWasSent.IsVisible = item.Sent;
+ IconWasDelivered.IsVisible = item.Delivered;
+ }
+ }
+ }
+
+ void UpdateContent(ChatMessage item)
+ {
+ if (item == null)
+ return;
+
+ if (ShowDate)
+ LabelFirstDate.Text = item.WhenDesc;
+ else
+ LabelFirstDate.Text = "";
+
+ if (item.Outgoing)
+ {
+ LabelMessage.Text = item.Text + " ";
+ }
+ else
+ {
+ LabelMessage.Text = item.Text + " ";
+ }
+
+ LabelMessage.Measure(Width - 100, Height); //reduce by side padding
+
+ LabelTime.Text = item.DisplayTime;
+
+ if (Template == ChatMetaType.Image || Template == ChatMetaType.Video)
+ {
+ Banner.Source = item.ImageMain;
+ }
+ else
+ if (Template == ChatMetaType.Article)
+ {
+ Banner.Source = item.Metadata.Image;
+ }
+ else
+ {
+ Banner.Source = null;
+ }
+ }
+
+ public ChatMetaType Template { get; protected set; } = ChatMetaType.Default;
+
+ void SetContentFull(ChatMessage item)
+ {
+ IsNew = item.Notify;
+ Template = item.PresentAs;
+ ShowDate = item.IsFirstDate;
+ CanBeTapped = Template != ChatMetaType.Default;
+
+ //if (_oldContext != null)
+ //{
+ // if (_oldContext.Outgoing != item.Outgoing || _oldContext.PresentAs != item.PresentAs)
+ // UpdateBubble(item);
+ //}
+ //else
+ //{
+ // UpdateBubble(item);
+ //}
+ UpdateContainer(item);
+ UpdateContent(item);
+ UpdateStatus(item);
+ Update();
+ }
+
+ protected override void OnBindingContextChanged()
+ {
+ var item = this.BindingContext as ChatMessage;
+ if (item != null && _oldContext != item)
+ {
+ if (_oldContext != null)
+ _oldContext.PropertyChanged -= OnContextPropertyChanged;
+
+ item.PropertyChanged += OnContextPropertyChanged;
+ _oldContext = item;
+
+ SetContentFull(item);
+ }
+
+ }
+
+
+
+
+ }
+
+}
diff --git a/src/samples/Sandbox/Chat/WidgetListCell.cs b/src/samples/Sandbox/Chat/WidgetListCell.cs
new file mode 100644
index 0000000..b714f48
--- /dev/null
+++ b/src/samples/Sandbox/Chat/WidgetListCell.cs
@@ -0,0 +1,209 @@
+using System.Diagnostics;
+using System.Windows.Input;
+using AppoMobi.Maui.Gestures;
+using DrawnUi.Maui.Extensions;
+using Newtonsoft.Json;
+using Sandbox;
+
+
+namespace AppoMobi.Forms.Controls
+{
+ public enum ChatMetaType
+ {
+ Default,
+ Image,
+ Video,
+ Article,
+ File,
+ Separator,
+ System
+ }
+
+ public class WidgetListCell : SkiaLayout
+ {
+ public static SkiaImage CreateStandartAvatar(ISkiaControl parent, double scale)
+ {
+ return new SkiaImage()
+ {
+ Tag = "avatar",
+ BackgroundColor = App.Current.Resources.Get("ColorPrimaryLight"),
+ Margin = new Thickness(2),
+ HorizontalOptions = LayoutOptions.Fill,
+ VerticalOptions = LayoutOptions.Fill,
+ IsClippedToBounds = true,
+ Clipping = (path, dest) =>
+ {
+ //avatar circle
+ path.AddCircle(dest.Left + dest.Width / 2, dest.Top + dest.Height / 2,
+ dest.Width / 2);
+ }
+ }.WithParent(parent);
+ }
+
+ public static SkiaSvg CreateStandartEmptyAvatar(ISkiaControl parent, double scale)
+ {
+ return new SkiaSvg()
+ {
+ Tag = "EmptyAvatar",
+ IsVisible = false,
+ TranslationY = -2,
+ IsClippedToBounds = true,
+ SvgString = App.Current.Resources.Get("SvgAvatarEmpty"),
+ VerticalOptions = LayoutOptions.Center,
+ HorizontalOptions = LayoutOptions.Center,
+ HeightRequest = 32,
+ WidthRequest = 32
+ }.WithParent(parent);
+ }
+
+ public static bool DisableShadows => false;
+
+ public static readonly BindableProperty CommandTappedProperty = BindableProperty.Create(nameof(CommandTapped), typeof(ICommand), typeof(WidgetListCell),
+ null);
+ public ICommand CommandTapped
+ {
+ get { return (ICommand)GetValue(CommandTappedProperty); }
+ set { SetValue(CommandTappedProperty, value); }
+ }
+
+ public static readonly BindableProperty CommandLongPressingProperty = BindableProperty.Create(nameof(CommandLongPressing), typeof(ICommand), typeof(WidgetListCell),
+ null);
+ public ICommand CommandLongPressing
+ {
+ get { return (ICommand)GetValue(CommandLongPressingProperty); }
+ set { SetValue(CommandLongPressingProperty, value); }
+ }
+
+ public SkiaShape MainFrame { get; set; }
+
+ public bool HasGestures
+ {
+ get
+ {
+ return _touchHandler != null;
+ }
+ }
+
+ protected void DetachGestures()
+ {
+ if (!HasGestures)
+ return;
+
+ _touchHandler.Dispose();
+ }
+
+
+ protected void AttachGestures()
+ {
+ if (HasGestures)
+ return;
+
+ _touchHandler = new TouchEffect
+ {
+ Capture = true
+ };
+ _touchHandler.LongPressing += OnLongPressing;
+ _touchHandler.Tapped += OnTapped;
+ _touchHandler.Down += OnDown;
+ _touchHandler.Up += OnUp;
+ _touchHandler.TouchAction += OnTouch;
+ this.Effects.Add(_touchHandler);
+ }
+
+ public bool IsSelected { get; set; }
+
+ public bool CanBeSelected { get; set; } = true;
+
+ public bool CanBeTapped { get; set; } = true;
+
+ public bool IsNew { get; set; }
+
+ private void OnLongPressing(object sender, TouchActionEventArgs args)
+ {
+
+ Debug.WriteLine($"[TOUCH] LongPressing!");
+
+ if (CommandLongPressing != null)
+ {
+ if (CanBeSelected)
+ {
+ IsSelected = true;
+ Update();
+ }
+ Device.StartTimer(TimeSpan.FromMilliseconds(2500), () =>
+ {
+ if (CanBeSelected)
+ {
+ IsSelected = false;
+ Update();
+ }
+ return false;
+ });
+ CommandLongPressing.Execute(BindingContext);
+
+ }
+
+ }
+
+ private bool lockTap;
+ private TouchEffect _touchHandler;
+
+ private void OnTapped(object sender, TouchActionEventArgs args)
+ {
+ if (!CanBeTapped)
+ return;
+
+ if (lockTap)
+ return;
+
+ Debug.WriteLine($"[TOUCH] Tapped!");
+
+ lockTap = true;
+ if (CanBeSelected)
+ {
+ IsSelected = true;
+ Update();
+ }
+
+ Device.StartTimer(TimeSpan.FromMilliseconds(50), () =>
+ {
+ //invoke action
+ CommandTapped?.Execute(BindingContext);
+
+ Device.StartTimer(TimeSpan.FromMilliseconds(2500), () =>
+ {
+ if (CanBeSelected)
+ {
+ IsSelected = false;
+ Update();
+ }
+ lockTap = false;
+ return false;
+ });
+
+ return false;
+ });
+
+ }
+
+ private void OnUp(object sender, TouchActionEventArgs args)
+ {
+ Debug.WriteLine($"[TOUCH] UP");
+
+ //MainFrame.StrokeWidth = 1 / RenderingScale; // 1 precise pixel
+ //MainFrame.StrokeColor = StaticResources.DropShadow;
+ //Update();
+ }
+
+ private void OnDown(object sender, TouchActionEventArgs args)
+ {
+ Debug.WriteLine($"[TOUCH] DOWN");
+ }
+
+ private void OnTouch(object sender, TouchActionEventArgs args)
+ {
+ Debug.WriteLine($"[TOUCH] {args.Type} {JsonConvert.SerializeObject(args)}");
+ }
+
+ }
+}
diff --git a/src/samples/Sandbox/MainPageCodeDeformation.cs b/src/samples/Sandbox/MainPageCodeDeformation.cs
new file mode 100644
index 0000000..d810a36
--- /dev/null
+++ b/src/samples/Sandbox/MainPageCodeDeformation.cs
@@ -0,0 +1,119 @@
+using Sandbox.Views;
+using Canvas = DrawnUi.Maui.Views.Canvas;
+
+namespace Sandbox
+{
+
+
+ public class MainPageCodeDeformation : BasePage, IDisposable
+ {
+ Canvas Canvas;
+
+ public void Dispose()
+ {
+ this.Content = null;
+ Canvas?.Dispose();
+ }
+
+
+ public static (SkiaShape Shape, Point Point) CreateSkiaLine(Color color, double x1, double y1, double x2, double y2, int width = 5)
+ {
+ var distX = x1 - x2;
+ var distY = y1 - y2;
+ var lineDist = Math.Sqrt(distX * distX + distY * distY);
+
+ var lineCenterX = x2 + (distX / 2);
+ var lineCenterY = y2 + (distY / 2);
+ var lineFinalX = lineCenterX - (lineDist / 2);
+
+ var lineRotateAngle = Math.Acos(distX / lineDist) * (180 / Math.PI);
+
+ var line = new SkiaShape()
+ {
+ Type = ShapeType.Rectangle,
+ BackgroundColor = color,
+ WidthRequest = lineDist,
+ HeightRequest = width,
+ Rotation = (distY < 0) ? 360 - lineRotateAngle : lineRotateAngle
+ };
+
+ return (line, new Point(lineFinalX, lineCenterY));
+ }
+
+ public MainPageCodeDeformation()
+ {
+#if DEBUG
+ HotReloadService.UpdateApplicationEvent += ReloadUI;
+#endif
+ Build();
+ }
+
+ private int _reloads;
+
+ void Build()
+ {
+ Canvas?.Dispose();
+
+ Canvas = new Canvas()
+ {
+ Gestures = GesturesMode.Enabled,
+ HardwareAcceleration = HardwareAccelerationMode.Disabled,
+
+ VerticalOptions = LayoutOptions.Fill,
+ HorizontalOptions = LayoutOptions.Fill,
+ BackgroundColor = Colors.LightGray,
+
+ Content = new SkiaLayout()
+ {
+ VerticalOptions = LayoutOptions.Fill,
+ HorizontalOptions = LayoutOptions.Fill,
+ Children = new List()
+ {
+ new SkiaLayout()
+ {
+ Children = new List()
+ {
+ CreateSkiaLine(Colors.Red, 50, 50, 300, 102, 5).Shape
+
+ }
+ }.With((c) =>
+ {
+ foreach (Point node in new Point[]
+ {
+ new(150, 150),
+ new(250,200),
+ new(250,150),
+ new(250,300),
+ new(250,250),
+ new(350,150),
+ new(450,150),
+ new(300,250),
+ new(300,300),
+ new(300,360),
+ new(150,350)
+ })
+ {
+ c.AddSubView(CreateSkiaLine(Colors.Blue, 75, 175, node.X, node.Y).Shape);
+ }
+ })
+ }
+ }
+
+
+ };
+
+ _reloads++;
+
+ this.Content = Canvas;
+ }
+
+ private void ReloadUI(Type[] obj)
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ Build();
+ });
+ }
+
+ }
+}
diff --git a/src/samples/Sandbox/MainPageRepro.xaml b/src/samples/Sandbox/MainPageRepro.xaml
index 1dd41a6..afc5ca8 100644
--- a/src/samples/Sandbox/MainPageRepro.xaml
+++ b/src/samples/Sandbox/MainPageRepro.xaml
@@ -3,25 +3,53 @@
x:Class="MauiNet8.MainPageRepro"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:draw="http://schemas.appomobi.com/drawnUi/2023/draw"
xmlns:mauiNet8="clr-namespace:MauiNet8"
xmlns:sandbox="clr-namespace:Sandbox"
xmlns:views="clr-namespace:Sandbox.Views"
x:DataType="sandbox:MainPageViewModel"
BackgroundColor="#000000">
+
+
+
+
+
+
+
+
+
+
diff --git a/src/samples/Sandbox/MainPageRepro.xaml.cs b/src/samples/Sandbox/MainPageRepro.xaml.cs
index a8d3e9d..bcd0b44 100644
--- a/src/samples/Sandbox/MainPageRepro.xaml.cs
+++ b/src/samples/Sandbox/MainPageRepro.xaml.cs
@@ -19,8 +19,6 @@ public MainPageRepro()
}
}
-
-
}
public class DebugCanvas : Canvas
@@ -29,15 +27,14 @@ public class DebugCanvas : Canvas
public DebugCanvas()
{
- var shape = CreateSkiaLine(Colors.White, 10, 10, 100, 100, 5);
- Children.Add(shape.Item1);
+
}
protected override void Draw(SkiaDrawingContext context, SKRect destination, float scale)
{
base.Draw(context, destination, scale);
-
+ /*
if (Height > 0 && Width > 0)
{
var canvas = context.Canvas;
@@ -66,31 +63,9 @@ protected override void Draw(SkiaDrawingContext context, SKRect destination, flo
}
+ */
}
- private static (SkiaShape, Point) CreateSkiaLine(Color color, double x1, double y1, double x2, double y2, int width = 5)
- {
- var distX = x1 - x2;
- var distY = y1 - y2;
- var lineDist = Math.Sqrt(distX * distX + distY * distY);
-
- var lineCenterX = x2 + (distX / 2);
- var lineCenterY = y2 + (distY / 2);
- var lineFinalX = lineCenterX - (lineDist / 2);
-
- var lineRotateAngle = Math.Acos(distX / lineDist) * (180 / Math.PI);
-
- var line = new SkiaShape()
- {
- Type = ShapeType.Rectangle,
- BackgroundColor = color,
- WidthRequest = lineDist,
- HeightRequest = width,
- Rotation = (distY < 0) ? 360 - lineRotateAngle : lineRotateAngle
- };
-
- return (line, new Point(lineFinalX, lineCenterY));
- }
(SKSurface, SKImage) CreateImage(int width, int height, float renderingScale)
{
@@ -120,4 +95,6 @@ private static (SkiaShape, Point) CreateSkiaLine(Color color, double x1, double
return (surface, image);
}
+
+
}
diff --git a/src/samples/Sandbox/Resources/Svg.xaml b/src/samples/Sandbox/Resources/Svg.xaml
index 9a252f0..514256a 100644
--- a/src/samples/Sandbox/Resources/Svg.xaml
+++ b/src/samples/Sandbox/Resources/Svg.xaml
@@ -4,6 +4,12 @@
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
+
+
+ ]]>
+
+
+
+ MainDrawnCells.xaml
+
+
+
MainPageXaml2PdfPages.xaml
@@ -371,6 +376,9 @@
MSBuild:Compile
+
+ MSBuild:Compile
+
MSBuild:Compile
diff --git a/src/samples/Sandbox/ViewModels/ChatMessage.cs b/src/samples/Sandbox/ViewModels/ChatMessage.cs
new file mode 100644
index 0000000..8e8430a
--- /dev/null
+++ b/src/samples/Sandbox/ViewModels/ChatMessage.cs
@@ -0,0 +1,436 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using AppoMobi.Forms.Controls;
+using AppoMobi.Specials;
+using Newtonsoft.Json;
+
+namespace Sandbox
+{
+ public class ChatMessageMetadata
+ {
+ public ChatMetaType Type { get; set; }
+ public string Title { get; set; }
+ public string Description { get; set; }
+ public string Url { get; set; }
+ public string Domain { get; set; }
+ public string SiteName { get; set; }
+ public string Image { get; set; }
+ public string Video { get; set; }
+ }
+
+ public enum ChatMessageType
+ {
+ Default,
+ Attachment,
+ Call,
+ Action //use attachment field for more
+ }
+
+ public class ChatMessage : MockChatViewModel.ChatMessageDto, INotifyPropertyChanged
+ {
+ public string Id { get; set; }
+
+ public int DbId { get; set; }
+
+ public string Author { get; set; }
+
+ public string Meta { get; set; }
+
+ public ChatMessageType MessageType { get; set; }
+
+ public DateTime CreatedTime { get; set; }
+
+ public string Group { get; set; }
+
+ [JsonProperty("AType")]
+ public int AttachmentType { get; set; }
+
+
+
+ [JsonProperty("AStatus")]
+ public int AttachmentStatus { get; set; }
+
+ //todo attachments
+ [JsonProperty("Thumb")]
+ public string Thumbnail { get; set; }
+
+ ///
+ /// Blacklisted
+ ///
+ public bool Blocked { get; set; }
+
+ ///
+ /// by server or from client when sending
+ ///
+ public bool MetaChecked { get; set; }
+
+ private bool _Notify;
+ public bool Notify
+ {
+ get { return _Notify; }
+ set
+ {
+ if (_Notify != value)
+ {
+ _Notify = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public bool Outgoing
+ {
+ get
+ {
+ return Author == "71980d5c-2659-4ccb-ba79-e4433514fae5";
+ }
+ }
+
+ #region INotifyPropertyChanged
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
+ {
+ var changed = PropertyChanged;
+ if (changed == null)
+ return;
+
+ changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ #endregion
+
+ #region DTO UPDATABLES
+
+ private int _Status;
+ public new int Status
+ {
+ get { return _Status; }
+ set
+ {
+ if (_Status != value)
+ {
+ _Status = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private string _Text;
+ public new string Text
+ {
+ get { return _Text; }
+ set
+ {
+ if (_Text != value)
+ {
+ _Text = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private string _Attachment;
+ public new string Attachment
+ {
+ get { return _Attachment; }
+ set
+ {
+ if (_Attachment != value)
+ {
+ _Attachment = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ #endregion
+
+ private string _ImageMain;
+ public string ImageMain
+ {
+ get { return _ImageMain; }
+ set
+ {
+ if (_ImageMain != value)
+ {
+ _ImageMain = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public void Init()
+ {
+
+ try
+ {
+ if (!string.IsNullOrEmpty(this.Meta))
+ Metadata = JsonConvert.DeserializeObject(this.Meta);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ }
+
+ if (!string.IsNullOrEmpty(Thumbnail))
+ ImageMain = Thumbnail;
+ else
+ ImageMain = Attachment;
+
+ Sent = Status == 1;
+
+ //RU
+ DisplayDateTime = $"{this.CreatedTime.UtcToLocal():HH:mm d.m.yy}";
+ DisplayTime = $"{this.CreatedTime.UtcToLocal():HH:mm}";
+
+ var lastOnline = CreatedTime.UtcToLocal();
+ var todayDate = DateTime.Now;
+ var today = todayDate.Date;
+ var when = lastOnline.ToLocalTime().Date;
+ var delta = when - today;
+ var days = Math.Abs(delta.Days);
+
+ WhenDesc = "todo when";//ProjectExtensions.CorrectStringUponNumber(days,
+ // ResStrings.ExplainDate_Today,
+ // ResStrings.ExplainDate_Yest,
+ // ResStrings.ExplainDate_X1past,
+ // ResStrings.ExplainDate_X2past,
+ // ResStrings.ExplainDate_Xpast);
+
+ OnPropertyChanged("Outgoing");
+ OnPropertyChanged("IsText");
+
+ }
+
+ private bool _Read;
+ public bool Read
+ {
+ get { return _Read; }
+ set
+ {
+ if (_Read != value)
+ {
+ _Read = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+
+
+ private string _DisplayDateTime;
+ public string DisplayDateTime
+ {
+ get { return _DisplayDateTime; }
+ set
+ {
+ if (_DisplayDateTime != value)
+ {
+ _DisplayDateTime = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private string _WhenDesc;
+ public string WhenDesc
+ {
+ get { return _WhenDesc; }
+ set
+ {
+ if (_WhenDesc != value)
+ {
+ _WhenDesc = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private bool _IsFirst;
+ public bool IsFirst
+ {
+ get { return _IsFirst; }
+ set
+ {
+ if (_IsFirst != value)
+ {
+ _IsFirst = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private bool _IsFirstDate;
+ public bool IsFirstDate
+ {
+ get { return _IsFirstDate; }
+ set
+ {
+ if (_IsFirstDate != value)
+ {
+ _IsFirstDate = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private bool _Delivered;
+ public bool Delivered
+ {
+ get { return _Delivered; }
+ set
+ {
+ if (_Delivered != value)
+ {
+ _Delivered = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private bool _Sent;
+ public bool Sent
+ {
+ get { return _Sent; }
+ set
+ {
+ if (_Sent != value)
+ {
+ _Sent = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+
+ private ChatMessageMetadata _metadata;
+ public ChatMessageMetadata Metadata
+ {
+ get { return _metadata; }
+ set
+ {
+ if (_metadata != value)
+ {
+ _metadata = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private string _DisplayTime;
+ public string DisplayTime
+ {
+ get { return _DisplayTime; }
+ set
+ {
+ if (_DisplayTime != value)
+ {
+ _DisplayTime = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private bool _IsBusy;
+ public bool IsBusy
+ {
+ get { return _IsBusy; }
+ set
+ {
+ if (_IsBusy != value)
+ {
+ _IsBusy = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+
+ public ChatMetaType PresentAs
+ {
+ get
+ {
+ if (AttachmentType == (int)ChatMetaType.Separator)
+ return ChatMetaType.Separator;
+
+ if (AttachmentType == (int)ChatMetaType.System)
+ return ChatMetaType.System;
+
+ if (AttachmentType == (int)ChatMetaType.Image)
+ return ChatMetaType.Image;
+
+ if (AttachmentType == (int)ChatMetaType.Video)
+ return ChatMetaType.Video;
+
+ if (AttachmentType > 0)
+ return ChatMetaType.File;
+
+ if (Metadata != null)
+ return Metadata.Type;
+
+ return ChatMetaType.Default;
+ }
+ }
+
+ public bool Attached { get; set; }
+
+ protected string Linkify(string SearchText)
+ {
+ if (string.IsNullOrEmpty(SearchText))
+ return "";
+ // this will find links like:
+ // http://www.mysite.com
+ // as well as any links with other characters directly in front of it like:
+ // href="http://www.mysite.com"
+ // you can then use your own logic to determine which links to linkify
+ Regex regx = new Regex(@"\b(((\S+)?)(@|mailto\:|(news|(ht|f)tp(s?))\://)\S+)\b", RegexOptions.IgnoreCase);
+ SearchText = SearchText.Replace(" ", " ");
+ MatchCollection matches = regx.Matches(SearchText);
+
+ foreach (Match match in matches)
+ {
+ if (match.Value.StartsWith("http"))
+ { // if it starts with anything else then dont linkify -- may already be linked!
+ SearchText = SearchText.Replace(match.Value, "" + match.Value + "");
+ }
+ }
+
+ return SearchText;
+ }
+
+ // var invisibleGlyph = "⠀";
+
+
+
+
+ private string _PlayerName = "Аноним";
+ public string PlayerName
+ {
+ get { return _PlayerName; }
+ set
+ {
+ if (_PlayerName != value)
+ {
+ _PlayerName = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private Player _player;
+ public Player Player
+ {
+ get { return _player; }
+ set
+ {
+ if (_player != value)
+ {
+ _player = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+ }
+}
diff --git a/src/samples/Sandbox/ViewModels/MockChat2ViewModel.cs b/src/samples/Sandbox/ViewModels/MockChat2ViewModel.cs
new file mode 100644
index 0000000..c20a442
--- /dev/null
+++ b/src/samples/Sandbox/ViewModels/MockChat2ViewModel.cs
@@ -0,0 +1,80 @@
+using System.Collections.ObjectModel;
+using Newtonsoft.Json;
+
+namespace Sandbox
+{
+ public class MockChat2ViewModel : BindableObject
+ {
+
+ public class ChatMessageDto
+ {
+ public string Id { get; set; }
+ ///
+ /// Profile Key
+ ///
+ public string Author { get; set; }
+
+ public string Text { get; set; }
+
+ ///
+ /// Utc
+ ///
+ public DateTime CreatedTime { get; set; }
+ }
+
+
+
+ private bool _IsBusy;
+ public bool IsBusy
+ {
+ get
+ {
+ return _IsBusy;
+ }
+ set
+ {
+ if (_IsBusy != value)
+ {
+ _IsBusy = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public ObservableCollection Items { get; } = new();
+
+ public async Task LoadData()
+ {
+
+ try
+ {
+ IsBusy = true;
+
+ using var stream = await FileSystem.OpenAppPackageFileAsync("Json/chat.json");
+ using var reader = new StreamReader(stream);
+ var json = await reader.ReadToEndAsync();
+ var messages = JsonConvert.DeserializeObject>(json);
+
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ foreach (var chatMessage in messages)
+ {
+ Items.Add(chatMessage);
+ }
+ });
+
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ throw;
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+
+
+ }
+ }
+}
diff --git a/src/samples/Sandbox/ViewModels/MockChatViewModel.cs b/src/samples/Sandbox/ViewModels/MockChatViewModel.cs
index 10dbe4d..b3b8e5b 100644
--- a/src/samples/Sandbox/ViewModels/MockChatViewModel.cs
+++ b/src/samples/Sandbox/ViewModels/MockChatViewModel.cs
@@ -1,29 +1,8 @@
-using Newtonsoft.Json;
-using System.Collections.ObjectModel;
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
+using System.Collections.ObjectModel;
+using Newtonsoft.Json;
namespace Sandbox;
-public class ChatMessage : MockChatViewModel.ChatMessageDto, INotifyPropertyChanged
-{
-
- #region INotifyPropertyChanged
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
- {
- var changed = PropertyChanged;
- if (changed == null)
- return;
-
- changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
- #endregion
-
-}
-
public class MockChatViewModel : BindableObject
{
@@ -97,4 +76,4 @@ public async Task LoadData()
}
-}
\ No newline at end of file
+}
diff --git a/src/samples/Sandbox/ViewModels/Player.cs b/src/samples/Sandbox/ViewModels/Player.cs
new file mode 100644
index 0000000..a9cfadc
--- /dev/null
+++ b/src/samples/Sandbox/ViewModels/Player.cs
@@ -0,0 +1,290 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using AppoMobi.Specials;
+using Newtonsoft.Json;
+
+namespace Sandbox
+{
+ public class Player : INotifyPropertyChanged, IHasStringId, ICanBeSelected//, IPatchableDto
+ {
+
+ private string _Id;
+ public string Id
+ {
+ get
+ {
+ return _Id;
+ }
+ set
+ {
+ if (_Id != value)
+ {
+ _Id = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public bool IsMe
+ {
+ get
+ {
+ return "71980d5c-2659-4ccb-ba79-e4433514fae5" == this.Id;
+ }
+ }
+
+ public Player()
+ {
+ Id = Guid.NewGuid().ToString();
+ }
+
+ private string _PersonDesc;
+ public string PersonDesc
+ {
+ get { return _PersonDesc; }
+ set
+ {
+ if (_PersonDesc != value)
+ {
+ _PersonDesc = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private bool _IsOnline;
+ public new bool IsOnline
+ {
+ get
+ {
+ return _IsOnline;
+ }
+ set
+ {
+ if (_IsOnline != value)
+ {
+ _IsOnline = value;
+ OnPropertyChanged();
+ OnPropertyChanged("OnlineStatus");
+ OnPropertyChanged("OnlineDesc");
+ }
+ }
+ }
+
+ private bool _IsDND;
+ public new bool IsDND
+ {
+ get { return _IsDND; }
+ set
+ {
+ if (_IsDND != value)
+ {
+ _IsDND = value;
+ OnPropertyChanged();
+ OnPropertyChanged("OnlineStatus");
+ OnPropertyChanged("OnlineDesc");
+ }
+ }
+ }
+
+ [JsonIgnore]
+ public int OnlineStatus
+ {
+ get
+ {
+ if (IsDND && IsOnline)
+ {
+ return 2;
+ }
+
+ return IsOnline ? 1 : 0;
+ }
+ //set
+ //{
+ // if (_OnlineStatus != value)
+ // {
+ // _OnlineStatus = value;
+ // OnPropertyChanged();
+ // }
+ //}
+ }
+
+
+
+
+ private string _ImageMain;
+ public string ImageMain
+ {
+ get { return _ImageMain; }
+ set
+ {
+ if (_ImageMain != value)
+ {
+ _ImageMain = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+
+
+ private string _SpokenLanguagesDesc;
+ public string SpokenLanguagesDesc
+ {
+ get { return _SpokenLanguagesDesc; }
+ set
+ {
+ if (_SpokenLanguagesDesc != value)
+ {
+ _SpokenLanguagesDesc = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+
+
+ private bool _Selected;
+ public bool Selected
+ {
+ get { return _Selected; }
+ set
+ {
+ if (_Selected != value)
+ {
+ _Selected = value;
+ OnPropertyChanged("Selected");
+ }
+ }
+ }
+
+ public void RaiseProperties()
+ {
+ var props = this.GetType().GetProperties();
+ foreach (var property in props)
+ {
+ if (property.CanRead)
+ {
+ OnPropertyChanged(property.Name);
+ }
+ }
+ }
+
+ private bool _NeedShowLetter;
+ public bool NeedShowLetter
+ {
+ get { return _NeedShowLetter; }
+ set
+ {
+ if (_NeedShowLetter != value)
+ {
+ _NeedShowLetter = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private string _DisplayLetter;
+ public string DisplayLetter
+ {
+ get { return _DisplayLetter; }
+ set
+ {
+ if (_DisplayLetter != value)
+ {
+ _DisplayLetter = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+
+ private decimal _PayAmount;
+ public decimal PayAmount
+ {
+ get { return _PayAmount; }
+ set
+ {
+ if (_PayAmount != value)
+ {
+ _PayAmount = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private bool _ManualAmount;
+ public bool ManualAmount
+ {
+ get { return _ManualAmount; }
+ set
+ {
+ if (_ManualAmount != value)
+ {
+ _ManualAmount = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private bool _IsBusy;
+ public bool IsBusy
+ {
+ get { return _IsBusy; }
+ set
+ {
+ if (_IsBusy != value)
+ {
+ _IsBusy = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+
+
+
+ #region INotifyPropertyChanged
+
+ public void RaisePropertyChanged(string propertyName)
+ {
+ var changed = PropertyChanged;
+ if (changed == null)
+ return;
+
+ changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
+ {
+ var changed = PropertyChanged;
+ if (changed == null)
+ return;
+
+ changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ #endregion
+
+ private bool _Notify;
+ public bool Notify
+ {
+ get { return _Notify; }
+ set
+ {
+ if (_Notify != value)
+ {
+ _Notify = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+
+
+
+
+
+
+ }
+}