From 159166e32e2a4177785bf75f2fe25cfe18ef05fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 02:42:50 +0000 Subject: [PATCH 1/4] Initial plan From 0d450e970984019757a8c4041f251610a6c2e805 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 02:48:58 +0000 Subject: [PATCH 2/4] Add SKSignaturePadView control with pressure-sensitive ink rendering Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com> --- .../Controls/Signature/SKInkStroke.shared.cs | 328 +++++++++++ .../Signature/SKSignaturePadView.shared.cs | 527 ++++++++++++++++++ .../SKSignaturePadViewResources.shared.xaml | 22 + ...SKSignaturePadViewResources.shared.xaml.cs | 15 + ...ignatureStrokeCompletedEventArgs.shared.cs | 21 + 5 files changed, 913 insertions(+) create mode 100644 source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKInkStroke.shared.cs create mode 100644 source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKSignaturePadView.shared.cs create mode 100644 source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKSignaturePadViewResources.shared.xaml create mode 100644 source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKSignaturePadViewResources.shared.xaml.cs create mode 100644 source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKSignatureStrokeCompletedEventArgs.shared.cs diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKInkStroke.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKInkStroke.shared.cs new file mode 100644 index 00000000..45729878 --- /dev/null +++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKInkStroke.shared.cs @@ -0,0 +1,328 @@ +namespace SkiaSharp.Extended.UI.Controls; + +/// +/// Represents a single ink stroke with pressure-sensitive variable-width rendering. +/// Uses quadratic Bezier curves for smooth path interpolation and renders +/// variable-width strokes as filled polygons based on pressure data. +/// +public class SKInkStroke +{ + // Implementation adapted from @colinta on StackOverflow: https://stackoverflow.com/a/35229104 + // Enhanced with pressure sensitivity for fluid ink rendering + + private const float MinimumPointDistance = 2.0f; + private const float DefaultPressure = 0.5f; + + private readonly List points = new(); + private readonly List pressures = new(); + private readonly float minStrokeWidth; + private readonly float maxStrokeWidth; + + private SKPath? cachedPath; + private bool isDirty = true; + + /// + /// Creates a new ink stroke with the specified stroke width range. + /// + /// Minimum stroke width (at zero pressure). + /// Maximum stroke width (at full pressure). + public SKInkStroke(float minStrokeWidth = 1f, float maxStrokeWidth = 8f) + { + this.minStrokeWidth = minStrokeWidth; + this.maxStrokeWidth = maxStrokeWidth; + } + + /// + /// Gets the list of points in this stroke. + /// + public IReadOnlyList Points => points; + + /// + /// Gets the list of pressure values for each point. + /// + public IReadOnlyList Pressures => pressures; + + /// + /// Gets whether the stroke has no points. + /// + public bool IsEmpty => points.Count == 0; + + /// + /// Gets the filled path representing this variable-width stroke. + /// The path is cached and only regenerated when points change. + /// + public SKPath? Path + { + get + { + if (isDirty) + { + cachedPath?.Dispose(); + cachedPath = GenerateVariableWidthPath(); + isDirty = false; + } + return cachedPath; + } + } + + /// + /// Adds a point to the stroke with pressure information. + /// + /// The point location. + /// The pressure value (0.0 to 1.0). + /// Whether this is the final point in the stroke. + public void AddPoint(SKPoint point, float pressure, bool isLastPoint = false) + { + // Clamp pressure to valid range + pressure = Math.Clamp(pressure, 0f, 1f); + + // Use default pressure if no pressure data is available (finger touch) + if (pressure == 0f && !isLastPoint) + pressure = DefaultPressure; + + // Do not add a point if the current point is too close to the previous point + if (!isLastPoint && points.Count > 0 && !HasMovedFarEnough(points[^1], point)) + return; + + points.Add(point); + pressures.Add(pressure); + isDirty = true; + } + + /// + /// Clears all points from the stroke. + /// + public void Clear() + { + points.Clear(); + pressures.Clear(); + cachedPath?.Dispose(); + cachedPath = null; + isDirty = true; + } + + /// + /// Disposes of the cached path resources. + /// + public void Dispose() + { + cachedPath?.Dispose(); + cachedPath = null; + } + + private bool HasMovedFarEnough(SKPoint prevPoint, SKPoint currPoint) + { + var deltaX = currPoint.X - prevPoint.X; + var deltaY = currPoint.Y - prevPoint.Y; + var distance = MathF.Sqrt((deltaX * deltaX) + (deltaY * deltaY)); + return distance >= MinimumPointDistance; + } + + /// + /// Generates a variable-width path by creating a filled polygon + /// that represents the stroke with width varying based on pressure. + /// + private SKPath? GenerateVariableWidthPath() + { + if (points.Count < 2) + { + // For a single point, draw a circle + if (points.Count == 1) + { + var path = new SKPath(); + var radius = GetStrokeWidth(pressures[0]) / 2f; + path.AddCircle(points[0].X, points[0].Y, radius); + return path; + } + return null; + } + + // Generate smoothed centerline points using quadratic Bezier interpolation + var smoothedPoints = GenerateSmoothedPoints(); + if (smoothedPoints.Count < 2) + return null; + + // Generate left and right offset points based on pressure + var leftPoints = new List(); + var rightPoints = new List(); + + for (int i = 0; i < smoothedPoints.Count; i++) + { + var point = smoothedPoints[i].Point; + var pressure = smoothedPoints[i].Pressure; + var halfWidth = GetStrokeWidth(pressure) / 2f; + + // Calculate the normal (perpendicular) at this point + SKPoint tangent; + if (i == 0) + { + // First point: use direction to next point + tangent = Normalize(smoothedPoints[i + 1].Point - point); + } + else if (i == smoothedPoints.Count - 1) + { + // Last point: use direction from previous point + tangent = Normalize(point - smoothedPoints[i - 1].Point); + } + else + { + // Middle point: average direction + var prev = smoothedPoints[i - 1].Point; + var next = smoothedPoints[i + 1].Point; + tangent = Normalize(next - prev); + } + + // Normal is perpendicular to tangent + var normal = new SKPoint(-tangent.Y, tangent.X); + + // Offset points on both sides + leftPoints.Add(new SKPoint(point.X + normal.X * halfWidth, point.Y + normal.Y * halfWidth)); + rightPoints.Add(new SKPoint(point.X - normal.X * halfWidth, point.Y - normal.Y * halfWidth)); + } + + // Build the filled polygon path + var path = new SKPath(); + + // Start with the left side (forward direction) + path.MoveTo(leftPoints[0]); + for (int i = 1; i < leftPoints.Count; i++) + { + path.LineTo(leftPoints[i]); + } + + // Add rounded end cap at the end + var endRadius = GetStrokeWidth(smoothedPoints[^1].Pressure) / 2f; + var endCenter = smoothedPoints[^1].Point; + var endTangent = Normalize(smoothedPoints[^1].Point - smoothedPoints[^2].Point); + AddRoundedCap(path, endCenter, endTangent, endRadius); + + // Continue with the right side (reverse direction) + for (int i = rightPoints.Count - 1; i >= 0; i--) + { + path.LineTo(rightPoints[i]); + } + + // Add rounded start cap + var startRadius = GetStrokeWidth(smoothedPoints[0].Pressure) / 2f; + var startCenter = smoothedPoints[0].Point; + var startTangent = Normalize(smoothedPoints[0].Point - smoothedPoints[1].Point); + AddRoundedCap(path, startCenter, startTangent, startRadius); + + path.Close(); + + return path; + } + + /// + /// Generates smoothed points using quadratic Bezier interpolation. + /// + private List<(SKPoint Point, float Pressure)> GenerateSmoothedPoints() + { + var result = new List<(SKPoint Point, float Pressure)>(); + + if (points.Count == 0) + return result; + + if (points.Count == 1) + { + result.Add((points[0], pressures[0])); + return result; + } + + // Add the first point + result.Add((points[0], pressures[0])); + + // Generate intermediate points using quadratic Bezier interpolation + for (int i = 0; i < points.Count - 1; i++) + { + var p0 = points[i]; + var p1 = points[i + 1]; + var pressure0 = pressures[i]; + var pressure1 = pressures[i + 1]; + + // Calculate midpoint + var midPoint = new SKPoint((p0.X + p1.X) / 2f, (p0.Y + p1.Y) / 2f); + var midPressure = (pressure0 + pressure1) / 2f; + + if (i == 0) + { + // First segment: add midpoint + result.Add((midPoint, midPressure)); + } + else if (i < points.Count - 2) + { + // Middle segments: add quadratic curve samples + var prevMid = result[^1]; + AddQuadraticSamples(result, prevMid.Point, prevMid.Pressure, p0, pressure0, midPoint, midPressure); + } + } + + // Add the last point + result.Add((points[^1], pressures[^1])); + + return result; + } + + /// + /// Adds samples along a quadratic Bezier curve for smooth interpolation. + /// + private static void AddQuadraticSamples( + List<(SKPoint Point, float Pressure)> result, + SKPoint p0, float pressure0, + SKPoint control, float controlPressure, + SKPoint p1, float pressure1) + { + // Sample the quadratic curve at a few points for smooth rendering + const int samples = 4; + for (int i = 1; i <= samples; i++) + { + float t = i / (float)samples; + float u = 1 - t; + + // Quadratic Bezier formula: B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2 + var x = u * u * p0.X + 2 * u * t * control.X + t * t * p1.X; + var y = u * u * p0.Y + 2 * u * t * control.Y + t * t * p1.Y; + var pressure = u * u * pressure0 + 2 * u * t * controlPressure + t * t * pressure1; + + result.Add((new SKPoint(x, y), pressure)); + } + } + + /// + /// Adds a rounded cap at the end of the stroke. + /// + private static void AddRoundedCap(SKPath path, SKPoint center, SKPoint direction, float radius) + { + // Add a semicircle cap + var perpendicular = new SKPoint(-direction.Y, direction.X); + var startAngle = MathF.Atan2(perpendicular.Y, perpendicular.X) * 180f / MathF.PI; + + var rect = new SKRect( + center.X - radius, + center.Y - radius, + center.X + radius, + center.Y + radius); + + path.ArcTo(rect, startAngle, 180, false); + } + + /// + /// Calculates the stroke width based on pressure. + /// + private float GetStrokeWidth(float pressure) + { + return minStrokeWidth + (maxStrokeWidth - minStrokeWidth) * pressure; + } + + /// + /// Normalizes a vector to unit length. + /// + private static SKPoint Normalize(SKPoint vector) + { + var length = MathF.Sqrt(vector.X * vector.X + vector.Y * vector.Y); + if (length < 0.0001f) + return new SKPoint(1, 0); // Default direction for zero-length vectors + + return new SKPoint(vector.X / length, vector.Y / length); + } +} diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKSignaturePadView.shared.cs b/source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKSignaturePadView.shared.cs new file mode 100644 index 00000000..1e6088df --- /dev/null +++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKSignaturePadView.shared.cs @@ -0,0 +1,527 @@ +using System.Windows.Input; + +namespace SkiaSharp.Extended.UI.Controls; + +/// +/// A signature pad control that provides fluid, pressure-sensitive ink rendering. +/// Supports stylus/pen pressure for variable stroke width (harder pressure = thicker lines). +/// Uses quadratic Bezier curves to smooth jagged input from finger or pen movement. +/// +public class SKSignaturePadView : SKSurfaceView +{ + /// + /// Bindable property for the stroke color. + /// + public static readonly BindableProperty StrokeColorProperty = BindableProperty.Create( + nameof(StrokeColor), + typeof(Color), + typeof(SKSignaturePadView), + Colors.Black, + propertyChanged: OnStrokeColorChanged); + + /// + /// Bindable property for the minimum stroke width (at zero pressure). + /// + public static readonly BindableProperty MinStrokeWidthProperty = BindableProperty.Create( + nameof(MinStrokeWidth), + typeof(float), + typeof(SKSignaturePadView), + 1f, + propertyChanged: OnStrokeWidthChanged); + + /// + /// Bindable property for the maximum stroke width (at full pressure). + /// + public static readonly BindableProperty MaxStrokeWidthProperty = BindableProperty.Create( + nameof(MaxStrokeWidth), + typeof(float), + typeof(SKSignaturePadView), + 8f, + propertyChanged: OnStrokeWidthChanged); + + /// + /// Bindable property for the pad background color. + /// + public static readonly BindableProperty PadBackgroundColorProperty = BindableProperty.Create( + nameof(PadBackgroundColor), + typeof(Color), + typeof(SKSignaturePadView), + Colors.White, + propertyChanged: OnPadBackgroundColorChanged); + + /// + /// Bindable property for the command executed when strokes are cleared. + /// + public static readonly BindableProperty ClearedCommandProperty = BindableProperty.Create( + nameof(ClearedCommand), + typeof(ICommand), + typeof(SKSignaturePadView), + default(ICommand)); + + /// + /// Bindable property for the parameter passed to the ClearedCommand. + /// + public static readonly BindableProperty ClearedCommandParameterProperty = BindableProperty.Create( + nameof(ClearedCommandParameter), + typeof(object), + typeof(SKSignaturePadView), + null); + + /// + /// Bindable property for the command executed when a stroke is completed. + /// + public static readonly BindableProperty StrokeCompletedCommandProperty = BindableProperty.Create( + nameof(StrokeCompletedCommand), + typeof(ICommand), + typeof(SKSignaturePadView), + default(ICommand)); + + /// + /// Bindable property for the parameter passed to the StrokeCompletedCommand. + /// + public static readonly BindableProperty StrokeCompletedCommandParameterProperty = BindableProperty.Create( + nameof(StrokeCompletedCommandParameter), + typeof(object), + typeof(SKSignaturePadView), + null); + + /// + /// Internal bindable property key for the read-only IsBlank property. + /// + internal static readonly BindablePropertyKey IsBlankPropertyKey = BindableProperty.CreateReadOnly( + nameof(IsBlank), + typeof(bool), + typeof(SKSignaturePadView), + true); + + /// + /// Bindable property for the IsBlank property. + /// + public static readonly BindableProperty IsBlankProperty = IsBlankPropertyKey.BindableProperty; + + private readonly List strokes = new(); + private readonly SKPaint strokePaint; + private SKInkStroke? currentStroke; + private SKColor skStrokeColor = SKColors.Black; + private SKColor skBackgroundColor = SKColors.White; + private SKCanvasView? canvasView; + + /// + /// Creates a new signature pad view. + /// + public SKSignaturePadView() + { + ResourceLoader.EnsureRegistered(this); + + strokePaint = new SKPaint + { + Color = skStrokeColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + } + + /// + /// Occurs when all strokes are cleared from the pad. + /// + public event EventHandler? Cleared; + + /// + /// Occurs when a stroke is completed. + /// + public event EventHandler? StrokeCompleted; + + /// + /// Gets whether the signature pad is blank (has no strokes). + /// + public bool IsBlank + { + get => (bool)GetValue(IsBlankProperty); + private set => SetValue(IsBlankPropertyKey, value); + } + + /// + /// Gets or sets the stroke color. + /// + public Color StrokeColor + { + get => (Color)GetValue(StrokeColorProperty); + set => SetValue(StrokeColorProperty, value); + } + + /// + /// Gets or sets the minimum stroke width (at zero pressure). + /// + public float MinStrokeWidth + { + get => (float)GetValue(MinStrokeWidthProperty); + set => SetValue(MinStrokeWidthProperty, value); + } + + /// + /// Gets or sets the maximum stroke width (at full pressure). + /// + public float MaxStrokeWidth + { + get => (float)GetValue(MaxStrokeWidthProperty); + set => SetValue(MaxStrokeWidthProperty, value); + } + + /// + /// Gets or sets the background color of the signature pad. + /// + public Color PadBackgroundColor + { + get => (Color)GetValue(PadBackgroundColorProperty); + set => SetValue(PadBackgroundColorProperty, value); + } + + /// + /// Gets or sets the command executed when strokes are cleared. + /// + public ICommand? ClearedCommand + { + get => (ICommand?)GetValue(ClearedCommandProperty); + set => SetValue(ClearedCommandProperty, value); + } + + /// + /// Gets or sets the parameter passed to the ClearedCommand. + /// + public object? ClearedCommandParameter + { + get => GetValue(ClearedCommandParameterProperty); + set => SetValue(ClearedCommandParameterProperty, value); + } + + /// + /// Gets or sets the command executed when a stroke is completed. + /// + public ICommand? StrokeCompletedCommand + { + get => (ICommand?)GetValue(StrokeCompletedCommandProperty); + set => SetValue(StrokeCompletedCommandProperty, value); + } + + /// + /// Gets or sets the parameter passed to the StrokeCompletedCommand. + /// + public object? StrokeCompletedCommandParameter + { + get => GetValue(StrokeCompletedCommandParameterProperty); + set => SetValue(StrokeCompletedCommandParameterProperty, value); + } + + /// + /// Gets the number of strokes in the signature. + /// + public int StrokeCount => strokes.Count; + + /// + /// Clears all strokes from the signature pad. + /// + public void Clear() + { + foreach (var stroke in strokes) + { + stroke.Dispose(); + } + strokes.Clear(); + + currentStroke?.Dispose(); + currentStroke = null; + + UpdateIsBlank(); + Invalidate(); + + Cleared?.Invoke(this, EventArgs.Empty); + + if (ClearedCommand?.CanExecute(ClearedCommandParameter) == true) + { + ClearedCommand.Execute(ClearedCommandParameter); + } + } + + /// + /// Removes the last stroke from the signature pad (undo). + /// + /// True if a stroke was removed, false if there were no strokes. + public bool Undo() + { + if (strokes.Count == 0) + return false; + + var lastStroke = strokes[^1]; + strokes.RemoveAt(strokes.Count - 1); + lastStroke.Dispose(); + + UpdateIsBlank(); + Invalidate(); + + return true; + } + + /// + /// Gets a combined path of all strokes. + /// + /// An SKPath containing all strokes, or null if the pad is blank. + public SKPath? ToPath() + { + if (strokes.Count == 0) + return null; + + var combinedPath = new SKPath(); + foreach (var stroke in strokes) + { + if (stroke.Path is SKPath path) + { + combinedPath.AddPath(path); + } + } + + return combinedPath; + } + + /// + /// Renders the signature to an SKImage. + /// + /// The width of the output image. + /// The height of the output image. + /// The background color, or null for transparent. + /// An SKImage containing the rendered signature. + public SKImage? ToImage(int width, int height, SKColor? backgroundColor = null) + { + if (strokes.Count == 0) + return null; + + var info = new SKImageInfo(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); + using var surface = SKSurface.Create(info); + + if (surface == null) + return null; + + var canvas = surface.Canvas; + + if (backgroundColor.HasValue) + { + canvas.Clear(backgroundColor.Value); + } + else + { + canvas.Clear(SKColors.Transparent); + } + + // Calculate scale to fit the signature in the image + var bounds = GetStrokeBounds(); + if (bounds.IsEmpty) + return null; + + var scale = Math.Min(width / bounds.Width, height / bounds.Height) * 0.9f; + var offsetX = (width - bounds.Width * scale) / 2f - bounds.Left * scale; + var offsetY = (height - bounds.Height * scale) / 2f - bounds.Top * scale; + + canvas.Translate(offsetX, offsetY); + canvas.Scale(scale); + + // Draw all strokes + foreach (var stroke in strokes) + { + if (stroke.Path is SKPath path) + { + canvas.DrawPath(path, strokePaint); + } + } + + return surface.Snapshot(); + } + + /// + /// Gets the bounding rectangle of all strokes. + /// + /// The bounding rectangle. + public SKRect GetStrokeBounds() + { + if (strokes.Count == 0) + return SKRect.Empty; + + var bounds = SKRect.Empty; + foreach (var stroke in strokes) + { + if (stroke.Path is SKPath path) + { + var pathBounds = path.Bounds; + if (bounds.IsEmpty) + { + bounds = pathBounds; + } + else + { + bounds = SKRect.Union(bounds, pathBounds); + } + } + } + + return bounds; + } + + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + // Get access to the underlying canvas view to enable touch events + var templateChild = GetTemplateChild("PART_DrawingSurface"); + + if (templateChild is SKCanvasView view) + { + canvasView = view; + canvasView.EnableTouchEvents = true; + canvasView.Touch += OnCanvasTouch; + } + } + + /// + protected override void OnPaintSurface(SKCanvas canvas, SKSize size) + { + // Clear with background color + canvas.Clear(skBackgroundColor); + + // Draw all completed strokes + foreach (var stroke in strokes) + { + if (stroke.Path is SKPath path) + { + canvas.DrawPath(path, strokePaint); + } + } + + // Draw the current stroke being drawn + if (currentStroke?.Path is SKPath currentPath) + { + canvas.DrawPath(currentPath, strokePaint); + } + } + + /// + /// Handles touch events on the canvas. + /// + private void OnCanvasTouch(object? sender, SKTouchEventArgs e) + { + var scale = (float)(canvasView?.CanvasSize.Width / Width ?? 1); + var location = new SKPoint(e.Location.X / scale, e.Location.Y / scale); + var pressure = e.Pressure; + + switch (e.ActionType) + { + case SKTouchAction.Pressed: + StartStroke(location, pressure); + e.Handled = true; + break; + + case SKTouchAction.Moved: + if (e.InContact) + { + ContinueStroke(location, pressure); + e.Handled = true; + } + break; + + case SKTouchAction.Released: + EndStroke(location, pressure); + e.Handled = true; + break; + + case SKTouchAction.Cancelled: + CancelStroke(); + e.Handled = true; + break; + } + } + + private void StartStroke(SKPoint point, float pressure) + { + currentStroke = new SKInkStroke(MinStrokeWidth, MaxStrokeWidth); + currentStroke.AddPoint(point, pressure); + Invalidate(); + } + + private void ContinueStroke(SKPoint point, float pressure) + { + currentStroke?.AddPoint(point, pressure); + Invalidate(); + } + + private void EndStroke(SKPoint point, float pressure) + { + if (currentStroke != null) + { + currentStroke.AddPoint(point, pressure, isLastPoint: true); + + if (!currentStroke.IsEmpty) + { + strokes.Add(currentStroke); + UpdateIsBlank(); + + StrokeCompleted?.Invoke(this, new SKSignatureStrokeCompletedEventArgs(strokes.Count)); + + if (StrokeCompletedCommand?.CanExecute(StrokeCompletedCommandParameter) == true) + { + StrokeCompletedCommand.Execute(StrokeCompletedCommandParameter); + } + } + else + { + currentStroke.Dispose(); + } + + currentStroke = null; + } + + Invalidate(); + } + + private void CancelStroke() + { + currentStroke?.Dispose(); + currentStroke = null; + Invalidate(); + } + + private void UpdateIsBlank() + { + IsBlank = strokes.Count == 0; + } + + private static void OnStrokeColorChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SKSignaturePadView view && newValue is Color color) + { + view.skStrokeColor = new SKColor( + (byte)(color.Red * 255), + (byte)(color.Green * 255), + (byte)(color.Blue * 255), + (byte)(color.Alpha * 255)); + view.strokePaint.Color = view.skStrokeColor; + view.Invalidate(); + } + } + + private static void OnStrokeWidthChanged(BindableObject bindable, object oldValue, object newValue) + { + // Stroke width changes will affect new strokes only + // No need to invalidate existing strokes + } + + private static void OnPadBackgroundColorChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SKSignaturePadView view && newValue is Color color) + { + view.skBackgroundColor = new SKColor( + (byte)(color.Red * 255), + (byte)(color.Green * 255), + (byte)(color.Blue * 255), + (byte)(color.Alpha * 255)); + view.Invalidate(); + } + } +} diff --git a/source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKSignaturePadViewResources.shared.xaml b/source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKSignaturePadViewResources.shared.xaml new file mode 100644 index 00000000..1cc82ac7 --- /dev/null +++ b/source/SkiaSharp.Extended.UI.Maui/Controls/Signature/SKSignaturePadViewResources.shared.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + +