Image and ImageButton
This commit is contained in:
@@ -121,13 +121,13 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
|
|||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
if (imageButton.StrokeColor is not null)
|
if (imageButton.StrokeColor is not null)
|
||||||
handler.PlatformView.StrokeColor = imageButton.StrokeColor.ToSKColor();
|
handler.PlatformView.StrokeColor = imageButton.StrokeColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapStrokeThickness(ImageButtonHandler handler, IImageButton imageButton)
|
public static void MapStrokeThickness(ImageButtonHandler handler, IImageButton imageButton)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
handler.PlatformView.StrokeThickness = (float)imageButton.StrokeThickness;
|
handler.PlatformView.StrokeThickness = imageButton.StrokeThickness;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapCornerRadius(ImageButtonHandler handler, IImageButton imageButton)
|
public static void MapCornerRadius(ImageButtonHandler handler, IImageButton imageButton)
|
||||||
@@ -139,12 +139,7 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
|
|||||||
public static void MapPadding(ImageButtonHandler handler, IImageButton imageButton)
|
public static void MapPadding(ImageButtonHandler handler, IImageButton imageButton)
|
||||||
{
|
{
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
handler.PlatformView.Padding = imageButton.Padding;
|
||||||
var padding = imageButton.Padding;
|
|
||||||
handler.PlatformView.PaddingLeft = (float)padding.Left;
|
|
||||||
handler.PlatformView.PaddingTop = (float)padding.Top;
|
|
||||||
handler.PlatformView.PaddingRight = (float)padding.Right;
|
|
||||||
handler.PlatformView.PaddingBottom = (float)padding.Bottom;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapBackground(ImageButtonHandler handler, IImageButton imageButton)
|
public static void MapBackground(ImageButtonHandler handler, IImageButton imageButton)
|
||||||
@@ -153,7 +148,7 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
|
|||||||
|
|
||||||
if (imageButton.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
if (imageButton.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
handler.PlatformView.ImageBackgroundColor = solidPaint.Color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +158,7 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
|
|||||||
|
|
||||||
if (imageButton is Microsoft.Maui.Controls.ImageButton imgBtn && imgBtn.BackgroundColor is not null)
|
if (imageButton is Microsoft.Maui.Controls.ImageButton imgBtn && imgBtn.BackgroundColor is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BackgroundColor = imgBtn.BackgroundColor.ToSKColor();
|
handler.PlatformView.ImageBackgroundColor = imgBtn.BackgroundColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ public partial class ImageHandler : ViewHandler<IImage, SkiaImage>
|
|||||||
|
|
||||||
if (image.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
if (image.Background is SolidPaint solidPaint && solidPaint.Color is not null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor();
|
handler.PlatformView.ImageBackgroundColor = solidPaint.Color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,25 +2,199 @@
|
|||||||
// 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.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Timers;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
|
using Microsoft.Maui.Graphics;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
using Svg.Skia;
|
using Svg.Skia;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform;
|
namespace Microsoft.Maui.Platform;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Skia-rendered image control with SVG support.
|
/// Skia-rendered image control with SVG support and GIF animation.
|
||||||
|
/// Full MAUI-compliant implementation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SkiaImage : SkiaView
|
public class SkiaImage : SkiaView
|
||||||
{
|
{
|
||||||
|
#region Image Cache
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Static image cache for decoded bitmaps to avoid re-decoding.
|
||||||
|
/// Key is the file path or URI, value is the cached bitmap data.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly ConcurrentDictionary<string, CachedImage> _imageCache = new();
|
||||||
|
private static readonly object _cacheLock = new();
|
||||||
|
private const int MaxCacheSize = 50; // Maximum number of cached images
|
||||||
|
private const long MaxCacheMemoryBytes = 100 * 1024 * 1024; // 100MB max cache
|
||||||
|
|
||||||
|
private class CachedImage
|
||||||
|
{
|
||||||
|
public SKBitmap? Bitmap { get; set; }
|
||||||
|
public List<AnimationFrame>? Frames { get; set; }
|
||||||
|
public bool IsAnimated { get; set; }
|
||||||
|
public DateTime LastAccessed { get; set; }
|
||||||
|
public long MemorySize { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the image cache.
|
||||||
|
/// </summary>
|
||||||
|
public static void ClearCache()
|
||||||
|
{
|
||||||
|
lock (_cacheLock)
|
||||||
|
{
|
||||||
|
foreach (var cached in _imageCache.Values)
|
||||||
|
{
|
||||||
|
cached.Bitmap?.Dispose();
|
||||||
|
if (cached.Frames != null)
|
||||||
|
{
|
||||||
|
foreach (var frame in cached.Frames)
|
||||||
|
{
|
||||||
|
frame.Bitmap?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_imageCache.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TrimCacheIfNeeded()
|
||||||
|
{
|
||||||
|
lock (_cacheLock)
|
||||||
|
{
|
||||||
|
if (_imageCache.Count <= MaxCacheSize)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Calculate total memory
|
||||||
|
long totalMemory = 0;
|
||||||
|
foreach (var cached in _imageCache.Values)
|
||||||
|
{
|
||||||
|
totalMemory += cached.MemorySize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If under memory limit and count limit, don't trim
|
||||||
|
if (totalMemory < MaxCacheMemoryBytes && _imageCache.Count <= MaxCacheSize)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Remove oldest entries until under limits
|
||||||
|
var sortedEntries = _imageCache.ToArray();
|
||||||
|
Array.Sort(sortedEntries, (a, b) => a.Value.LastAccessed.CompareTo(b.Value.LastAccessed));
|
||||||
|
|
||||||
|
int removeCount = Math.Max(1, _imageCache.Count - MaxCacheSize + 10);
|
||||||
|
for (int i = 0; i < removeCount && i < sortedEntries.Length; i++)
|
||||||
|
{
|
||||||
|
if (_imageCache.TryRemove(sortedEntries[i].Key, out var removed))
|
||||||
|
{
|
||||||
|
removed.Bitmap?.Dispose();
|
||||||
|
if (removed.Frames != null)
|
||||||
|
{
|
||||||
|
foreach (var frame in removed.Frames)
|
||||||
|
{
|
||||||
|
frame.Bitmap?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Animation Support
|
||||||
|
|
||||||
|
private class AnimationFrame
|
||||||
|
{
|
||||||
|
public SKBitmap? Bitmap { get; set; }
|
||||||
|
public int Duration { get; set; } // Duration in milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<AnimationFrame>? _animationFrames;
|
||||||
|
private int _currentFrameIndex;
|
||||||
|
private System.Timers.Timer? _animationTimer;
|
||||||
|
private bool _isAnimatedImage;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region BindableProperties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bindable property for Aspect.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly BindableProperty AspectProperty =
|
||||||
|
BindableProperty.Create(
|
||||||
|
nameof(Aspect),
|
||||||
|
typeof(Aspect),
|
||||||
|
typeof(SkiaImage),
|
||||||
|
Aspect.AspectFit,
|
||||||
|
BindingMode.TwoWay,
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImage)b).Invalidate());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bindable property for IsOpaque.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly BindableProperty IsOpaqueProperty =
|
||||||
|
BindableProperty.Create(
|
||||||
|
nameof(IsOpaque),
|
||||||
|
typeof(bool),
|
||||||
|
typeof(SkiaImage),
|
||||||
|
false,
|
||||||
|
BindingMode.TwoWay,
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImage)b).Invalidate());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bindable property for IsAnimationPlaying.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly BindableProperty IsAnimationPlayingProperty =
|
||||||
|
BindableProperty.Create(
|
||||||
|
nameof(IsAnimationPlaying),
|
||||||
|
typeof(bool),
|
||||||
|
typeof(SkiaImage),
|
||||||
|
false,
|
||||||
|
BindingMode.TwoWay,
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImage)b).OnIsAnimationPlayingChanged((bool)n));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bindable property for ImageBackgroundColor (MAUI Color for background).
|
||||||
|
/// </summary>
|
||||||
|
public static readonly BindableProperty ImageBackgroundColorProperty =
|
||||||
|
BindableProperty.Create(
|
||||||
|
nameof(ImageBackgroundColor),
|
||||||
|
typeof(Color),
|
||||||
|
typeof(SkiaImage),
|
||||||
|
Colors.Transparent,
|
||||||
|
BindingMode.TwoWay,
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImage)b).Invalidate());
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Color Conversion Helper
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a MAUI Color to SkiaSharp SKColor.
|
||||||
|
/// </summary>
|
||||||
|
private static SKColor ToSKColor(Color color)
|
||||||
|
{
|
||||||
|
if (color == null) return SKColors.Transparent;
|
||||||
|
return new SKColor(
|
||||||
|
(byte)(color.Red * 255),
|
||||||
|
(byte)(color.Green * 255),
|
||||||
|
(byte)(color.Blue * 255),
|
||||||
|
(byte)(color.Alpha * 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
private SKBitmap? _bitmap;
|
private SKBitmap? _bitmap;
|
||||||
private SKImage? _image;
|
private SKImage? _image;
|
||||||
private bool _isLoading;
|
private bool _isLoading;
|
||||||
private string? _currentFilePath;
|
private string? _currentFilePath;
|
||||||
|
private string? _cacheKey;
|
||||||
private bool _isSvg;
|
private bool _isSvg;
|
||||||
private CancellationTokenSource? _loadCts;
|
private CancellationTokenSource? _loadCts;
|
||||||
private readonly object _loadLock = new object();
|
private readonly object _loadLock = new object();
|
||||||
@@ -34,7 +208,11 @@ public class SkiaImage : SkiaView
|
|||||||
get => _bitmap;
|
get => _bitmap;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_bitmap?.Dispose();
|
// Don't dispose if this is a cached bitmap
|
||||||
|
if (_bitmap != null && (_cacheKey == null || !_imageCache.ContainsKey(_cacheKey)))
|
||||||
|
{
|
||||||
|
_bitmap.Dispose();
|
||||||
|
}
|
||||||
_bitmap = value;
|
_bitmap = value;
|
||||||
_image?.Dispose();
|
_image?.Dispose();
|
||||||
_image = value != null ? SKImage.FromBitmap(value) : null;
|
_image = value != null ? SKImage.FromBitmap(value) : null;
|
||||||
@@ -42,13 +220,48 @@ public class SkiaImage : SkiaView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Aspect Aspect { get; set; }
|
/// <summary>
|
||||||
|
/// Gets or sets the aspect ratio scaling mode.
|
||||||
|
/// </summary>
|
||||||
|
public Aspect Aspect
|
||||||
|
{
|
||||||
|
get => (Aspect)GetValue(AspectProperty);
|
||||||
|
set => SetValue(AspectProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsOpaque { get; set; }
|
/// <summary>
|
||||||
|
/// Gets or sets whether the image is opaque.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsOpaque
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(IsOpaqueProperty);
|
||||||
|
set => SetValue(IsOpaqueProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether the image is currently loading.
|
||||||
|
/// </summary>
|
||||||
public bool IsLoading => _isLoading;
|
public bool IsLoading => _isLoading;
|
||||||
|
|
||||||
public bool IsAnimationPlaying { get; set; }
|
/// <summary>
|
||||||
|
/// Gets or sets whether animation is playing (for GIF support).
|
||||||
|
/// When set to true, animated GIFs will play their animation.
|
||||||
|
/// When set to false, the first frame is displayed.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAnimationPlaying
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(IsAnimationPlayingProperty);
|
||||||
|
set => SetValue(IsAnimationPlayingProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the image background color (MAUI Color type).
|
||||||
|
/// </summary>
|
||||||
|
public Color ImageBackgroundColor
|
||||||
|
{
|
||||||
|
get => (Color)GetValue(ImageBackgroundColorProperty);
|
||||||
|
set => SetValue(ImageBackgroundColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public new double WidthRequest
|
public new double WidthRequest
|
||||||
{
|
{
|
||||||
@@ -73,6 +286,78 @@ public class SkiaImage : SkiaView
|
|||||||
public event EventHandler? ImageLoaded;
|
public event EventHandler? ImageLoaded;
|
||||||
public event EventHandler<ImageLoadingErrorEventArgs>? ImageLoadingError;
|
public event EventHandler<ImageLoadingErrorEventArgs>? ImageLoadingError;
|
||||||
|
|
||||||
|
private void OnIsAnimationPlayingChanged(bool isPlaying)
|
||||||
|
{
|
||||||
|
if (_isAnimatedImage && _animationFrames != null && _animationFrames.Count > 1)
|
||||||
|
{
|
||||||
|
if (isPlaying)
|
||||||
|
{
|
||||||
|
StartAnimation();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StopAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartAnimation()
|
||||||
|
{
|
||||||
|
if (_animationFrames == null || _animationFrames.Count <= 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
StopAnimation();
|
||||||
|
|
||||||
|
var frame = _animationFrames[_currentFrameIndex];
|
||||||
|
int duration = frame.Duration > 0 ? frame.Duration : 100; // Default 100ms if not specified
|
||||||
|
|
||||||
|
_animationTimer = new System.Timers.Timer(duration);
|
||||||
|
_animationTimer.Elapsed += OnAnimationTimerElapsed;
|
||||||
|
_animationTimer.AutoReset = false;
|
||||||
|
_animationTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopAnimation()
|
||||||
|
{
|
||||||
|
if (_animationTimer != null)
|
||||||
|
{
|
||||||
|
_animationTimer.Stop();
|
||||||
|
_animationTimer.Elapsed -= OnAnimationTimerElapsed;
|
||||||
|
_animationTimer.Dispose();
|
||||||
|
_animationTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAnimationTimerElapsed(object? sender, ElapsedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_animationFrames == null || _animationFrames.Count <= 1 || !IsAnimationPlaying)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Move to next frame
|
||||||
|
_currentFrameIndex = (_currentFrameIndex + 1) % _animationFrames.Count;
|
||||||
|
|
||||||
|
// Update the displayed image
|
||||||
|
var frame = _animationFrames[_currentFrameIndex];
|
||||||
|
if (frame.Bitmap != null)
|
||||||
|
{
|
||||||
|
_image?.Dispose();
|
||||||
|
_image = SKImage.FromBitmap(frame.Bitmap);
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule next frame
|
||||||
|
if (IsAnimationPlaying)
|
||||||
|
{
|
||||||
|
int duration = frame.Duration > 0 ? frame.Duration : 100;
|
||||||
|
_animationTimer?.Stop();
|
||||||
|
if (_animationTimer != null)
|
||||||
|
{
|
||||||
|
_animationTimer.Interval = duration;
|
||||||
|
_animationTimer.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ScheduleSvgReloadIfNeeded()
|
private void ScheduleSvgReloadIfNeeded()
|
||||||
{
|
{
|
||||||
if (_isSvg && !string.IsNullOrEmpty(_currentFilePath))
|
if (_isSvg && !string.IsNullOrEmpty(_currentFilePath))
|
||||||
@@ -95,7 +380,6 @@ public class SkiaImage : SkiaView
|
|||||||
_pendingSvgReload = false;
|
_pendingSvgReload = false;
|
||||||
if (!string.IsNullOrEmpty(_currentFilePath) && WidthRequest > 0.0 && HeightRequest > 0.0)
|
if (!string.IsNullOrEmpty(_currentFilePath) && WidthRequest > 0.0 && HeightRequest > 0.0)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[SkiaImage] Reloading SVG at {WidthRequest}x{HeightRequest} (was {_svgLoadedWidth}x{_svgLoadedHeight})");
|
|
||||||
await LoadSvgAtSizeAsync(_currentFilePath, WidthRequest, HeightRequest);
|
await LoadSvgAtSizeAsync(_currentFilePath, WidthRequest, HeightRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,11 +387,12 @@ public class SkiaImage : SkiaView
|
|||||||
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
||||||
{
|
{
|
||||||
// Draw background if not opaque
|
// Draw background if not opaque
|
||||||
if (!IsOpaque && BackgroundColor != SKColors.Transparent)
|
var bgColor = ImageBackgroundColor != null ? ToSKColor(ImageBackgroundColor) : SKColors.Transparent;
|
||||||
|
if (!IsOpaque && bgColor != SKColors.Transparent)
|
||||||
{
|
{
|
||||||
using var bgPaint = new SKPaint
|
using var bgPaint = new SKPaint
|
||||||
{
|
{
|
||||||
Color = BackgroundColor,
|
Color = bgColor,
|
||||||
Style = SKPaintStyle.Fill
|
Style = SKPaintStyle.Fill
|
||||||
};
|
};
|
||||||
canvas.DrawRect(bounds, bgPaint);
|
canvas.DrawRect(bounds, bgPaint);
|
||||||
@@ -176,7 +461,6 @@ public class SkiaImage : SkiaView
|
|||||||
{
|
{
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
Invalidate();
|
Invalidate();
|
||||||
Console.WriteLine($"[SkiaImage] LoadFromFileAsync: {filePath}, WidthRequest={WidthRequest}, HeightRequest={HeightRequest}");
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -204,40 +488,63 @@ public class SkiaImage : SkiaView
|
|||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
foundPath = path;
|
foundPath = path;
|
||||||
Console.WriteLine("[SkiaImage] Found file at: " + path);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundPath == null)
|
if (foundPath == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("[SkiaImage] File not found: " + filePath);
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_isSvg = false;
|
_isSvg = false;
|
||||||
_currentFilePath = null;
|
_currentFilePath = null;
|
||||||
|
_cacheKey = null;
|
||||||
ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(new FileNotFoundException(filePath)));
|
ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(new FileNotFoundException(filePath)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_isSvg = foundPath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase);
|
_isSvg = foundPath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase);
|
||||||
_currentFilePath = foundPath;
|
_currentFilePath = foundPath;
|
||||||
|
_cacheKey = foundPath;
|
||||||
|
|
||||||
if (!_isSvg)
|
// Check cache first
|
||||||
|
if (_imageCache.TryGetValue(foundPath, out var cached))
|
||||||
{
|
{
|
||||||
await Task.Run(() =>
|
cached.LastAccessed = DateTime.UtcNow;
|
||||||
|
if (cached.IsAnimated && cached.Frames != null)
|
||||||
{
|
{
|
||||||
using FileStream stream = File.OpenRead(foundPath);
|
_isAnimatedImage = true;
|
||||||
SKBitmap? bitmap = SKBitmap.Decode(stream);
|
_animationFrames = cached.Frames;
|
||||||
if (bitmap != null)
|
_currentFrameIndex = 0;
|
||||||
|
if (cached.Frames.Count > 0 && cached.Frames[0].Bitmap != null)
|
||||||
{
|
{
|
||||||
Bitmap = bitmap;
|
_image?.Dispose();
|
||||||
Console.WriteLine("[SkiaImage] Loaded image: " + foundPath);
|
_image = SKImage.FromBitmap(cached.Frames[0].Bitmap);
|
||||||
}
|
}
|
||||||
});
|
if (IsAnimationPlaying)
|
||||||
|
{
|
||||||
|
StartAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (cached.Bitmap != null)
|
||||||
|
{
|
||||||
|
_isAnimatedImage = false;
|
||||||
|
_bitmap = cached.Bitmap;
|
||||||
|
_image?.Dispose();
|
||||||
|
_image = SKImage.FromBitmap(cached.Bitmap);
|
||||||
|
}
|
||||||
|
_isLoading = false;
|
||||||
|
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
||||||
|
Invalidate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isSvg)
|
||||||
|
{
|
||||||
|
await LoadSvgAtSizeAsync(foundPath, WidthRequest, HeightRequest);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await LoadSvgAtSizeAsync(foundPath, WidthRequest, HeightRequest);
|
await LoadImageWithAnimationSupportAsync(foundPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
@@ -252,6 +559,103 @@ public class SkiaImage : SkiaView
|
|||||||
Invalidate();
|
Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task LoadImageWithAnimationSupportAsync(string filePath)
|
||||||
|
{
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead(filePath);
|
||||||
|
using var codec = SKCodec.Create(stream);
|
||||||
|
|
||||||
|
if (codec == null)
|
||||||
|
{
|
||||||
|
// Fallback to simple decode
|
||||||
|
stream.Position = 0;
|
||||||
|
var bitmap = SKBitmap.Decode(stream);
|
||||||
|
if (bitmap != null)
|
||||||
|
{
|
||||||
|
CacheAndSetBitmap(filePath, bitmap, false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int frameCount = codec.FrameCount;
|
||||||
|
|
||||||
|
if (frameCount > 1)
|
||||||
|
{
|
||||||
|
// Animated image (GIF)
|
||||||
|
_isAnimatedImage = true;
|
||||||
|
_animationFrames = new List<AnimationFrame>();
|
||||||
|
var info = codec.Info;
|
||||||
|
|
||||||
|
for (int i = 0; i < frameCount; i++)
|
||||||
|
{
|
||||||
|
var frameInfo = codec.FrameInfo[i];
|
||||||
|
var bitmap = new SKBitmap(info.Width, info.Height);
|
||||||
|
|
||||||
|
var options = new SKCodecOptions(i);
|
||||||
|
codec.GetPixels(bitmap.Info, bitmap.GetPixels(), options);
|
||||||
|
|
||||||
|
_animationFrames.Add(new AnimationFrame
|
||||||
|
{
|
||||||
|
Bitmap = bitmap,
|
||||||
|
Duration = frameInfo.Duration > 0 ? frameInfo.Duration : 100
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the animation frames
|
||||||
|
long memorySize = _animationFrames.Sum(f => (long)(f.Bitmap?.ByteCount ?? 0));
|
||||||
|
_imageCache[filePath] = new CachedImage
|
||||||
|
{
|
||||||
|
Frames = _animationFrames,
|
||||||
|
IsAnimated = true,
|
||||||
|
LastAccessed = DateTime.UtcNow,
|
||||||
|
MemorySize = memorySize
|
||||||
|
};
|
||||||
|
TrimCacheIfNeeded();
|
||||||
|
|
||||||
|
// Set first frame as current image
|
||||||
|
_currentFrameIndex = 0;
|
||||||
|
if (_animationFrames.Count > 0 && _animationFrames[0].Bitmap != null)
|
||||||
|
{
|
||||||
|
_image?.Dispose();
|
||||||
|
_image = SKImage.FromBitmap(_animationFrames[0].Bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start animation if requested
|
||||||
|
if (IsAnimationPlaying)
|
||||||
|
{
|
||||||
|
StartAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Static image
|
||||||
|
_isAnimatedImage = false;
|
||||||
|
var bitmap = SKBitmap.Decode(codec, codec.Info);
|
||||||
|
if (bitmap != null)
|
||||||
|
{
|
||||||
|
CacheAndSetBitmap(filePath, bitmap, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CacheAndSetBitmap(string cacheKey, SKBitmap bitmap, bool isAnimated)
|
||||||
|
{
|
||||||
|
_imageCache[cacheKey] = new CachedImage
|
||||||
|
{
|
||||||
|
Bitmap = bitmap,
|
||||||
|
IsAnimated = isAnimated,
|
||||||
|
LastAccessed = DateTime.UtcNow,
|
||||||
|
MemorySize = bitmap.ByteCount
|
||||||
|
};
|
||||||
|
TrimCacheIfNeeded();
|
||||||
|
|
||||||
|
_bitmap = bitmap;
|
||||||
|
_image?.Dispose();
|
||||||
|
_image = SKImage.FromBitmap(bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadSvgAtSizeAsync(string svgPath, double targetWidth, double targetHeight)
|
private async Task LoadSvgAtSizeAsync(string svgPath, double targetWidth, double targetHeight)
|
||||||
{
|
{
|
||||||
_loadCts?.Cancel();
|
_loadCts?.Cancel();
|
||||||
@@ -293,8 +697,6 @@ public class SkiaImage : SkiaView
|
|||||||
canvas.Clear(SKColors.Transparent);
|
canvas.Clear(SKColors.Transparent);
|
||||||
canvas.Scale(scale);
|
canvas.Scale(scale);
|
||||||
canvas.DrawPicture(svg.Picture, null);
|
canvas.DrawPicture(svg.Picture, null);
|
||||||
|
|
||||||
Console.WriteLine($"[SkiaImage] Loaded SVG: {svgPath} at {bitmapWidth}x{bitmapHeight} (requested {targetWidth}x{targetHeight})");
|
|
||||||
}
|
}
|
||||||
}, cts.Token);
|
}, cts.Token);
|
||||||
|
|
||||||
@@ -302,6 +704,7 @@ public class SkiaImage : SkiaView
|
|||||||
{
|
{
|
||||||
_svgLoadedWidth = (targetWidth > 0.0) ? targetWidth : newBitmap.Width;
|
_svgLoadedWidth = (targetWidth > 0.0) ? targetWidth : newBitmap.Width;
|
||||||
_svgLoadedHeight = (targetHeight > 0.0) ? targetHeight : newBitmap.Height;
|
_svgLoadedHeight = (targetHeight > 0.0) ? targetHeight : newBitmap.Height;
|
||||||
|
_isAnimatedImage = false;
|
||||||
Bitmap = newBitmap;
|
Bitmap = newBitmap;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -318,17 +721,72 @@ public class SkiaImage : SkiaView
|
|||||||
public async Task LoadFromStreamAsync(Stream stream)
|
public async Task LoadFromStreamAsync(Stream stream)
|
||||||
{
|
{
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
|
_cacheKey = null; // Streams are not cached by default
|
||||||
Invalidate();
|
Invalidate();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Run(() =>
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
SKBitmap? bitmap = SKBitmap.Decode(stream);
|
using var codec = SKCodec.Create(stream);
|
||||||
|
|
||||||
|
if (codec == null)
|
||||||
|
{
|
||||||
|
stream.Position = 0;
|
||||||
|
var bitmap = SKBitmap.Decode(stream);
|
||||||
|
if (bitmap != null)
|
||||||
|
{
|
||||||
|
_isAnimatedImage = false;
|
||||||
|
Bitmap = bitmap;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int frameCount = codec.FrameCount;
|
||||||
|
|
||||||
|
if (frameCount > 1)
|
||||||
|
{
|
||||||
|
// Animated image
|
||||||
|
_isAnimatedImage = true;
|
||||||
|
_animationFrames = new List<AnimationFrame>();
|
||||||
|
var info = codec.Info;
|
||||||
|
|
||||||
|
for (int i = 0; i < frameCount; i++)
|
||||||
|
{
|
||||||
|
var frameInfo = codec.FrameInfo[i];
|
||||||
|
var bitmap = new SKBitmap(info.Width, info.Height);
|
||||||
|
|
||||||
|
var options = new SKCodecOptions(i);
|
||||||
|
codec.GetPixels(bitmap.Info, bitmap.GetPixels(), options);
|
||||||
|
|
||||||
|
_animationFrames.Add(new AnimationFrame
|
||||||
|
{
|
||||||
|
Bitmap = bitmap,
|
||||||
|
Duration = frameInfo.Duration > 0 ? frameInfo.Duration : 100
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentFrameIndex = 0;
|
||||||
|
if (_animationFrames.Count > 0 && _animationFrames[0].Bitmap != null)
|
||||||
|
{
|
||||||
|
_image?.Dispose();
|
||||||
|
_image = SKImage.FromBitmap(_animationFrames[0].Bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAnimationPlaying)
|
||||||
|
{
|
||||||
|
StartAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_isAnimatedImage = false;
|
||||||
|
var bitmap = SKBitmap.Decode(codec, codec.Info);
|
||||||
if (bitmap != null)
|
if (bitmap != null)
|
||||||
{
|
{
|
||||||
Bitmap = bitmap;
|
Bitmap = bitmap;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
@@ -346,17 +804,120 @@ public class SkiaImage : SkiaView
|
|||||||
public async Task LoadFromUriAsync(Uri uri)
|
public async Task LoadFromUriAsync(Uri uri)
|
||||||
{
|
{
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
|
_cacheKey = uri.ToString();
|
||||||
Invalidate();
|
Invalidate();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Check cache first
|
||||||
|
if (_imageCache.TryGetValue(_cacheKey, out var cached))
|
||||||
|
{
|
||||||
|
cached.LastAccessed = DateTime.UtcNow;
|
||||||
|
if (cached.IsAnimated && cached.Frames != null)
|
||||||
|
{
|
||||||
|
_isAnimatedImage = true;
|
||||||
|
_animationFrames = cached.Frames;
|
||||||
|
_currentFrameIndex = 0;
|
||||||
|
if (cached.Frames.Count > 0 && cached.Frames[0].Bitmap != null)
|
||||||
|
{
|
||||||
|
_image?.Dispose();
|
||||||
|
_image = SKImage.FromBitmap(cached.Frames[0].Bitmap);
|
||||||
|
}
|
||||||
|
if (IsAnimationPlaying)
|
||||||
|
{
|
||||||
|
StartAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (cached.Bitmap != null)
|
||||||
|
{
|
||||||
|
_isAnimatedImage = false;
|
||||||
|
_bitmap = cached.Bitmap;
|
||||||
|
_image?.Dispose();
|
||||||
|
_image = SKImage.FromBitmap(cached.Bitmap);
|
||||||
|
}
|
||||||
|
_isLoading = false;
|
||||||
|
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
||||||
|
Invalidate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using HttpClient httpClient = new HttpClient();
|
using HttpClient httpClient = new HttpClient();
|
||||||
using MemoryStream stream = new MemoryStream(await httpClient.GetByteArrayAsync(uri));
|
var data = await httpClient.GetByteArrayAsync(uri);
|
||||||
SKBitmap? bitmap = SKBitmap.Decode(stream);
|
using var stream = new MemoryStream(data);
|
||||||
|
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
using var codec = SKCodec.Create(stream);
|
||||||
|
|
||||||
|
if (codec == null)
|
||||||
|
{
|
||||||
|
stream.Position = 0;
|
||||||
|
var bitmap = SKBitmap.Decode(stream);
|
||||||
if (bitmap != null)
|
if (bitmap != null)
|
||||||
{
|
{
|
||||||
Bitmap = bitmap;
|
_isAnimatedImage = false;
|
||||||
|
CacheAndSetBitmap(_cacheKey, bitmap, false);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int frameCount = codec.FrameCount;
|
||||||
|
|
||||||
|
if (frameCount > 1)
|
||||||
|
{
|
||||||
|
// Animated image
|
||||||
|
_isAnimatedImage = true;
|
||||||
|
_animationFrames = new List<AnimationFrame>();
|
||||||
|
var info = codec.Info;
|
||||||
|
|
||||||
|
for (int i = 0; i < frameCount; i++)
|
||||||
|
{
|
||||||
|
var frameInfo = codec.FrameInfo[i];
|
||||||
|
var bitmap = new SKBitmap(info.Width, info.Height);
|
||||||
|
|
||||||
|
var options = new SKCodecOptions(i);
|
||||||
|
codec.GetPixels(bitmap.Info, bitmap.GetPixels(), options);
|
||||||
|
|
||||||
|
_animationFrames.Add(new AnimationFrame
|
||||||
|
{
|
||||||
|
Bitmap = bitmap,
|
||||||
|
Duration = frameInfo.Duration > 0 ? frameInfo.Duration : 100
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the animation frames
|
||||||
|
long memorySize = _animationFrames.Sum(f => (long)(f.Bitmap?.ByteCount ?? 0));
|
||||||
|
_imageCache[_cacheKey] = new CachedImage
|
||||||
|
{
|
||||||
|
Frames = _animationFrames,
|
||||||
|
IsAnimated = true,
|
||||||
|
LastAccessed = DateTime.UtcNow,
|
||||||
|
MemorySize = memorySize
|
||||||
|
};
|
||||||
|
TrimCacheIfNeeded();
|
||||||
|
|
||||||
|
_currentFrameIndex = 0;
|
||||||
|
if (_animationFrames.Count > 0 && _animationFrames[0].Bitmap != null)
|
||||||
|
{
|
||||||
|
_image?.Dispose();
|
||||||
|
_image = SKImage.FromBitmap(_animationFrames[0].Bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAnimationPlaying)
|
||||||
|
{
|
||||||
|
StartAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_isAnimatedImage = false;
|
||||||
|
var bitmap = SKBitmap.Decode(codec, codec.Info);
|
||||||
|
if (bitmap != null)
|
||||||
|
{
|
||||||
|
CacheAndSetBitmap(_cacheKey, bitmap, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
||||||
@@ -374,12 +935,69 @@ public class SkiaImage : SkiaView
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using MemoryStream stream = new MemoryStream(data);
|
_cacheKey = null;
|
||||||
SKBitmap? bitmap = SKBitmap.Decode(stream);
|
using var stream = new MemoryStream(data);
|
||||||
|
using var codec = SKCodec.Create(stream);
|
||||||
|
|
||||||
|
if (codec == null)
|
||||||
|
{
|
||||||
|
stream.Position = 0;
|
||||||
|
var bitmap = SKBitmap.Decode(stream);
|
||||||
|
if (bitmap != null)
|
||||||
|
{
|
||||||
|
_isAnimatedImage = false;
|
||||||
|
Bitmap = bitmap;
|
||||||
|
}
|
||||||
|
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int frameCount = codec.FrameCount;
|
||||||
|
|
||||||
|
if (frameCount > 1)
|
||||||
|
{
|
||||||
|
// Animated image
|
||||||
|
_isAnimatedImage = true;
|
||||||
|
_animationFrames = new List<AnimationFrame>();
|
||||||
|
var info = codec.Info;
|
||||||
|
|
||||||
|
for (int i = 0; i < frameCount; i++)
|
||||||
|
{
|
||||||
|
var frameInfo = codec.FrameInfo[i];
|
||||||
|
var bitmap = new SKBitmap(info.Width, info.Height);
|
||||||
|
|
||||||
|
var options = new SKCodecOptions(i);
|
||||||
|
codec.GetPixels(bitmap.Info, bitmap.GetPixels(), options);
|
||||||
|
|
||||||
|
_animationFrames.Add(new AnimationFrame
|
||||||
|
{
|
||||||
|
Bitmap = bitmap,
|
||||||
|
Duration = frameInfo.Duration > 0 ? frameInfo.Duration : 100
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentFrameIndex = 0;
|
||||||
|
if (_animationFrames.Count > 0 && _animationFrames[0].Bitmap != null)
|
||||||
|
{
|
||||||
|
_image?.Dispose();
|
||||||
|
_image = SKImage.FromBitmap(_animationFrames[0].Bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAnimationPlaying)
|
||||||
|
{
|
||||||
|
StartAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_isAnimatedImage = false;
|
||||||
|
var bitmap = SKBitmap.Decode(codec, codec.Info);
|
||||||
if (bitmap != null)
|
if (bitmap != null)
|
||||||
{
|
{
|
||||||
Bitmap = bitmap;
|
Bitmap = bitmap;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -397,6 +1015,10 @@ public class SkiaImage : SkiaView
|
|||||||
{
|
{
|
||||||
_isSvg = false;
|
_isSvg = false;
|
||||||
_currentFilePath = null;
|
_currentFilePath = null;
|
||||||
|
_cacheKey = null;
|
||||||
|
_isAnimatedImage = false;
|
||||||
|
StopAnimation();
|
||||||
|
_animationFrames = null;
|
||||||
Bitmap = bitmap;
|
Bitmap = bitmap;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
||||||
@@ -426,7 +1048,6 @@ public class SkiaImage : SkiaView
|
|||||||
(width != _lastArrangedBounds.Width || height != _lastArrangedBounds.Height))
|
(width != _lastArrangedBounds.Width || height != _lastArrangedBounds.Height))
|
||||||
{
|
{
|
||||||
_lastArrangedBounds = bounds;
|
_lastArrangedBounds = bounds;
|
||||||
Console.WriteLine($"[SkiaImage] Arrange detected larger bounds: {width}x{height} vs loaded {_svgLoadedWidth}x{_svgLoadedHeight}");
|
|
||||||
_ = LoadSvgAtSizeAsync(_currentFilePath, width, height);
|
_ = LoadSvgAtSizeAsync(_currentFilePath, width, height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,42 +1056,24 @@ public class SkiaImage : SkiaView
|
|||||||
|
|
||||||
protected override SKRect ArrangeOverride(SKRect bounds)
|
protected override SKRect ArrangeOverride(SKRect bounds)
|
||||||
{
|
{
|
||||||
// If we have explicit size requests, constrain to desired size
|
|
||||||
// This follows MAUI standard behavior - controls respect WidthRequest/HeightRequest
|
|
||||||
var desiredWidth = DesiredSize.Width;
|
var desiredWidth = DesiredSize.Width;
|
||||||
var desiredHeight = DesiredSize.Height;
|
var desiredHeight = DesiredSize.Height;
|
||||||
|
|
||||||
// If desired size is smaller than available bounds, align within bounds
|
|
||||||
if (desiredWidth > 0 && desiredHeight > 0 &&
|
if (desiredWidth > 0 && desiredHeight > 0 &&
|
||||||
(desiredWidth < bounds.Width || desiredHeight < bounds.Height))
|
(desiredWidth < bounds.Width || desiredHeight < bounds.Height))
|
||||||
{
|
{
|
||||||
float finalWidth = Math.Min(desiredWidth, bounds.Width);
|
float finalWidth = Math.Min(desiredWidth, bounds.Width);
|
||||||
float finalHeight = Math.Min(desiredHeight, bounds.Height);
|
float finalHeight = Math.Min(desiredHeight, bounds.Height);
|
||||||
|
|
||||||
// Calculate position based on HorizontalOptions
|
|
||||||
// LayoutAlignment: Start=0, Center=1, End=2, Fill=3
|
|
||||||
float x = bounds.Left;
|
float x = bounds.Left;
|
||||||
var hAlignValue = (int)HorizontalOptions.Alignment;
|
var hAlignValue = (int)HorizontalOptions.Alignment;
|
||||||
if (hAlignValue == 1) // Center
|
if (hAlignValue == 1) x = bounds.Left + (bounds.Width - finalWidth) / 2;
|
||||||
{
|
else if (hAlignValue == 2) x = bounds.Right - finalWidth;
|
||||||
x = bounds.Left + (bounds.Width - finalWidth) / 2;
|
|
||||||
}
|
|
||||||
else if (hAlignValue == 2) // End
|
|
||||||
{
|
|
||||||
x = bounds.Right - finalWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate position based on VerticalOptions
|
|
||||||
float y = bounds.Top;
|
float y = bounds.Top;
|
||||||
var vAlignValue = (int)VerticalOptions.Alignment;
|
var vAlignValue = (int)VerticalOptions.Alignment;
|
||||||
if (vAlignValue == 1) // Center
|
if (vAlignValue == 1) y = bounds.Top + (bounds.Height - finalHeight) / 2;
|
||||||
{
|
else if (vAlignValue == 2) y = bounds.Bottom - finalHeight;
|
||||||
y = bounds.Top + (bounds.Height - finalHeight) / 2;
|
|
||||||
}
|
|
||||||
else if (vAlignValue == 2) // End
|
|
||||||
{
|
|
||||||
y = bounds.Bottom - finalHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SKRect(x, y, x + finalWidth, y + finalHeight);
|
return new SKRect(x, y, x + finalWidth, y + finalHeight);
|
||||||
}
|
}
|
||||||
@@ -483,40 +1086,31 @@ public class SkiaImage : SkiaView
|
|||||||
double widthRequest = base.WidthRequest;
|
double widthRequest = base.WidthRequest;
|
||||||
double heightRequest = base.HeightRequest;
|
double heightRequest = base.HeightRequest;
|
||||||
|
|
||||||
// If both dimensions explicitly requested, use them
|
|
||||||
if (widthRequest > 0.0 && heightRequest > 0.0)
|
if (widthRequest > 0.0 && heightRequest > 0.0)
|
||||||
{
|
|
||||||
return new SKSize((float)widthRequest, (float)heightRequest);
|
return new SKSize((float)widthRequest, (float)heightRequest);
|
||||||
}
|
|
||||||
|
|
||||||
// If no image, return default or requested size
|
|
||||||
if (_image == null)
|
if (_image == null)
|
||||||
{
|
{
|
||||||
if (widthRequest > 0.0)
|
if (widthRequest > 0.0) return new SKSize((float)widthRequest, (float)widthRequest);
|
||||||
return new SKSize((float)widthRequest, (float)widthRequest);
|
if (heightRequest > 0.0) return new SKSize((float)heightRequest, (float)heightRequest);
|
||||||
if (heightRequest > 0.0)
|
|
||||||
return new SKSize((float)heightRequest, (float)heightRequest);
|
|
||||||
return new SKSize(100f, 100f);
|
return new SKSize(100f, 100f);
|
||||||
}
|
}
|
||||||
|
|
||||||
float imageWidth = _image.Width;
|
float imageWidth = _image.Width;
|
||||||
float imageHeight = _image.Height;
|
float imageHeight = _image.Height;
|
||||||
|
|
||||||
// If only width requested, scale height proportionally
|
|
||||||
if (widthRequest > 0.0)
|
if (widthRequest > 0.0)
|
||||||
{
|
{
|
||||||
float scale = (float)widthRequest / imageWidth;
|
float scale = (float)widthRequest / imageWidth;
|
||||||
return new SKSize((float)widthRequest, imageHeight * scale);
|
return new SKSize((float)widthRequest, imageHeight * scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If only height requested, scale width proportionally
|
|
||||||
if (heightRequest > 0.0)
|
if (heightRequest > 0.0)
|
||||||
{
|
{
|
||||||
float scale = (float)heightRequest / imageHeight;
|
float scale = (float)heightRequest / imageHeight;
|
||||||
return new SKSize(imageWidth * scale, (float)heightRequest);
|
return new SKSize(imageWidth * scale, (float)heightRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale to fit available size
|
|
||||||
if (availableSize.Width < float.MaxValue && availableSize.Height < float.MaxValue)
|
if (availableSize.Width < float.MaxValue && availableSize.Height < float.MaxValue)
|
||||||
{
|
{
|
||||||
float scale = Math.Min(availableSize.Width / imageWidth, availableSize.Height / imageHeight);
|
float scale = Math.Min(availableSize.Width / imageWidth, availableSize.Height / imageHeight);
|
||||||
@@ -541,8 +1135,21 @@ public class SkiaImage : SkiaView
|
|||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
|
{
|
||||||
|
StopAnimation();
|
||||||
|
|
||||||
|
// Only dispose if not cached
|
||||||
|
if (_cacheKey == null || !_imageCache.ContainsKey(_cacheKey))
|
||||||
{
|
{
|
||||||
_bitmap?.Dispose();
|
_bitmap?.Dispose();
|
||||||
|
if (_animationFrames != null)
|
||||||
|
{
|
||||||
|
foreach (var frame in _animationFrames)
|
||||||
|
{
|
||||||
|
frame.Bitmap?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_image?.Dispose();
|
_image?.Dispose();
|
||||||
}
|
}
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
|
using Microsoft.Maui.Graphics;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
using Svg.Skia;
|
using Svg.Skia;
|
||||||
|
|
||||||
@@ -14,12 +17,79 @@ namespace Microsoft.Maui.Platform;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Skia-rendered image button control.
|
/// Skia-rendered image button control.
|
||||||
/// Combines button behavior with image display.
|
/// Combines button behavior with image display.
|
||||||
|
/// Implements MAUI IImageButton interface requirements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SkiaImageButton : SkiaView
|
public class SkiaImageButton : SkiaView
|
||||||
{
|
{
|
||||||
|
#region Private Fields
|
||||||
private SKBitmap? _bitmap;
|
private SKBitmap? _bitmap;
|
||||||
private SKImage? _image;
|
private SKImage? _image;
|
||||||
private bool _isLoading;
|
private bool _isLoading;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region SKColor Helper
|
||||||
|
private static SKColor ToSKColor(Color? color)
|
||||||
|
{
|
||||||
|
if (color == null) return SKColors.Transparent;
|
||||||
|
return new SKColor(
|
||||||
|
(byte)(color.Red * 255),
|
||||||
|
(byte)(color.Green * 255),
|
||||||
|
(byte)(color.Blue * 255),
|
||||||
|
(byte)(color.Alpha * 255));
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region BindableProperties
|
||||||
|
|
||||||
|
public static readonly BindableProperty AspectProperty = BindableProperty.Create(
|
||||||
|
nameof(Aspect), typeof(Aspect), typeof(SkiaImageButton), Aspect.AspectFit,
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImageButton)b).Invalidate());
|
||||||
|
|
||||||
|
public static readonly BindableProperty IsOpaqueProperty = BindableProperty.Create(
|
||||||
|
nameof(IsOpaque), typeof(bool), typeof(SkiaImageButton), false,
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImageButton)b).Invalidate());
|
||||||
|
|
||||||
|
public static readonly BindableProperty StrokeColorProperty = BindableProperty.Create(
|
||||||
|
nameof(StrokeColor), typeof(Color), typeof(SkiaImageButton), Colors.Transparent,
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImageButton)b).Invalidate());
|
||||||
|
|
||||||
|
public static readonly BindableProperty StrokeThicknessProperty = BindableProperty.Create(
|
||||||
|
nameof(StrokeThickness), typeof(double), typeof(SkiaImageButton), 0.0,
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImageButton)b).Invalidate());
|
||||||
|
|
||||||
|
public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(
|
||||||
|
nameof(CornerRadius), typeof(int), typeof(SkiaImageButton), 0,
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImageButton)b).Invalidate());
|
||||||
|
|
||||||
|
public static readonly BindableProperty PaddingProperty = BindableProperty.Create(
|
||||||
|
nameof(Padding), typeof(Thickness), typeof(SkiaImageButton), new Thickness(0),
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImageButton)b).Invalidate());
|
||||||
|
|
||||||
|
public static readonly BindableProperty PressedBackgroundColorProperty = BindableProperty.Create(
|
||||||
|
nameof(PressedBackgroundColor), typeof(Color), typeof(SkiaImageButton),
|
||||||
|
Color.FromRgba(0, 0, 0, 30),
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImageButton)b).Invalidate());
|
||||||
|
|
||||||
|
public static readonly BindableProperty HoveredBackgroundColorProperty = BindableProperty.Create(
|
||||||
|
nameof(HoveredBackgroundColor), typeof(Color), typeof(SkiaImageButton),
|
||||||
|
Color.FromRgba(0, 0, 0, 15),
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImageButton)b).Invalidate());
|
||||||
|
|
||||||
|
public static readonly BindableProperty ImageBackgroundColorProperty = BindableProperty.Create(
|
||||||
|
nameof(ImageBackgroundColor), typeof(Color), typeof(SkiaImageButton), null,
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImageButton)b).Invalidate());
|
||||||
|
|
||||||
|
public static readonly BindableProperty CommandProperty = BindableProperty.Create(
|
||||||
|
nameof(Command), typeof(ICommand), typeof(SkiaImageButton), null,
|
||||||
|
propertyChanged: OnCommandChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(
|
||||||
|
nameof(CommandParameter), typeof(object), typeof(SkiaImageButton), null,
|
||||||
|
propertyChanged: (b, o, n) => ((SkiaImageButton)b).UpdateCommandCanExecute());
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
public SKBitmap? Bitmap
|
public SKBitmap? Bitmap
|
||||||
{
|
{
|
||||||
@@ -34,57 +104,172 @@ public class SkiaImageButton : SkiaView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image properties
|
public Aspect Aspect
|
||||||
public Aspect Aspect { get; set; } = Aspect.AspectFit;
|
{
|
||||||
public bool IsOpaque { get; set; }
|
get => (Aspect)GetValue(AspectProperty);
|
||||||
|
set => SetValue(AspectProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsOpaque
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(IsOpaqueProperty);
|
||||||
|
set => SetValue(IsOpaqueProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsLoading => _isLoading;
|
public bool IsLoading => _isLoading;
|
||||||
|
|
||||||
// Button stroke properties
|
public Color StrokeColor
|
||||||
public SKColor StrokeColor { get; set; } = SKColors.Transparent;
|
{
|
||||||
public float StrokeThickness { get; set; } = 0;
|
get => (Color)GetValue(StrokeColorProperty);
|
||||||
public float CornerRadius { get; set; } = 0;
|
set => SetValue(StrokeColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double StrokeThickness
|
||||||
|
{
|
||||||
|
get => (double)GetValue(StrokeThicknessProperty);
|
||||||
|
set => SetValue(StrokeThicknessProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CornerRadius
|
||||||
|
{
|
||||||
|
get => (int)GetValue(CornerRadiusProperty);
|
||||||
|
set => SetValue(CornerRadiusProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Thickness Padding
|
||||||
|
{
|
||||||
|
get => (Thickness)GetValue(PaddingProperty);
|
||||||
|
set => SetValue(PaddingProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color PressedBackgroundColor
|
||||||
|
{
|
||||||
|
get => (Color)GetValue(PressedBackgroundColorProperty);
|
||||||
|
set => SetValue(PressedBackgroundColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color HoveredBackgroundColor
|
||||||
|
{
|
||||||
|
get => (Color)GetValue(HoveredBackgroundColorProperty);
|
||||||
|
set => SetValue(HoveredBackgroundColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color? ImageBackgroundColor
|
||||||
|
{
|
||||||
|
get => (Color?)GetValue(ImageBackgroundColorProperty);
|
||||||
|
set => SetValue(ImageBackgroundColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand? Command
|
||||||
|
{
|
||||||
|
get => (ICommand?)GetValue(CommandProperty);
|
||||||
|
set => SetValue(CommandProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? CommandParameter
|
||||||
|
{
|
||||||
|
get => GetValue(CommandParameterProperty);
|
||||||
|
set => SetValue(CommandParameterProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
// Button state
|
// Button state
|
||||||
public bool IsPressed { get; private set; }
|
public bool IsPressed { get; private set; }
|
||||||
public bool IsHovered { get; private set; }
|
public bool IsHovered { get; private set; }
|
||||||
|
|
||||||
// Visual state colors
|
#endregion
|
||||||
public SKColor PressedBackgroundColor { get; set; } = new SKColor(0, 0, 0, 30);
|
|
||||||
public SKColor HoveredBackgroundColor { get; set; } = new SKColor(0, 0, 0, 15);
|
|
||||||
|
|
||||||
// Padding for the image content
|
|
||||||
public float PaddingLeft { get; set; }
|
|
||||||
public float PaddingTop { get; set; }
|
|
||||||
public float PaddingRight { get; set; }
|
|
||||||
public float PaddingBottom { get; set; }
|
|
||||||
|
|
||||||
|
#region Events
|
||||||
public event EventHandler? Clicked;
|
public event EventHandler? Clicked;
|
||||||
public event EventHandler? Pressed;
|
public event EventHandler? Pressed;
|
||||||
public event EventHandler? Released;
|
public event EventHandler? Released;
|
||||||
public event EventHandler? ImageLoaded;
|
public event EventHandler? ImageLoaded;
|
||||||
public event EventHandler<ImageLoadingErrorEventArgs>? ImageLoadingError;
|
public event EventHandler<ImageLoadingErrorEventArgs>? ImageLoadingError;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructor
|
||||||
|
|
||||||
public SkiaImageButton()
|
public SkiaImageButton()
|
||||||
{
|
{
|
||||||
IsFocusable = true;
|
IsFocusable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Command Support
|
||||||
|
|
||||||
|
private static void OnCommandChanged(BindableObject bindable, object oldValue, object newValue)
|
||||||
|
{
|
||||||
|
var button = (SkiaImageButton)bindable;
|
||||||
|
|
||||||
|
if (oldValue is ICommand oldCommand)
|
||||||
|
{
|
||||||
|
oldCommand.CanExecuteChanged -= button.OnCommandCanExecuteChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue is ICommand newCommand)
|
||||||
|
{
|
||||||
|
newCommand.CanExecuteChanged += button.OnCommandCanExecuteChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.UpdateCommandCanExecute();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCommandCanExecuteChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
UpdateCommandCanExecute();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCommandCanExecute()
|
||||||
|
{
|
||||||
|
if (Command != null)
|
||||||
|
{
|
||||||
|
IsEnabled = Command.CanExecute(CommandParameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteCommand()
|
||||||
|
{
|
||||||
|
if (Command?.CanExecute(CommandParameter) == true)
|
||||||
|
{
|
||||||
|
Command.Execute(CommandParameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Rendering
|
||||||
|
|
||||||
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
||||||
{
|
{
|
||||||
// Apply padding
|
var padding = Padding;
|
||||||
var contentBounds = new SKRect(
|
var contentBounds = new SKRect(
|
||||||
bounds.Left + PaddingLeft,
|
bounds.Left + (float)padding.Left,
|
||||||
bounds.Top + PaddingTop,
|
bounds.Top + (float)padding.Top,
|
||||||
bounds.Right - PaddingRight,
|
bounds.Right - (float)padding.Right,
|
||||||
bounds.Bottom - PaddingBottom);
|
bounds.Bottom - (float)padding.Bottom);
|
||||||
|
|
||||||
// Draw background based on state
|
// Determine background color
|
||||||
if (IsPressed || IsHovered || !IsOpaque && BackgroundColor != SKColors.Transparent)
|
SKColor bgColor;
|
||||||
|
if (IsPressed)
|
||||||
{
|
{
|
||||||
var bgColor = IsPressed ? PressedBackgroundColor
|
bgColor = ToSKColor(PressedBackgroundColor);
|
||||||
: IsHovered ? HoveredBackgroundColor
|
}
|
||||||
: BackgroundColor;
|
else if (IsHovered)
|
||||||
|
{
|
||||||
|
bgColor = ToSKColor(HoveredBackgroundColor);
|
||||||
|
}
|
||||||
|
else if (ImageBackgroundColor != null)
|
||||||
|
{
|
||||||
|
bgColor = ToSKColor(ImageBackgroundColor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bgColor = BackgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
if (bgColor != SKColors.Transparent || !IsOpaque)
|
||||||
|
{
|
||||||
using var bgPaint = new SKPaint
|
using var bgPaint = new SKPaint
|
||||||
{
|
{
|
||||||
Color = bgColor,
|
Color = bgColor,
|
||||||
@@ -130,13 +315,15 @@ public class SkiaImageButton : SkiaView
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw stroke/border
|
// Draw stroke/border
|
||||||
if (StrokeThickness > 0 && StrokeColor != SKColors.Transparent)
|
var strokeThickness = (float)StrokeThickness;
|
||||||
|
var strokeColor = ToSKColor(StrokeColor);
|
||||||
|
if (strokeThickness > 0 && strokeColor != SKColors.Transparent)
|
||||||
{
|
{
|
||||||
using var strokePaint = new SKPaint
|
using var strokePaint = new SKPaint
|
||||||
{
|
{
|
||||||
Color = StrokeColor,
|
Color = strokeColor,
|
||||||
Style = SKPaintStyle.Stroke,
|
Style = SKPaintStyle.Stroke,
|
||||||
StrokeWidth = StrokeThickness,
|
StrokeWidth = strokeThickness,
|
||||||
IsAntialias = true
|
IsAntialias = true
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -210,7 +397,10 @@ public class SkiaImageButton : SkiaView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image loading methods
|
#endregion
|
||||||
|
|
||||||
|
#region Image Loading
|
||||||
|
|
||||||
public async Task LoadFromFileAsync(string filePath)
|
public async Task LoadFromFileAsync(string filePath)
|
||||||
{
|
{
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
@@ -257,6 +447,7 @@ public class SkiaImageButton : SkiaView
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var padding = Padding;
|
||||||
await Task.Run(() =>
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
if (foundPath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase))
|
if (foundPath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -272,10 +463,10 @@ public class SkiaImageButton : SkiaView
|
|||||||
// Default to 24x24 for icons when no size specified
|
// Default to 24x24 for icons when no size specified
|
||||||
const float DefaultIconSize = 24f;
|
const float DefaultIconSize = 24f;
|
||||||
float targetWidth = hasWidth
|
float targetWidth = hasWidth
|
||||||
? (float)(WidthRequest - PaddingLeft - PaddingRight)
|
? (float)(WidthRequest - padding.Left - padding.Right)
|
||||||
: DefaultIconSize;
|
: DefaultIconSize;
|
||||||
float targetHeight = hasHeight
|
float targetHeight = hasHeight
|
||||||
? (float)(HeightRequest - PaddingTop - PaddingBottom)
|
? (float)(HeightRequest - padding.Top - padding.Bottom)
|
||||||
: DefaultIconSize;
|
: DefaultIconSize;
|
||||||
|
|
||||||
float scale = Math.Min(targetWidth / cullRect.Width, targetHeight / cullRect.Height);
|
float scale = Math.Min(targetWidth / cullRect.Width, targetHeight / cullRect.Height);
|
||||||
@@ -376,6 +567,12 @@ public class SkiaImageButton : SkiaView
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (data == null || data.Length == 0)
|
||||||
|
{
|
||||||
|
Bitmap = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var stream = new MemoryStream(data);
|
using var stream = new MemoryStream(data);
|
||||||
var bitmap = SKBitmap.Decode(stream);
|
var bitmap = SKBitmap.Decode(stream);
|
||||||
if (bitmap != null)
|
if (bitmap != null)
|
||||||
@@ -390,7 +587,16 @@ public class SkiaImageButton : SkiaView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pointer event handlers
|
public void LoadFromBitmap(SKBitmap bitmap)
|
||||||
|
{
|
||||||
|
Bitmap = bitmap;
|
||||||
|
ImageLoaded?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Pointer Event Handlers
|
||||||
|
|
||||||
public override void OnPointerEntered(PointerEventArgs e)
|
public override void OnPointerEntered(PointerEventArgs e)
|
||||||
{
|
{
|
||||||
if (!IsEnabled) return;
|
if (!IsEnabled) return;
|
||||||
@@ -438,10 +644,14 @@ public class SkiaImageButton : SkiaView
|
|||||||
if (wasPressed && Bounds.Contains(new SKPoint(e.X, e.Y)))
|
if (wasPressed && Bounds.Contains(new SKPoint(e.X, e.Y)))
|
||||||
{
|
{
|
||||||
Clicked?.Invoke(this, EventArgs.Empty);
|
Clicked?.Invoke(this, EventArgs.Empty);
|
||||||
|
ExecuteCommand();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard event handlers
|
#endregion
|
||||||
|
|
||||||
|
#region Keyboard Event Handlers
|
||||||
|
|
||||||
public override void OnKeyDown(KeyEventArgs e)
|
public override void OnKeyDown(KeyEventArgs e)
|
||||||
{
|
{
|
||||||
if (!IsEnabled) return;
|
if (!IsEnabled) return;
|
||||||
@@ -467,13 +677,22 @@ public class SkiaImageButton : SkiaView
|
|||||||
Invalidate();
|
Invalidate();
|
||||||
Released?.Invoke(this, EventArgs.Empty);
|
Released?.Invoke(this, EventArgs.Empty);
|
||||||
Clicked?.Invoke(this, EventArgs.Empty);
|
Clicked?.Invoke(this, EventArgs.Empty);
|
||||||
|
ExecuteCommand();
|
||||||
}
|
}
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Layout
|
||||||
|
|
||||||
protected override SKSize MeasureOverride(SKSize availableSize)
|
protected override SKSize MeasureOverride(SKSize availableSize)
|
||||||
{
|
{
|
||||||
|
var padding = Padding;
|
||||||
|
var paddingWidth = (float)(padding.Left + padding.Right);
|
||||||
|
var paddingHeight = (float)(padding.Top + padding.Bottom);
|
||||||
|
|
||||||
// Respect explicit WidthRequest/HeightRequest first (MAUI standard behavior)
|
// Respect explicit WidthRequest/HeightRequest first (MAUI standard behavior)
|
||||||
if (WidthRequest > 0 && HeightRequest > 0)
|
if (WidthRequest > 0 && HeightRequest > 0)
|
||||||
{
|
{
|
||||||
@@ -497,10 +716,8 @@ public class SkiaImageButton : SkiaView
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No explicit size - calculate from content
|
// No explicit size - calculate from content
|
||||||
var padding = new SKSize(PaddingLeft + PaddingRight, PaddingTop + PaddingBottom);
|
|
||||||
|
|
||||||
if (_image == null)
|
if (_image == null)
|
||||||
return new SKSize(44 + padding.Width, 44 + padding.Height); // Default touch target size
|
return new SKSize(44 + paddingWidth, 44 + paddingHeight); // Default touch target size
|
||||||
|
|
||||||
var imageWidth = _image.Width;
|
var imageWidth = _image.Width;
|
||||||
var imageHeight = _image.Height;
|
var imageHeight = _image.Height;
|
||||||
@@ -508,25 +725,25 @@ public class SkiaImageButton : SkiaView
|
|||||||
if (availableSize.Width < float.MaxValue && availableSize.Height < float.MaxValue)
|
if (availableSize.Width < float.MaxValue && availableSize.Height < float.MaxValue)
|
||||||
{
|
{
|
||||||
var availableContent = new SKSize(
|
var availableContent = new SKSize(
|
||||||
availableSize.Width - padding.Width,
|
availableSize.Width - paddingWidth,
|
||||||
availableSize.Height - padding.Height);
|
availableSize.Height - paddingHeight);
|
||||||
var scale = Math.Min(availableContent.Width / imageWidth, availableContent.Height / imageHeight);
|
var scale = Math.Min(availableContent.Width / imageWidth, availableContent.Height / imageHeight);
|
||||||
return new SKSize(imageWidth * scale + padding.Width, imageHeight * scale + padding.Height);
|
return new SKSize(imageWidth * scale + paddingWidth, imageHeight * scale + paddingHeight);
|
||||||
}
|
}
|
||||||
else if (availableSize.Width < float.MaxValue)
|
else if (availableSize.Width < float.MaxValue)
|
||||||
{
|
{
|
||||||
var availableWidth = availableSize.Width - padding.Width;
|
var availableWidth = availableSize.Width - paddingWidth;
|
||||||
var scale = availableWidth / imageWidth;
|
var scale = availableWidth / imageWidth;
|
||||||
return new SKSize(availableSize.Width, imageHeight * scale + padding.Height);
|
return new SKSize(availableSize.Width, imageHeight * scale + paddingHeight);
|
||||||
}
|
}
|
||||||
else if (availableSize.Height < float.MaxValue)
|
else if (availableSize.Height < float.MaxValue)
|
||||||
{
|
{
|
||||||
var availableHeight = availableSize.Height - padding.Height;
|
var availableHeight = availableSize.Height - paddingHeight;
|
||||||
var scale = availableHeight / imageHeight;
|
var scale = availableHeight / imageHeight;
|
||||||
return new SKSize(imageWidth * scale + padding.Width, availableSize.Height);
|
return new SKSize(imageWidth * scale + paddingWidth, availableSize.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SKSize(imageWidth + padding.Width, imageHeight + padding.Height);
|
return new SKSize(imageWidth + paddingWidth, imageHeight + paddingHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SKRect ArrangeOverride(SKRect bounds)
|
protected override SKRect ArrangeOverride(SKRect bounds)
|
||||||
@@ -576,13 +793,25 @@ public class SkiaImageButton : SkiaView
|
|||||||
return bounds;
|
return bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Dispose
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
|
// Unsubscribe from command
|
||||||
|
if (Command != null)
|
||||||
|
{
|
||||||
|
Command.CanExecuteChanged -= OnCommandCanExecuteChanged;
|
||||||
|
}
|
||||||
|
|
||||||
_bitmap?.Dispose();
|
_bitmap?.Dispose();
|
||||||
_image?.Dispose();
|
_image?.Dispose();
|
||||||
}
|
}
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user