From 8b1c733943a38be21ec99898863278eaf0c3c6ab Mon Sep 17 00:00:00 2001 From: logikonline Date: Fri, 16 Jan 2026 05:49:20 +0000 Subject: [PATCH] Border completed --- Handlers/BorderHandler.cs | 78 +++++++++++++++- Views/SkiaBorder.cs | 186 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 250 insertions(+), 14 deletions(-) diff --git a/Handlers/BorderHandler.cs b/Handlers/BorderHandler.cs index 55e5dfa..6682fd0 100644 --- a/Handlers/BorderHandler.cs +++ b/Handlers/BorderHandler.cs @@ -21,6 +21,11 @@ public partial class BorderHandler : ViewHandler [nameof(IBorderView.Content)] = MapContent, [nameof(IBorderStroke.Stroke)] = MapStroke, [nameof(IBorderStroke.StrokeThickness)] = MapStrokeThickness, + ["StrokeDashArray"] = MapStrokeDashArray, + ["StrokeDashOffset"] = MapStrokeDashOffset, + [nameof(IBorderStroke.StrokeLineCap)] = MapStrokeLineCap, + [nameof(IBorderStroke.StrokeLineJoin)] = MapStrokeLineJoin, + [nameof(IBorderStroke.StrokeMiterLimit)] = MapStrokeMiterLimit, ["StrokeShape"] = MapStrokeShape, // StrokeShape is on Border, not IBorderStroke [nameof(IView.Background)] = MapBackground, ["BackgroundColor"] = MapBackgroundColor, @@ -151,10 +156,13 @@ public partial class BorderHandler : ViewHandler if (border is not Border borderControl) return; var shape = borderControl.StrokeShape; + + // Pass the shape directly to the platform view for full shape support + handler.PlatformView.StrokeShape = shape; + + // Also set CornerRadius for backward compatibility when StrokeShape is RoundRectangle if (shape is Microsoft.Maui.Controls.Shapes.RoundRectangle roundRect) { - // RoundRectangle can have different corner radii, but we use a uniform one - // Take the top-left corner as the uniform radius var cornerRadius = roundRect.CornerRadius; handler.PlatformView.CornerRadius = cornerRadius.TopLeft; } @@ -164,11 +172,71 @@ public partial class BorderHandler : ViewHandler } else if (shape is Microsoft.Maui.Controls.Shapes.Ellipse) { - // For ellipse, use half the min dimension as corner radius - // This will be applied during rendering when bounds are known - handler.PlatformView.CornerRadius = double.MaxValue; // Marker for "fully rounded" + handler.PlatformView.CornerRadius = double.MaxValue; } handler.PlatformView.Invalidate(); } + + public static void MapStrokeDashArray(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + + // StrokeDashArray is on Border class + if (border is Border borderControl && borderControl.StrokeDashArray != null) + { + var dashArray = new DoubleCollection(); + foreach (var value in borderControl.StrokeDashArray) + { + dashArray.Add(value); + } + handler.PlatformView.StrokeDashArray = dashArray; + } + handler.PlatformView.Invalidate(); + } + + public static void MapStrokeDashOffset(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + + // StrokeDashOffset is on Border class + if (border is Border borderControl) + { + handler.PlatformView.StrokeDashOffset = borderControl.StrokeDashOffset; + } + handler.PlatformView.Invalidate(); + } + + public static void MapStrokeLineCap(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + + if (border is IBorderStroke borderStroke) + { + handler.PlatformView.StrokeLineCap = borderStroke.StrokeLineCap; + } + handler.PlatformView.Invalidate(); + } + + public static void MapStrokeLineJoin(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + + if (border is IBorderStroke borderStroke) + { + handler.PlatformView.StrokeLineJoin = borderStroke.StrokeLineJoin; + } + handler.PlatformView.Invalidate(); + } + + public static void MapStrokeMiterLimit(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + + if (border is IBorderStroke borderStroke) + { + handler.PlatformView.StrokeMiterLimit = borderStroke.StrokeMiterLimit; + } + handler.PlatformView.Invalidate(); + } } diff --git a/Views/SkiaBorder.cs b/Views/SkiaBorder.cs index a523828..4281c5e 100644 --- a/Views/SkiaBorder.cs +++ b/Views/SkiaBorder.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Shapes; using Microsoft.Maui.Graphics; using Microsoft.Maui.Platform.Linux.Handlers; using SkiaSharp; @@ -53,6 +55,30 @@ public class SkiaBorder : SkiaLayoutView BindableProperty.Create(nameof(ShadowOffsetY), typeof(double), typeof(SkiaBorder), 2.0, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + public static readonly BindableProperty StrokeShapeProperty = + BindableProperty.Create(nameof(StrokeShape), typeof(IShape), typeof(SkiaBorder), null, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + + public static readonly BindableProperty StrokeDashArrayProperty = + BindableProperty.Create(nameof(StrokeDashArray), typeof(DoubleCollection), typeof(SkiaBorder), null, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + + public static readonly BindableProperty StrokeDashOffsetProperty = + BindableProperty.Create(nameof(StrokeDashOffset), typeof(double), typeof(SkiaBorder), 0.0, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + + public static readonly BindableProperty StrokeLineCapProperty = + BindableProperty.Create(nameof(StrokeLineCap), typeof(LineCap), typeof(SkiaBorder), LineCap.Butt, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + + public static readonly BindableProperty StrokeLineJoinProperty = + BindableProperty.Create(nameof(StrokeLineJoin), typeof(LineJoin), typeof(SkiaBorder), LineJoin.Miter, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + + public static readonly BindableProperty StrokeMiterLimitProperty = + BindableProperty.Create(nameof(StrokeMiterLimit), typeof(double), typeof(SkiaBorder), 10.0, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + #endregion private bool _isPressed; @@ -138,6 +164,60 @@ public class SkiaBorder : SkiaLayoutView set => SetValue(ShadowOffsetYProperty, value); } + /// + /// Gets or sets the shape of the border stroke (Rectangle, RoundRectangle, Ellipse, etc.). + /// + public IShape? StrokeShape + { + get => (IShape?)GetValue(StrokeShapeProperty); + set => SetValue(StrokeShapeProperty, value); + } + + /// + /// Gets or sets the dash pattern for the stroke. + /// + public DoubleCollection? StrokeDashArray + { + get => (DoubleCollection?)GetValue(StrokeDashArrayProperty); + set => SetValue(StrokeDashArrayProperty, value); + } + + /// + /// Gets or sets the offset into the dash pattern. + /// + public double StrokeDashOffset + { + get => (double)GetValue(StrokeDashOffsetProperty); + set => SetValue(StrokeDashOffsetProperty, value); + } + + /// + /// Gets or sets the cap style for the stroke line ends. + /// + public LineCap StrokeLineCap + { + get => (LineCap)GetValue(StrokeLineCapProperty); + set => SetValue(StrokeLineCapProperty, value); + } + + /// + /// Gets or sets the join style for stroke corners. + /// + public LineJoin StrokeLineJoin + { + get => (LineJoin)GetValue(StrokeLineJoinProperty); + set => SetValue(StrokeLineJoinProperty, value); + } + + /// + /// Gets or sets the miter limit for stroke joins. + /// + public double StrokeMiterLimit + { + get => (double)GetValue(StrokeMiterLimitProperty); + set => SetValue(StrokeMiterLimitProperty, value); + } + #endregion #region Events @@ -193,6 +273,78 @@ public class SkiaBorder : SkiaLayoutView #region Drawing + /// + /// Converts LineCap to SKStrokeCap. + /// + private static SKStrokeCap ToSKStrokeCap(LineCap lineCap) + { + return lineCap switch + { + LineCap.Round => SKStrokeCap.Round, + LineCap.Square => SKStrokeCap.Square, + _ => SKStrokeCap.Butt + }; + } + + /// + /// Converts LineJoin to SKStrokeJoin. + /// + private static SKStrokeJoin ToSKStrokeJoin(LineJoin lineJoin) + { + return lineJoin switch + { + LineJoin.Round => SKStrokeJoin.Round, + LineJoin.Bevel => SKStrokeJoin.Bevel, + _ => SKStrokeJoin.Miter + }; + } + + /// + /// Creates an SKPath for the border based on StrokeShape. + /// + private SKPath CreateShapePath(SKRect rect, float defaultCornerRadius) + { + var path = new SKPath(); + + if (StrokeShape is RoundRectangle roundRect) + { + // Use RoundRectangle's corner radii + var cr = roundRect.CornerRadius; + var radii = new SKPoint[] + { + new SKPoint((float)cr.TopLeft, (float)cr.TopLeft), + new SKPoint((float)cr.TopRight, (float)cr.TopRight), + new SKPoint((float)cr.BottomRight, (float)cr.BottomRight), + new SKPoint((float)cr.BottomLeft, (float)cr.BottomLeft) + }; + var skRoundRect = new SKRoundRect(); + skRoundRect.SetRectRadii(rect, radii); + path.AddRoundRect(skRoundRect); + } + else if (StrokeShape is Ellipse) + { + path.AddOval(rect); + } + else if (StrokeShape is Rectangle) + { + path.AddRect(rect); + } + else + { + // Default: use CornerRadius property + if (defaultCornerRadius > 0) + { + path.AddRoundRect(rect, defaultCornerRadius, defaultCornerRadius); + } + else + { + path.AddRect(rect); + } + } + + return path; + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { float strokeThickness = (float)StrokeThickness; @@ -204,6 +356,9 @@ public class SkiaBorder : SkiaLayoutView bounds.Right - strokeThickness / 2f, bounds.Bottom - strokeThickness / 2f); + // Create the shape path + using var shapePath = CreateShapePath(borderRect, cornerRadius); + // Draw shadow if enabled if (HasShadow) { @@ -213,12 +368,10 @@ public class SkiaBorder : SkiaLayoutView MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, (float)ShadowBlurRadius), Style = SKPaintStyle.Fill }; - var shadowRect = new SKRect( - borderRect.Left + (float)ShadowOffsetX, - borderRect.Top + (float)ShadowOffsetY, - borderRect.Right + (float)ShadowOffsetX, - borderRect.Bottom + (float)ShadowOffsetY); - canvas.DrawRoundRect(new SKRoundRect(shadowRect, cornerRadius), shadowPaint); + canvas.Save(); + canvas.Translate((float)ShadowOffsetX, (float)ShadowOffsetY); + canvas.DrawPath(shapePath, shadowPaint); + canvas.Restore(); } // Draw background @@ -228,7 +381,7 @@ public class SkiaBorder : SkiaLayoutView Style = SKPaintStyle.Fill, IsAntialias = true }; - canvas.DrawRoundRect(new SKRoundRect(borderRect, cornerRadius), bgPaint); + canvas.DrawPath(shapePath, bgPaint); // Draw border if (strokeThickness > 0f) @@ -238,9 +391,24 @@ public class SkiaBorder : SkiaLayoutView Color = ToSKColor(Stroke), Style = SKPaintStyle.Stroke, StrokeWidth = strokeThickness, - IsAntialias = true + IsAntialias = true, + StrokeCap = ToSKStrokeCap(StrokeLineCap), + StrokeJoin = ToSKStrokeJoin(StrokeLineJoin), + StrokeMiter = (float)StrokeMiterLimit }; - canvas.DrawRoundRect(new SKRoundRect(borderRect, cornerRadius), borderPaint); + + // Apply dash pattern if specified + if (StrokeDashArray != null && StrokeDashArray.Count > 0) + { + var dashArray = new float[StrokeDashArray.Count]; + for (int i = 0; i < StrokeDashArray.Count; i++) + { + dashArray[i] = (float)(StrokeDashArray[i] * strokeThickness); + } + borderPaint.PathEffect = SKPathEffect.CreateDash(dashArray, (float)(StrokeDashOffset * strokeThickness)); + } + + canvas.DrawPath(shapePath, borderPaint); } // Draw children