|
using System;
using System.ComponentModel;
using CoreAnimation;
using CoreGraphics;
using Microsoft.Maui.Controls.Shapes;
using Shape = Microsoft.Maui.Controls.Shapes.Shape;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls.Platform;
#if __MOBILE__
using ObjCRuntime;
using UIKit;
namespace Microsoft.Maui.Controls.Compatibility.Platform.iOS
#else
using AppKit;
namespace Microsoft.Maui.Controls.Compatibility.Platform.MacOS
#endif
{
[System.Obsolete(Compatibility.Hosting.MauiAppBuilderExtensions.UseMapperInstead)]
public class ShapeRenderer<TShape, TNativeShape> : ViewRenderer<TShape, TNativeShape>
where TShape : Shape
where TNativeShape : ShapeView
{
double _height;
double _width;
protected override void OnElementChanged(ElementChangedEventArgs<TShape> args)
{
base.OnElementChanged(args);
if (args.NewElement != null)
{
UpdateAspect();
UpdateFill();
UpdateStroke();
UpdateStrokeThickness();
UpdateStrokeDashArray();
UpdateStrokeDashOffset();
UpdateStrokeLineCap();
UpdateStrokeLineJoin();
UpdateStrokeMiterLimit();
if (!args.NewElement.Bounds.IsEmpty)
{
_height = Element.Height;
_width = Element.Width;
UpdateSize();
}
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs args)
{
base.OnElementPropertyChanged(sender, args);
if (args.PropertyName == VisualElement.HeightProperty.PropertyName)
{
_height = Element.Height;
UpdateSize();
}
else if (args.PropertyName == VisualElement.WidthProperty.PropertyName)
{
_width = Element.Width;
UpdateSize();
}
else if (args.PropertyName == Shape.AspectProperty.PropertyName)
UpdateAspect();
else if (args.PropertyName == Shape.FillProperty.PropertyName)
UpdateFill();
else if (args.PropertyName == Shape.StrokeProperty.PropertyName)
UpdateStroke();
else if (args.PropertyName == Shape.StrokeThicknessProperty.PropertyName)
UpdateStrokeThickness();
else if (args.PropertyName == Shape.StrokeDashArrayProperty.PropertyName)
UpdateStrokeDashArray();
else if (args.PropertyName == Shape.StrokeDashOffsetProperty.PropertyName)
UpdateStrokeDashOffset();
else if (args.PropertyName == Shape.StrokeLineCapProperty.PropertyName)
UpdateStrokeLineCap();
else if (args.PropertyName == Shape.StrokeLineJoinProperty.PropertyName)
UpdateStrokeLineJoin();
else if (args.PropertyName == Shape.StrokeMiterLimitProperty.PropertyName)
UpdateStrokeMiterLimit();
}
public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint)
{
if (Control != null)
{
return Control.ShapeLayer.GetDesiredSize();
}
return base.GetDesiredSize(widthConstraint, heightConstraint);
}
void UpdateAspect()
{
Control.ShapeLayer.UpdateAspect(Element.Aspect);
}
void UpdateSize()
{
Control.ShapeLayer.UpdateSize(new CGSize(new nfloat(_width), new nfloat(_height)));
}
void UpdateFill()
{
Control.ShapeLayer.UpdateFill(Element.Fill);
}
void UpdateStroke()
{
Control.ShapeLayer.UpdateStroke(Element.Stroke);
}
void UpdateStrokeThickness()
{
Control.ShapeLayer.UpdateStrokeThickness(Element.StrokeThickness);
}
void UpdateStrokeDashArray()
{
if (Element.StrokeDashArray == null || Element.StrokeDashArray.Count == 0)
Control.ShapeLayer.UpdateStrokeDash(Array.Empty<nfloat>());
else
{
nfloat[] dashArray;
double[] array;
if (Element.StrokeDashArray.Count % 2 == 0)
{
array = new double[Element.StrokeDashArray.Count];
dashArray = new nfloat[Element.StrokeDashArray.Count];
Element.StrokeDashArray.CopyTo(array, 0);
}
else
{
array = new double[2 * Element.StrokeDashArray.Count];
dashArray = new nfloat[2 * Element.StrokeDashArray.Count];
Element.StrokeDashArray.CopyTo(array, 0);
Element.StrokeDashArray.CopyTo(array, Element.StrokeDashArray.Count);
}
double thickness = Element.StrokeThickness;
for (int i = 0; i < array.Length; i++)
dashArray[i] = new nfloat(thickness * array[i]);
Control.ShapeLayer.UpdateStrokeDash(dashArray);
}
}
void UpdateStrokeDashOffset()
{
Control.ShapeLayer.UpdateStrokeDashOffset((nfloat)Element.StrokeDashOffset);
}
void UpdateStrokeLineCap()
{
PenLineCap lineCap = Element.StrokeLineCap;
CGLineCap iLineCap = CGLineCap.Butt;
switch (lineCap)
{
case PenLineCap.Flat:
iLineCap = CGLineCap.Butt;
break;
case PenLineCap.Square:
iLineCap = CGLineCap.Square;
break;
case PenLineCap.Round:
iLineCap = CGLineCap.Round;
break;
}
Control.ShapeLayer.UpdateStrokeLineCap(iLineCap);
}
void UpdateStrokeLineJoin()
{
PenLineJoin lineJoin = Element.StrokeLineJoin;
CGLineJoin iLineJoin = CGLineJoin.Miter;
switch (lineJoin)
{
case PenLineJoin.Miter:
iLineJoin = CGLineJoin.Miter;
break;
case PenLineJoin.Bevel:
iLineJoin = CGLineJoin.Bevel;
break;
case PenLineJoin.Round:
iLineJoin = CGLineJoin.Round;
break;
}
Control.ShapeLayer.UpdateStrokeLineJoin(iLineJoin);
}
void UpdateStrokeMiterLimit()
{
Control.ShapeLayer.UpdateStrokeMiterLimit(new nfloat(Element.StrokeMiterLimit));
}
}
public class ShapeView
#if __MOBILE__
: UIView
#else
: NSView
#endif
{
public ShapeView()
{
#if __MOBILE__
BackgroundColor = UIColor.Clear;
#else
WantsLayer = true;
#endif
ShapeLayer = new ShapeLayer();
Layer.AddSublayer(ShapeLayer);
Layer.MasksToBounds = false;
}
public ShapeLayer ShapeLayer
{
private set;
get;
}
#if !__MOBILE__
public override bool IsFlipped => true;
#endif
}
public class ShapeLayer : CALayer
{
CGPath _path;
CGRect _pathFillBounds;
CGRect _pathStrokeBounds;
CGPath _renderPath;
CGRect _renderPathFill;
CGRect _renderPathStroke;
bool _fillMode;
Brush _stroke;
Brush _fill;
nfloat _strokeWidth;
nfloat[] _strokeDash;
nfloat _dashOffset;
Stretch _stretch;
CGLineCap _strokeLineCap;
CGLineJoin _strokeLineJoin;
nfloat _strokeMiterLimit;
public ShapeLayer()
{
#if __MOBILE__
ContentsScale = UIScreen.MainScreen.Scale;
#else
ContentsScale = NSScreen.MainScreen.BackingScaleFactor;
#endif
_fillMode = false;
_stretch = Stretch.None;
_strokeLineCap = CGLineCap.Butt;
_strokeLineJoin = CGLineJoin.Miter;
_strokeMiterLimit = 10;
}
public override void DrawInContext(CGContext ctx)
{
base.DrawInContext(ctx);
RenderShape(ctx);
}
public void UpdateShape(CGPath path)
{
_path = path;
if (_path != null)
_pathFillBounds = _path.PathBoundingBox;
else
_pathFillBounds = new CGRect();
UpdatePathStrokeBounds();
}
public void UpdateFillMode(bool fillMode)
{
_fillMode = fillMode;
SetNeedsDisplay();
}
public SizeRequest GetDesiredSize()
{
return new SizeRequest(new Size(
Math.Max(0, nfloat.IsNaN(_pathStrokeBounds.Right) ? 0 : _pathStrokeBounds.Right),
Math.Max(0, nfloat.IsNaN(_pathStrokeBounds.Bottom) ? 0 : _pathStrokeBounds.Bottom)));
}
public void UpdateSize(CGSize size)
{
Bounds = new CGRect(new CGPoint(), size);
BuildRenderPath();
}
public void UpdateAspect(Stretch stretch)
{
_stretch = stretch;
BuildRenderPath();
}
public void UpdateFill(Brush fill)
{
_fill = fill;
SetNeedsDisplay();
}
public void UpdateStroke(Brush stroke)
{
_stroke = stroke;
SetNeedsDisplay();
}
public void UpdateStrokeThickness(double strokeWidth)
{
_strokeWidth = new nfloat(strokeWidth);
BuildRenderPath();
}
public void UpdateStrokeDash(nfloat[] dash)
{
_strokeDash = dash;
SetNeedsDisplay();
}
public void UpdateStrokeDashOffset(nfloat dashOffset)
{
_dashOffset = dashOffset;
SetNeedsDisplay();
}
public void UpdateStrokeLineCap(CGLineCap strokeLineCap)
{
_strokeLineCap = strokeLineCap;
UpdatePathStrokeBounds();
SetNeedsDisplay();
}
public void UpdateStrokeLineJoin(CGLineJoin strokeLineJoin)
{
_strokeLineJoin = strokeLineJoin;
UpdatePathStrokeBounds();
SetNeedsDisplay();
}
public void UpdateStrokeMiterLimit(nfloat strokeMiterLimit)
{
_strokeMiterLimit = strokeMiterLimit;
UpdatePathStrokeBounds();
SetNeedsDisplay();
}
void BuildRenderPath()
{
if (_path == null)
{
_renderPath = null;
_renderPathFill = new CGRect();
_renderPathStroke = new CGRect();
return;
}
CATransaction.Begin();
CATransaction.DisableActions = true;
if (_stretch != Stretch.None)
{
CGRect viewBounds = Bounds;
viewBounds.X += _strokeWidth / 2;
viewBounds.Y += _strokeWidth / 2;
viewBounds.Width -= _strokeWidth;
viewBounds.Height -= _strokeWidth;
nfloat widthScale = viewBounds.Width / _pathFillBounds.Width;
nfloat heightScale = viewBounds.Height / _pathFillBounds.Height;
var stretchTransform = CGAffineTransform.MakeIdentity();
switch (_stretch)
{
case Stretch.None:
break;
case Stretch.Fill:
stretchTransform.Scale(widthScale, heightScale);
stretchTransform.Translate(
viewBounds.Left - widthScale * _pathFillBounds.Left,
viewBounds.Top - heightScale * _pathFillBounds.Top);
break;
case Stretch.Uniform:
nfloat minScale = NMath.Min(widthScale, heightScale);
stretchTransform.Scale(minScale, minScale);
stretchTransform.Translate(
viewBounds.Left - minScale * _pathFillBounds.Left +
(viewBounds.Width - minScale * _pathFillBounds.Width) / 2,
viewBounds.Top - minScale * _pathFillBounds.Top +
(viewBounds.Height - minScale * _pathFillBounds.Height) / 2);
break;
case Stretch.UniformToFill:
nfloat maxScale = NMath.Max(widthScale, heightScale);
stretchTransform.Scale(maxScale, maxScale);
stretchTransform.Translate(
viewBounds.Left - maxScale * _pathFillBounds.Left,
viewBounds.Top - maxScale * _pathFillBounds.Top);
break;
}
Frame = Bounds;
_renderPath = _path.CopyByTransformingPath(stretchTransform);
}
else
{
nfloat adjustX = NMath.Min(0, _pathStrokeBounds.X);
nfloat adjustY = NMath.Min(0, _pathStrokeBounds.Y);
if (adjustX < 0 || adjustY < 0)
{
nfloat width = Bounds.Width;
nfloat height = Bounds.Height;
if (_pathStrokeBounds.Width > Bounds.Width)
width = Bounds.Width - adjustX;
if (_pathStrokeBounds.Height > Bounds.Height)
height = Bounds.Height - adjustY;
Frame = new CGRect(adjustX, adjustY, width, height);
var transform = new CGAffineTransform(Bounds.Width / width, 0, 0, Bounds.Height / height, -adjustX, -adjustY);
_renderPath = _path.CopyByTransformingPath(transform);
}
else
{
Frame = Bounds;
_renderPath = _path.CopyByTransformingPath(CGAffineTransform.MakeIdentity());
}
}
_renderPathFill = _renderPath.PathBoundingBox;
_renderPathStroke = _renderPath.CopyByStrokingPath(_strokeWidth, _strokeLineCap, _strokeLineJoin, _strokeMiterLimit).PathBoundingBox;
CATransaction.Commit();
SetNeedsDisplay();
}
void RenderShape(CGContext graphics)
{
if (_path == null)
return;
if (_stroke == null && _fill == null)
return;
CATransaction.Begin();
CATransaction.DisableActions = true;
graphics.SetLineWidth(_strokeWidth);
graphics.SetLineDash(_dashOffset * _strokeWidth, _strokeDash);
graphics.SetLineCap(_strokeLineCap);
graphics.SetLineJoin(_strokeLineJoin);
graphics.SetMiterLimit(_strokeMiterLimit * _strokeWidth / 4);
if (_fill is GradientBrush fillGradientBrush)
{
graphics.AddPath(_renderPath);
if (_fillMode)
graphics.Clip();
else
graphics.EOClip();
RenderBrush(graphics, _renderPathFill, fillGradientBrush);
}
else
{
CGColor fillColor =
#if __MOBILE__
UIColor.Clear.CGColor;
#else
NSColor.Clear.CGColor;
#endif
if (_fill is SolidColorBrush solidColorBrush && solidColorBrush.Color != null)
fillColor = solidColorBrush.Color.ToCGColor();
graphics.AddPath(_renderPath);
graphics.SetFillColor(fillColor);
graphics.DrawPath(_fillMode ? CGPathDrawingMode.FillStroke : CGPathDrawingMode.EOFillStroke);
}
if (_stroke is GradientBrush strokeGradientBrush)
{
graphics.AddPath(_renderPath);
graphics.ReplacePathWithStrokedPath();
graphics.Clip();
RenderBrush(graphics, _renderPathStroke, strokeGradientBrush);
}
else
{
CGColor strokeColor =
#if __MOBILE__
UIColor.Clear.CGColor;
#else
NSColor.Clear.CGColor;
#endif
if (_stroke is SolidColorBrush solidColorBrush && solidColorBrush.Color != null)
strokeColor = solidColorBrush.Color.ToCGColor();
graphics.AddPath(_renderPath);
graphics.SetStrokeColor(strokeColor);
graphics.DrawPath(CGPathDrawingMode.Stroke);
}
CATransaction.Commit();
}
void RenderBrush(CGContext graphics, CGRect pathBounds, GradientBrush brush)
{
if (brush == null)
return;
using (CGColorSpace rgb = CGColorSpace.CreateDeviceRGB())
{
CGColor[] colors = new CGColor[brush.GradientStops.Count];
nfloat[] locations = new nfloat[brush.GradientStops.Count];
for (int index = 0; index < brush.GradientStops.Count; index++)
{
Color color = brush.GradientStops[index].Color;
colors[index] = new CGColor(new nfloat(color.Red), new nfloat(color.Green), new nfloat(color.Blue), new nfloat(color.Alpha));
locations[index] = new nfloat(brush.GradientStops[index].Offset);
}
CGGradient gradient = new CGGradient(rgb, colors, locations);
if (brush is LinearGradientBrush linearGradientBrush)
{
graphics.DrawLinearGradient(
gradient,
new CGPoint(pathBounds.Left + linearGradientBrush.StartPoint.X * pathBounds.Width, pathBounds.Top + linearGradientBrush.StartPoint.Y * pathBounds.Height),
new CGPoint(pathBounds.Left + linearGradientBrush.EndPoint.X * pathBounds.Width, pathBounds.Top + linearGradientBrush.EndPoint.Y * pathBounds.Height),
CGGradientDrawingOptions.DrawsBeforeStartLocation | CGGradientDrawingOptions.DrawsAfterEndLocation);
}
if (brush is RadialGradientBrush radialGradientBrush)
{
graphics.DrawRadialGradient(
gradient,
new CGPoint(radialGradientBrush.Center.X * pathBounds.Width + pathBounds.Left, radialGradientBrush.Center.Y * pathBounds.Height + pathBounds.Top),
0.0f,
new CGPoint(radialGradientBrush.Center.X * pathBounds.Width + pathBounds.Left, radialGradientBrush.Center.Y * pathBounds.Height + pathBounds.Top),
(nfloat)(radialGradientBrush.Radius * Math.Max(pathBounds.Height, pathBounds.Width)),
CGGradientDrawingOptions.DrawsBeforeStartLocation | CGGradientDrawingOptions.DrawsAfterEndLocation);
}
}
}
void UpdatePathStrokeBounds()
{
if (_path != null)
_pathStrokeBounds = _path.CopyByStrokingPath(_strokeWidth, _strokeLineCap, _strokeLineJoin, _strokeMiterLimit).PathBoundingBox;
else
_pathStrokeBounds = new CGRect();
BuildRenderPath();
}
}
} |