Update SkiaView.cs

This commit is contained in:
2026-01-16 03:57:31 +00:00
parent aab53ee919
commit a2800464c8

View File

@@ -2,6 +2,7 @@
// 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 Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Shapes;
using Microsoft.Maui.Platform.Linux; using Microsoft.Maui.Platform.Linux;
using Microsoft.Maui.Platform.Linux.Handlers; using Microsoft.Maui.Platform.Linux.Handlers;
using Microsoft.Maui.Platform.Linux.Rendering; using Microsoft.Maui.Platform.Linux.Rendering;
@@ -333,6 +334,128 @@ public abstract class SkiaView : BindableObject, IDisposable
0.5, 0.5,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for InputTransparent.
/// When true, the view does not receive input events and they pass through to views below.
/// </summary>
public static readonly BindableProperty InputTransparentProperty =
BindableProperty.Create(
nameof(InputTransparent),
typeof(bool),
typeof(SkiaView),
false);
/// <summary>
/// Bindable property for FlowDirection.
/// Controls the layout direction for RTL language support.
/// </summary>
public static readonly BindableProperty FlowDirectionProperty =
BindableProperty.Create(
nameof(FlowDirection),
typeof(FlowDirection),
typeof(SkiaView),
FlowDirection.MatchParent,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary>
/// Bindable property for ZIndex.
/// Controls the rendering order within a layout.
/// </summary>
public static readonly BindableProperty ZIndexProperty =
BindableProperty.Create(
nameof(ZIndex),
typeof(int),
typeof(SkiaView),
0,
propertyChanged: (b, o, n) => ((SkiaView)b).Parent?.Invalidate());
/// <summary>
/// Bindable property for MaximumWidthRequest.
/// </summary>
public static readonly BindableProperty MaximumWidthRequestProperty =
BindableProperty.Create(
nameof(MaximumWidthRequest),
typeof(double),
typeof(SkiaView),
double.PositiveInfinity,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary>
/// Bindable property for MaximumHeightRequest.
/// </summary>
public static readonly BindableProperty MaximumHeightRequestProperty =
BindableProperty.Create(
nameof(MaximumHeightRequest),
typeof(double),
typeof(SkiaView),
double.PositiveInfinity,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary>
/// Bindable property for AutomationId.
/// Used for UI testing and accessibility.
/// </summary>
public static readonly BindableProperty AutomationIdProperty =
BindableProperty.Create(
nameof(AutomationId),
typeof(string),
typeof(SkiaView),
string.Empty);
/// <summary>
/// Bindable property for Padding.
/// </summary>
public static readonly BindableProperty PaddingProperty =
BindableProperty.Create(
nameof(Padding),
typeof(Thickness),
typeof(SkiaView),
default(Thickness),
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary>
/// Bindable property for Background (Brush).
/// </summary>
public static readonly BindableProperty BackgroundProperty =
BindableProperty.Create(
nameof(Background),
typeof(Brush),
typeof(SkiaView),
null,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for Clip geometry.
/// </summary>
public static readonly BindableProperty ClipProperty =
BindableProperty.Create(
nameof(Clip),
typeof(Geometry),
typeof(SkiaView),
null,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for Shadow.
/// </summary>
public static readonly BindableProperty ShadowProperty =
BindableProperty.Create(
nameof(Shadow),
typeof(Shadow),
typeof(SkiaView),
null,
propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate());
/// <summary>
/// Bindable property for Visual.
/// </summary>
public static readonly BindableProperty VisualProperty =
BindableProperty.Create(
nameof(Visual),
typeof(IVisual),
typeof(SkiaView),
VisualMarker.Default);
#endregion #endregion
private bool _disposed; private bool _disposed;
@@ -613,6 +736,107 @@ public abstract class SkiaView : BindableObject, IDisposable
set => SetValue(AnchorYProperty, value); set => SetValue(AnchorYProperty, value);
} }
/// <summary>
/// Gets or sets whether this view is transparent to input.
/// When true, input events pass through to views below.
/// </summary>
public bool InputTransparent
{
get => (bool)GetValue(InputTransparentProperty);
set => SetValue(InputTransparentProperty, value);
}
/// <summary>
/// Gets or sets the flow direction for RTL support.
/// </summary>
public FlowDirection FlowDirection
{
get => (FlowDirection)GetValue(FlowDirectionProperty);
set => SetValue(FlowDirectionProperty, value);
}
/// <summary>
/// Gets or sets the Z-index for rendering order.
/// Higher values render on top of lower values.
/// </summary>
public int ZIndex
{
get => (int)GetValue(ZIndexProperty);
set => SetValue(ZIndexProperty, value);
}
/// <summary>
/// Gets or sets the maximum width request.
/// </summary>
public double MaximumWidthRequest
{
get => (double)GetValue(MaximumWidthRequestProperty);
set => SetValue(MaximumWidthRequestProperty, value);
}
/// <summary>
/// Gets or sets the maximum height request.
/// </summary>
public double MaximumHeightRequest
{
get => (double)GetValue(MaximumHeightRequestProperty);
set => SetValue(MaximumHeightRequestProperty, value);
}
/// <summary>
/// Gets or sets the automation ID for UI testing.
/// </summary>
public string AutomationId
{
get => (string)GetValue(AutomationIdProperty);
set => SetValue(AutomationIdProperty, value);
}
/// <summary>
/// Gets or sets the padding inside the view.
/// </summary>
public Thickness Padding
{
get => (Thickness)GetValue(PaddingProperty);
set => SetValue(PaddingProperty, value);
}
/// <summary>
/// Gets or sets the background brush.
/// </summary>
public Brush? Background
{
get => (Brush?)GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
/// <summary>
/// Gets or sets the clip geometry.
/// </summary>
public Geometry? Clip
{
get => (Geometry?)GetValue(ClipProperty);
set => SetValue(ClipProperty, value);
}
/// <summary>
/// Gets or sets the shadow.
/// </summary>
public Shadow? Shadow
{
get => (Shadow?)GetValue(ShadowProperty);
set => SetValue(ShadowProperty, value);
}
/// <summary>
/// Gets or sets the visual style.
/// </summary>
public IVisual Visual
{
get => (IVisual)GetValue(VisualProperty);
set => SetValue(VisualProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the cursor type when hovering over this view. /// Gets or sets the cursor type when hovering over this view.
/// </summary> /// </summary>
@@ -865,13 +1089,21 @@ public abstract class SkiaView : BindableObject, IDisposable
canvas.SaveLayer(new SKPaint { Color = SKColors.White.WithAlpha((byte)(Opacity * 255)) }); canvas.SaveLayer(new SKPaint { Color = SKColors.White.WithAlpha((byte)(Opacity * 255)) });
} }
// Draw background at absolute bounds // Draw shadow if set
if (BackgroundColor != SKColors.Transparent) if (Shadow != null)
{ {
using var paint = new SKPaint { Color = BackgroundColor }; DrawShadow(canvas, Bounds);
canvas.DrawRect(Bounds, paint);
} }
// Apply clip geometry if set
if (Clip != null)
{
ApplyClip(canvas, Bounds);
}
// Draw background at absolute bounds
DrawBackground(canvas, Bounds);
// Draw content at absolute bounds // Draw content at absolute bounds
OnDraw(canvas, Bounds); OnDraw(canvas, Bounds);
@@ -896,6 +1128,166 @@ public abstract class SkiaView : BindableObject, IDisposable
{ {
} }
/// <summary>
/// Draws the shadow for this view.
/// </summary>
protected virtual void DrawShadow(SKCanvas canvas, SKRect bounds)
{
if (Shadow == null) return;
var shadowColor = Shadow.Brush is SolidColorBrush scb
? new SKColor(
(byte)(scb.Color.Red * 255),
(byte)(scb.Color.Green * 255),
(byte)(scb.Color.Blue * 255),
(byte)(scb.Color.Alpha * 255 * Shadow.Opacity))
: new SKColor(0, 0, 0, (byte)(255 * Shadow.Opacity));
using var shadowPaint = new SKPaint
{
Color = shadowColor,
IsAntialias = true,
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, (float)Shadow.Radius / 2)
};
var shadowBounds = new SKRect(
bounds.Left + (float)Shadow.Offset.X,
bounds.Top + (float)Shadow.Offset.Y,
bounds.Right + (float)Shadow.Offset.X,
bounds.Bottom + (float)Shadow.Offset.Y);
canvas.DrawRect(shadowBounds, shadowPaint);
}
/// <summary>
/// Applies the clip geometry to the canvas.
/// </summary>
protected virtual void ApplyClip(SKCanvas canvas, SKRect bounds)
{
if (Clip == null) return;
// Convert MAUI Geometry to SkiaSharp path
var path = ConvertGeometryToPath(Clip, bounds);
if (path != null)
{
canvas.ClipPath(path);
}
}
/// <summary>
/// Converts a MAUI Geometry to a SkiaSharp path.
/// </summary>
private SKPath? ConvertGeometryToPath(Geometry geometry, SKRect bounds)
{
var path = new SKPath();
if (geometry is RectangleGeometry rect)
{
var r = rect.Rect;
path.AddRect(new SKRect(
bounds.Left + (float)r.Left,
bounds.Top + (float)r.Top,
bounds.Left + (float)r.Right,
bounds.Top + (float)r.Bottom));
}
else if (geometry is EllipseGeometry ellipse)
{
path.AddOval(new SKRect(
bounds.Left + (float)(ellipse.Center.X - ellipse.RadiusX),
bounds.Top + (float)(ellipse.Center.Y - ellipse.RadiusY),
bounds.Left + (float)(ellipse.Center.X + ellipse.RadiusX),
bounds.Top + (float)(ellipse.Center.Y + ellipse.RadiusY)));
}
else if (geometry is RoundRectangleGeometry roundRect)
{
var r = roundRect.Rect;
var cr = roundRect.CornerRadius;
var skRect = new SKRect(
bounds.Left + (float)r.Left,
bounds.Top + (float)r.Top,
bounds.Left + (float)r.Right,
bounds.Top + (float)r.Bottom);
var skRoundRect = new SKRoundRect();
skRoundRect.SetRectRadii(skRect, new[]
{
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)
});
path.AddRoundRect(skRoundRect);
}
// Add more geometry types as needed
return path;
}
/// <summary>
/// Draws the background (color or brush) for this view.
/// </summary>
protected virtual void DrawBackground(SKCanvas canvas, SKRect bounds)
{
// First try to use Background brush
if (Background != null)
{
using var paint = new SKPaint { IsAntialias = true };
if (Background is SolidColorBrush scb)
{
paint.Color = new SKColor(
(byte)(scb.Color.Red * 255),
(byte)(scb.Color.Green * 255),
(byte)(scb.Color.Blue * 255),
(byte)(scb.Color.Alpha * 255));
canvas.DrawRect(bounds, paint);
}
else if (Background is LinearGradientBrush lgb)
{
var start = new SKPoint(
bounds.Left + (float)(lgb.StartPoint.X * bounds.Width),
bounds.Top + (float)(lgb.StartPoint.Y * bounds.Height));
var end = new SKPoint(
bounds.Left + (float)(lgb.EndPoint.X * bounds.Width),
bounds.Top + (float)(lgb.EndPoint.Y * bounds.Height));
var colors = lgb.GradientStops.Select(s =>
new SKColor(
(byte)(s.Color.Red * 255),
(byte)(s.Color.Green * 255),
(byte)(s.Color.Blue * 255),
(byte)(s.Color.Alpha * 255))).ToArray();
var positions = lgb.GradientStops.Select(s => s.Offset).ToArray();
paint.Shader = SKShader.CreateLinearGradient(start, end, colors, positions, SKShaderTileMode.Clamp);
canvas.DrawRect(bounds, paint);
}
else if (Background is RadialGradientBrush rgb)
{
var center = new SKPoint(
bounds.Left + (float)(rgb.Center.X * bounds.Width),
bounds.Top + (float)(rgb.Center.Y * bounds.Height));
var radius = (float)(rgb.Radius * Math.Max(bounds.Width, bounds.Height));
var colors = rgb.GradientStops.Select(s =>
new SKColor(
(byte)(s.Color.Red * 255),
(byte)(s.Color.Green * 255),
(byte)(s.Color.Blue * 255),
(byte)(s.Color.Alpha * 255))).ToArray();
var positions = rgb.GradientStops.Select(s => s.Offset).ToArray();
paint.Shader = SKShader.CreateRadialGradient(center, radius, colors, positions, SKShaderTileMode.Clamp);
canvas.DrawRect(bounds, paint);
}
}
// Fall back to BackgroundColor
else if (BackgroundColor != SKColors.Transparent)
{
using var paint = new SKPaint { Color = BackgroundColor };
canvas.DrawRect(bounds, paint);
}
}
/// <summary> /// <summary>
/// Called when the bounds change. /// Called when the bounds change.
/// </summary> /// </summary>
@@ -968,6 +1360,10 @@ public abstract class SkiaView : BindableObject, IDisposable
return hit; return hit;
} }
// If InputTransparent, don't capture input - let it pass through
if (InputTransparent)
return null;
return this; return this;
} }