using System; using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; namespace HandyControl.Controls; /// /// A sprite-sheet frame animation player. /// Slices the source image into a x grid and /// plays back the frame range [StartRow,StartColumn] … [EndRow,EndColumn]. /// public class ImageBlock : Control { private readonly DispatcherTimer _dispatcherTimer; private IImage? _source; private int _indexMax; private int _indexMin; private int _currentIndex; private int _blockWidth; private int _blockHeight; private bool _isDisposed; private int _columns = 1; public ImageBlock() { _dispatcherTimer = new DispatcherTimer { Interval = Interval }; _dispatcherTimer.Tick += DispatcherTimer_Tick; } ~ImageBlock() => Dispose(); public void Dispose() { if (_isDisposed) return; _dispatcherTimer.Tick -= DispatcherTimer_Tick; _dispatcherTimer.Stop(); _isDisposed = true; GC.SuppressFinalize(this); } private void UpdateDatas() { if (_source is null) return; _indexMin = StartRow * _columns + StartColumn; _indexMax = EndRow * _columns + EndColumn; _currentIndex = _indexMin; if (_source is Bitmap bitmap) { _blockWidth = bitmap.PixelSize.Width / _columns; _blockHeight = bitmap.PixelSize.Height / Rows; } } private void DispatcherTimer_Tick(object? sender, EventArgs e) => InvalidateVisual(); // ── Dependency Properties ── public static readonly StyledProperty StartColumnProperty = AvaloniaProperty.Register(nameof(StartColumn)); public int StartColumn { get => GetValue(StartColumnProperty); set => SetValue(StartColumnProperty, value); } public static readonly StyledProperty StartRowProperty = AvaloniaProperty.Register(nameof(StartRow)); public int StartRow { get => GetValue(StartRowProperty); set => SetValue(StartRowProperty, value); } public static readonly StyledProperty EndColumnProperty = AvaloniaProperty.Register(nameof(EndColumn)); public int EndColumn { get => GetValue(EndColumnProperty); set => SetValue(EndColumnProperty, value); } public static readonly StyledProperty EndRowProperty = AvaloniaProperty.Register(nameof(EndRow)); public int EndRow { get => GetValue(EndRowProperty); set => SetValue(EndRowProperty, value); } public static readonly StyledProperty IsPlayingProperty = AvaloniaProperty.Register(nameof(IsPlaying)); public bool IsPlaying { get => GetValue(IsPlayingProperty); set => SetValue(IsPlayingProperty, value); } public static readonly StyledProperty ColumnsProperty = AvaloniaProperty.Register(nameof(Columns), defaultValue: 1, validate: v => v >= 1); public int Columns { get => GetValue(ColumnsProperty); set => SetValue(ColumnsProperty, value); } public static readonly StyledProperty RowsProperty = AvaloniaProperty.Register(nameof(Rows), defaultValue: 1, validate: v => v >= 1); public int Rows { get => GetValue(RowsProperty); set => SetValue(RowsProperty, value); } public static readonly StyledProperty IntervalProperty = AvaloniaProperty.Register(nameof(Interval), defaultValue: TimeSpan.FromSeconds(1)); public TimeSpan Interval { get => GetValue(IntervalProperty); set => SetValue(IntervalProperty, value); } public static readonly StyledProperty SourceProperty = AvaloniaProperty.Register(nameof(Source)); public IImage? Source { get => GetValue(SourceProperty); set => SetValue(SourceProperty, value); } // ── Static constructor ── static ImageBlock() { AffectsRender( SourceProperty, StartColumnProperty, StartRowProperty, EndColumnProperty, EndRowProperty, ColumnsProperty, RowsProperty, IsPlayingProperty, IntervalProperty); SourceProperty.Changed.AddClassHandler((o, e) => o.OnSourceChanged(e)); ColumnsProperty.Changed.AddClassHandler((o, e) => o.OnPositionsChanged(e)); StartColumnProperty.Changed.AddClassHandler((o, e) => o.OnPositionsChanged(e)); StartRowProperty.Changed.AddClassHandler((o, e) => o.OnPositionsChanged(e)); EndColumnProperty.Changed.AddClassHandler((o, e) => o.OnPositionsChanged(e)); EndRowProperty.Changed.AddClassHandler((o, e) => o.OnPositionsChanged(e)); IsPlayingProperty.Changed.AddClassHandler((o, e) => o.OnIsPlayingChanged(e)); IntervalProperty.Changed.AddClassHandler((o, e) => o.OnIntervalChanged(e)); IsVisibleProperty.Changed.AddClassHandler((o, e) => o.OnIsVisibleChanged(e)); } // ── Event handlers ── private void OnSourceChanged(AvaloniaPropertyChangedEventArgs e) { _source = e.GetNewValue(); UpdateDatas(); } private void OnPositionsChanged(AvaloniaPropertyChangedEventArgs e) { if (e.Property == ColumnsProperty) { _columns = e.GetNewValue(); } UpdateDatas(); } private void OnIsPlayingChanged(AvaloniaPropertyChangedEventArgs e) { if (e.GetNewValue() && IsVisible) { _dispatcherTimer.Start(); } else { _dispatcherTimer.Stop(); } } private void OnIntervalChanged(AvaloniaPropertyChangedEventArgs e) { _dispatcherTimer.Interval = e.GetNewValue(); } private void OnIsVisibleChanged(AvaloniaPropertyChangedEventArgs e) { if (IsVisible && IsPlaying) { _dispatcherTimer.Start(); } else { _dispatcherTimer.Stop(); } } // ── Render ── public override void Render(DrawingContext context) { if (_source is null) return; var rect = CalDisplayRect(); var croppedBitmap = new CroppedBitmap(_source, rect); context.DrawImage(croppedBitmap, new Rect(0, 0, Bounds.Width, Bounds.Height)); } private PixelRect CalDisplayRect() { if (_currentIndex > _indexMax) { _currentIndex = _indexMin; } var x = _currentIndex % _columns * _blockWidth; var y = _currentIndex / _columns * _blockHeight; var rect = new PixelRect(x, y, _blockWidth, _blockHeight); _currentIndex++; return rect; } }