Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
SvgView.cs
1using System;
2using System.IO;
3using System.Reflection;
4using System.Threading.Tasks;
5using Microsoft.Maui.Controls;
6using Microsoft.Maui.Storage;
9using SkiaSharp;
10using SkiaSharp.Views.Maui;
11using SkiaSharp.Views.Maui.Controls;
12using SkiaSharp.Views.Maui.Handlers;
13using Svg.Skia;
14
16{
17 public class AutoHeightSKCanvasView : SKCanvasView
18 {
19 public virtual Size CustomMeasuredSize(double widthConstraint, double heightConstraint)
20 {
21 return Size.Zero;
22 }
23 }
24 internal class AutoHeightSKCanvasViewHandler : SKCanvasViewHandler
25 {
26 public override Size GetDesiredSize(double widthConstraint, double heightConstraint)
27 {
29 if (AutoHeightSKCanvasView is not null)
30 {
31 Size Custom = AutoHeightSKCanvasView.Measure(widthConstraint, heightConstraint);
32 if (Custom == Size.Zero)
33 return base.GetDesiredSize(widthConstraint, heightConstraint);
34 else
35 return Custom;
36 }
37 return base.GetDesiredSize(widthConstraint, heightConstraint);
38 }
39 }
121 public class SvgView : AutoHeightSKCanvasView, IDisposable
122 {
127 public static readonly BindableProperty SourceProperty =
128 BindableProperty.Create(
129 nameof(Source),
130 typeof(string),
131 typeof(SvgView),
132 default(string),
133 propertyChanged: OnSourceChanged);
134
139 public static readonly BindableProperty AspectProperty =
140 BindableProperty.Create(
141 nameof(Aspect),
142 typeof(Aspect),
143 typeof(SvgView),
144 Aspect.AspectFit,
145 propertyChanged: OnAspectChanged);
146
151 public static readonly BindableProperty TintColorProperty =
152 BindableProperty.Create(
153 nameof(TintColor),
154 typeof(Color),
155 typeof(SvgView),
156 null,
157 propertyChanged: OnTintColorChanged);
158
162 public string Source
163 {
164 get => (string)this.GetValue(SourceProperty);
165 set => this.SetValue(SourceProperty, value);
166 }
167
173 {
174 get => (Aspect)this.GetValue(AspectProperty);
175 set => this.SetValue(AspectProperty, value);
176 }
177
182 public Color? TintColor
183 {
184 get => (Color?)this.GetValue(TintColorProperty);
185 set => this.SetValue(TintColorProperty, value);
186 }
187
191 private SKSvg? svg;
192
196 private bool disposedValue;
197
202 public SvgView()
203 {
204 // Disable touch events if not needed.
205 this.EnableTouchEvents = false;
206 PaintSurface += this.OnPaintSurface;
207 }
208
216 private static void OnSourceChanged(BindableObject bindable, object oldValue, object newValue)
217 {
218 SvgView SvgView = (SvgView)bindable;
219 SvgView.LoadSvg();
220 }
221
229 private static void OnAspectChanged(BindableObject bindable, object oldValue, object newValue)
230 {
231 SvgView SvgView = (SvgView)bindable;
232 SvgView.InvalidateSurface();
233 }
234
242 private static void OnTintColorChanged(BindableObject bindable, object oldValue, object newValue)
243 {
244 SvgView SvgView = (SvgView)bindable;
245 SvgView.InvalidateSurface();
246 }
247
252 private async void LoadSvg()
253 {
254 if (string.IsNullOrEmpty(this.Source))
255 return;
256
257 // Dispose of any previous instance.
258 this.svg?.Dispose();
259 this.svg = null;
260
261 try
262 {
263 using Stream Stream = await FileSystem.OpenAppPackageFileAsync(this.Source);
264 this.svg = new SKSvg();
265 this.svg.Load(Stream);
266 }
267 catch (Exception Ex)
268 {
269 ServiceRef.LogService.LogWarning($"Error loading SVG asset '{this.Source}': {Ex}", this.GetClassAndMethod(MethodBase.GetCurrentMethod()));
270 this.svg = null;
271 }
272 finally
273 {
274 // Request a redraw regardless of success or failure.
275 this.InvalidateSurface();
276
277 this.InvalidateMeasure();
278
279
280
281 }
282 }
283
290 private void OnPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
291 {
292 SKCanvas Canvas = e.Surface.Canvas;
293 Canvas.Clear();
294
295 if (this.svg?.Picture is null)
296 return;
297
298 SKImageInfo Info = e.Info;
299 SKRect SvgRect = this.svg.Picture.CullRect;
300
301 // Calculate scaling factors.
302 float ScaleX = Info.Width / SvgRect.Width;
303 float ScaleY = Info.Height / SvgRect.Height;
304 float Scale;
305 float TranslateX = 0;
306 float TranslateY = 0;
307
308 switch (this.Aspect)
309 {
310 case Aspect.Fill:
311 // Non-uniform scaling to completely fill the view (might distort the image).
312 Canvas.Scale(ScaleX, ScaleY);
313 this.DrawPictureWithOptionalTint(Canvas, this.svg.Picture);
314 return;
315
316 case Aspect.AspectFill:
317 // Uniform scaling that covers the view (parts may be clipped).
318 Scale = Math.Max(ScaleX, ScaleY);
319 break;
320
321 case Aspect.AspectFit:
322 default:
323 // Uniform scaling to fit entirely within the view.
324 Scale = Math.Min(ScaleX, ScaleY);
325 break;
326 }
327
328 // Calculate dimensions after scaling.
329 float ScaledWidth = SvgRect.Width * Scale;
330 float ScaledHeight = SvgRect.Height * Scale;
331
332 // Center the SVG.
333 TranslateX = (Info.Width - ScaledWidth) / 2;
334 TranslateY = (Info.Height - ScaledHeight) / 2;
335
336 Canvas.Translate(TranslateX, TranslateY);
337 Canvas.Scale(Scale);
338
339 this.DrawPictureWithOptionalTint(Canvas, this.svg.Picture);
340 }
341
347 private void DrawPictureWithOptionalTint(SKCanvas canvas, SKPicture picture)
348 {
349 if (this.TintColor is null)
350 {
351 canvas.DrawPicture(picture);
352 }
353 else
354 {
355 // Apply tint using an SKPaint with a color filter.
356 using SKPaint Paint = new()
357 {
358 ColorFilter = SKColorFilter.CreateBlendMode(this.TintColor.ToSKColor(), SKBlendMode.SrcIn)
359 };
360 canvas.DrawPicture(picture, Paint);
361 }
362 }
363
364
365 protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
366 {
367 // If the SVG is not loaded yet, fall back to the default measurement.
368 if (this.svg?.Picture is null)
369 {
370 return base.OnMeasure(widthConstraint, heightConstraint);
371 }
372
373 // Retrieve the intrinsic dimensions of the SVG.
374 SKRect svgBounds = this.svg.Picture.CullRect;
375 double intrinsicWidth = svgBounds.Width;
376 double intrinsicHeight = svgBounds.Height;
377
378 // Check if constraints are unbounded.
379 bool unboundedWidth = double.IsInfinity(widthConstraint);
380 bool unboundedHeight = double.IsInfinity(heightConstraint);
381
382 // If both dimensions are unbounded, return the intrinsic size.
383 if (unboundedWidth && unboundedHeight)
384 {
385 return new SizeRequest(new Size(intrinsicWidth, intrinsicHeight));
386 }
387 // If only one dimension is unbounded, calculate it from the other dimension
388 // while preserving the aspect ratio of the SVG.
389 else if (unboundedWidth)
390 {
391 double scale = heightConstraint / intrinsicHeight;
392 return new SizeRequest(new Size(intrinsicWidth * scale, heightConstraint));
393 }
394 else if (unboundedHeight)
395 {
396 double scale = widthConstraint / intrinsicWidth;
397 return new SizeRequest(new Size(widthConstraint, intrinsicHeight * scale));
398 }
399 else
400 {
401 // If both dimensions are constrained, you can decide whether to honor the constraints
402 // (for example, scaling the intrinsic size) or simply return the constraints.
403 // Here we preserve the intrinsic aspect ratio.
404 double constraintAspect = widthConstraint / heightConstraint;
405 double intrinsicAspect = intrinsicWidth / intrinsicHeight;
406
407 if (intrinsicAspect > constraintAspect)
408 {
409 // Constrained by width.
410 double scaledHeight = widthConstraint / intrinsicAspect;
411 return new SizeRequest(new Size(widthConstraint, scaledHeight));
412 }
413 else
414 {
415 // Constrained by height.
416 double scaledWidth = heightConstraint * intrinsicAspect;
417 return new SizeRequest(new Size(scaledWidth, heightConstraint));
418 }
419 }
420 }
421
422 #region IDisposable Implementation
423
427 public void Dispose()
428 {
429 this.Dispose(disposing: true);
430 GC.SuppressFinalize(this);
431 }
432
440 protected virtual void Dispose(bool disposing)
441 {
442 if (!this.disposedValue)
443 {
444 if (disposing)
445 {
446 // Unsubscribe from events and dispose managed objects.
447 PaintSurface -= this.OnPaintSurface;
448 this.svg?.Dispose();
449 this.svg = null;
450 }
451 // Free unmanaged resources, if any.
452 this.disposedValue = true;
453 }
454 }
455
460 ~SvgView()
461 {
462 this.Dispose(disposing: false);
463 }
464
465 #endregion
466 }
467}
Base class that references services in the app.
Definition: ServiceRef.cs:43
static ILogService LogService
Log service.
Definition: ServiceRef.cs:214
A custom .NET MAUI control for rendering Scalable Vector Graphics (SVG) files using SkiaSharp and Svg...
Definition: SvgView.cs:122
Color? TintColor
Gets or sets the tint color to override the SVG’s original colors. When set to a non-null value,...
Definition: SvgView.cs:183
static readonly BindableProperty SourceProperty
Identifies the Source bindable property. This property represents the SVG file name bundled as a MAUI...
Definition: SvgView.cs:127
static readonly BindableProperty TintColorProperty
Identifies the TintColor bindable property. When provided, the SVG will be rendered using this tint c...
Definition: SvgView.cs:151
SvgView()
Initializes a new instance of the SvgView class. Registers the paint surface event handler and disabl...
Definition: SvgView.cs:202
void Dispose()
Releases all resources used by the SvgView control.
Definition: SvgView.cs:427
static readonly BindableProperty AspectProperty
Identifies the Aspect bindable property. Determines how the SVG is scaled to fill the view.
Definition: SvgView.cs:139
Aspect Aspect
Gets or sets the scaling mode used to display the SVG. The default is Aspect.AspectFit.
Definition: SvgView.cs:173
virtual void Dispose(bool disposing)
Releases resources used by the control.
Definition: SvgView.cs:440
string Source
Gets or sets the SVG asset file name. For example: "foo.svg".
Definition: SvgView.cs:163