Border completed
This commit is contained in:
@@ -21,6 +21,11 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
|||||||
[nameof(IBorderView.Content)] = MapContent,
|
[nameof(IBorderView.Content)] = MapContent,
|
||||||
[nameof(IBorderStroke.Stroke)] = MapStroke,
|
[nameof(IBorderStroke.Stroke)] = MapStroke,
|
||||||
[nameof(IBorderStroke.StrokeThickness)] = MapStrokeThickness,
|
[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
|
["StrokeShape"] = MapStrokeShape, // StrokeShape is on Border, not IBorderStroke
|
||||||
[nameof(IView.Background)] = MapBackground,
|
[nameof(IView.Background)] = MapBackground,
|
||||||
["BackgroundColor"] = MapBackgroundColor,
|
["BackgroundColor"] = MapBackgroundColor,
|
||||||
@@ -151,10 +156,13 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
|||||||
if (border is not Border borderControl) return;
|
if (border is not Border borderControl) return;
|
||||||
|
|
||||||
var shape = borderControl.StrokeShape;
|
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)
|
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;
|
var cornerRadius = roundRect.CornerRadius;
|
||||||
handler.PlatformView.CornerRadius = cornerRadius.TopLeft;
|
handler.PlatformView.CornerRadius = cornerRadius.TopLeft;
|
||||||
}
|
}
|
||||||
@@ -164,11 +172,71 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
|
|||||||
}
|
}
|
||||||
else if (shape is Microsoft.Maui.Controls.Shapes.Ellipse)
|
else if (shape is Microsoft.Maui.Controls.Shapes.Ellipse)
|
||||||
{
|
{
|
||||||
// For ellipse, use half the min dimension as corner radius
|
handler.PlatformView.CornerRadius = double.MaxValue;
|
||||||
// This will be applied during rendering when bounds are known
|
|
||||||
handler.PlatformView.CornerRadius = double.MaxValue; // Marker for "fully rounded"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.PlatformView.Invalidate();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
// The .NET Foundation licenses this file to you under the MIT license.
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
|
using Microsoft.Maui.Controls.Shapes;
|
||||||
using Microsoft.Maui.Graphics;
|
using Microsoft.Maui.Graphics;
|
||||||
using Microsoft.Maui.Platform.Linux.Handlers;
|
using Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
@@ -53,6 +55,30 @@ public class SkiaBorder : SkiaLayoutView
|
|||||||
BindableProperty.Create(nameof(ShadowOffsetY), typeof(double), typeof(SkiaBorder), 2.0,
|
BindableProperty.Create(nameof(ShadowOffsetY), typeof(double), typeof(SkiaBorder), 2.0,
|
||||||
BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate());
|
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
|
#endregion
|
||||||
|
|
||||||
private bool _isPressed;
|
private bool _isPressed;
|
||||||
@@ -138,6 +164,60 @@ public class SkiaBorder : SkiaLayoutView
|
|||||||
set => SetValue(ShadowOffsetYProperty, value);
|
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
|
#endregion
|
||||||
|
|
||||||
#region Events
|
#region Events
|
||||||
@@ -193,6 +273,78 @@ public class SkiaBorder : SkiaLayoutView
|
|||||||
|
|
||||||
#region Drawing
|
#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)
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
||||||
{
|
{
|
||||||
float strokeThickness = (float)StrokeThickness;
|
float strokeThickness = (float)StrokeThickness;
|
||||||
@@ -204,6 +356,9 @@ public class SkiaBorder : SkiaLayoutView
|
|||||||
bounds.Right - strokeThickness / 2f,
|
bounds.Right - strokeThickness / 2f,
|
||||||
bounds.Bottom - strokeThickness / 2f);
|
bounds.Bottom - strokeThickness / 2f);
|
||||||
|
|
||||||
|
// Create the shape path
|
||||||
|
using var shapePath = CreateShapePath(borderRect, cornerRadius);
|
||||||
|
|
||||||
// Draw shadow if enabled
|
// Draw shadow if enabled
|
||||||
if (HasShadow)
|
if (HasShadow)
|
||||||
{
|
{
|
||||||
@@ -213,12 +368,10 @@ public class SkiaBorder : SkiaLayoutView
|
|||||||
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, (float)ShadowBlurRadius),
|
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, (float)ShadowBlurRadius),
|
||||||
Style = SKPaintStyle.Fill
|
Style = SKPaintStyle.Fill
|
||||||
};
|
};
|
||||||
var shadowRect = new SKRect(
|
canvas.Save();
|
||||||
borderRect.Left + (float)ShadowOffsetX,
|
canvas.Translate((float)ShadowOffsetX, (float)ShadowOffsetY);
|
||||||
borderRect.Top + (float)ShadowOffsetY,
|
canvas.DrawPath(shapePath, shadowPaint);
|
||||||
borderRect.Right + (float)ShadowOffsetX,
|
canvas.Restore();
|
||||||
borderRect.Bottom + (float)ShadowOffsetY);
|
|
||||||
canvas.DrawRoundRect(new SKRoundRect(shadowRect, cornerRadius), shadowPaint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw background
|
// Draw background
|
||||||
@@ -228,7 +381,7 @@ public class SkiaBorder : SkiaLayoutView
|
|||||||
Style = SKPaintStyle.Fill,
|
Style = SKPaintStyle.Fill,
|
||||||
IsAntialias = true
|
IsAntialias = true
|
||||||
};
|
};
|
||||||
canvas.DrawRoundRect(new SKRoundRect(borderRect, cornerRadius), bgPaint);
|
canvas.DrawPath(shapePath, bgPaint);
|
||||||
|
|
||||||
// Draw border
|
// Draw border
|
||||||
if (strokeThickness > 0f)
|
if (strokeThickness > 0f)
|
||||||
@@ -238,9 +391,24 @@ public class SkiaBorder : SkiaLayoutView
|
|||||||
Color = ToSKColor(Stroke),
|
Color = ToSKColor(Stroke),
|
||||||
Style = SKPaintStyle.Stroke,
|
Style = SKPaintStyle.Stroke,
|
||||||
StrokeWidth = strokeThickness,
|
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
|
// Draw children
|
||||||
|
|||||||
Reference in New Issue
Block a user