Border completed

This commit is contained in:
2026-01-16 05:49:20 +00:00
parent 331d6839d9
commit 8b1c733943
2 changed files with 250 additions and 14 deletions

View File

@@ -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);
}
/// <summary>
/// Gets or sets the shape of the border stroke (Rectangle, RoundRectangle, Ellipse, etc.).
/// </summary>
public IShape? StrokeShape
{
get => (IShape?)GetValue(StrokeShapeProperty);
set => SetValue(StrokeShapeProperty, value);
}
/// <summary>
/// Gets or sets the dash pattern for the stroke.
/// </summary>
public DoubleCollection? StrokeDashArray
{
get => (DoubleCollection?)GetValue(StrokeDashArrayProperty);
set => SetValue(StrokeDashArrayProperty, value);
}
/// <summary>
/// Gets or sets the offset into the dash pattern.
/// </summary>
public double StrokeDashOffset
{
get => (double)GetValue(StrokeDashOffsetProperty);
set => SetValue(StrokeDashOffsetProperty, value);
}
/// <summary>
/// Gets or sets the cap style for the stroke line ends.
/// </summary>
public LineCap StrokeLineCap
{
get => (LineCap)GetValue(StrokeLineCapProperty);
set => SetValue(StrokeLineCapProperty, value);
}
/// <summary>
/// Gets or sets the join style for stroke corners.
/// </summary>
public LineJoin StrokeLineJoin
{
get => (LineJoin)GetValue(StrokeLineJoinProperty);
set => SetValue(StrokeLineJoinProperty, value);
}
/// <summary>
/// Gets or sets the miter limit for stroke joins.
/// </summary>
public double StrokeMiterLimit
{
get => (double)GetValue(StrokeMiterLimitProperty);
set => SetValue(StrokeMiterLimitProperty, value);
}
#endregion
#region Events
@@ -193,6 +273,78 @@ public class SkiaBorder : SkiaLayoutView
#region Drawing
/// <summary>
/// Converts LineCap to SKStrokeCap.
/// </summary>
private static SKStrokeCap ToSKStrokeCap(LineCap lineCap)
{
return lineCap switch
{
LineCap.Round => SKStrokeCap.Round,
LineCap.Square => SKStrokeCap.Square,
_ => SKStrokeCap.Butt
};
}
/// <summary>
/// Converts LineJoin to SKStrokeJoin.
/// </summary>
private static SKStrokeJoin ToSKStrokeJoin(LineJoin lineJoin)
{
return lineJoin switch
{
LineJoin.Round => SKStrokeJoin.Round,
LineJoin.Bevel => SKStrokeJoin.Bevel,
_ => SKStrokeJoin.Miter
};
}
/// <summary>
/// Creates an SKPath for the border based on StrokeShape.
/// </summary>
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