Reputation: 83
I am highly confused on this. I have created a Custom SKCanvasView with some additional logic for moving Images and especially a CropImage() method. The confusion now is, how can i execute this Method from a Binding in the ViewModel containing this Custom SKCanvasView? I tried creating a BindableProperty of type bool to execute the Method when the bool is set to true but this did not quite work and seemed fishy. I also tried creating an ICommand but as the Microsoft Documentation (https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/data-binding/commanding?view=net-maui-7.0) does not show how to create Custom Commands i am very confused on that too. How can i now Execute the CropMap() Method in my Custom SkCanvasView from the ViewModel assigned to the View which implements this Custom View?
Custom View:
public class CroppingCanvasView : SKCanvasView, INotifyPropertyChanged
{
// == Properties ==
public SKBitmap Bitmap
{
get
{
return (SKBitmap)GetValue(BitmapProperty);
}
set
{
SetValue(BitmapProperty, value);
}
}
SKRect croppingRect;
SKRect CroppingRect
{
get
{
return croppingRect;
}
set
{
SKRect rect = Matrix.Invert().MapRect(value);
WeakReferenceMessenger.Default.Send(new CroppingRectChangedMessage(rect));
croppingRect = rect;
}
}
SKMatrix Matrix { get; set; }
SKRect BitmapRect { get; set; }
public SKBitmap CropBitmap { get; set; }
public byte[] CroppedImage
{
get
{
return (byte[])GetValue(CroppedImageProperty);
}
set
{
SetValue(CroppedImageProperty, value);
}
}
// == Bindable Properties ==
public static readonly BindableProperty BitmapProperty =
BindableProperty.Create(nameof(Bitmap), typeof(SKBitmap), typeof(CroppingCanvasView));
public static readonly BindableProperty CroppedImageProperty =
BindableProperty.Create(nameof(CroppedImage), typeof(byte[]), typeof(CroppingCanvasView),
defaultBindingMode: BindingMode.OneWayToSource);
// == Command Properties ==
public ICommand CropImage
{
get
{
return (ICommand)GetValue(CropImageProperty);
}
set
{
SetValue(CropImageProperty, value);
}
}
// == Commands ==
public static readonly BindableProperty CropImageProperty =
BindableProperty.Create(nameof(CropImage), typeof(ICommand), typeof(CroppingCanvasView));
// == fields ==
TouchEffect touchEffect;
Dictionary<long, SKPoint> touchDictionary;
public CroppingCanvasView()
{
Matrix = SKMatrix.CreateIdentity();
touchEffect = new TouchEffect();
touchDictionary = new Dictionary<long, SKPoint>();
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = nameof(Bitmap))
{
base.OnPropertyChanged(propertyName);
if (Bitmap != null)
{
BitmapRect = SKRect.Create(0,0,Bitmap.Width,Bitmap.Height);
touchEffect.TouchAction += TouchEffect_TouchAction;
}
}
// == override Methods ==
protected override void OnPaintSurface(SkiaSharp.Views.Maui.SKPaintSurfaceEventArgs e)
{
base.OnPaintSurface(e);
SKImageInfo info = e.Info;
SKSurface surface = e.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
// Display the bitmap
Matrix = CalculateMatrix(Matrix);
canvas.SetMatrix(Matrix);
canvas.DrawBitmap(Bitmap, new SKPoint(0,0));
CroppingRect = SKRect.Create(0, 0, CanvasSize.Width, CanvasSize.Height);
}
protected override void OnParentSet()
{
base.OnParentSet();
// Attach TouchEffect to parent view
touchEffect.Capture = true;
Parent.Effects.Add(touchEffect);
}
// == private Methods ==
private void TouchEffect_TouchAction(object sender, Maui.FreakyEffects.TouchTracking.TouchActionEventArgs e)
{
Maui.FreakyEffects.TouchTracking.TouchTrackingPoint pt = e.Location;
// Convert Xamarin.Forms point to pixels
SKPoint point =
new SKPoint((float)(CanvasSize.Width * pt.X / Width),
(float)(CanvasSize.Height * pt.Y / Height));
switch (e.Type)
{
case TouchActionType.Pressed:
// Find transformed bitmap rectangle
SKRect rect = new SKRect(0, 0, Bitmap.Width, Bitmap.Height);
rect = Matrix.MapRect(rect);
// Determine if the touch was within that rectangle
if (rect.Contains(point) && !touchDictionary.ContainsKey(e.Id))
{
touchDictionary.Add(e.Id, point);
}
break;
case TouchActionType.Moved:
if (touchDictionary.ContainsKey(e.Id))
{
// Single-finger drag
if (touchDictionary.Count == 1)
{
SKPoint prevPoint = touchDictionary[e.Id];
// Adjust the matrix for the new position
SKMatrix matrix = Matrix;
matrix.TransX += point.X - prevPoint.X;
matrix.TransY += point.Y - prevPoint.Y;
matrix = CalculateMatrix(matrix);
Matrix = matrix;
InvalidateSurface();
}
// Double-finger scale and drag
else if (touchDictionary.Count == 2)
{
// Copy two dictionary keys into array
long[] keys = new long[touchDictionary.Count];
touchDictionary.Keys.CopyTo(keys, 0);
// Find index of non-moving (pivot) finger
int pivotIndex = (keys[0] == e.Id) ? 1 : 0;
// Get the three points involved in the transform
SKPoint pivotPoint = touchDictionary[keys[pivotIndex]];
SKPoint prevPoint = touchDictionary[e.Id];
SKPoint newPoint = point;
// Calculate two vectors
SKPoint oldVector = prevPoint - pivotPoint;
SKPoint newVector = newPoint - pivotPoint;
//SKPoint oldVector = prevPoint - new SKPoint(canvasView.CanvasSize.Width/2, (canvasView.CanvasSize.Width/2);
float scale = newVector.LengthSquared / oldVector.LengthSquared;
// Scaling factors are ratios of those
float scaleX = newVector.X / oldVector.X;
float scaleY = newVector.Y / oldVector.Y;
if (!float.IsNaN(scaleX) && !float.IsInfinity(scaleX) &&
!float.IsNaN(scaleY) && !float.IsInfinity(scaleY))
{
// If something bad hasn't happened, calculate a scale and translation matrix
SKMatrix scaleMatrix = SKMatrix.CreateScale(scale, scale, pivotPoint.X, pivotPoint.Y);
SKMatrix matrix = Matrix;
matrix = matrix.PostConcat(scaleMatrix);
matrix = CalculateMatrix(matrix);
Matrix = matrix;
InvalidateSurface();
}
}
// Store the new point in the dictionary
touchDictionary[e.Id] = point;
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (touchDictionary.ContainsKey(e.Id))
{
touchDictionary.Remove(e.Id);
CropMap();
}
break;
}
}
private SKMatrix CalculateMatrix(SKMatrix getMatrix)
{
SKMatrix matrix = getMatrix;
if (matrix.MapRect(BitmapRect).Size.Width < CanvasSize.Width ||
matrix.MapRect(BitmapRect).Size.Height < CanvasSize.Height)
{
float scaleX = CanvasSize.Width / Bitmap.Width;
float scaleY = CanvasSize.Height / Bitmap.Height;
if (matrix.MapRect(BitmapRect).Size.Width == matrix.MapRect(BitmapRect).Size.Height)
{
matrix.ScaleX = scaleX;
matrix.ScaleY = scaleY;
}
else if (matrix.MapRect(BitmapRect).Size.Width < matrix.MapRect(BitmapRect).Size.Height)
{
matrix.ScaleX = scaleX;
matrix.ScaleY = scaleX;
}
else if (matrix.MapRect(BitmapRect).Size.Width > matrix.MapRect(BitmapRect).Size.Height)
{
matrix.ScaleX = scaleY;
matrix.ScaleY = scaleY;
}
}
if (matrix.MapRect(BitmapRect).Left > 0)
{
matrix.TransX -= matrix.MapRect(BitmapRect).Left;
}
if(matrix.MapRect(BitmapRect).Size.Height < CanvasSize.Height)
{
matrix.ScaleY = Bitmap.Height / CanvasSize.Height;
}
if (matrix.MapRect(BitmapRect).Left > 0)
{
matrix.TransX -= matrix.MapRect(BitmapRect).Left;
}
if (matrix.MapRect(BitmapRect).Right < CanvasSize.Width)
{
matrix.TransX += -matrix.MapRect(BitmapRect).Right + CanvasSize.Width;
}
if (matrix.MapRect(BitmapRect).Top > 0)
{
matrix.TransY -= matrix.MapRect(BitmapRect).Top;
}
if (matrix.MapRect(BitmapRect).Bottom < CanvasSize.Height)
{
matrix.TransY += -matrix.MapRect(BitmapRect).Bottom + CanvasSize.Height;
}
return matrix;
}
private void CropMap()
{
SKBitmap destination = new SKBitmap(1080, 1080);
SKRect destRect = SKRect.Create(0, 0, 1080, 1080);
using (SKCanvas canvas = new SKCanvas(destination))
{
canvas.Clear();
canvas.DrawBitmap(Bitmap, CroppingRect, destRect);
}
SKImage skImage = SKImage.FromBitmap(destination);
SKData encoded = skImage.Encode(SKEncodedImageFormat.Jpeg, 100);
using (MemoryStream memory = new MemoryStream())
{
encoded.AsStream().CopyTo(memory);
CroppedImage = null;
CroppedImage = memory.ToArray();
}
}
Implementing View:
<ContentPage.Resources>
<ResourceDictionary>
<toolkit:ByteArrayToImageSourceConverter x:Key="ByteArrayToImageSourceConverter" />
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.ToolbarItems>
<ToolbarItem Text="Test" Command="{Binding CropCommand}"/>
</ContentPage.ToolbarItems>
<Grid x:Name="test">
<Grid.RowDefinitions>
<RowDefinition Height="0.5*"/>
<RowDefinition Height="0.5*"/>
</Grid.RowDefinitions>
<local:CroppingCanvasView Grid.Row="0" Bitmap="{Binding Map, Mode=OneWay}" CroppedImage="{Binding CroppedImage}" CropImage="{Binding ClickCommand}"
BackgroundColor="Aqua" VerticalOptions="CenterAndExpand"/>
<VerticalStackLayout Grid.Row="1">
<Image Source="{Binding CroppedImage, Converter={StaticResource ByteArrayToImageSourceConverter}}" Grid.Row="1"
MaximumHeightRequest="200" MaximumWidthRequest="200" Margin="50"/>
</VerticalStackLayout>
</Grid>
The Code behind just sets the BindignContext as a new CutImagesViewModel.
ViewModel:
public partial class CutImagesViewModel : ObservableObject
{
[ObservableProperty]
public ObservableCollection<UploadImageModel> uploadImages;
[ObservableProperty]
public SKBitmap map;
[ObservableProperty]
public byte[] croppedImage;
partial void OnCroppedImageChanged(byte[] value)
{
Console.WriteLine("cropped Image changed");
}
public CutImagesViewModel(ObservableCollection<UploadImageModel> files)
{
UploadImages = new ObservableCollection<UploadImageModel>();
UploadImages = files;
Map = UploadImages[0].Image;
}
[RelayCommand]
public void Click()
{
Console.WriteLine("clicked");
}
[RelayCommand]
public void Crop()
{
Click();
Console.WriteLine("crop");
}
}
Edit 1: What i especially do not understand is when i create a Bindable property of type bool like this:
public bool CropImage
{
get
{
return (bool)GetValue(CropImageProperty);
}
set
{
SetValue(CropImageProperty, value);
}
}
public static readonly BindableProperty CropImageProperty =
BindableProperty.Create(nameof(CropImage), typeof(bool), typeof(CroppingCanvasView)
, propertyChanged: (bindable, oldValue, newValue) =>
{
Console.WriteLine("prop changes");
} );
And Bind it in the Viewmodel with periodically changing the bool value to the opposite:
[ObservableProperty]
public bool crop;
while(true)
{
if(Crop)
Crop = false;
else
Crop = true;
Thread.Sleep(1000);
}
and set a Breakpoint to the get as well as the set of the Property weather the get nor the set is ever called even thoug the Bindign is set
<local:CroppingCanvasView Grid.Row="0" Bitmap="{Binding Map, Mode=OneWay}" CroppedImage="{Binding CroppedImage}" CropImage="{Binding Crop}"
BackgroundColor="Aqua" VerticalOptions="CenterAndExpand"/>
and the get of the Bitmap gets called correctly as expected.
Upvotes: 0
Views: 116
Reputation: 83
Obviously the PropertyChanged
event will not get called when the constructor of the ViewModel
is not finished. When i create a Button to change the Value of the bool custom Bindable Property so that i only changes when the Class is created completely it works fine.
Upvotes: 0