protected void SetStrokeDashEffect(Paint strokePaint) { if (StrokeDashArray != null && StrokeDashArray.Count > 0) { // If only value specified in the dash array, copy and add it if (StrokeDashArray.Count == 1) { StrokeDashArray.Add(StrokeDashArray[0]); } // Make sure the dash array has a postive number of items, Android cannot have an odd number // of items in the array (in such a case we skip the dash effect and log the error) // https://developer.android.com/reference/android/graphics/DashPathEffect.html // ** The intervals array must contain an even number of entries (>=2), with // the even indices specifying the "on" intervals, and the odd indices // specifying the "off" intervals. ** if (StrokeDashArray.Count % 2 == 0) { var pattern = StrokeDashArray.Select(d => (float)d).ToArray(); strokePaint.SetPathEffect(new DashPathEffect(pattern, 0)); } else { this.Log().ErrorIfEnabled(() => "StrokeDashArray containing an odd number of values is not supported on Android."); } } }
private CALayer CreateLayer(CGPath path) { var pathLayer = new CAShapeLayer() { Path = path, StrokeColor = (Stroke as SolidColorBrush)?.ColorWithOpacity ?? Colors.Transparent, LineWidth = (nfloat)ActualStrokeThickness, }; switch (Fill) { case SolidColorBrush colorFill: pathLayer.FillColor = colorFill.ColorWithOpacity; break; case ImageBrush imageFill when TryCreateImageBrushLayers(imageFill, GetFillMask(path), out var imageLayer): pathLayer.FillColor = Colors.Transparent; pathLayer.AddSublayer(imageLayer); break; case LinearGradientBrush gradientFill: var gradientLayer = gradientFill.GetLayer(Frame.Size); gradientLayer.Frame = Bounds; gradientLayer.Mask = GetFillMask(path); gradientLayer.MasksToBounds = true; pathLayer.FillColor = Colors.Transparent; pathLayer.AddSublayer(gradientLayer); break; case null: pathLayer.FillColor = Colors.Transparent; break; default: Application.Current.RaiseRecoverableUnhandledException(new NotSupportedException($"The brush {Fill} is not supported as Fill for a {this} on this platform.")); pathLayer.FillColor = Colors.Transparent; break; } if (StrokeDashArray != null) { var pattern = StrokeDashArray.Select(d => (global::Foundation.NSNumber)d).ToArray(); pathLayer.LineDashPhase = 0; // Starting position of the pattern pathLayer.LineDashPattern = pattern; } return(pathLayer); CAShapeLayer GetFillMask(CGPath mask) => new CAShapeLayer { Path = mask, Frame = Bounds, // We only use the fill color to create the mask area FillColor = _Color.White.CGColor, }; }
partial void OnStrokeDashArrayUpdatedPartial() { var svgElement = GetMainSvgElement(); if (StrokeDashArray == null) { svgElement.ResetStyle("stroke-dasharray"); } else { var str = string.Join(",", StrokeDashArray.Select(d => $"{d}px")); svgElement.SetStyle("stroke-dasharray", str); } }
protected override void AssignDefaultValuesToSceneNode(SceneNode2D node) { base.AssignDefaultValuesToSceneNode(node); var c = node as ShapeNode2D; c.StrokeDashArray = StrokeDashArray == null ? new float[0] : StrokeDashArray.Select(x => (float)x).ToArray(); c.StrokeDashCap = StrokeDashCap.ToD2DCapStyle(); c.StrokeDashOffset = (float)StrokeDashOffset; c.StrokeEndLineCap = StrokeEndLineCap.ToD2DCapStyle(); c.StrokeLineJoin = StrokeLineJoin.ToD2DLineJoin(); c.StrokeMiterLimit = (float)StrokeMiterLimit; c.StrokeStartLineCap = StrokeStartLineCap.ToD2DCapStyle(); c.StrokeThickness = (float)StrokeThickness; c.StrokeDashStyle = DashStyle.ToD2DDashStyle(); }
private CALayer CreateLayer() { var path = this.GetPath(); if (path == null) { return(null); } var pathBounds = path.PathBoundingBox; if ( nfloat.IsInfinity(pathBounds.Left) || nfloat.IsInfinity(pathBounds.Left) ) { if (this.Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) { this.Log().Debug($"Ignoring path with invalid bounds {pathBounds}"); } return(null); } var transform = CGAffineTransform.MakeIdentity(); var scaleX = _scaleX; var scaleY = _scaleY; switch (this.Stretch) { case Stretch.Fill: case Stretch.None: break; case Stretch.Uniform: scaleX = (nfloat)Math.Min(_scaleX, _scaleY); scaleY = scaleX; break; case Stretch.UniformToFill: scaleX = (nfloat)Math.Max(_scaleX, _scaleY); scaleY = scaleX; break; } transform = CGAffineTransform.MakeScale(scaleX, scaleY); if (Stretch != Stretch.None) { // When stretching, we can't use 0,0 as the origin, but must instead // use the path's bounds. transform.Translate(-pathBounds.Left * scaleX, -pathBounds.Top * scaleY); } if (!ShouldPreserveOrigin) { //We need to translate the shape to take in account the stroke thickness transform.Translate((nfloat)ActualStrokeThickness * 0.5f, (nfloat)ActualStrokeThickness * 0.5f); } if (nfloat.IsNaN(transform.x0) || nfloat.IsNaN(transform.y0) || nfloat.IsNaN(transform.xx) || nfloat.IsNaN(transform.yy) || nfloat.IsNaN(transform.xy) || nfloat.IsNaN(transform.yx) ) { //transformedPath creation will crash natively if the transform contains NaNs throw new InvalidOperationException($"transform {transform} contains NaN values, transformation will fail."); } var colorFill = Fill as SolidColorBrush ?? SolidColorBrushHelper.Transparent; var imageFill = Fill as ImageBrush; var gradientFill = Fill as LinearGradientBrush; var stroke = this.Stroke as SolidColorBrush ?? SolidColorBrushHelper.Transparent; var transformedPath = new CGPath(path, transform); var layer = new CAShapeLayer() { Path = transformedPath, StrokeColor = stroke.ColorWithOpacity, LineWidth = (nfloat)ActualStrokeThickness, }; if (colorFill != null) { layer.FillColor = colorFill.ColorWithOpacity; } if (imageFill != null) { var fillMask = new CAShapeLayer() { Path = path, Frame = Bounds, // We only use the fill color to create the mask area FillColor = _Color.White.CGColor, }; CreateImageBrushLayers( layer, imageFill, fillMask ); } else if (gradientFill != null) { var fillMask = new CAShapeLayer() { Path = transformedPath, Frame = Bounds, // We only use the fill color to create the mask area FillColor = _Color.White.CGColor, }; var gradientLayer = gradientFill.GetLayer(Frame.Size); gradientLayer.Frame = Bounds; gradientLayer.Mask = fillMask; gradientLayer.MasksToBounds = true; layer.AddSublayer(gradientLayer); } if (StrokeDashArray != null) { var pattern = StrokeDashArray.Select(d => (global::Foundation.NSNumber)d).ToArray(); layer.LineDashPhase = 0; // Starting position of the pattern layer.LineDashPattern = pattern; } return(layer); }
/// <summary> /// Renders the stroke of the <see cref="SvgVisualElement"/> to the specified <see cref="ISvgRenderer"/> /// </summary> /// <param name="renderer">The <see cref="ISvgRenderer"/> object to render to.</param> protected internal virtual bool RenderStroke(ISvgRenderer renderer) { if (Stroke != null && Stroke != SvgPaintServer.None && StrokeWidth > 0f) { var strokeWidth = StrokeWidth.ToDeviceValue(renderer, UnitRenderingType.Other, this); using (var brush = Stroke.GetBrush(this, renderer, Math.Min(Math.Max(StrokeOpacity, 0f), 1f), true)) { if (brush != null) { var path = Path(renderer); var bounds = path.GetBounds(); if (path.PointCount < 1) { return(false); } if (bounds.Width <= 0f && bounds.Height <= 0f) { switch (StrokeLineCap) { case SvgStrokeLineCap.Round: using (var capPath = new GraphicsPath()) { capPath.AddEllipse(path.PathPoints[0].X - strokeWidth / 2f, path.PathPoints[0].Y - strokeWidth / 2f, strokeWidth, strokeWidth); renderer.FillPath(brush, capPath); } break; case SvgStrokeLineCap.Square: using (var capPath = new GraphicsPath()) { capPath.AddRectangle(new RectangleF(path.PathPoints[0].X - strokeWidth / 2f, path.PathPoints[0].Y - strokeWidth / 2f, strokeWidth, strokeWidth)); renderer.FillPath(brush, capPath); } break; } } else { using (var pen = new Pen(brush, strokeWidth)) { if (StrokeDashArray != null && StrokeDashArray.Count > 0) { strokeWidth = strokeWidth <= 0 ? 1f : strokeWidth; if (StrokeDashArray.Count % 2 != 0) { // handle odd dash arrays by repeating them once StrokeDashArray.AddRange(StrokeDashArray); } var dashOffset = StrokeDashOffset; /* divide by stroke width - GDI uses stroke width as unit.*/ var dashPattern = StrokeDashArray.Select(u => ((u.ToDeviceValue(renderer, UnitRenderingType.Other, this) <= 0f) ? 1f : u.ToDeviceValue(renderer, UnitRenderingType.Other, this)) / strokeWidth).ToArray(); var length = dashPattern.Length; if (StrokeLineCap == SvgStrokeLineCap.Round) { // to handle round caps, we have to adapt the dash pattern // by increasing the dash length by the stroke width - GDI draws the rounded // edge inside the dash line, SVG draws it outside the line var pattern = new float[length]; var offset = 1; // the values are already normalized to dash width for (var i = 0; i < length; i++) { pattern[i] = dashPattern[i] + offset; if (pattern[i] <= 0f) { // overlapping caps - remove the gap for simplicity, see #508 if (i < length - 1) { // add the next dash segment to the current one dashPattern[i - 1] += dashPattern[i] + dashPattern[i + 1]; length -= 2; for (var k = i; k < length; k++) { dashPattern[k] = dashPattern[k + 2]; } // and handle the combined segment again i -= 2; } else if (i > 2) { // add the last dash segment to the first one // this will change the start point, so adapt the offset var dashLength = dashPattern[i - 1] + dashPattern[i]; pattern[0] += dashLength; length -= 2; dashOffset += dashLength * strokeWidth; } else { // we have only one dash with the gap too small - // do not use dash at all length = 0; break; } } offset *= -1; // increase dash length, decrease spaces } if (length > 0) { if (length < dashPattern.Length) { Array.Resize(ref pattern, length); } dashPattern = pattern; pen.DashCap = DashCap.Round; } } if (length > 0) { pen.DashPattern = dashPattern; if (dashOffset != 0f) { pen.DashOffset = ((dashOffset.ToDeviceValue(renderer, UnitRenderingType.Other, this) <= 0f) ? 1f : dashOffset.ToDeviceValue(renderer, UnitRenderingType.Other, this)) / strokeWidth; } } } switch (StrokeLineJoin) { case SvgStrokeLineJoin.Bevel: pen.LineJoin = LineJoin.Bevel; break; case SvgStrokeLineJoin.Round: pen.LineJoin = LineJoin.Round; break; default: pen.LineJoin = LineJoin.Miter; break; } pen.MiterLimit = StrokeMiterLimit; switch (StrokeLineCap) { case SvgStrokeLineCap.Round: pen.StartCap = LineCap.Round; pen.EndCap = LineCap.Round; break; case SvgStrokeLineCap.Square: pen.StartCap = LineCap.Square; pen.EndCap = LineCap.Square; break; } renderer.DrawPath(pen, path); return(true); } } } } } return(false); }