using System; using System.Threading; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Logging; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; namespace DownKyi.CustomControl.AsyncImageLoader.Loaders; public class AdvancedImage : ContentControl { /// /// Defines the property. /// public static readonly StyledProperty LoaderProperty = AvaloniaProperty.Register(nameof(Loader)); /// /// Defines the property. /// public static readonly StyledProperty SourceProperty = AvaloniaProperty.Register(nameof(Source)); /// /// Defines the property. /// public static readonly DirectProperty ShouldLoaderChangeTriggerUpdateProperty = AvaloniaProperty.RegisterDirect( nameof(ShouldLoaderChangeTriggerUpdate), image => image._shouldLoaderChangeTriggerUpdate, (image, b) => image._shouldLoaderChangeTriggerUpdate = b ); /// /// Defines the property. /// public static readonly DirectProperty IsLoadingProperty = AvaloniaProperty.RegisterDirect( nameof(IsLoading), image => image._isLoading); /// /// Defines the property. /// public static readonly DirectProperty CurrentImageProperty = AvaloniaProperty.RegisterDirect( nameof(CurrentImage), image => image._currentImage); /// /// Defines the property. /// public static readonly StyledProperty StretchProperty = Image.StretchProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty StretchDirectionProperty = Image.StretchDirectionProperty.AddOwner(); private readonly Uri? _baseUri; private RoundedRect _cornerRadiusClip; private IImage? _currentImage; private bool _isCornerRadiusUsed; private bool _isLoading; private bool _shouldLoaderChangeTriggerUpdate; private CancellationTokenSource? _updateCancellationToken; private readonly ParametrizedLogger? _logger; static AdvancedImage() { AffectsRender(CurrentImageProperty, StretchProperty, StretchDirectionProperty, CornerRadiusProperty); AffectsMeasure(CurrentImageProperty, StretchProperty, StretchDirectionProperty); } /// /// Initializes a new instance of the class. /// /// The base URL for the XAML context. public AdvancedImage(Uri? baseUri) { _baseUri = baseUri; _logger = Logger.TryGet(LogEventLevel.Error, ImageLoader.AsyncImageLoaderLogArea); } /// /// Initializes a new instance of the class. /// /// The XAML service provider. public AdvancedImage(IServiceProvider serviceProvider) : this((serviceProvider.GetService(typeof(IUriContext)) as IUriContext)?.BaseUri) { } /// /// Gets or sets the URI for image that will be displayed. /// public IAsyncImageLoader? Loader { get => GetValue(LoaderProperty); set => SetValue(LoaderProperty, value); } /// /// Gets or sets the URI for image that will be displayed. /// public string? Source { get => GetValue(SourceProperty); set => SetValue(SourceProperty, value); } /// /// Gets or sets the value controlling whether the image should be reloaded after changing the loader. /// public bool ShouldLoaderChangeTriggerUpdate { get => _shouldLoaderChangeTriggerUpdate; set => SetAndRaise(ShouldLoaderChangeTriggerUpdateProperty, ref _shouldLoaderChangeTriggerUpdate, value); } /// /// Gets a value indicating is image currently is loading state. /// public bool IsLoading { get => _isLoading; private set => SetAndRaise(IsLoadingProperty, ref _isLoading, value); } /// /// Gets a currently loaded IImage. /// public IImage? CurrentImage { get => _currentImage; set => SetAndRaise(CurrentImageProperty, ref _currentImage, value); } /// /// Gets or sets a value controlling how the image will be stretched. /// public Stretch Stretch { get => GetValue(StretchProperty); set => SetValue(StretchProperty, value); } /// /// Gets or sets a value controlling in what direction the image will be stretched. /// public StretchDirection StretchDirection { get => GetValue(StretchDirectionProperty); set => SetValue(StretchDirectionProperty, value); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == SourceProperty) UpdateImage(change.GetNewValue(), Loader); else if (change.Property == LoaderProperty && ShouldLoaderChangeTriggerUpdate) UpdateImage(change.GetNewValue(), Loader); else if (change.Property == CurrentImageProperty) ClearSourceIfUserProvideImage(); else if (change.Property == CornerRadiusProperty) UpdateCornerRadius(change.GetNewValue()); else if (change.Property == BoundsProperty && CornerRadius != default) UpdateCornerRadius(CornerRadius); base.OnPropertyChanged(change); } private void ClearSourceIfUserProvideImage() { if (CurrentImage is not null and not ImageWrapper) { // User provided image himself Source = null; } } private async void UpdateImage(string? source, IAsyncImageLoader? loader) { var cancellationTokenSource = new CancellationTokenSource(); var oldCancellationToken = Interlocked.Exchange(ref _updateCancellationToken, cancellationTokenSource); try { oldCancellationToken?.Cancel(); } catch (ObjectDisposedException) { } if (source is null && CurrentImage is not ImageWrapper) { // User provided image himself return; } IsLoading = true; CurrentImage = null; var bitmap = await Task.Run(async () => { try { if (source == null) return null; // A small delay allows to cancel early if the image goes out of screen too fast (eg. scrolling) // The Bitmap constructor is expensive and cannot be cancelled await Task.Delay(10, cancellationTokenSource.Token); // Hack to support relative URI // TODO: Refactor IAsyncImageLoader to support BaseUri try { var uri = new Uri(source, UriKind.RelativeOrAbsolute); if (AssetLoader.Exists(uri, _baseUri)) return new Bitmap(AssetLoader.Open(uri, _baseUri)); } catch (Exception) { // ignored } loader ??= ImageLoader.AsyncImageLoader; return await loader.ProvideImageAsync(source); } catch (TaskCanceledException) { return null; } catch (Exception e) { _logger?.Log(this, "AdvancedImage image resolution failed: {0}", e); return null; } finally { cancellationTokenSource.Dispose(); } }, CancellationToken.None); if (cancellationTokenSource.IsCancellationRequested) return; CurrentImage = bitmap is null ? null : new ImageWrapper(bitmap); IsLoading = false; } private void UpdateCornerRadius(CornerRadius radius) { _isCornerRadiusUsed = radius != default; _cornerRadiusClip = new RoundedRect(new Rect(0, 0, Bounds.Width, Bounds.Height), radius); } /// /// Renders the control. /// /// The drawing context. public override void Render(DrawingContext context) { var source = CurrentImage; if (source != null && Bounds is { Width: > 0, Height: > 0 }) { var viewPort = new Rect(Bounds.Size); var sourceSize = source.Size; var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); var scaledSize = sourceSize * scale; var destRect = viewPort .CenterRect(new Rect(scaledSize)) .Intersect(viewPort); var sourceRect = new Rect(sourceSize) .CenterRect(new Rect(destRect.Size / scale)); DrawingContext.PushedState? pushedState = _isCornerRadiusUsed ? context.PushClip(_cornerRadiusClip) : null; context.DrawImage(source, sourceRect, destRect); pushedState?.Dispose(); } else { base.Render(context); } } /// /// Measures the control. /// /// The available size. /// The desired size of the control. protected override Size MeasureOverride(Size availableSize) { return CurrentImage != null ? Stretch.CalculateSize(availableSize, CurrentImage.Size, StretchDirection) : base.MeasureOverride(availableSize); } /// protected override Size ArrangeOverride(Size finalSize) { return CurrentImage != null ? Stretch.CalculateSize(finalSize, CurrentImage.Size) : base.ArrangeOverride(finalSize); } public sealed class ImageWrapper : IImage { public IImage ImageImplementation { get; } internal ImageWrapper(IImage imageImplementation) { ImageImplementation = imageImplementation; } /// public void Draw(DrawingContext context, Rect sourceRect, Rect destRect) { ImageImplementation.Draw(context, sourceRect, destRect); } /// public Size Size => ImageImplementation.Size; } }